lib/before_send.ex

defmodule AbsintheCacheFairy.BeforeSend do
  @moduledoc ~s"""
  Cache & Persist API Call Data right before sending the response.

  This module is responsible for persisting the whole result of some queries
  right before it is send to the client.

  All queries that did not raise exceptions and were successfully handled
  by the GraphQL layer pass through this module.

  The Blueprint's `result` field contains the final result as a single map.
  This result is made up of the top-level resolver and all custom resolvers.

  Caching the end result instead of each resolver separately allows to
  resolve the whole query with a single cache call - some queries could have
  thousands of custom resolver invocations.

  In order to cache a result all of the following conditions must be true:
  - All queries must be present in the `@cached_queries` list
  - The resolved value must not be an error
  - During resolving there must not be any `:nocache` returned.

  Most of the simple queries use 1 cache call and won't benefit from this approach.
  Only queries with many resolvers are included in the list of allowed queries.
  """

  defmacro __using__(opts) do
    quote location: :keep, bind_quoted: [opts: opts] do
      @compile :inline_list_funcs
      @compile inline: [cache_result: 2, queries_in_request: 1, has_graphql_errors?: 1]

      @cached_queries Keyword.get(opts, :cached_queries, [])
      def before_send(conn, %Absinthe.Blueprint{} = blueprint) do
        # Do not cache in case of:
        # -`:nocache` returend from a resolver
        # - result is taken from the cache and should not be stored again. Storing
        # it again `touch`es it and the TTL timer is restarted. This can lead
        # to infinite storing the same value if there are enough requests

        queries = queries_in_request(blueprint)
        do_not_cache? = is_nil(Process.get(:do_not_cache_query))

        case do_not_cache? or has_graphql_errors?(blueprint) do
          true -> :ok
          false -> cache_result(queries, blueprint)
        end

        conn
      end

      defp cache_result(queries, blueprint) do
        all_queries_cachable? = queries |> Enum.all?(&Enum.member?(@cached_queries, &1))

        if all_queries_cachable? do
          AbsintheCacheFairy.store(
            blueprint.execution.context.query_cache_key,
            blueprint.result
          )
        end
      end

      defp queries_in_request(%{operations: operations}) do
        operations
        |> Enum.flat_map(fn %{selections: selections} ->
          selections
          |> Enum.map(fn %{name: name} -> Inflex.camelize(name, :lower) end)
        end)
      end

      defp has_graphql_errors?(%Absinthe.Blueprint{result: %{errors: _}}), do: true
      defp has_graphql_errors?(_), do: false
    end
  end
end