Skip to content

Commit

Permalink
feat: add ability to whitelist specific query names when adding graph…
Browse files Browse the repository at this point in the history
…ql middleware (#29)

* feat: add ability to whitelist specific query names when adding graphql middleware

* chore: add docs for whitelisting
  • Loading branch information
MikaAK committed Sep 22, 2023
1 parent d38708d commit 149e3b5
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 9 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 19 additions & 6 deletions lib/request_cache/middleware.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down
11 changes: 9 additions & 2 deletions lib/request_cache/plug.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
65 changes: 65 additions & 0 deletions test/request_cache_absinthe_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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}} ->
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 149e3b5

Please sign in to comment.