defmodule AbsinthePlugCache.Plug.DocumentProvider.Compiled do @moduledoc """ Provide pre-compiled documents for retrieval by request parameter key. Important: This module shouldn't be used as a document provider itself, but as a toolkit to build one. See the examples below. ### Examples Define a new module and `use AbsinthePlugCache.Plug.DocumentProvider.Compiled`: defmodule MyAppWeb.Schema.Documents do use AbsinthePlugCache.Plug.DocumentProvider.Compiled # ... Configure here end You can provide documents as literals within the module, by key, using the `provide/2` macro: provide "item", "query Item($id: ID!) { item(id: $id) { name } }" You can also load a map of key value pairs using `provide/1`. provide %{ "item" => "query Item($id: ID!) { item(id: $id) { name } }", "time" => "{ currentTime }" } This can be used to support loading documents extracted using Apollo's [persistgraphql](https://github.com/apollographql/persistgraphql) tool by parsing the file and inverting the key/value pairs. provide File.read!("/path/to/extracted_queries.json") |> Jason.decode! |> Map.new(fn {k, v} -> {v, k} end) By default, the request parameter that will be used to lookup documents is `"id"`. You can change this by passing a `:key_param` option to `use`, e.g.: use AbsinthePlugCache.Plug.DocumentProvider.Compiled, key_param: "lookup_key" ## Configuring You need to configure `AbsinthePlugCache.Plug` to use any document providers that you create. (Only `AbsinthePlugCache.Plug.DocumentProviders.Default` is configured by default.) Make sure that a `Compiled` document provider is placed before the `Default` provider. See the documentation on `AbsinthePlugCache.Plug.init/1` for more details. Look for the `:document_providers` option. """ defmacro __using__(opts) do key_param = Keyword.get(opts, :key_param, "id") |> to_string quote do @behaviour AbsinthePlugCache.Plug.DocumentProvider @before_compile {unquote(__MODULE__.Writer), :write} @absinthe_documents_to_compile %{} # Can be overridden in the document provider module @compilation_pipeline Absinthe.Pipeline.for_document(nil, jump_phases: false) |> Absinthe.Pipeline.before(Absinthe.Phase.Document.Variables) |> Absinthe.Pipeline.without(Absinthe.Phase.Telemetry) import unquote(__MODULE__), only: [provide: 2, provide: 1] def process(request, _) do do_process(request) end defp do_process(%{params: %{unquote(key_param) => document_key}} = request) do case __absinthe_plug_doc__(:compiled, document_key) do nil -> {:cont, request} document -> {:halt, %{request | document: document, document_provider_key: document_key}} end end defp do_process(request) do {:cont, request} end @doc """ Determine the remaining pipeline for an request with a pre-compiled document. Usually this can be changed simply by setting `@compilation_pipeline` in your document provider. This may need to be overridden if your compilation phase is not a subset of the full pipeline. """ def pipeline(%{pipeline: as_configured}) do remaining_pipeline_marker = __absinthe_plug_doc__(:remaining_pipeline) telemetry_phase = {Absinthe.Phase.Telemetry, event: [:execute, :operation, :start]} as_configured |> Absinthe.Pipeline.from(remaining_pipeline_marker) |> Absinthe.Pipeline.insert_before(remaining_pipeline_marker, telemetry_phase) end defoverridable pipeline: 1, process: 2 end end @doc ~s""" Provide a GraphQL document for a given key. Note that the key will be coerced to a string to ensure compatibility with the expected request parameter. For more information, see the module-level documentation. ## Examples provide "foo", \""" query ShowItem($id: ID!) { item(id: $id) { name } } \""" """ @spec provide(any, String.t()) :: Macro.t() defmacro provide(document_key, document_source) do quote do @absinthe_documents_to_compile Map.put( @absinthe_documents_to_compile, to_string(unquote(document_key)), unquote(document_source) ) end end @doc ~s""" Provide multiple GraphQL documents by key. Note that keys will be coerced to strings to ensure compatibility with the expected request parameter. For more information, see the module-level documentation. ## Examples provide %{ "item" => "query Item($id: ID!) { item(id: $id) { name } }", "time" => "{ currentTime }" } """ @spec provide(%{any => String.t()}) :: Macro.t() defmacro provide(documents) do quote do @absinthe_documents_to_compile Map.merge( @absinthe_documents_to_compile, Map.new( unquote(documents), &{to_string(elem(&1, 0)), elem(&1, 1)} ) ) end end @doc """ Lookup a document by id. ## Examples Get a compiled document: iex> get(CompiledProvider, "provided") #Absinthe.Blueprint<> With an explicit `:compiled` flag: iex> get(CompiledProvider, "provided", :compiled) #Absinthe.Blueprint<> Get the source: iex> get(CompiledProvider, "provided", :source) "query Item { item { name } }" When a value isn't present: iex> get(CompiledProvider, "not-provided") nil """ @spec get(module, String.t(), :compiled | :source) :: nil | Absinthe.Blueprint.t() def get(dp, id, format \\ :compiled) def get(dp, id, :compiled) do dp.__absinthe_plug_doc__(:compiled, id) end def get(dp, id, :source) do dp.__absinthe_plug_doc__(:source, id) end end