Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading