diff --git a/README.md b/README.md index 8d4171d..ef69f43 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,11 @@ For GraphQL endpoints it is possible to provide a list of atoms that will be pas field :user, :user do arg :id, non_null(:id) - middleware RequestCache.Middleware, ttl: :timer.seconds(60), cache: MyCacheModule, labels: [:service, :endpoint] + middleware RequestCache.Middleware, + ttl: :timer.seconds(60), + cache: MyCacheModule, + labels: [:service, :endpoint], + whitelisted_query_names: ["MyQueryName"] # By default all queries are cached, can also whitelist based off query name from GQL Document resolve &Resolvers.User.find/2 end diff --git a/lib/request_cache/middleware.ex b/lib/request_cache/middleware.ex index 6307595..56bfae4 100644 --- a/lib/request_cache/middleware.ex +++ b/lib/request_cache/middleware.ex @@ -18,18 +18,33 @@ if absinthe_loaded? do enable_cache_for_resolution(resolution, ttl: ttl) end - defp enable_cache_for_resolution(resolution, opts) do + defp ensure_valid_ttl(opts) do + ttl = opts[:ttl] || RequestCache.Config.default_ttl() + + Keyword.put(opts, :ttl, ttl) + end + + defp enable_cache_for_resolution(%Absinthe.Resolution{} = resolution, opts) do resolution = resolve_resolver_func_middleware(resolution, opts) if resolution.context[RequestCache.Config.conn_private_key()][:enabled?] do Util.verbose_log("[RequestCache.Middleware] Enabling cache for resolution") + root_resolution_path_item = List.last(resolution.path) + + cache_request? = !!root_resolution_path_item && + root_resolution_path_item.schema_node.name === "RootQueryType" && + query_name_whitelisted?(root_resolution_path_item.name, opts) + %{resolution | value: resolution.value || opts[:value], context: Map.update!( resolution.context, RequestCache.Config.conn_private_key(), - &Util.deep_merge(&1, request: opts, cache_request?: true) + &Util.deep_merge(&1, + request: opts, + cache_request?: cache_request? + ) ) } else @@ -49,10 +64,8 @@ if absinthe_loaded? do defp resolver_middleware?(opts), do: opts[:value] - defp ensure_valid_ttl(opts) do - ttl = opts[:ttl] || RequestCache.Config.default_ttl() - - Keyword.put(opts, :ttl, ttl) + defp query_name_whitelisted?(query_name, opts) do + is_nil(opts[:whitelisted_query_names]) or query_name in opts[:whitelisted_query_names] end @spec store_result( diff --git a/lib/request_cache/plug.ex b/lib/request_cache/plug.ex index 0e98bd5..17381cb 100644 --- a/lib/request_cache/plug.ex +++ b/lib/request_cache/plug.ex @@ -34,13 +34,20 @@ defmodule RequestCache.Plug do end end - defp call_for_api_type(%Plug.Conn{request_path: path, method: "GET", query_string: query_string} = conn, opts) when path in @graphql_paths do + defp call_for_api_type(%Plug.Conn{ + request_path: path, + method: "GET", + query_string: query_string + } = conn, opts) when path in @graphql_paths do Util.verbose_log("[RequestCache.Plug] GraphQL query detected") maybe_return_cached_result(conn, opts, path, query_string) end - defp call_for_api_type(%Plug.Conn{request_path: path, method: "GET"} = conn, opts) when path not in @graphql_paths do + defp call_for_api_type(%Plug.Conn{ + request_path: path, + method: "GET" + } = conn, opts) when path not in @graphql_paths do Util.verbose_log("[RequestCache.Plug] REST path detected") cache_key = rest_cache_key(conn) diff --git a/test/request_cache_absinthe_test.exs b/test/request_cache_absinthe_test.exs index 668cf4e..ff2e0f0 100644 --- a/test/request_cache_absinthe_test.exs +++ b/test/request_cache_absinthe_test.exs @@ -8,6 +8,14 @@ defmodule RequestCacheAbsintheTest do defmodule Schema do use Absinthe.Schema + object :nested_item do + field :world, :string + end + + object :item do + field :tester, :nested_item + end + query do field :hello, :string do resolve fn _, %{context: %{call_pid: pid}} -> @@ -69,6 +77,18 @@ defmodule RequestCacheAbsintheTest do {:error, %{code: :not_found, message: "TesT"}} end end + + field :whitelist_cached_hello, :item do + middleware RequestCache.Middleware, whitelisted_query_names: ["SmallHello"] + + resolve fn _, _ -> + {:ok, %{ + tester: %{ + world: "hello" + } + }} + end + end end end @@ -108,6 +128,9 @@ defmodule RequestCacheAbsintheTest do @uncached_error_query "query UncachedFound { uncachedError }" @cached_all_error_query "query CachedAllFound { cachedAllError(code: \"not_found\") }" @cached_not_found_error_query "query CachedNotFound { cachedNotFoundError }" + @whitelist_named_query "query SmallHello { whitelistCachedHello { tester { world }} }" + @whitelist_uncached_named_query "query SmallerHello { whitelistCachedHello { tester { world }} }" + @whitelist_unnamed_query "query { whitelistCachedHello { tester { world }} }" setup do {:ok, pid} = EnsureCalledOnlyOnce.start_link() @@ -245,6 +268,48 @@ defmodule RequestCacheAbsintheTest do end) =~ "RequestCache requested" end + @tag capture_log: true + test "whitelist doesn't cache unspecified queries" do + assert [] = :get + |> conn(graphql_url(@whitelist_uncached_named_query)) + |> RequestCache.Support.Utils.ensure_default_opts(request: [whitelisted_query_names: ["SmallHello"]]) + |> Router.call([]) + |> get_resp_header(RequestCache.Plug.request_cache_header()) + + assert [] = :get + |> conn(graphql_url(@whitelist_uncached_named_query)) + |> RequestCache.Support.Utils.ensure_default_opts(request: [whitelisted_query_names: ["SmallHello"]]) + |> Router.call([]) + |> get_resp_header(RequestCache.Plug.request_cache_header()) + + assert [] = :get + |> conn(graphql_url(@whitelist_unnamed_query)) + |> RequestCache.Support.Utils.ensure_default_opts(request: [whitelisted_query_names: ["SmallHello"]]) + |> Router.call([]) + |> get_resp_header(RequestCache.Plug.request_cache_header()) + + assert [] = :get + |> conn(graphql_url(@whitelist_unnamed_query)) + |> RequestCache.Support.Utils.ensure_default_opts(request: [whitelisted_query_names: ["SmallHello"]]) + |> Router.call([]) + |> get_resp_header(RequestCache.Plug.request_cache_header()) + end + + @tag capture_log: true + test "whitelist caches specific named queries" do + assert [] = :get + |> conn(graphql_url(@whitelist_named_query)) + |> RequestCache.Support.Utils.ensure_default_opts(request: [whitelisted_query_names: ["SmallHello"]]) + |> Router.call([]) + |> get_resp_header(RequestCache.Plug.request_cache_header()) + + assert ["HIT"] = :get + |> conn(graphql_url(@whitelist_named_query)) + |> RequestCache.Support.Utils.ensure_default_opts(request: [whitelisted_query_names: ["SmallHello"]]) + |> Router.call([]) + |> get_resp_header(RequestCache.Plug.request_cache_header()) + end + defp graphql_url(query) do "/graphql?#{URI.encode_query(%{query: query})}" end