From 13e68d5dc2d48eeae646bb811b0835e8f7c38988 Mon Sep 17 00:00:00 2001 From: Andrei Mochalov Date: Tue, 29 Jul 2025 18:31:43 +0400 Subject: [PATCH 1/2] feat: add cache tags support --- lib/graphiti/resource/interface.rb | 5 +++-- lib/graphiti/resource_proxy.rb | 34 +++++++++++++++++++++++++----- lib/graphiti/runner.rb | 13 +++++++++--- lib/graphiti/util/cache_debug.rb | 4 +++- spec/resource_proxy_spec.rb | 11 ++++++++-- 5 files changed, 54 insertions(+), 13 deletions(-) diff --git a/lib/graphiti/resource/interface.rb b/lib/graphiti/resource/interface.rb index 0f702cf3..6315043f 100644 --- a/lib/graphiti/resource/interface.rb +++ b/lib/graphiti/resource/interface.rb @@ -4,9 +4,10 @@ module Interface extend ActiveSupport::Concern class_methods do - def cache_resource(expires_in: false) + def cache_resource(expires_in: false, tag: nil) @cache_resource = true @cache_expires_in = expires_in + @cache_tag = tag end def all(params = {}, base_scope = nil) @@ -55,7 +56,7 @@ def build(params, base_scope = nil) private def caching_options - {cache: @cache_resource, cache_expires_in: @cache_expires_in} + { cache: @cache_resource, cache_expires_in: @cache_expires_in, cache_tag: @cache_tag } end def validate_request!(params) diff --git a/lib/graphiti/resource_proxy.rb b/lib/graphiti/resource_proxy.rb index b95b5feb..059a8117 100644 --- a/lib/graphiti/resource_proxy.rb +++ b/lib/graphiti/resource_proxy.rb @@ -2,14 +2,19 @@ module Graphiti class ResourceProxy include Enumerable - attr_reader :resource, :query, :scope, :payload, :cache_expires_in, :cache + attr_reader :resource, :query, :scope, :payload, :cache_expires_in, :cache, :cache_tag - def initialize(resource, scope, query, + def initialize( + resource, + scope, + query, payload: nil, single: false, raise_on_missing: false, cache: nil, - cache_expires_in: nil) + cache_expires_in: nil, + cache_tag: nil + ) @resource = resource @scope = scope @@ -19,6 +24,7 @@ def initialize(resource, scope, query, @raise_on_missing = raise_on_missing @cache = cache @cache_expires_in = cache_expires_in + @cache_tag = cache_tag end def cache? @@ -207,12 +213,30 @@ def etag "W/#{ActiveSupport::Digest.hexdigest(cache_key_with_version.to_s)}" end + def resource_cache_tag + return unless @cache_tag.present? && @resource.respond_to?(@cache_tag) + + @resource.try(@cache_tag) + end + def cache_key - ActiveSupport::Cache.expand_cache_key([@scope.cache_key, @query.cache_key]) + ActiveSupport::Cache.expand_cache_key( + [ + @scope.cache_key, + @query.cache_key, + resource_cache_tag, + ].compact_blank + ) end def cache_key_with_version - ActiveSupport::Cache.expand_cache_key([@scope.cache_key_with_version, @query.cache_key]) + ActiveSupport::Cache.expand_cache_key( + [ + @scope.cache_key_with_version, + @query.cache_key, + resource_cache_tag, + ].compact_blank + ) end private diff --git a/lib/graphiti/runner.rb b/lib/graphiti/runner.rb index d2b9d070..bce0c26b 100644 --- a/lib/graphiti/runner.rb +++ b/lib/graphiti/runner.rb @@ -58,22 +58,29 @@ def jsonapi_render_options def proxy(base = nil, opts = {}) base ||= jsonapi_resource.base_scope - scope_opts = opts.slice :sideload_parent_length, + scope_opts = opts.slice( + :sideload_parent_length, :default_paginate, :after_resolve, :sideload, :parent, :params, :bypass_required_filters + ) + scope = jsonapi_scope(base, scope_opts) - ResourceProxy.new jsonapi_resource, + + ::Graphiti::ResourceProxy.new( + jsonapi_resource, scope, query, payload: deserialized_payload, single: opts[:single], raise_on_missing: opts[:raise_on_missing], cache: opts[:cache], - cache_expires_in: opts[:cache_expires_in] + cache_expires_in: opts[:cache_expires_in], + cache_tag: opts[:cache_tag] + ) end end end diff --git a/lib/graphiti/util/cache_debug.rb b/lib/graphiti/util/cache_debug.rb index cc7c3783..bedbd2e1 100644 --- a/lib/graphiti/util/cache_debug.rb +++ b/lib/graphiti/util/cache_debug.rb @@ -12,7 +12,9 @@ def last_version end def name - "#{Graphiti.context[:object]&.request&.method} #{Graphiti.context[:object]&.request&.url}" + tag = proxy.resource_cache_tag + + "#{::Graphiti.context[:object]&.request&.method} #{::Graphiti.context[:object]&.request&.url} #{tag}" end def key diff --git a/spec/resource_proxy_spec.rb b/spec/resource_proxy_spec.rb index ee87e37c..44ca666f 100644 --- a/spec/resource_proxy_spec.rb +++ b/spec/resource_proxy_spec.rb @@ -14,13 +14,20 @@ let(:query) { double(cache_key: "query-hash") } let(:scope) { double(cache_key: "scope-hash", cache_key_with_version: "scope-hash-123456") } - subject { described_class.new(resource, scope, query, **{}) } + subject { described_class.new(resource, scope, query, **{ cache_tag: :cache_tag }) } - it "cache_key combines query and scope cache keys" do + it "cache_key combines query and scope cache keys if no tags are set" do cache_key = subject.cache_key expect(cache_key).to eq("scope-hash/query-hash") end + it "cache_key combines query, scope and tag cache keys if a tag is set" do + allow(resource).to receive(:cache_tag).and_return("tag_value") + + cache_key = subject.cache_key + expect(cache_key).to eq("scope-hash/query-hash/tag_value") + end + it "generates stable etag" do instance1 = described_class.new(resource, scope, query, **{}) instance2 = described_class.new(resource, scope, query, **{}) From 34736eeb8c2cba0f5e8b82701542b8f9d4bf61f6 Mon Sep 17 00:00:00 2001 From: Andrei Mochalov Date: Tue, 29 Jul 2025 20:21:03 +0400 Subject: [PATCH 2/2] refactor: make linter happy --- lib/graphiti/resource/interface.rb | 2 +- lib/graphiti/resource_proxy.rb | 4 ++-- spec/resource_proxy_spec.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/graphiti/resource/interface.rb b/lib/graphiti/resource/interface.rb index 6315043f..46f10773 100644 --- a/lib/graphiti/resource/interface.rb +++ b/lib/graphiti/resource/interface.rb @@ -56,7 +56,7 @@ def build(params, base_scope = nil) private def caching_options - { cache: @cache_resource, cache_expires_in: @cache_expires_in, cache_tag: @cache_tag } + {cache: @cache_resource, cache_expires_in: @cache_expires_in, cache_tag: @cache_tag} end def validate_request!(params) diff --git a/lib/graphiti/resource_proxy.rb b/lib/graphiti/resource_proxy.rb index 059a8117..37d469c2 100644 --- a/lib/graphiti/resource_proxy.rb +++ b/lib/graphiti/resource_proxy.rb @@ -224,7 +224,7 @@ def cache_key [ @scope.cache_key, @query.cache_key, - resource_cache_tag, + resource_cache_tag ].compact_blank ) end @@ -234,7 +234,7 @@ def cache_key_with_version [ @scope.cache_key_with_version, @query.cache_key, - resource_cache_tag, + resource_cache_tag ].compact_blank ) end diff --git a/spec/resource_proxy_spec.rb b/spec/resource_proxy_spec.rb index 44ca666f..5917bef5 100644 --- a/spec/resource_proxy_spec.rb +++ b/spec/resource_proxy_spec.rb @@ -14,7 +14,7 @@ let(:query) { double(cache_key: "query-hash") } let(:scope) { double(cache_key: "scope-hash", cache_key_with_version: "scope-hash-123456") } - subject { described_class.new(resource, scope, query, **{ cache_tag: :cache_tag }) } + subject { described_class.new(resource, scope, query, **{cache_tag: :cache_tag}) } it "cache_key combines query and scope cache keys if no tags are set" do cache_key = subject.cache_key