defmodule BridgeEx.Graphql do @moduledoc """ Main module to be used to implement graphql bridges. You need to provide an `endpoint` on `use`, e.g. ``` use BridgeEx.Graphql, endpoint: "https://your.auth0.endpoint" ``` """ @doc """ Create a Graphql bridge in the given module. Once created, a graphql request can be made via `MyBridge.call("my-query", %{"variable": "var"})` ## Options * `endpoint` (required): URL of the remote Graphql endpoint. * `auth0`: enable and configure Auth0 for authentication of requests. Takes the form of `[enabled: false, audience: "target-audience"]`. * `encode_variables`: if true, encode the Graphql variables to JSON. Defaults to `false`. * `format_response`: transforms camelCase keys in response to snake_case. Defaults to `false`. * `http_headers`: HTTP headers for the request. Defaults to `%{"Content-type": "application/json"}` * `http_options`: HTTP options to be passed to Telepoison. Defaults to `[timeout: 1_000, recv_timeout: 16_000]`. * `log_options`: override global configuration for logging errors. Takes the form of `[log_query_on_error: false, log_response_on_error: false]` * `max_attempts`: number of times the request will be retried upon failure. Defaults to `1`. ## Examples ```elixir defmodule MyBridge do use BridgeEx.Graphql, endpoint: "http://my-api.com/graphql" end ``` ```elixir defmodule MyBridge do use BridgeEx.Graphql, endpoint: "http://my-api.com/graphql", auth0: [enabled: true, audience: "target-audience"] end ``` """ # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity defmacro __using__(opts) when is_list(opts) do quote do require Logger alias BridgeEx.Auth0AuthorizationProvider alias BridgeEx.Graphql.Client # global config @global_log_options Application.compile_env(:bridge_ex, :log_options, log_query_on_error: false, log_response_on_error: false ) # local config # mandatory opts @endpoint Keyword.fetch!(unquote(opts), :endpoint) # optional opts with defaults @auth0_enabled get_in(unquote(opts), [:auth0, :enabled]) || false @audience get_in(unquote(opts), [:auth0, :audience]) @http_options Keyword.get(unquote(opts), :http_options, timeout: 1_000, recv_timeout: 16_000) @http_headers Keyword.get(unquote(opts), :http_headers, %{ "Content-type" => "application/json" }) @max_attempts Keyword.get(unquote(opts), :max_attempts, 1) @log_options Keyword.get(unquote(opts), :log_options, @global_log_options) @doc """ Run a graphql query or mutation over the configured bridge. ## Options * `options`: extra HTTP options to be passed to Telepoison. * `headers`: extra HTTP headers. * `max_attempts`: override the configured `max_attempts` parameter. ## Examples iex> MyBridge.call("some_query", %{var_key: "var_value"}) iex> MyBridge.call("some_query", %{var_key: "var_value"}, max_attempts: 3) """ @spec call( query :: String.t(), variables :: map(), options :: Keyword.t() ) :: Client.bridge_response() def call(query, variables, options \\ []) do http_options = Keyword.merge(@http_options, Keyword.get(options, :options, [])) http_headers = Map.merge(@http_headers, Keyword.get(options, :headers, %{})) max_attempts = Keyword.get(options, :max_attempts, @max_attempts) with {:ok, http_headers} <- with_authorization_headers(http_headers) do @endpoint |> Client.call( query, encode_variables(variables), http_options, http_headers, max_attempts, @log_options ) |> format_response() end end # define helpers at compile-time, to avoid dialyzer errors about pattern matching constants if Keyword.get(unquote(opts), :encode_variables, false) do defp encode_variables(variables), do: Jason.encode!(variables) else defp encode_variables(variables), do: variables end if Keyword.get(unquote(opts), :format_response, false) do defp format_response({ret, response}), do: {ret, Client.format_response(response)} else defp format_response({ret, response}), do: {ret, response} end if @audience == nil && @auth0_enabled do raise CompileError, file: __ENV__.file, line: __ENV__.line, description: """ Auth0 is enabled but audience is not set for bridge in module #{__MODULE__}. Please either set an audience for this bridge or disable auth0 locally: # Either this use BridgeEx.Graphql, auth0: [audience: "my-audience"] # or this use BridgeEx.Graphql, auth0: [enabled: false] """ end if @audience && @auth0_enabled do defp with_authorization_headers(headers) do with {:ok, authorization_headers} <- get_authorization_headers_for(@audience) do {:ok, Enum.into(authorization_headers, headers)} end end else defp with_authorization_headers(headers), do: {:ok, headers} end defp get_authorization_headers_for(audience) do if auth0_enabled_for_app(), do: Auth0AuthorizationProvider.authorization_headers(@audience), else: report_auth0_disabled_error() end defp report_auth0_disabled_error do Logger.error("Auth0 is not enabled for this application! To enable it set `config :bridge_ex, auth0_enabled: true` in your config.") {:error, "Auth0 not enabled in application"} end defp auth0_enabled_for_app, do: Application.get_env(:bridge_ex, :auth0_enabled, false) end end end