From aee78ca40e79ad952cae48f48bd39ba77a427c6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Illi-Zuberb=C3=BChler?= Date: Tue, 27 Feb 2024 18:35:01 +0100 Subject: [PATCH 01/46] Fix private call (#446) This should make method `relationship_option` private instead `relationship_option`. Co-authored-by: Jeff Keen --- lib/graphiti/resource/dsl.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/graphiti/resource/dsl.rb b/lib/graphiti/resource/dsl.rb index f33be74c..8d073b8f 100644 --- a/lib/graphiti/resource/dsl.rb +++ b/lib/graphiti/resource/dsl.rb @@ -205,7 +205,7 @@ def relationship_option(options, name) options[name] ||= send(:"relationships_#{name}_by_default") end end - private :attribute_option + private :relationship_option end end end From 91c42101071145494249d31b77a6fc0944805465 Mon Sep 17 00:00:00 2001 From: Leszek Kalwa Date: Tue, 27 Feb 2024 19:13:13 +0100 Subject: [PATCH 02/46] fix: resolve inconsistency for filters containing curly brackets (#452) --- lib/graphiti/scoping/filter.rb | 6 +++--- spec/filtering_spec.rb | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/graphiti/scoping/filter.rb b/lib/graphiti/scoping/filter.rb index 9ab425f1..9d75feef 100644 --- a/lib/graphiti/scoping/filter.rb +++ b/lib/graphiti/scoping/filter.rb @@ -193,14 +193,14 @@ def parse_string_arrays(value, singular_filter) # Find the quoted strings quotes = value.scan(/{{.*?}}/) # remove them from the rest - quotes.each { |q| value.gsub!(q, "") } + non_quotes = quotes.inject(value) { |v, q| v.gsub(q, "") } # remove the quote characters from the quoted strings quotes.each { |q| q.gsub!("{{", "").gsub!("}}", "") } # merge everything back together into an array value = if singular_filter - Array(value) + quotes + Array(non_quotes) + quotes else - Array(value.split(",")) + quotes + Array(non_quotes.split(",")) + quotes end # remove any blanks that are left value.reject! { |v| v.length.zero? } diff --git a/spec/filtering_spec.rb b/spec/filtering_spec.rb index a47f2619..613aa947 100644 --- a/spec/filtering_spec.rb +++ b/spec/filtering_spec.rb @@ -29,6 +29,20 @@ def self.name expect(records.map(&:id)).to eq([employee1.id]) end + context "retains filtering value" do + it "when value includes curly brackets" do + params[:filter] = {first_name: "{{John}}"} + records + expect(params[:filter]).to eq(first_name: "{{John}}") + end + + it "when value does not include curly brackets" do + params[:filter] = {first_name: "John"} + records + expect(params[:filter]).to eq(first_name: "John") + end + end + context "when filter is type hash" do before do resource.filter :by_json, :hash do From d3e09c2d246ae7f84793a7018562f415235d9d9c Mon Sep 17 00:00:00 2001 From: Pedro Chambino Date: Tue, 27 Feb 2024 19:10:16 +0000 Subject: [PATCH 03/46] fix: Re-use resource class for remote sideloads to avoid memory leak (#421) --- lib/graphiti.rb | 2 +- lib/graphiti/query.rb | 9 ++++++--- spec/query_spec.rb | 25 ++++++++++++++++++++++--- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/lib/graphiti.rb b/lib/graphiti.rb index 0b545924..78904e94 100644 --- a/lib/graphiti.rb +++ b/lib/graphiti.rb @@ -137,7 +137,6 @@ def self.setup! require "graphiti/request_validator" require "graphiti/request_validators/validator" require "graphiti/request_validators/update_validator" -require "graphiti/query" require "graphiti/scope" require "graphiti/deserializer" require "graphiti/renderer" @@ -176,6 +175,7 @@ def self.setup! require "graphiti/extensions/boolean_attribute" require "graphiti/extensions/temp_id" require "graphiti/serializer" +require "graphiti/query" require "graphiti/debugger" if defined?(ActiveRecord) diff --git a/lib/graphiti/query.rb b/lib/graphiti/query.rb index e314e32a..a336a7d3 100644 --- a/lib/graphiti/query.rb +++ b/lib/graphiti/query.rb @@ -76,11 +76,14 @@ def sideload_hash end end + class RemoteSideloadResource < ::Graphiti::Resource + self.remote = "_remote_sideload_".freeze + self.abstract_class = true # exclude from schema + end + def resource_for_sideload(sideload) if @resource.remote? - Class.new(Graphiti::Resource) { - self.remote = "_remote_sideload_" - }.new + RemoteSideloadResource.new else sideload.resource end diff --git a/spec/query_spec.rb b/spec/query_spec.rb index a2e3bfe2..1d72bc51 100644 --- a/spec/query_spec.rb +++ b/spec/query_spec.rb @@ -11,6 +11,7 @@ before do employee_resource.has_many :positions, resource: position_resource + employee_resource.belongs_to :remote, remote: true position_resource.belongs_to :department, resource: department_resource employee_resource.attribute :name, :string @@ -986,11 +987,29 @@ end describe "sideloads" do - before { params[:include] = "positions" } subject(:sideloads) { instance.sideloads } - it "does not cascate the action" do - expect(sideloads.values.map(&:action)).to eq([:all]) + context "when including an has_many resource" do + before { params[:include] = "positions" } + + it "does not cascate the action" do + expect(sideloads.values.map(&:action)).to eq([:all]) + end + end + + context "when including a resource from a remote resource" do + before { params[:include] = "remote.resource" } + + let(:sideloads_of_another_query) { described_class.new(resource, params).sideloads } + + def resource_class_of_remote_sideload(sideloads) + sideloads.fetch(:remote).sideloads.fetch(:resource).resource.class + end + + it "re-uses resource class across multiple queries (avoid memory leak)" do + expect(resource_class_of_remote_sideload(sideloads)) + .to eq(resource_class_of_remote_sideload(sideloads_of_another_query)) + end end end end From c94cd33e7e065047a9db38942a6787b812d8f56a Mon Sep 17 00:00:00 2001 From: Pedro Chambino Date: Tue, 27 Feb 2024 20:11:25 +0000 Subject: [PATCH 04/46] fix: sort types in generated schema (#407) Otherwise, added types order may depend on file load order which is not always deterministic if the files don't depend on each other. --- lib/graphiti/schema.rb | 2 +- spec/schema_spec.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/graphiti/schema.rb b/lib/graphiti/schema.rb index 835cfb57..7b08b2ad 100644 --- a/lib/graphiti/schema.rb +++ b/lib/graphiti/schema.rb @@ -42,7 +42,7 @@ def generate def generate_types {}.tap do |types| - Graphiti::Types.map.each_pair do |name, config| + Graphiti::Types.map.sort.each_entry do |name, config| types[name] = config.slice(:kind, :description) end end diff --git a/spec/schema_spec.rb b/spec/schema_spec.rb index a27fc1cd..9d87778c 100644 --- a/spec/schema_spec.rb +++ b/spec/schema_spec.rb @@ -244,6 +244,10 @@ def self.name expect(schema[:types]).to eq(expected[:types]) end + it "has sorted types" do + expect(schema[:types].to_a).to eq(expected[:types].sort) + end + # Dynamically-created resources, e.g. remote resources context "when resource has missing name" do let(:no_name) do From 9de191cfdb3ac03a733eecfe711883b113814fbc Mon Sep 17 00:00:00 2001 From: Jeff Keen Date: Tue, 27 Feb 2024 15:16:20 -0600 Subject: [PATCH 05/46] fix: check for url presence before trying to append to it (#464) --- lib/graphiti/util/link.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/graphiti/util/link.rb b/lib/graphiti/util/link.rb index 49f8332a..9390a2eb 100644 --- a/lib/graphiti/util/link.rb +++ b/lib/graphiti/util/link.rb @@ -48,6 +48,7 @@ def raw_url def on_demand_links(url) return url unless Graphiti.config.links_on_demand + return unless url url << if url.include?("?") "&links=true" From 689aac7d997a42b2129837d270218026ebb31562 Mon Sep 17 00:00:00 2001 From: Jeff Keen Date: Sun, 17 Mar 2024 15:54:46 -0500 Subject: [PATCH 06/46] Bump version and manually create a 1.4.0 release --- CHANGELOG.md | 16 +++++++++++++++- lib/graphiti/version.rb | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce8872f..ceae6933 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,18 @@ -## Unreleased +## 1.4.0, Sun March 17th 2024 +Features: +- [461](https://github.com/graphiti-api/graphiti/pull/461), [463](https://github.com/graphiti-api/graphiti/pull/463) Add support for Rails 7.1 + Ruby 3.2 + Ruby 3.3 + +Fixes: +- [464](https://github.com/graphiti-api/graphiti/pull/464) Check for url presence before trying to append +- [407](https://github.com/graphiti-api/graphiti/pull/407) Sort types in generated schema +- [421](https://github.com/graphiti-api/graphiti/pull/421) Re-use resource class for remote sideloads to avoid memory leak +- [452](https://github.com/graphiti-api/graphiti/pull/452) Resolve inconsistency for filters containing curly brackets +- [446](https://github.com/graphiti-api/graphiti/pull/446) Fix private call + +## 1.3.9, May 25th 2022 +Use an options hash for log subscriber instead of positional arguments + +## 1.x ?? Features: - [329](https://github.com/graphiti-api/graphiti/pull/329) Propagate `extra_fields` to related resource links. diff --git a/lib/graphiti/version.rb b/lib/graphiti/version.rb index 04bbe7cc..15fc4271 100644 --- a/lib/graphiti/version.rb +++ b/lib/graphiti/version.rb @@ -1,3 +1,3 @@ module Graphiti - VERSION = "1.3.9" + VERSION = "1.4.0" end From f68b61ff09ec61ecf23acc5bc37d0accba14aeed Mon Sep 17 00:00:00 2001 From: Johan van Zonneveld Date: Mon, 18 Mar 2024 13:46:56 +0100 Subject: [PATCH 07/46] feat: add before_sideload hook (#371) * Add before_sideload hook to allow restoration of global state in sideload threads, .e.g. setting current tenant for sideloads Co-authored-by: Jeff Keen --- lib/graphiti/configuration.rb | 1 + lib/graphiti/scope.rb | 1 + spec/scope_spec.rb | 10 ++++++++++ 3 files changed, 12 insertions(+) diff --git a/lib/graphiti/configuration.rb b/lib/graphiti/configuration.rb index 59f2ed15..be7b4f19 100644 --- a/lib/graphiti/configuration.rb +++ b/lib/graphiti/configuration.rb @@ -15,6 +15,7 @@ class Configuration attr_accessor :pagination_links attr_accessor :typecast_reads attr_accessor :raise_on_missing_sidepost + attr_accessor :before_sideload attr_reader :debug, :debug_models diff --git a/lib/graphiti/scope.rb b/lib/graphiti/scope.rb index aa45289a..af1f6ed0 100644 --- a/lib/graphiti/scope.rb +++ b/lib/graphiti/scope.rb @@ -43,6 +43,7 @@ def resolve_sideloads(results) parent_resource = @resource graphiti_context = Graphiti.context resolve_sideload = -> { + Graphiti.config.before_sideload&.call(graphiti_context) Graphiti.context = graphiti_context sideload.resolve(results, q, parent_resource) @resource.adapter.close if concurrent diff --git a/spec/scope_spec.rb b/spec/scope_spec.rb index 8e42f4cd..2ae9dd75 100644 --- a/spec/scope_spec.rb +++ b/spec/scope_spec.rb @@ -94,7 +94,10 @@ end context "with concurrency" do + let(:before_sideload) { double("BeforeSideload", call: nil) } + before { allow(Graphiti.config).to receive(:concurrency).and_return(true) } + before { allow(Graphiti.config).to receive(:before_sideload).and_return(before_sideload) } it "closes db connections" do allow(sideload).to receive(:resolve).and_return(sideload) @@ -102,6 +105,13 @@ expect(resource.adapter).to receive(:close) instance.resolve_sideloads(results) end + + it "calls configiration.before_sideload with context" do + Graphiti.context[:tenant_id] = 1 + allow(sideload).to receive(:resolve).and_return(sideload) + expect(before_sideload).to receive(:call).with(hash_including(tenant_id: 1)) + instance.resolve_sideloads(results) + end end context "without concurrency" do From 3f8b4a9eba1c4a5a5b8e4670d91c2e5ad00d32cc Mon Sep 17 00:00:00 2001 From: Jeff Keen Date: Mon, 18 Mar 2024 07:54:53 -0500 Subject: [PATCH 08/46] chore: Add semantic release to automatically cut releases, generate changelogs, and publish a new gem version after tests pass (#466) --- .github/workflows/release.yml | 36 +++++++++++ .gitignore | 1 + package.json | 112 ++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 package.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..74be4195 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,36 @@ +--- +name: Generate New Release + +on: + workflow_dispatch: + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + persist-credentials: false + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + - name: Build + run: | + gem install bundler + git submodule update --init --recursive + bundle install --jobs 4 --retry 3 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 14 + - name: Install Dependencies + run: yarn install --frozen-lockfile + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GEM_HOST_API_KEY: ${{ secrets.GEM_HOST_API_KEY }} + run: node_modules/.bin/semantic-release diff --git a/.gitignore b/.gitignore index 2e7c7f9a..92d55345 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ /spec/reports/ /gemfiles/*.lock /tmp/ +node_modules .byebug_history spec/.rspec-examples spec/dummy/log/* diff --git a/package.json b/package.json new file mode 100644 index 00000000..6eab3817 --- /dev/null +++ b/package.json @@ -0,0 +1,112 @@ +{ + "name": "graphiti", + "version": "1.4.0", + "repository": { + "type": "git", + "url": "git+https://github.com/jkeen/graphiti.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/graphiti-api/graphiti/issues" + }, + "homepage": "https://graphiti.dev", + "scripts": { + "semantic-release": "semantic-release" + }, + "devDependencies": { + "semantic-release-rubygem": "^1.2.0", + "semantic-release": "^19.0.3", + "@semantic-release/changelog": "^6.0.1", + "@semantic-release/git": "^10.0.1" + }, + "release": { + "branches": [ + "master", + "main", + { + "name": "beta", + "prerelease": true + }, + { + "name": "alpha", + "prerelease": true + } + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "releaseRules": [ + { + "type": "*!", + "release": "major" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "build", + "release": "patch" + }, + { + "type": "ci", + "release": "patch" + }, + { + "type": "chore", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "refactor", + "release": "patch" + }, + { + "type": "style", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + } + ], + "parserOpts": { + "noteKeywords": [ + "BREAKING CHANGE", + "BREAKING CHANGES", + "BREAKING", + "BREAKING CHANGE!", + "BREAKING CHANGES!", + "BREAKING!" + ] + } + } + ], + "@semantic-release/release-notes-generator", + [ + "@semantic-release/changelog", + { + "changelogTitle": "graphiti changelog", + "changelogFile": "CHANGELOG.md" + } + ], + "@semantic-release/npm", + "@semantic-release/github", + [ + "@semantic-release/git", + { + "assets": [ + "CHANGELOG.md" + ], + "message": "${nextRelease.version} CHANGELOG [skip ci]\n\n${nextRelease.notes}" + } + ] + ], + "debug": true, + "dryRun": false + } +} From 42a0a4e41f1cd752b5ef0ae541d5143b2a45cf28 Mon Sep 17 00:00:00 2001 From: Jeff Keen Date: Mon, 18 Mar 2024 08:02:06 -0500 Subject: [PATCH 09/46] add publish step to CI, correct error in release.yml [skip ci] --- .github/workflows/ci.yml | 11 +++++++++++ .github/workflows/release.yml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af4f0e60..e0cf57c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,3 +101,14 @@ jobs: bundler-cache: true - name: Run tests run: bundle exec rspec + publish: + name: Release + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/master' + needs: [test] + steps: + - name: Dispatch Release + uses: benc-uk/workflow-dispatch@v1 + with: + workflow: Generate New Release + token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 74be4195..944ffe9e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: ${{ matrix.ruby }} + ruby-version: "3.1" - name: Build run: | gem install bundler From c269283c2ca47d0d1f71b65613f6bc3d585f26f0 Mon Sep 17 00:00:00 2001 From: Jeff Keen Date: Mon, 18 Mar 2024 08:07:16 -0500 Subject: [PATCH 10/46] Correct release branch name [skip ci] --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 6eab3817..cf3e2803 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.4.0", "repository": { "type": "git", - "url": "git+https://github.com/jkeen/graphiti.git" + "url": "git+https://github.com/graphiti-api/graphiti.git" }, "license": "MIT", "bugs": { @@ -22,7 +22,6 @@ "release": { "branches": [ "master", - "main", { "name": "beta", "prerelease": true From efdef21c6fc49ce3bda963b52ce85a134498242b Mon Sep 17 00:00:00 2001 From: Jeff Keen Date: Mon, 18 Mar 2024 08:21:00 -0500 Subject: [PATCH 11/46] Remove incorrect semantic-release package [skip ci] --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index cf3e2803..cf0ce740 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,6 @@ "changelogFile": "CHANGELOG.md" } ], - "@semantic-release/npm", "@semantic-release/github", [ "@semantic-release/git", From 2c722653a37ccea60bcdaa265bc5882aea9a22b1 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 18 Mar 2024 13:21:58 +0000 Subject: [PATCH 12/46] 1.5.0 CHANGELOG [skip ci] # [1.5.0](https://github.com/graphiti-api/graphiti/compare/v1.4.0...v1.5.0) (2024-03-18) ### Features * add before_sideload hook ([#371](https://github.com/graphiti-api/graphiti/issues/371)) ([f68b61f](https://github.com/graphiti-api/graphiti/commit/f68b61ff09ec61ecf23acc5bc37d0accba14aeed)) --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceae6933..f9d56169 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +graphiti changelog + +# [1.5.0](https://github.com/graphiti-api/graphiti/compare/v1.4.0...v1.5.0) (2024-03-18) + + +### Features + +* add before_sideload hook ([#371](https://github.com/graphiti-api/graphiti/issues/371)) ([f68b61f](https://github.com/graphiti-api/graphiti/commit/f68b61ff09ec61ecf23acc5bc37d0accba14aeed)) + ## 1.4.0, Sun March 17th 2024 Features: - [461](https://github.com/graphiti-api/graphiti/pull/461), [463](https://github.com/graphiti-api/graphiti/pull/463) Add support for Rails 7.1 + Ruby 3.2 + Ruby 3.3 From 629757a9c52c509cbd40b1ba7f66b49f8115c1d8 Mon Sep 17 00:00:00 2001 From: Jeff Keen Date: Mon, 18 Mar 2024 08:24:17 -0500 Subject: [PATCH 13/46] Bump version and release [skip ci] --- lib/graphiti/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/graphiti/version.rb b/lib/graphiti/version.rb index 15fc4271..95347d14 100644 --- a/lib/graphiti/version.rb +++ b/lib/graphiti/version.rb @@ -1,3 +1,3 @@ module Graphiti - VERSION = "1.4.0" + VERSION = "1.5.0" end From 3a29ea109a7b5fb7d046d9fc3cd189d9b3e04dd2 Mon Sep 17 00:00:00 2001 From: Jeff Keen Date: Mon, 18 Mar 2024 08:28:38 -0500 Subject: [PATCH 14/46] Add semantic-release rubygem to release pipeline [skip ci] --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index cf0ce740..4ea9bfe3 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "changelogFile": "CHANGELOG.md" } ], + "semantic-release-rubygem", "@semantic-release/github", [ "@semantic-release/git", From 4e587021265323bd0b170b57e9c7aecaa7f826d7 Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Mon, 18 Mar 2024 09:34:35 -0400 Subject: [PATCH 15/46] fix: polymorphic `on` expects a symbol (#433) Fix polymorphic type mismatch in polymorphic_belongs_to by ensuring symbol conversion --- lib/graphiti/sideload/polymorphic_belongs_to.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/graphiti/sideload/polymorphic_belongs_to.rb b/lib/graphiti/sideload/polymorphic_belongs_to.rb index 141ad5e1..40946a6e 100644 --- a/lib/graphiti/sideload/polymorphic_belongs_to.rb +++ b/lib/graphiti/sideload/polymorphic_belongs_to.rb @@ -41,7 +41,7 @@ def ignore?(group_name) end def on(name, &blk) - group = Group.new(name) + group = Group.new(name.to_sym) @groups << group group end From 0b2d9ffb003f465ac5c7ac3beffece86eb06797c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 18 Mar 2024 13:40:58 +0000 Subject: [PATCH 16/46] 1.5.1 CHANGELOG [skip ci] ## [1.5.1](https://github.com/graphiti-api/graphiti/compare/v1.5.0...v1.5.1) (2024-03-18) ### Bug Fixes * polymorphic `on` expects a symbol ([#433](https://github.com/graphiti-api/graphiti/issues/433)) ([4e58702](https://github.com/graphiti-api/graphiti/commit/4e587021265323bd0b170b57e9c7aecaa7f826d7)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9d56169..b3c4be8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ graphiti changelog +## [1.5.1](https://github.com/graphiti-api/graphiti/compare/v1.5.0...v1.5.1) (2024-03-18) + + +### Bug Fixes + +* polymorphic `on` expects a symbol ([#433](https://github.com/graphiti-api/graphiti/issues/433)) ([4e58702](https://github.com/graphiti-api/graphiti/commit/4e587021265323bd0b170b57e9c7aecaa7f826d7)) + # [1.5.0](https://github.com/graphiti-api/graphiti/compare/v1.4.0...v1.5.0) (2024-03-18) From 56d34fd4801bc32c13d64aca880b82b717b2ab81 Mon Sep 17 00:00:00 2001 From: Jason Karns Date: Mon, 18 Mar 2024 11:26:50 -0400 Subject: [PATCH 17/46] fix: Enum should allow the conventionally case-sensitive operators (#434) Add eql and not_eql operators to enum for case-sensitive matching, aligning with string behavior and improving polymorphic association ergonomics --------- Co-authored-by: Jeff Keen --- lib/graphiti/adapters/abstract.rb | 2 +- lib/graphiti/adapters/active_record.rb | 2 ++ spec/filtering_spec.rb | 27 ++++++++++++++++++++++++++ spec/fixtures/poro.rb | 13 +++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/graphiti/adapters/abstract.rb b/lib/graphiti/adapters/abstract.rb index 92f789cf..8b3dfb96 100644 --- a/lib/graphiti/adapters/abstract.rb +++ b/lib/graphiti/adapters/abstract.rb @@ -39,7 +39,7 @@ def self.default_operators :not_match ], uuid: [:eq, :not_eq], - enum: [:eq, :not_eq], + enum: [:eq, :not_eq, :eql, :not_eql], integer_id: numerical_operators, integer: numerical_operators, big_decimal: numerical_operators, diff --git a/lib/graphiti/adapters/active_record.rb b/lib/graphiti/adapters/active_record.rb index 3cedfae6..c290f92c 100644 --- a/lib/graphiti/adapters/active_record.rb +++ b/lib/graphiti/adapters/active_record.rb @@ -26,6 +26,7 @@ def filter_eq(scope, attribute, value) alias_method :filter_boolean_eq, :filter_eq alias_method :filter_uuid_eq, :filter_eq alias_method :filter_enum_eq, :filter_eq + alias_method :filter_enum_eql, :filter_eq def filter_not_eq(scope, attribute, value) scope.where.not(attribute => value) @@ -37,6 +38,7 @@ def filter_not_eq(scope, attribute, value) alias_method :filter_boolean_not_eq, :filter_not_eq alias_method :filter_uuid_not_eq, :filter_not_eq alias_method :filter_enum_not_eq, :filter_not_eq + alias_method :filter_enum_not_eql, :filter_not_eq def filter_string_eq(scope, attribute, value, is_not: false) column = column_for(scope, attribute) diff --git a/spec/filtering_spec.rb b/spec/filtering_spec.rb index 613aa947..d206ed22 100644 --- a/spec/filtering_spec.rb +++ b/spec/filtering_spec.rb @@ -580,6 +580,33 @@ def self.name end end + context "when filtering on an string_enum field" do + before do + resource.config[:filters] = {} + resource.filter :first_name, :string_enum, single: true, allow: ["William", "Harold"] do + eq do |scope, value| + scope[:conditions][:first_name] = value + scope + end + end + end + + it "accepts values in the allowlist with eq operator" do + params[:filter] = {first_name: {eq: "William"}} + expect(records.map(&:id)).to eq([employee3.id]) + end + + it "accepts values in the allowlist with eql operator" do + params[:filter] = {first_name: {eql: "Harold"}} + expect(records.map(&:id)).to eq([employee4.id]) + end + + it "accepts values in the allowlist with not_eql operator" do + params[:filter] = {first_name: {not_eql: "Harold"}} + expect(records.map(&:id)).to eq([employee1.id, employee2.id, employee3.id]) + end + end + context "when only allowing single values" do before do resource.filter :first_name, :string, single: true do diff --git a/spec/fixtures/poro.rb b/spec/fixtures/poro.rb index 31b0d842..f7d0d64e 100644 --- a/spec/fixtures/poro.rb +++ b/spec/fixtures/poro.rb @@ -74,6 +74,8 @@ def apply_filtering(records, params) end if value.is_a?(Array) value.include?(db_value) + elsif value.is_a?(Hash) && value[:not] + db_value != value[:not] else db_value == value end @@ -305,6 +307,13 @@ def filter(scope, name, value) scope[:conditions][name] = value scope end + + def filter_not_eq(scope, name, value) + scope[:conditions] ||= {} + scope[:conditions][name] = {not: value} + scope + end + alias_method :filter_integer_eq, :filter alias_method :filter_string_eq, :filter alias_method :filter_big_decimal_eq, :filter @@ -314,6 +323,10 @@ def filter(scope, name, value) alias_method :filter_boolean_eq, :filter alias_method :filter_hash_eq, :filter alias_method :filter_array_eq, :filter + alias_method :filter_enum_eq, :filter + alias_method :filter_enum_not_eq, :filter_not_eq + alias_method :filter_enum_eql, :filter + alias_method :filter_enum_not_eql, :filter_not_eq # No need for actual logic to fire def count(scope, attr) From c79b4dc73077a894fbf133e7642f74a13cf5f462 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 18 Mar 2024 15:28:54 +0000 Subject: [PATCH 18/46] 1.5.2 CHANGELOG [skip ci] ## [1.5.2](https://github.com/graphiti-api/graphiti/compare/v1.5.1...v1.5.2) (2024-03-18) ### Bug Fixes * Enum should allow the conventionally case-sensitive operators ([#434](https://github.com/graphiti-api/graphiti/issues/434)) ([56d34fd](https://github.com/graphiti-api/graphiti/commit/56d34fd4801bc32c13d64aca880b82b717b2ab81)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3c4be8f..eba79f98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ graphiti changelog +## [1.5.2](https://github.com/graphiti-api/graphiti/compare/v1.5.1...v1.5.2) (2024-03-18) + + +### Bug Fixes + +* Enum should allow the conventionally case-sensitive operators ([#434](https://github.com/graphiti-api/graphiti/issues/434)) ([56d34fd](https://github.com/graphiti-api/graphiti/commit/56d34fd4801bc32c13d64aca880b82b717b2ab81)) + ## [1.5.1](https://github.com/graphiti-api/graphiti/compare/v1.5.0...v1.5.1) (2024-03-18) From e1dd811283f6e6fe7a36b925934df0ecbb4d3411 Mon Sep 17 00:00:00 2001 From: doublevoid Date: Mon, 18 Mar 2024 14:04:45 -0300 Subject: [PATCH 19/46] fix: prevent :id stripping when :id not in path (#447) Address edge case where ResourceName.find({id: id}) would error if controller was set up as a show action --- lib/graphiti/resource/links.rb | 2 +- spec/resource_spec.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/graphiti/resource/links.rb b/lib/graphiti/resource/links.rb index 627c7560..82a91c77 100644 --- a/lib/graphiti/resource/links.rb +++ b/lib/graphiti/resource/links.rb @@ -76,7 +76,7 @@ def allow_request?(request_path, params, action) path = request_path if [:update, :show, :destroy].include?(context_namespace) && has_id path = request_path.split("/") - path.pop + path.pop if path.last == has_id.to_s path = path.join("/") end e[:full_path].to_s == path && e[:actions].include?(context_namespace) diff --git a/spec/resource_spec.rb b/spec/resource_spec.rb index db1e9e4c..fae3bc21 100644 --- a/spec/resource_spec.rb +++ b/spec/resource_spec.rb @@ -1225,6 +1225,19 @@ def self.name end end end + + # singular resource with singular route + context "that is a singular show route without an id but finding the resource by id" do + before do + request.env["PATH_INFO"] += "" + end + + it "works" do + Graphiti.with_context ctx, :show do + expect { klass.find(id: 123) }.to_not raise_error + end + end + end end context "and the request matches a secondary endpoint" do From ae09a464b2156742bb093537deac0578a1a3e40e Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 18 Mar 2024 13:05:07 -0400 Subject: [PATCH 20/46] fix: leverage ruby-2.7 parameter forwarding (#431) --- .standard.yml | 2 +- graphiti.gemspec | 2 +- lib/graphiti/serializer.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.standard.yml b/.standard.yml index 8f2de2ad..39471890 100644 --- a/.standard.yml +++ b/.standard.yml @@ -1,6 +1,6 @@ fix: true parallel: true -ruby_version: 2.4 +ruby_version: 2.7 ignore: - 'vendor/**/*' diff --git a/graphiti.gemspec b/graphiti.gemspec index 40f9589a..1c7e67ef 100644 --- a/graphiti.gemspec +++ b/graphiti.gemspec @@ -16,7 +16,7 @@ Gem::Specification.new do |spec| spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.required_ruby_version = ">= 2.6" + spec.required_ruby_version = ">= 2.7" spec.add_dependency "jsonapi-serializable", "~> 0.3.0" spec.add_dependency "jsonapi-renderer", "~> 0.2", ">= 0.2.2" diff --git a/lib/graphiti/serializer.rb b/lib/graphiti/serializer.rb index 6465bd9e..706dbdca 100644 --- a/lib/graphiti/serializer.rb +++ b/lib/graphiti/serializer.rb @@ -71,9 +71,9 @@ def as_jsonapi(kwargs = {}) end # Allow access to resource methods - def method_missing(id, *args, &blk) + def method_missing(id, ...) if @resource.respond_to?(id, true) - @resource.send(id, *args, &blk) + @resource.send(id, ...) else super end From 18519b6b83068f1ed36c228e02d1997fbefbdda2 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 18 Mar 2024 17:07:27 +0000 Subject: [PATCH 21/46] 1.5.3 CHANGELOG [skip ci] ## [1.5.3](https://github.com/graphiti-api/graphiti/compare/v1.5.2...v1.5.3) (2024-03-18) ### Bug Fixes * leverage ruby-2.7 parameter forwarding ([#431](https://github.com/graphiti-api/graphiti/issues/431)) ([ae09a46](https://github.com/graphiti-api/graphiti/commit/ae09a464b2156742bb093537deac0578a1a3e40e)) * prevent :id stripping when :id not in path ([#447](https://github.com/graphiti-api/graphiti/issues/447)) ([e1dd811](https://github.com/graphiti-api/graphiti/commit/e1dd811283f6e6fe7a36b925934df0ecbb4d3411)) --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eba79f98..f15b57ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ graphiti changelog +## [1.5.3](https://github.com/graphiti-api/graphiti/compare/v1.5.2...v1.5.3) (2024-03-18) + + +### Bug Fixes + +* leverage ruby-2.7 parameter forwarding ([#431](https://github.com/graphiti-api/graphiti/issues/431)) ([ae09a46](https://github.com/graphiti-api/graphiti/commit/ae09a464b2156742bb093537deac0578a1a3e40e)) +* prevent :id stripping when :id not in path ([#447](https://github.com/graphiti-api/graphiti/issues/447)) ([e1dd811](https://github.com/graphiti-api/graphiti/commit/e1dd811283f6e6fe7a36b925934df0ecbb4d3411)) + ## [1.5.2](https://github.com/graphiti-api/graphiti/compare/v1.5.1...v1.5.2) (2024-03-18) From 697d76172adec24cd7e7522300c8335233fdcc36 Mon Sep 17 00:00:00 2001 From: Matt Fenelon Date: Wed, 20 Mar 2024 17:45:35 +0000 Subject: [PATCH 22/46] feat: add thread pool and concurrency_max_threads configuration option (#470) This option allows to limit the maximum number of resources that can be sideloaded concurrently. With a properly configured connection pool, this ensures that the activerecord's connection pool is not exhausted by the sideloading process. --- lib/graphiti/configuration.rb | 15 +++++++++++++++ lib/graphiti/scope.rb | 19 ++++++++++++++++++- spec/configuration_spec.rb | 15 +++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/lib/graphiti/configuration.rb b/lib/graphiti/configuration.rb index be7b4f19..d1a69098 100644 --- a/lib/graphiti/configuration.rb +++ b/lib/graphiti/configuration.rb @@ -8,6 +8,20 @@ class Configuration # Defaults to false OR if classes are cached (Rails-only) attr_accessor :concurrency + # This number must be considered in accordance with the database + # connection pool size configured in `database.yml`. The connection + # pool should be large enough to accommodate both the foreground + # threads (ie. web server or job worker threads) and background + # threads. For each process, Graphiti will create one global + # executor that uses this many threads to sideload resources + # asynchronously. Thus, the pool size should be at least + # `thread_count + concurrency_max_threads + 1`. For example, if your + # web server has a maximum of 3 threads, and + # `concurrency_max_threads` is set to 4, then your pool size should + # be at least 8. + # @return [Integer] Maximum number of threads to use when fetching sideloads concurrently + attr_accessor :concurrency_max_threads + attr_accessor :respond_to attr_accessor :context_for_endpoint attr_accessor :links_on_demand @@ -26,6 +40,7 @@ class Configuration def initialize @raise_on_missing_sideload = true @concurrency = false + @concurrency_max_threads = 4 @respond_to = [:json, :jsonapi, :xml] @links_on_demand = false @pagination_links_on_demand = false diff --git a/lib/graphiti/scope.rb b/lib/graphiti/scope.rb index af1f6ed0..fcb0731f 100644 --- a/lib/graphiti/scope.rb +++ b/lib/graphiti/scope.rb @@ -2,6 +2,23 @@ module Graphiti class Scope attr_accessor :object, :unpaginated_object attr_reader :pagination + + @thread_pool_executor_mutex = Mutex.new + + def self.thread_pool_executor + return @thread_pool_executor if @thread_pool_executor + + concurrency = Graphiti.config.concurrency_max_threads || 4 + @thread_pool_executor_mutex.synchronize do + @thread_pool_executor ||= Concurrent::ThreadPoolExecutor.new( + min_threads: 0, + max_threads: concurrency, + max_queue: concurrency * 4, + fallback_policy: :caller_runs + ) + end + end + def initialize(object, resource, query, opts = {}) @object = object @resource = resource @@ -49,7 +66,7 @@ def resolve_sideloads(results) @resource.adapter.close if concurrent } if concurrent - promises << Concurrent::Promise.execute(&resolve_sideload) + promises << Concurrent::Promise.execute(executor: self.class.thread_pool_executor, &resolve_sideload) else resolve_sideload.call end diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb index 22acf1f4..42013fbe 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -150,6 +150,21 @@ end end + describe "#concurrency_max_threads" do + include_context "with config", :concurrency_max_threads + + it "defaults" do + expect(Graphiti.config.concurrency_max_threads).to eq(4) + end + + it "is overridable" do + Graphiti.configure do |c| + c.concurrency_max_threads = 1 + end + expect(Graphiti.config.concurrency_max_threads).to eq(1) + end + end + describe "#raise_on_missing_sideload" do include_context "with config", :raise_on_missing_sideload From 389985576f8b1260f17282b60745d1a3530a34e3 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 20 Mar 2024 17:49:35 +0000 Subject: [PATCH 23/46] 1.6.0 CHANGELOG [skip ci] # [1.6.0](https://github.com/graphiti-api/graphiti/compare/v1.5.3...v1.6.0) (2024-03-20) ### Features * add thread pool and concurrency_max_threads configuration option ([#470](https://github.com/graphiti-api/graphiti/issues/470)) ([697d761](https://github.com/graphiti-api/graphiti/commit/697d76172adec24cd7e7522300c8335233fdcc36)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f15b57ec..34af193a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ graphiti changelog +# [1.6.0](https://github.com/graphiti-api/graphiti/compare/v1.5.3...v1.6.0) (2024-03-20) + + +### Features + +* add thread pool and concurrency_max_threads configuration option ([#470](https://github.com/graphiti-api/graphiti/issues/470)) ([697d761](https://github.com/graphiti-api/graphiti/commit/697d76172adec24cd7e7522300c8335233fdcc36)) + ## [1.5.3](https://github.com/graphiti-api/graphiti/compare/v1.5.2...v1.5.3) (2024-03-18) From 0400ab0d97a1382b66b5295fdc7aa7db680e77cc Mon Sep 17 00:00:00 2001 From: Jeff Keen Date: Thu, 21 Mar 2024 22:30:38 -0500 Subject: [PATCH 24/46] fix: correct thread-pool mutex logic which was causing a deadlock --- lib/graphiti/scope.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/graphiti/scope.rb b/lib/graphiti/scope.rb index fcb0731f..8f8102ee 100644 --- a/lib/graphiti/scope.rb +++ b/lib/graphiti/scope.rb @@ -9,8 +9,8 @@ def self.thread_pool_executor return @thread_pool_executor if @thread_pool_executor concurrency = Graphiti.config.concurrency_max_threads || 4 - @thread_pool_executor_mutex.synchronize do - @thread_pool_executor ||= Concurrent::ThreadPoolExecutor.new( + @thread_pool_executor ||= @thread_pool_executor_mutex.synchronize do + Concurrent::ThreadPoolExecutor.new( min_threads: 0, max_threads: concurrency, max_queue: concurrency * 4, From 4854c749c4fc3e1e8c6bf003427e8fccb62726f5 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 22 Mar 2024 03:35:45 +0000 Subject: [PATCH 25/46] 1.6.1 CHANGELOG [skip ci] ## [1.6.1](https://github.com/graphiti-api/graphiti/compare/v1.6.0...v1.6.1) (2024-03-22) ### Bug Fixes * correct thread-pool mutex logic which was causing a deadlock ([0400ab0](https://github.com/graphiti-api/graphiti/commit/0400ab0d97a1382b66b5295fdc7aa7db680e77cc)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34af193a..b5e2f21f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ graphiti changelog +## [1.6.1](https://github.com/graphiti-api/graphiti/compare/v1.6.0...v1.6.1) (2024-03-22) + + +### Bug Fixes + +* correct thread-pool mutex logic which was causing a deadlock ([0400ab0](https://github.com/graphiti-api/graphiti/commit/0400ab0d97a1382b66b5295fdc7aa7db680e77cc)) + # [1.6.0](https://github.com/graphiti-api/graphiti/compare/v1.5.3...v1.6.0) (2024-03-20) From 99f469f6576c12d378def79133d5e097378fae5b Mon Sep 17 00:00:00 2001 From: Jeff Keen Date: Thu, 21 Mar 2024 22:38:32 -0500 Subject: [PATCH 26/46] [skip ci] ensure version up to date --- lib/graphiti/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/graphiti/version.rb b/lib/graphiti/version.rb index 95347d14..4574909c 100644 --- a/lib/graphiti/version.rb +++ b/lib/graphiti/version.rb @@ -1,3 +1,3 @@ module Graphiti - VERSION = "1.5.0" + VERSION = "1.6.1" end From 51fb51c31f0043d98aa07f689a8cf8c758fa823b Mon Sep 17 00:00:00 2001 From: Matt Fenelon Date: Fri, 22 Mar 2024 13:24:49 +0000 Subject: [PATCH 27/46] fix: thread pool scope and mutex need to be global across all instances of Scope for it to be a global thread pool (#471) --- lib/graphiti/scope.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/graphiti/scope.rb b/lib/graphiti/scope.rb index 8f8102ee..6c7ac272 100644 --- a/lib/graphiti/scope.rb +++ b/lib/graphiti/scope.rb @@ -3,14 +3,14 @@ class Scope attr_accessor :object, :unpaginated_object attr_reader :pagination - @thread_pool_executor_mutex = Mutex.new + @@thread_pool_executor_mutex = Mutex.new def self.thread_pool_executor return @thread_pool_executor if @thread_pool_executor concurrency = Graphiti.config.concurrency_max_threads || 4 - @thread_pool_executor ||= @thread_pool_executor_mutex.synchronize do - Concurrent::ThreadPoolExecutor.new( + @@thread_pool_executor_mutex.synchronize do + @@thread_pool_executor ||= Concurrent::ThreadPoolExecutor.new( min_threads: 0, max_threads: concurrency, max_queue: concurrency * 4, From 8f46cf322ef82ea9f1076e5f69ac14ce3e6d0319 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 22 Mar 2024 13:35:04 +0000 Subject: [PATCH 28/46] 1.6.2 CHANGELOG [skip ci] ## [1.6.2](https://github.com/graphiti-api/graphiti/compare/v1.6.1...v1.6.2) (2024-03-22) ### Bug Fixes * thread pool scope and mutex need to be global across all instances of Scope for it to be a global thread pool ([#471](https://github.com/graphiti-api/graphiti/issues/471)) ([51fb51c](https://github.com/graphiti-api/graphiti/commit/51fb51c31f0043d98aa07f689a8cf8c758fa823b)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5e2f21f..14d850d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ graphiti changelog +## [1.6.2](https://github.com/graphiti-api/graphiti/compare/v1.6.1...v1.6.2) (2024-03-22) + + +### Bug Fixes + +* thread pool scope and mutex need to be global across all instances of Scope for it to be a global thread pool ([#471](https://github.com/graphiti-api/graphiti/issues/471)) ([51fb51c](https://github.com/graphiti-api/graphiti/commit/51fb51c31f0043d98aa07f689a8cf8c758fa823b)) + ## [1.6.1](https://github.com/graphiti-api/graphiti/compare/v1.6.0...v1.6.1) (2024-03-22) From 7941b6f75ce1001b034ed6e83c148b893e9f3d99 Mon Sep 17 00:00:00 2001 From: Jeff Keen Date: Tue, 26 Mar 2024 17:27:21 -0500 Subject: [PATCH 29/46] fix: Remove thread pool executor logic until we get a better handle on what's causing thread pool hangs. refs #469 revert: "thread pool scope and mutex need to be global across all instances of Scope for it to be a global thread pool (#471)" revert: "add thread pool and concurrency_max_threads configuration option (#470)" This reverts commit 51fb51c31f0043d98aa07f689a8cf8c758fa823b. This reverts commit 697d76172adec24cd7e7522300c8335233fdcc36. --- lib/graphiti/configuration.rb | 15 --------------- lib/graphiti/scope.rb | 19 +------------------ spec/configuration_spec.rb | 15 --------------- 3 files changed, 1 insertion(+), 48 deletions(-) diff --git a/lib/graphiti/configuration.rb b/lib/graphiti/configuration.rb index d1a69098..be7b4f19 100644 --- a/lib/graphiti/configuration.rb +++ b/lib/graphiti/configuration.rb @@ -8,20 +8,6 @@ class Configuration # Defaults to false OR if classes are cached (Rails-only) attr_accessor :concurrency - # This number must be considered in accordance with the database - # connection pool size configured in `database.yml`. The connection - # pool should be large enough to accommodate both the foreground - # threads (ie. web server or job worker threads) and background - # threads. For each process, Graphiti will create one global - # executor that uses this many threads to sideload resources - # asynchronously. Thus, the pool size should be at least - # `thread_count + concurrency_max_threads + 1`. For example, if your - # web server has a maximum of 3 threads, and - # `concurrency_max_threads` is set to 4, then your pool size should - # be at least 8. - # @return [Integer] Maximum number of threads to use when fetching sideloads concurrently - attr_accessor :concurrency_max_threads - attr_accessor :respond_to attr_accessor :context_for_endpoint attr_accessor :links_on_demand @@ -40,7 +26,6 @@ class Configuration def initialize @raise_on_missing_sideload = true @concurrency = false - @concurrency_max_threads = 4 @respond_to = [:json, :jsonapi, :xml] @links_on_demand = false @pagination_links_on_demand = false diff --git a/lib/graphiti/scope.rb b/lib/graphiti/scope.rb index 6c7ac272..af1f6ed0 100644 --- a/lib/graphiti/scope.rb +++ b/lib/graphiti/scope.rb @@ -2,23 +2,6 @@ module Graphiti class Scope attr_accessor :object, :unpaginated_object attr_reader :pagination - - @@thread_pool_executor_mutex = Mutex.new - - def self.thread_pool_executor - return @thread_pool_executor if @thread_pool_executor - - concurrency = Graphiti.config.concurrency_max_threads || 4 - @@thread_pool_executor_mutex.synchronize do - @@thread_pool_executor ||= Concurrent::ThreadPoolExecutor.new( - min_threads: 0, - max_threads: concurrency, - max_queue: concurrency * 4, - fallback_policy: :caller_runs - ) - end - end - def initialize(object, resource, query, opts = {}) @object = object @resource = resource @@ -66,7 +49,7 @@ def resolve_sideloads(results) @resource.adapter.close if concurrent } if concurrent - promises << Concurrent::Promise.execute(executor: self.class.thread_pool_executor, &resolve_sideload) + promises << Concurrent::Promise.execute(&resolve_sideload) else resolve_sideload.call end diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb index 42013fbe..22acf1f4 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -150,21 +150,6 @@ end end - describe "#concurrency_max_threads" do - include_context "with config", :concurrency_max_threads - - it "defaults" do - expect(Graphiti.config.concurrency_max_threads).to eq(4) - end - - it "is overridable" do - Graphiti.configure do |c| - c.concurrency_max_threads = 1 - end - expect(Graphiti.config.concurrency_max_threads).to eq(1) - end - end - describe "#raise_on_missing_sideload" do include_context "with config", :raise_on_missing_sideload From 4eb2fc85a6aec3ebc404cee9de966eabcc2ff478 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 26 Mar 2024 22:46:44 +0000 Subject: [PATCH 30/46] 1.6.3 CHANGELOG [skip ci] ## [1.6.3](https://github.com/graphiti-api/graphiti/compare/v1.6.2...v1.6.3) (2024-03-26) ### Bug Fixes * Remove thread pool executor logic until we get a better handle on what's causing thread pool hangs. refs [#469](https://github.com/graphiti-api/graphiti/issues/469) ([7941b6f](https://github.com/graphiti-api/graphiti/commit/7941b6f75ce1001b034ed6e83c148b893e9f3d99)), closes [#471](https://github.com/graphiti-api/graphiti/issues/471) [#470](https://github.com/graphiti-api/graphiti/issues/470) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14d850d1..e4dbf38b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ graphiti changelog +## [1.6.3](https://github.com/graphiti-api/graphiti/compare/v1.6.2...v1.6.3) (2024-03-26) + + +### Bug Fixes + +* Remove thread pool executor logic until we get a better handle on what's causing thread pool hangs. refs [#469](https://github.com/graphiti-api/graphiti/issues/469) ([7941b6f](https://github.com/graphiti-api/graphiti/commit/7941b6f75ce1001b034ed6e83c148b893e9f3d99)), closes [#471](https://github.com/graphiti-api/graphiti/issues/471) [#470](https://github.com/graphiti-api/graphiti/issues/470) + ## [1.6.2](https://github.com/graphiti-api/graphiti/compare/v1.6.1...v1.6.2) (2024-03-22) From 0d0e218f9db9b2365ca04fa5abbee6c802f8b820 Mon Sep 17 00:00:00 2001 From: Jeff Keen Date: Wed, 27 Mar 2024 17:19:42 -0500 Subject: [PATCH 31/46] refactor: rename and alias some methods for clarity --- lib/graphiti/resource/interface.rb | 8 ++++---- lib/graphiti/resource_proxy.rb | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/graphiti/resource/interface.rb b/lib/graphiti/resource/interface.rb index b424b064..b79a31a2 100644 --- a/lib/graphiti/resource/interface.rb +++ b/lib/graphiti/resource/interface.rb @@ -5,7 +5,7 @@ module Interface class_methods do def all(params = {}, base_scope = nil) - validate!(params) + validate_request!(params) _all(params, {}, base_scope) end @@ -17,7 +17,7 @@ def _all(params, opts, base_scope) end def find(params = {}, base_scope = nil) - validate!(params) + validate_request!(params) _find(params, base_scope) end @@ -38,14 +38,14 @@ def _find(params = {}, base_scope = nil) end def build(params, base_scope = nil) - validate!(params) + validate_request!(params) runner = Runner.new(self, params) runner.proxy(base_scope, single: true, raise_on_missing: true) end private - def validate!(params) + def validate_request!(params) return if Graphiti.context[:graphql] || !validate_endpoints? if context&.respond_to?(:request) diff --git a/lib/graphiti/resource_proxy.rb b/lib/graphiti/resource_proxy.rb index d03105be..4372ce9c 100644 --- a/lib/graphiti/resource_proxy.rb +++ b/lib/graphiti/resource_proxy.rb @@ -74,6 +74,7 @@ def data end end alias_method :to_a, :data + alias_method :resolve_data, :data def meta @meta ||= data.respond_to?(:meta) ? data.meta : {} @@ -136,7 +137,7 @@ def save(action: :create) end def destroy - data + resolve_data transaction_response = @resource.transaction do metadata = {method: :destroy} model = @resource.destroy(@query.filters[:id], metadata) @@ -154,7 +155,7 @@ def destroy end def update - data + resolve_data save(action: :update) end From 512123aa55e9ceea9a74813d8e65d771289e956a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 27 Mar 2024 22:24:15 +0000 Subject: [PATCH 32/46] 1.6.4 CHANGELOG [skip ci] ## [1.6.4](https://github.com/graphiti-api/graphiti/compare/v1.6.3...v1.6.4) (2024-03-27) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4dbf38b..761fbf18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ graphiti changelog +## [1.6.4](https://github.com/graphiti-api/graphiti/compare/v1.6.3...v1.6.4) (2024-03-27) + ## [1.6.3](https://github.com/graphiti-api/graphiti/compare/v1.6.2...v1.6.3) (2024-03-26) From 8bae50ab82559e2644d506e16a4f715effd89317 Mon Sep 17 00:00:00 2001 From: Jeff Keen Date: Wed, 27 Mar 2024 18:22:12 -0500 Subject: [PATCH 33/46] feat: Add support for caching renders in Graphiti, and better support using etags and stale? in the controller (#424) - add `cache_key` method to resource instance, which generates a combined stable cache key based on resource identifiers, the specified sideloads, and any specified extra_fields or fields, pages, or links which will affect the response. - add `cache_key_with_version` method to resource instance, which is the same as above with the last updated_at added in - add `updated_at` method to resource instance, which returns the max `updated_at` date of the resource and any specified sideloads - add `etag` method to resource instance, which generates a Weak Etag based on the `cache_key_with_version` response. With `etag` and `updated_at` methods on a resource instance, using `stale?(@resource)` will work out of the box. - allow `cache_resource` directive combined when `Graphiti.config.cache_rendering=true` and `Graphiti.cache = ::Rails.cache` to execute rendering logic in Graphiti wrapped in a cache block using the keys above, often times dramatically improving response time. --- lib/graphiti.rb | 9 +++ lib/graphiti/configuration.rb | 12 ++++ lib/graphiti/debugger.rb | 25 ++++++++- lib/graphiti/query.rb | 16 ++++++ lib/graphiti/renderer.rb | 9 ++- lib/graphiti/resource/interface.rb | 17 +++++- lib/graphiti/resource_proxy.rb | 31 ++++++++++- lib/graphiti/runner.rb | 4 +- lib/graphiti/scope.rb | 56 +++++++++++++++++++ lib/graphiti/serializer.rb | 2 +- lib/graphiti/sideload.rb | 13 ++++- lib/graphiti/util/cache_debug.rb | 88 ++++++++++++++++++++++++++++++ spec/configuration_spec.rb | 23 ++++++++ spec/query_spec.rb | 19 +++++++ spec/resource_proxy_spec.rb | 26 +++++++++ spec/scope_spec.rb | 45 +++++++++++++++ 16 files changed, 384 insertions(+), 11 deletions(-) create mode 100644 lib/graphiti/util/cache_debug.rb diff --git a/lib/graphiti.rb b/lib/graphiti.rb index 78904e94..45df3091 100644 --- a/lib/graphiti.rb +++ b/lib/graphiti.rb @@ -106,6 +106,14 @@ def self.setup! r.apply_sideloads_to_serializer end end + + def self.cache=(val) + @cache = val + end + + def self.cache + @cache + end end require "graphiti/version" @@ -177,6 +185,7 @@ def self.setup! require "graphiti/serializer" require "graphiti/query" require "graphiti/debugger" +require "graphiti/util/cache_debug" if defined?(ActiveRecord) require "graphiti/adapters/active_record" diff --git a/lib/graphiti/configuration.rb b/lib/graphiti/configuration.rb index be7b4f19..121c4317 100644 --- a/lib/graphiti/configuration.rb +++ b/lib/graphiti/configuration.rb @@ -20,6 +20,7 @@ class Configuration attr_reader :debug, :debug_models attr_writer :schema_path + attr_writer :cache_rendering # Set defaults # @api private @@ -32,6 +33,7 @@ def initialize @pagination_links = false @typecast_reads = true @raise_on_missing_sidepost = true + @cache_rendering = false self.debug = ENV.fetch("GRAPHITI_DEBUG", true) self.debug_models = ENV.fetch("GRAPHITI_DEBUG_MODELS", false) @@ -52,6 +54,16 @@ def initialize end end + def cache_rendering? + use_caching = @cache_rendering && Graphiti.cache.respond_to?(:fetch) + + use_caching.tap do |use| + if @cache_rendering && !Graphiti.cache&.respond_to?(:fetch) + raise "You must configure a cache store in order to use cache_rendering. Set Graphiti.cache = Rails.cache, for example." + end + end + end + def schema_path @schema_path ||= raise("No schema_path defined! Set Graphiti.config.schema_path to save your schema.") end diff --git a/lib/graphiti/debugger.rb b/lib/graphiti/debugger.rb index b5fd3991..92165d35 100644 --- a/lib/graphiti/debugger.rb +++ b/lib/graphiti/debugger.rb @@ -98,7 +98,30 @@ def on_render(name, start, stop, id, payload) took = ((stop - start) * 1000.0).round(2) logs << [""] logs << ["=== Graphiti Debug", :green, true] - logs << ["Rendering:", :green, true] + if payload[:proxy]&.cached? && Graphiti.config.cache_rendering? + logs << ["Rendering (cached):", :green, true] + + Graphiti::Util::CacheDebug.new(payload[:proxy]).analyze do |cache_debug| + logs << ["Cache key for #{cache_debug.name}", :blue, true] + logs << if cache_debug.volatile? + [" \\_ volatile | Request count: #{cache_debug.request_count} | Hit count: #{cache_debug.hit_count}", :red, true] + else + [" \\_ stable | Request count: #{cache_debug.request_count} | Hit count: #{cache_debug.hit_count}", :blue, true] + end + + if cache_debug.changed_key? + logs << [" [x] cache key changed #{cache_debug.last_version[:etag]} -> #{cache_debug.current_version[:etag]}", :red] + logs << [" removed: #{cache_debug.removed_segments}", :red] + logs << [" added: #{cache_debug.added_segments}", :red] + elsif cache_debug.new_key? + logs << [" [+] cache key added #{cache_debug.current_version[:etag]}", :red, true] + else + logs << [" [✓] #{cache_debug.current_version[:etag]}", :green, true] + end + end + else + logs << ["Rendering:", :green, true] + end logs << ["Took: #{took}ms", :magenta, true] end end diff --git a/lib/graphiti/query.rb b/lib/graphiti/query.rb index a336a7d3..1b59ccbe 100644 --- a/lib/graphiti/query.rb +++ b/lib/graphiti/query.rb @@ -1,3 +1,5 @@ +require "digest" + module Graphiti class Query attr_reader :resource, :association_name, :params, :action @@ -232,8 +234,22 @@ def paginate? ![false, "false"].include?(@params[:paginate]) end + def cache_key + "args-#{query_cache_key}" + end + private + def query_cache_key + attrs = {extra_fields: extra_fields, + fields: fields, + links: links?, + pagination_links: pagination_links?, + format: params[:format]} + + Digest::SHA1.hexdigest(attrs.to_s) + end + def cast_page_param(name, value) if [:before, :after].include?(name) decode_cursor(value) diff --git a/lib/graphiti/renderer.rb b/lib/graphiti/renderer.rb index 28b4026c..a8e3d6e6 100644 --- a/lib/graphiti/renderer.rb +++ b/lib/graphiti/renderer.rb @@ -68,7 +68,14 @@ def render(renderer) options[:meta][:debug] = Debugger.to_a if debug_json? options[:proxy] = proxy - renderer.render(records, options) + if proxy.cache? && Graphiti.config.cache_rendering? + Graphiti.cache.fetch("graphiti:render/#{proxy.cache_key}", version: proxy.updated_at, expires_in: proxy.cache_expires_in) do + options.delete(:cache) # ensure that we don't use JSONAPI-Resources's built-in caching logic + renderer.render(records, options) + end + else + renderer.render(records, options) + end end end diff --git a/lib/graphiti/resource/interface.rb b/lib/graphiti/resource/interface.rb index b79a31a2..0f702cf3 100644 --- a/lib/graphiti/resource/interface.rb +++ b/lib/graphiti/resource/interface.rb @@ -4,6 +4,11 @@ module Interface extend ActiveSupport::Concern class_methods do + def cache_resource(expires_in: false) + @cache_resource = true + @cache_expires_in = expires_in + end + def all(params = {}, base_scope = nil) validate_request!(params) _all(params, {}, base_scope) @@ -13,7 +18,7 @@ def all(params = {}, base_scope = nil) def _all(params, opts, base_scope) runner = Runner.new(self, params, opts.delete(:query), :all) opts[:params] = params - runner.proxy(base_scope, opts) + runner.proxy(base_scope, opts.merge(caching_options)) end def find(params = {}, base_scope = nil) @@ -31,10 +36,14 @@ def _find(params = {}, base_scope = nil) params[:filter][:id] = id if id runner = Runner.new(self, params, nil, :find) - runner.proxy base_scope, + + find_options = { single: true, raise_on_missing: true, bypass_required_filters: true + }.merge(caching_options) + + runner.proxy base_scope, find_options end def build(params, base_scope = nil) @@ -45,6 +54,10 @@ def build(params, base_scope = nil) private + def caching_options + {cache: @cache_resource, cache_expires_in: @cache_expires_in} + end + def validate_request!(params) return if Graphiti.context[:graphql] || !validate_endpoints? diff --git a/lib/graphiti/resource_proxy.rb b/lib/graphiti/resource_proxy.rb index 4372ce9c..a698f8d9 100644 --- a/lib/graphiti/resource_proxy.rb +++ b/lib/graphiti/resource_proxy.rb @@ -2,20 +2,31 @@ module Graphiti class ResourceProxy include Enumerable - attr_reader :resource, :query, :scope, :payload + attr_reader :resource, :query, :scope, :payload, :cache_expires_in, :cache def initialize(resource, scope, query, payload: nil, single: false, - raise_on_missing: false) + raise_on_missing: false, + cache: nil, + cache_expires_in: nil) + @resource = resource @scope = scope @query = query @payload = payload @single = single @raise_on_missing = raise_on_missing + @cache = cache + @cache_expires_in = cache_expires_in + end + + def cache? + !!@cache end + alias_method :cached?, :cache? + def single? !!@single end @@ -180,6 +191,22 @@ def debug_requested? query.debug_requested? end + def updated_at + @scope.updated_at + end + + def etag + "W/#{ActiveSupport::Digest.hexdigest(cache_key_with_version.to_s)}" + end + + def cache_key + ActiveSupport::Cache.expand_cache_key([@scope.cache_key, @query.cache_key]) + end + + def cache_key_with_version + ActiveSupport::Cache.expand_cache_key([@scope.cache_key_with_version, @query.cache_key]) + end + private def persist diff --git a/lib/graphiti/runner.rb b/lib/graphiti/runner.rb index 43703714..d2b9d070 100644 --- a/lib/graphiti/runner.rb +++ b/lib/graphiti/runner.rb @@ -71,7 +71,9 @@ def proxy(base = nil, opts = {}) query, payload: deserialized_payload, single: opts[:single], - raise_on_missing: opts[:raise_on_missing] + raise_on_missing: opts[:raise_on_missing], + cache: opts[:cache], + cache_expires_in: opts[:cache_expires_in] end end end diff --git a/lib/graphiti/scope.rb b/lib/graphiti/scope.rb index af1f6ed0..0b951260 100644 --- a/lib/graphiti/scope.rb +++ b/lib/graphiti/scope.rb @@ -67,8 +67,64 @@ def resolve_sideloads(results) end end + def parent_resource + @resource + end + + def cache_key + # This is the combined cache key for the base query and the query for all sideloads + # Changing the query will yield a different cache key + + cache_keys = sideload_resource_proxies.map { |proxy| proxy.try(:cache_key) } + + cache_keys << @object.try(:cache_key) # this is what calls into the ORM (ActiveRecord, most likely) + ActiveSupport::Cache.expand_cache_key(cache_keys.flatten.compact) + end + + def cache_key_with_version + # This is the combined and versioned cache key for the base query and the query for all sideloads + # If any returned model's updated_at changes, this key will change + + cache_keys = sideload_resource_proxies.map { |proxy| proxy.try(:cache_key_with_version) } + + cache_keys << @object.try(:cache_key_with_version) # this is what calls into ORM (ActiveRecord, most likely) + ActiveSupport::Cache.expand_cache_key(cache_keys.flatten.compact) + end + + def updated_at + updated_ats = sideload_resource_proxies.map(&:updated_at) + + begin + updated_ats << @object.maximum(:updated_at) + rescue => e + Graphiti.log("error calculating last_modified_at for #{@resource.class}") + Graphiti.log(e) + end + + updated_ats.compact.max + end + alias_method :last_modified_at, :updated_at + private + def sideload_resource_proxies + @sideload_resource_proxies ||= begin + @object = @resource.before_resolve(@object, @query) + results = @resource.resolve(@object) + + [].tap do |proxies| + unless @query.sideloads.empty? + @query.sideloads.each_pair do |name, q| + sideload = @resource.class.sideload(name) + next if sideload.nil? || sideload.shared_remote? + + proxies << sideload.build_resource_proxy(results, q, parent_resource) + end + end + end.flatten + end + end + def broadcast_data opts = { resource: @resource, diff --git a/lib/graphiti/serializer.rb b/lib/graphiti/serializer.rb index 706dbdca..47ebd7bd 100644 --- a/lib/graphiti/serializer.rb +++ b/lib/graphiti/serializer.rb @@ -99,7 +99,7 @@ def strip_relationships!(hash) def strip_relationships? return false unless Graphiti.config.links_on_demand - params = Graphiti.context[:object].params || {} + params = Graphiti.context[:object]&.params || {} [false, nil, "false"].include?(params[:links]) end end diff --git a/lib/graphiti/sideload.rb b/lib/graphiti/sideload.rb index c87d7981..fdf02df5 100644 --- a/lib/graphiti/sideload.rb +++ b/lib/graphiti/sideload.rb @@ -209,13 +209,16 @@ def base_scope end end - def load(parents, query, graph_parent) - params, opts, proxy = nil, nil, nil + def build_resource_proxy(parents, query, graph_parent) + params = nil + opts = nil + proxy = nil with_error_handling Errors::SideloadParamsError do params = load_params(parents, query) params_proc&.call(params, parents, context) return [] if blank_query?(params) + opts = load_options(parents, query) opts[:sideload] = self opts[:parent] = graph_parent @@ -228,7 +231,11 @@ def load(parents, query, graph_parent) pre_load_proc&.call(proxy, parents) end - proxy.to_a + proxy + end + + def load(parents, query, graph_parent) + build_resource_proxy(parents, query, graph_parent).to_a end # Override in subclass diff --git a/lib/graphiti/util/cache_debug.rb b/lib/graphiti/util/cache_debug.rb new file mode 100644 index 00000000..de42cf77 --- /dev/null +++ b/lib/graphiti/util/cache_debug.rb @@ -0,0 +1,88 @@ +module Graphiti + module Util + class CacheDebug + attr_reader :proxy + + def initialize(proxy) + @proxy = proxy + end + + def last_version + @last_version ||= Graphiti.cache.read(key) || {} + end + + def name + "#{Graphiti.context[:object].request.method} #{Graphiti.context[:object].request.url}" + end + + def key + "graphiti:debug/#{name}" + end + + def current_version + @current_version ||= { + cache_key: proxy.cache_key_with_version, + version: proxy.updated_at, + expires_in: proxy.cache_expires_in, + etag: proxy.etag, + miss_count: last_version[:miss_count].to_i + (changed_key? ? 1 : 0), + hit_count: last_version[:hit_count].to_i + (!changed_key? && !new_key? ? 1 : 0), + request_count: last_version[:request_count].to_i + (last_version.present? ? 1 : 0) + } + end + + def analyze + yield self + save + end + + def request_count + current_version[:request_count] + end + + def miss_count + current_version[:miss_count] + end + + def hit_count + current_version[:hit_count] + end + + def change_percentage + return 0 if request_count == 0 + (miss_count.to_i / request_count.to_f * 100).round(1) + end + + def volatile? + change_percentage > 50 + end + + def new_key? + last_version[:cache_key].blank? && proxy.cache_key_with_version + end + + def changed_key? + last_version[:cache_key] != proxy.cache_key_with_version && !new_key? + end + + def removed_segments + changes[1] - changes[0] + end + + def added_segments + changes[0] - changes[1] + end + + def changes + sub_keys_old = last_version[:cache_key]&.scan(/\w+\/query-[a-z0-9-]+\/args-[a-z0-9-]+/).to_a || [] + sub_keys_new = current_version[:cache_key]&.scan(/\w+\/query-[a-z0-9-]+\/args-[a-z0-9-]+/).to_a || [] + + [sub_keys_old, sub_keys_new] + end + + def save + Graphiti.cache.write(key, current_version) + end + end + end +end diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb index 22acf1f4..9fe96d37 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -164,4 +164,27 @@ expect(Graphiti.config.raise_on_missing_sideload).to eq(false) end end + + describe "#cache_rendering" do + it "defaults" do + expect(Graphiti.config.cache_rendering?).to eq(false) + end + + it "is settable" do + Graphiti.configure do |c| + c.cache_rendering = true + end + Graphiti.cache = double(fetch: nil) # looks like a cache store + expect(Graphiti.config.cache_rendering?).to eq(true) + end + + it "warns about not being configured correctly if cache_rendering is true without Graphiti.cache set up" do + Graphiti.cache = nil + Graphiti.configure do |c| + c.cache_rendering = true + end + + expect { Graphiti.config.cache_rendering? }.to raise_error(/You must configure a cache store in order to use cache_rendering/) + end + end end diff --git a/spec/query_spec.rb b/spec/query_spec.rb index 1d72bc51..85c3d279 100644 --- a/spec/query_spec.rb +++ b/spec/query_spec.rb @@ -1077,4 +1077,23 @@ def resource_class_of_remote_sideload(sideloads) end end end + + describe "cache_key" do + it "generates a stable key" do + instance1 = described_class.new(resource, params) + instance2 = described_class.new(resource, params) + + expect(instance1.cache_key).to be_present + expect(instance1.cache_key).to eq(instance2.cache_key) + end + + it "generates a different key with different params" do + instance1 = described_class.new(resource, params) + instance2 = described_class.new(resource, {extra_fields: {positions: ["foo"]}}) + + expect(instance1.cache_key).to be_present + expect(instance2.cache_key).to be_present + expect(instance1.cache_key).not_to eq(instance2.cache_key) + end + end end diff --git a/spec/resource_proxy_spec.rb b/spec/resource_proxy_spec.rb index 635260a9..ee87e37c 100644 --- a/spec/resource_proxy_spec.rb +++ b/spec/resource_proxy_spec.rb @@ -8,4 +8,30 @@ expect(subject).to be_kind_of(Graphiti::Delegates::Pagination) end end + + describe "caching" do + let(:resource) { double } + 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, **{}) } + + it "cache_key combines query and scope cache keys" do + cache_key = subject.cache_key + expect(cache_key).to eq("scope-hash/query-hash") + end + + it "generates stable etag" do + instance1 = described_class.new(resource, scope, query, **{}) + instance2 = described_class.new(resource, scope, query, **{}) + + expect(instance1.etag).to be_present + expect(instance1.etag).to start_with("W/") + + expect(instance2.etag).to be_present + expect(instance2.etag).to start_with("W/") + + expect(instance1.etag).to eq(instance2.etag) + end + end end diff --git a/spec/scope_spec.rb b/spec/scope_spec.rb index 2ae9dd75..87e9f909 100644 --- a/spec/scope_spec.rb +++ b/spec/scope_spec.rb @@ -135,4 +135,49 @@ end end end + + describe "cache_key" do + let(:employee1) { + time = Time.parse("2022-06-24 16:36:00.000000000 -0500") + double(cache_key: "employee/1", cache_key_with_version: "employee/1-#{time.to_i}", updated_at: time).as_null_object + } + + let(:employee2) { + time = Time.parse("2022-06-24 16:37:00.000000000 -0500") + double(cache_key: "employee/2", cache_key_with_version: "employee/2-#{time.to_i}", updated_at: time).as_null_object + } + + it "generates a stable key" do + instance1 = described_class.new(employee1, resource, query) + instance2 = described_class.new(employee1, resource, query) + + expect(instance1.cache_key).to be_present + expect(instance1.cache_key).to eq(instance2.cache_key) + end + + it "only caches off of the scoped object " do + instance1 = described_class.new(employee1, resource, query) + instance2 = described_class.new(employee1, resource, Graphiti::Query.new(resource, {extra_fields: {positions: ["foo"]}})) + + expect(instance1.cache_key).to be_present + expect(instance2.cache_key).to be_present + expect(instance1.cache_key).to eq(instance2.cache_key) + + expect(instance1.cache_key_with_version).to be_present + expect(instance2.cache_key_with_version).to be_present + expect(instance1.cache_key_with_version).to eq(instance2.cache_key_with_version) + end + + it "generates a different key with a different scope query" do + instance1 = described_class.new(employee1, resource, query) + instance2 = described_class.new(employee2, resource, query) + expect(instance1.cache_key).to be_present + expect(instance2.cache_key).to be_present + expect(instance1.cache_key).not_to eq(instance2.cache_key) + + expect(instance1.cache_key_with_version).to be_present + expect(instance2.cache_key_with_version).to be_present + expect(instance1.cache_key_with_version).not_to eq(instance2.cache_key) + end + end end From 764eb93c9206f5c619d452ed753cb4d7d518cd3c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 27 Mar 2024 23:24:12 +0000 Subject: [PATCH 34/46] 1.7.0 CHANGELOG [skip ci] # [1.7.0](https://github.com/graphiti-api/graphiti/compare/v1.6.4...v1.7.0) (2024-03-27) ### Features * Add support for caching renders in Graphiti, and better support using etags and stale? in the controller ([#424](https://github.com/graphiti-api/graphiti/issues/424)) ([8bae50a](https://github.com/graphiti-api/graphiti/commit/8bae50ab82559e2644d506e16a4f715effd89317)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 761fbf18..bceb2df4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ graphiti changelog +# [1.7.0](https://github.com/graphiti-api/graphiti/compare/v1.6.4...v1.7.0) (2024-03-27) + + +### Features + +* Add support for caching renders in Graphiti, and better support using etags and stale? in the controller ([#424](https://github.com/graphiti-api/graphiti/issues/424)) ([8bae50a](https://github.com/graphiti-api/graphiti/commit/8bae50ab82559e2644d506e16a4f715effd89317)) + ## [1.6.4](https://github.com/graphiti-api/graphiti/compare/v1.6.3...v1.6.4) (2024-03-27) ## [1.6.3](https://github.com/graphiti-api/graphiti/compare/v1.6.2...v1.6.3) (2024-03-26) From dcee71be472339e01384d4b4641085c666e5072e Mon Sep 17 00:00:00 2001 From: Jeff Keen Date: Thu, 28 Mar 2024 13:03:03 -0500 Subject: [PATCH 35/46] docs: update badges [skip ci] --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4eae0bef..de135942 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ -![Build Status](https://travis-ci.org/graphiti-api/graphiti.svg?branch=master) +[![CI](https://github.com/graphiti-api/graphiti/actions/workflows/ci.yml/badge.svg)](https://github.com/graphiti-api/graphiti/actions/workflows/ci.yml) [![Gem Version](https://badge.fury.io/rb/graphiti.svg)](https://badge.fury.io/rb/graphiti) [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard) +[![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release)

From 661e3b5212e2649870a200067d0d5d52fa962637 Mon Sep 17 00:00:00 2001 From: Jeff Keen Date: Tue, 16 Apr 2024 23:41:34 -0500 Subject: [PATCH 36/46] fix: rescue error from sideloads updated_at calculation, defaulting to the current time --- lib/graphiti/scope.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/graphiti/scope.rb b/lib/graphiti/scope.rb index 0b951260..a759a715 100644 --- a/lib/graphiti/scope.rb +++ b/lib/graphiti/scope.rb @@ -92,16 +92,17 @@ def cache_key_with_version end def updated_at - updated_ats = sideload_resource_proxies.map(&:updated_at) - + updated_time = nil begin + updated_ats = sideload_resource_proxies.map(&:updated_at) updated_ats << @object.maximum(:updated_at) + updated_time = updated_ats.compact.max rescue => e - Graphiti.log("error calculating last_modified_at for #{@resource.class}") + Graphiti.log(["error calculating last_modified_at for #{@resource.class}", :red]) Graphiti.log(e) end - updated_ats.compact.max + return updated_time || Time.now end alias_method :last_modified_at, :updated_at From d2a7a038a649818979d52ccd898e68dba78b051f Mon Sep 17 00:00:00 2001 From: Jeff Keen Date: Wed, 17 Apr 2024 18:28:26 -0500 Subject: [PATCH 37/46] fix: properly display .find vs .all in debugger statements --- lib/graphiti/debugger.rb | 6 +++--- lib/graphiti/scope.rb | 5 +++-- lib/graphiti/util/cache_debug.rb | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/graphiti/debugger.rb b/lib/graphiti/debugger.rb index 92165d35..8b81f8f7 100644 --- a/lib/graphiti/debugger.rb +++ b/lib/graphiti/debugger.rb @@ -36,7 +36,7 @@ def on_data(name, start, stop, id, payload) json[:sideload] = sideload.name end if params - query = "#{payload[:resource].class.name}.all(#{JSON.pretty_generate(params)}).data" + query = "#{payload[:resource].class.name}.#{payload[:action]}(#{JSON.pretty_generate(params)}).data" logs << [query, :cyan, true] logs << ["The error occurred when running the above query. Copy/paste it into a rake task or Rails console session to reproduce. Keep in mind you may have to set context.", :yellow, true] else @@ -64,7 +64,7 @@ def on_data(name, start, stop, id, payload) query = if sideload.class.scope_proc "#{payload[:resource].class.name}: Manual sideload via .scope" else - "#{payload[:resource].class.name}.all(#{params.inspect})" + "#{payload[:resource].class.name}.#{payload[:action]}(#{params.inspect})" end logs << [" #{query}", :cyan, true] json[:query] = query @@ -82,7 +82,7 @@ def on_data(name, start, stop, id, payload) title = "Top Level Data Retrieval (+ sideloads):" logs << [title, :green, true] json[:title] = title - query = "#{payload[:resource].class.name}.all(#{params.inspect})" + query = "#{payload[:resource].class.name}.#{payload[:action]}(#{params.inspect})" logs << [query, :cyan, true] json[:query] = query logs << ["Returned Models: #{results}"] if debug_models diff --git a/lib/graphiti/scope.rb b/lib/graphiti/scope.rb index a759a715..ae106bf1 100644 --- a/lib/graphiti/scope.rb +++ b/lib/graphiti/scope.rb @@ -129,9 +129,10 @@ def sideload_resource_proxies def broadcast_data opts = { resource: @resource, - params: @opts[:params], + params: @opts[:params] || @query.params, sideload: @opts[:sideload], - parent: @opts[:parent] + parent: @opts[:parent], + action: @query.action # Set once data is resolved within block # results: ... } diff --git a/lib/graphiti/util/cache_debug.rb b/lib/graphiti/util/cache_debug.rb index de42cf77..cc7c3783 100644 --- a/lib/graphiti/util/cache_debug.rb +++ b/lib/graphiti/util/cache_debug.rb @@ -12,7 +12,7 @@ def last_version end def name - "#{Graphiti.context[:object].request.method} #{Graphiti.context[:object].request.url}" + "#{Graphiti.context[:object]&.request&.method} #{Graphiti.context[:object]&.request&.url}" end def key From 1479411646a841be56f8dda613b4e9f63ba2b576 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 18 Apr 2024 02:54:43 +0000 Subject: [PATCH 38/46] 1.7.1 CHANGELOG [skip ci] ## [1.7.1](https://github.com/graphiti-api/graphiti/compare/v1.7.0...v1.7.1) (2024-04-18) ### Bug Fixes * properly display .find vs .all in debugger statements ([d2a7a03](https://github.com/graphiti-api/graphiti/commit/d2a7a038a649818979d52ccd898e68dba78b051f)) * rescue error from sideloads updated_at calculation, defaulting to the current time ([661e3b5](https://github.com/graphiti-api/graphiti/commit/661e3b5212e2649870a200067d0d5d52fa962637)) --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bceb2df4..e78ef44e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ graphiti changelog +## [1.7.1](https://github.com/graphiti-api/graphiti/compare/v1.7.0...v1.7.1) (2024-04-18) + + +### Bug Fixes + +* properly display .find vs .all in debugger statements ([d2a7a03](https://github.com/graphiti-api/graphiti/commit/d2a7a038a649818979d52ccd898e68dba78b051f)) +* rescue error from sideloads updated_at calculation, defaulting to the current time ([661e3b5](https://github.com/graphiti-api/graphiti/commit/661e3b5212e2649870a200067d0d5d52fa962637)) + # [1.7.0](https://github.com/graphiti-api/graphiti/compare/v1.6.4...v1.7.0) (2024-03-27) From bb2a48874a6533522df6eb027d0df8ec14c80a20 Mon Sep 17 00:00:00 2001 From: Andrei Mochalov Date: Tue, 11 Jun 2024 19:26:36 +0300 Subject: [PATCH 39/46] fix: require necessary ActiveSupport parts in proper order * fix: require necessary ActiveSupport parts in proper order * fix: make gem compatible with older ActiveSupport versions --- lib/graphiti.rb | 5 ++++- lib/graphiti/scope.rb | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/graphiti.rb b/lib/graphiti.rb index 45df3091..ddae562d 100644 --- a/lib/graphiti.rb +++ b/lib/graphiti.rb @@ -1,13 +1,16 @@ require "json" require "forwardable" require "uri" + +require "active_support/version" +require "active_support/deprecation" +require "active_support/deprecator" if ::ActiveSupport.version >= Gem::Version.new("7.1") require "active_support/core_ext/string" require "active_support/core_ext/enumerable" require "active_support/core_ext/class/attribute" require "active_support/core_ext/hash/conversions" # to_xml require "active_support/concern" require "active_support/time" -require "active_support/deprecation" require "dry-types" require "graphiti_errors" diff --git a/lib/graphiti/scope.rb b/lib/graphiti/scope.rb index ae106bf1..0ddfd313 100644 --- a/lib/graphiti/scope.rb +++ b/lib/graphiti/scope.rb @@ -102,7 +102,7 @@ def updated_at Graphiti.log(e) end - return updated_time || Time.now + updated_time || Time.now end alias_method :last_modified_at, :updated_at From ad4f04e69e1a4d7c948680201fc4c02d0f2df106 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 11 Jun 2024 16:30:22 +0000 Subject: [PATCH 40/46] 1.7.2 CHANGELOG [skip ci] ## [1.7.2](https://github.com/graphiti-api/graphiti/compare/v1.7.1...v1.7.2) (2024-06-11) ### Bug Fixes * require necessary ActiveSupport parts in proper order ([bb2a488](https://github.com/graphiti-api/graphiti/commit/bb2a48874a6533522df6eb027d0df8ec14c80a20)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e78ef44e..e160340d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ graphiti changelog +## [1.7.2](https://github.com/graphiti-api/graphiti/compare/v1.7.1...v1.7.2) (2024-06-11) + + +### Bug Fixes + +* require necessary ActiveSupport parts in proper order ([bb2a488](https://github.com/graphiti-api/graphiti/commit/bb2a48874a6533522df6eb027d0df8ec14c80a20)) + ## [1.7.1](https://github.com/graphiti-api/graphiti/compare/v1.7.0...v1.7.1) (2024-04-18) From e0fa18a8d7f051e385e6e081f79f2ecae92a9260 Mon Sep 17 00:00:00 2001 From: Andrei Mochalov Date: Wed, 26 Jun 2024 20:32:22 +0300 Subject: [PATCH 41/46] fix: require OpenStruct explicitly (#475) --- lib/graphiti.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/graphiti.rb b/lib/graphiti.rb index ddae562d..9e23e554 100644 --- a/lib/graphiti.rb +++ b/lib/graphiti.rb @@ -1,6 +1,7 @@ require "json" require "forwardable" require "uri" +require "ostruct" unless defined?(::OpenStruct) require "active_support/version" require "active_support/deprecation" From 15ba979ce9e57a4cb7505cbdcf639c96d11146ba Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 26 Jun 2024 17:35:55 +0000 Subject: [PATCH 42/46] 1.7.3 CHANGELOG [skip ci] ## [1.7.3](https://github.com/graphiti-api/graphiti/compare/v1.7.2...v1.7.3) (2024-06-26) ### Bug Fixes * require OpenStruct explicitly ([#475](https://github.com/graphiti-api/graphiti/issues/475)) ([e0fa18a](https://github.com/graphiti-api/graphiti/commit/e0fa18a8d7f051e385e6e081f79f2ecae92a9260)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e160340d..4d4da11a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ graphiti changelog +## [1.7.3](https://github.com/graphiti-api/graphiti/compare/v1.7.2...v1.7.3) (2024-06-26) + + +### Bug Fixes + +* require OpenStruct explicitly ([#475](https://github.com/graphiti-api/graphiti/issues/475)) ([e0fa18a](https://github.com/graphiti-api/graphiti/commit/e0fa18a8d7f051e385e6e081f79f2ecae92a9260)) + ## [1.7.2](https://github.com/graphiti-api/graphiti/compare/v1.7.1...v1.7.2) (2024-06-11) From 8313e3359f0dde28d9940867c7ded964db4c854d Mon Sep 17 00:00:00 2001 From: Mihai Muntenas Date: Wed, 11 Sep 2024 19:04:08 +0200 Subject: [PATCH 43/46] fix: update ActiveRecord adapter w/ support for Rails 7.2+ (#478) In ActiveRecord 7.2 the `clear_active_connections` was (re)moved to the `connection_handler` property. --- lib/graphiti/adapters/active_record.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/graphiti/adapters/active_record.rb b/lib/graphiti/adapters/active_record.rb index c290f92c..ee66a8ef 100644 --- a/lib/graphiti/adapters/active_record.rb +++ b/lib/graphiti/adapters/active_record.rb @@ -304,7 +304,11 @@ def destroy(model_instance) end def close - ::ActiveRecord::Base.clear_active_connections! + if ::ActiveRecord.version > 7.2 + ::ActiveRecord::Base.connection_handler.clear_active_connections! + else + ::ActiveRecord::Base.clear_active_connections! + end end def can_group? From 4dca1ef79aa2263174066314b6308f3c6432fba9 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 11 Sep 2024 17:07:41 +0000 Subject: [PATCH 44/46] 1.7.4 CHANGELOG [skip ci] ## [1.7.4](https://github.com/graphiti-api/graphiti/compare/v1.7.3...v1.7.4) (2024-09-11) ### Bug Fixes * update ActiveRecord adapter w/ support for Rails 7.2+ ([#478](https://github.com/graphiti-api/graphiti/issues/478)) ([8313e33](https://github.com/graphiti-api/graphiti/commit/8313e3359f0dde28d9940867c7ded964db4c854d)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d4da11a..cacd0856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ graphiti changelog +## [1.7.4](https://github.com/graphiti-api/graphiti/compare/v1.7.3...v1.7.4) (2024-09-11) + + +### Bug Fixes + +* update ActiveRecord adapter w/ support for Rails 7.2+ ([#478](https://github.com/graphiti-api/graphiti/issues/478)) ([8313e33](https://github.com/graphiti-api/graphiti/commit/8313e3359f0dde28d9940867c7ded964db4c854d)) + ## [1.7.3](https://github.com/graphiti-api/graphiti/compare/v1.7.2...v1.7.3) (2024-06-26) From 42c82c397f20eb91c02835e518ff4c351c028ea7 Mon Sep 17 00:00:00 2001 From: Mihai Muntenas Date: Mon, 16 Sep 2024 15:03:41 +0200 Subject: [PATCH 45/46] fix: Fixes error in version check for ActiveRecord adapter introduced in #478 (#479) --- lib/graphiti/adapters/active_record.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/graphiti/adapters/active_record.rb b/lib/graphiti/adapters/active_record.rb index ee66a8ef..366ac71a 100644 --- a/lib/graphiti/adapters/active_record.rb +++ b/lib/graphiti/adapters/active_record.rb @@ -304,7 +304,7 @@ def destroy(model_instance) end def close - if ::ActiveRecord.version > 7.2 + if ::ActiveRecord.version > "7.2" ::ActiveRecord::Base.connection_handler.clear_active_connections! else ::ActiveRecord::Base.clear_active_connections! From 4cad908fd054606b60bbeb1bb57c40e32b028d62 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 16 Sep 2024 13:24:05 +0000 Subject: [PATCH 46/46] 1.7.5 CHANGELOG [skip ci] ## [1.7.5](https://github.com/graphiti-api/graphiti/compare/v1.7.4...v1.7.5) (2024-09-16) ### Bug Fixes * Fixes error in version check for ActiveRecord adapter introduced in [#478](https://github.com/graphiti-api/graphiti/issues/478) ([#479](https://github.com/graphiti-api/graphiti/issues/479)) ([42c82c3](https://github.com/graphiti-api/graphiti/commit/42c82c397f20eb91c02835e518ff4c351c028ea7)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cacd0856..9e84a3dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ graphiti changelog +## [1.7.5](https://github.com/graphiti-api/graphiti/compare/v1.7.4...v1.7.5) (2024-09-16) + + +### Bug Fixes + +* Fixes error in version check for ActiveRecord adapter introduced in [#478](https://github.com/graphiti-api/graphiti/issues/478) ([#479](https://github.com/graphiti-api/graphiti/issues/479)) ([42c82c3](https://github.com/graphiti-api/graphiti/commit/42c82c397f20eb91c02835e518ff4c351c028ea7)) + ## [1.7.4](https://github.com/graphiti-api/graphiti/compare/v1.7.3...v1.7.4) (2024-09-11)