graphql-persisted_queries 1.1.1 → 1.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +4 -3
  3. data/CHANGELOG.md +22 -0
  4. data/README.md +24 -12
  5. data/Rakefile +29 -1
  6. data/benchmark/compiled_queries.rb +41 -0
  7. data/benchmark/helpers.rb +31 -0
  8. data/benchmark/persisted_queries.rb +41 -0
  9. data/benchmark/plain_gql.rb +33 -0
  10. data/docs/alternative_stores.md +3 -0
  11. data/docs/compiled_queries_benchmark.md +75 -0
  12. data/gemfiles/{graphql_1_8.gemfile → graphql_1_11.gemfile} +1 -1
  13. data/gemfiles/{graphql_1_9.gemfile → graphql_1_12_0.gemfile} +1 -1
  14. data/gemfiles/graphql_1_12_4.gemfile +5 -0
  15. data/graphql-persisted_queries.gemspec +1 -1
  16. data/lib/graphql/persisted_queries.rb +24 -1
  17. data/lib/graphql/persisted_queries/compiled_queries/multiplex_patch.rb +30 -0
  18. data/lib/graphql/persisted_queries/compiled_queries/query_patch.rb +33 -0
  19. data/lib/graphql/persisted_queries/compiled_queries/resolver.rb +38 -0
  20. data/lib/graphql/persisted_queries/errors.rb +19 -0
  21. data/lib/graphql/persisted_queries/multiplex_resolver.rb +1 -1
  22. data/lib/graphql/persisted_queries/resolver.rb +11 -33
  23. data/lib/graphql/persisted_queries/resolver_helpers.rb +26 -0
  24. data/lib/graphql/persisted_queries/schema_patch.rb +29 -20
  25. data/lib/graphql/persisted_queries/store_adapters/base_store_adapter.rb +14 -4
  26. data/lib/graphql/persisted_queries/store_adapters/redis_store_adapter.rb +1 -1
  27. data/lib/graphql/persisted_queries/store_adapters/redis_with_local_cache_store_adapter.rb +2 -2
  28. data/lib/graphql/persisted_queries/version.rb +1 -1
  29. metadata +17 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff010e4d5d11f8e7028d4152a880de6d4503bda6b6e777fbc0e7b3bc19058ac8
4
- data.tar.gz: 0d213b909ab7668c8248d64ab43c1e80f223d5b46b3d9b3954272728e5b96cec
3
+ metadata.gz: 14b9f8eee9d0aefb838939b35f5ef59595e752f45aaf91eef16d43c3e2ed7b79
4
+ data.tar.gz: 365916fb01423286c20c8272339f475aca06b3647bd20a7c9e5a7a1269d98a87
5
5
  SHA512:
6
- metadata.gz: fc0ec99c3950301f1d23779b7706f24f77b5bbffd4f6df9d167379fc25eff9e6eebeb12296331c9a50972152b9fd7e457262bf6fa8379e2dce6d9e4668cb6e9b
7
- data.tar.gz: dd09cab2aff3905cd6afca029f16377b963856f160eb9c4730ce64fdc2b8cbce861f402c26f748d38fa1fd466a9af04165d284f9792f33f777ffe5c8e2eaebcb
6
+ metadata.gz: f1bfe7b99038394699faeabc5184f2150ed5144a54c88fd8080ed6f6deca7822d6ea0ce6cee8ee3cad2eabdb3978951d6989ff9a91e0429a0c967d1cf9459f05
7
+ data.tar.gz: bd1b40c0d63eb69aeed81cc361d7c6d9932c9d97a48e423a317bd426bfe0fbd3b47d8d27262b4c86c66695f53108ddcd3418b94cacf9c60630267f5919cbb1ed
@@ -20,9 +20,10 @@ jobs:
20
20
  matrix:
21
21
  ruby: [2.3, 2.4, 2.5, 2.6, 2.7]
22
22
  gemfile: [
23
- "gemfiles/graphql_1_8.gemfile",
24
- "gemfiles/graphql_1_9.gemfile",
25
23
  "gemfiles/graphql_1_10.gemfile",
24
+ "gemfiles/graphql_1_11.gemfile",
25
+ "gemfiles/graphql_1_12_0.gemfile",
26
+ "gemfiles/graphql_1_12_4.gemfile",
26
27
  "gemfiles/graphql_master.gemfile"
27
28
  ]
28
29
 
@@ -49,4 +50,4 @@ jobs:
49
50
  bundle update
50
51
  - name: Run RSpec
51
52
  run: |
52
- bundle exec rake spec
53
+ bundle exec rake ci_specs
data/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.2.4 (2021-06-07)
6
+
7
+ - [PR#50](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/50) Support empty redis_client arg on redis with locale cache ([@louim][])
8
+
9
+ ## 1.2.3 (2021-05-14)
10
+
11
+ - [PR#49](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/49) Allow nil redis_client with ENV["REDIS_URL"] ([@louim][])
12
+
13
+ ## 1.2.2 (2021-04-21)
14
+
15
+ - [PR#47](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/47) Properly initialize memory adapter inside RedisWithLocalCacheStoreAdapter ([@DmitryTsepelev][])
16
+
17
+ ## 1.2.1 (2021-03-07)
18
+
19
+ - [PR#43](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/43) Properly handle configuration when schema is inherited ([@DmitryTsepelev][])
20
+ - [PR#44](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/44) Deprecate graphql-ruby 1.8 and 1.9 ([@DmitryTsepelev][])
21
+
22
+ ## 1.2.0 (2021-02-24)
23
+
24
+ - [PR#39](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/39) Implement compiled queries ([@DmitryTsepelev][])
25
+
5
26
  ## 1.1.1 (2020-12-03)
6
27
 
7
28
  - [PR#37](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/37) Fix deprecation warnings ([@rbviz][])
@@ -65,3 +86,4 @@
65
86
  [@JanStevens]: https://github.com/JanStevens
66
87
  [@ogidow]: https://github.com/ogidow
67
88
  [@rbviz]: https://github.com/rbviz
89
+ [@louim]: https://github.com/louim
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GraphQL::PersistedQueries
2
2
 
3
- `GraphQL::PersistedQueries` is the implementation of [persisted queries](https://github.com/apollographql/apollo-link-persisted-queries) for [graphql-ruby](https://github.com/rmosolgo/graphql-ruby). With this plugin your backend will cache all the queries, while frontend will send the full query only when it's not found at the backend storage.
3
+ `GraphQL::PersistedQueries` is the implementation of [persisted queries](https://www.apollographql.com/docs/react/api/link/persisted-queries/) for [graphql-ruby](https://github.com/rmosolgo/graphql-ruby). With this plugin your backend will cache all the queries, while frontend will send the full query only when it's not found at the backend storage.
4
4
 
5
5
  - 🗑**Heavy query parameter will be omitted in most of cases** – network requests will become less heavy
6
6
  - 🤝**Clients share cached queries** – it's enough to miss cache only once for each unique query
@@ -15,20 +15,18 @@
15
15
 
16
16
  ## Getting started
17
17
 
18
- First of all, install and configure [apollo-link-persisted-queries](https://github.com/apollographql/apollo-link-persisted-queries) on the front–end side:
18
+ First of all, install and configure [apollo's persisted queries](https://www.apollographql.com/docs/react/api/link/persisted-queries/) on the front–end side:
19
19
 
20
20
  ```js
21
- import { createPersistedQueryLink } from "apollo-link-persisted-queries";
22
- import { createHttpLink } from "apollo-link-http";
23
- import { InMemoryCache } from "apollo-cache-inmemory";
24
- import ApolloClient from "apollo-client";
21
+ import { HttpLink, InMemoryCache, ApolloClient } from "@apollo/client";
22
+ import { createPersistedQueryLink } from "@apollo/client/link/persisted-queries";
23
+ import { sha256 } from 'crypto-hash';
25
24
 
26
-
27
- // use this with Apollo Client
28
- const link = createPersistedQueryLink().concat(createHttpLink({ uri: "/graphql" }));
25
+ const httpLink = new HttpLink({ uri: "/graphql" });
26
+ const persistedQueriesLink = createPersistedQueryLink({ sha256 });
29
27
  const client = new ApolloClient({
30
28
  cache: new InMemoryCache(),
31
- link: link,
29
+ link: persistedQueriesLink.concat(httpLink);
32
30
  });
33
31
  ```
34
32
 
@@ -65,6 +63,20 @@ GraphqlSchema.execute(
65
63
 
66
64
  You're all set!
67
65
 
66
+ ## Compiled queries (increases performance up to 2x!)
67
+
68
+ When query arrives to the backend, GraphQL execution engine needs some time to _parse_ it and build the AST. In case of a huge query it might take [a lot](https://gist.github.com/DmitryTsepelev/36e290cf64b4ec0b18294d0a57fb26ff#file-1_result-md) of time. What if we cache the AST instead of a query text and skip parsing completely? The only thing you need to do is to turn `:compiled_queries` option on:
69
+
70
+ ```ruby
71
+ class GraphqlSchema < GraphQL::Schema
72
+ use GraphQL::PersistedQueries, compiled_queries: true
73
+ end
74
+ ```
75
+
76
+ Using this option might make your endpoint up to 2x faster according to the [benchmark](docs/compiled_queries_benchmark.md).
77
+
78
+ **Heads up!** This feature only works on `graphql-ruby` 1.12.0 or later, but I guess it might be backported.
79
+
68
80
  ## Advanced usage
69
81
 
70
82
  All the queries are stored in memory by default, but you can easily switch to another storage (e.g., _redis_:
@@ -81,9 +93,9 @@ When the error occurs, the gem tries to not interrupt the regular flow of the ap
81
93
 
82
94
  Since our queries are slim now, we can switch back to HTTP GET, you can find a [guide](docs/http_cache.md) here.
83
95
 
84
- [batch-link](https://www.apollographql.com/docs/link/links/batch-http/) allows to group queries on the client side into a single HTTP request before sending to the server. In this case you need to use `GraphqlSchema.multiplex(queries)` instead of `#execute`. The gem supports it too, no action required!
96
+ [batch-link](https://www.apollographql.com/docs/react/api/link/apollo-link-batch-http/) allows to group queries on the client side into a single HTTP request before sending to the server. In this case you need to use `GraphqlSchema.multiplex(queries)` instead of `#execute`. The gem supports it too, no action required!
85
97
 
86
- [apollo-link-persisted-queries](https://github.com/apollographql/apollo-link-persisted-queries) uses _SHA256_ for building hashes by default. Check out this [guide](docs/hash.md) if you want to override this behavior.
98
+ [persisted-queries-link](https://www.apollographql.com/docs/react/api/link/persisted-queries/) uses _SHA256_ for building hashes by default. Check out this [guide](docs/hash.md) if you want to override this behavior.
87
99
 
88
100
  An experimental tracing feature can be enabled by setting `tracing: true` when configuring the plugin. Read more about this feature in the [Tracing guide](docs/tracing.md).
89
101
 
data/Rakefile CHANGED
@@ -5,4 +5,32 @@ require "rubocop/rake_task"
5
5
  RSpec::Core::RakeTask.new(:spec)
6
6
  RuboCop::RakeTask.new
7
7
 
8
- task default: [:rubocop, :spec]
8
+ desc "Run specs for compiled queries"
9
+ RSpec::Core::RakeTask.new("spec:compiled_queries") do |task|
10
+ task.pattern = "**/compiled_queries/**"
11
+ task.verbose = false
12
+ end
13
+
14
+ RSpec::Core::RakeTask.new("spec:without_compiled_queries") do |task|
15
+ task.exclude_pattern = "**/compiled_queries/**"
16
+ task.verbose = false
17
+ end
18
+
19
+ task ci_specs: ["spec:without_compiled_queries", "spec:compiled_queries"]
20
+
21
+ task :bench_gql do
22
+ cmd = %w[bundle exec ruby benchmark/plain_gql.rb]
23
+ system(*cmd)
24
+ end
25
+
26
+ task :bench_pq do
27
+ cmd = %w[bundle exec ruby benchmark/persisted_queries.rb]
28
+ system(*cmd)
29
+ end
30
+
31
+ task :bench_compiled do
32
+ cmd = %w[bundle exec ruby benchmark/compiled_queries.rb]
33
+ system(*cmd)
34
+ end
35
+
36
+ task bench: [:bench_gql, :bench_pq, :bench_compiled]
@@ -0,0 +1,41 @@
1
+ require "bundler/inline"
2
+
3
+ gemfile do
4
+ source "https://rubygems.org"
5
+ gem "graphql", "1.12.4"
6
+ end
7
+
8
+ $:.push File.expand_path("../lib", __dir__)
9
+
10
+ require "benchmark"
11
+ require "graphql/persisted_queries"
12
+ require_relative "helpers"
13
+
14
+ class GraphqlSchema < GraphQL::Schema
15
+ use GraphQL::PersistedQueries, compiled_queries: true
16
+
17
+ query QueryType
18
+ end
19
+
20
+ GraphqlSchema.to_definition
21
+
22
+ puts
23
+ puts "Schema with compiled queries:"
24
+ puts
25
+
26
+ Benchmark.bm(28) do |x|
27
+ [false, true].each do |with_nested|
28
+ FIELD_COUNTS.each do |field_count|
29
+ query = generate_query(field_count, with_nested)
30
+ sha256 = Digest::SHA256.hexdigest(query)
31
+
32
+ context = { extensions: { "persistedQuery" => { "sha256Hash" => sha256 } } }
33
+ # warmup
34
+ GraphqlSchema.execute(query, context: context)
35
+
36
+ x.report("#{field_count} fields#{" (nested)" if with_nested}") do
37
+ GraphqlSchema.execute(query, context: context)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,31 @@
1
+ FIELD_COUNTS = [10, 50, 100, 200, 300]
2
+
3
+ def generate_fields(field_count, with_nested)
4
+ fields = field_count.times.map do |i|
5
+ field = "field#{i+1}"
6
+ field += "\s{#{generate_fields(field_count, false)}}" if with_nested
7
+ field
8
+ end
9
+
10
+ fields.join("\n")
11
+ end
12
+
13
+ def generate_query(field_count, with_nested)
14
+ <<-gql
15
+ query {
16
+ #{generate_fields(field_count, with_nested)}
17
+ }
18
+ gql
19
+ end
20
+
21
+ class ChildType < GraphQL::Schema::Object
22
+ FIELD_COUNTS.max.times do |i|
23
+ field "field#{i + 1}".to_sym, String, null: false, method: :itself
24
+ end
25
+ end
26
+
27
+ class QueryType < GraphQL::Schema::Object
28
+ FIELD_COUNTS.max.times do |i|
29
+ field "field#{i + 1}".to_sym, ChildType, null: false, method: :itself
30
+ end
31
+ end
@@ -0,0 +1,41 @@
1
+ require "bundler/inline"
2
+
3
+ gemfile do
4
+ source "https://rubygems.org"
5
+ gem "graphql", "1.12.4"
6
+ end
7
+
8
+ $:.push File.expand_path("../lib", __dir__)
9
+
10
+ require "benchmark"
11
+ require "graphql/persisted_queries"
12
+ require_relative "helpers"
13
+
14
+ class GraphqlSchema < GraphQL::Schema
15
+ use GraphQL::PersistedQueries
16
+
17
+ query QueryType
18
+ end
19
+
20
+ GraphqlSchema.to_definition
21
+
22
+ puts
23
+ puts "Schema with persisted queries:"
24
+ puts
25
+
26
+ Benchmark.bm(28) do |x|
27
+ [false, true].each do |with_nested|
28
+ FIELD_COUNTS.each do |field_count|
29
+ query = generate_query(field_count, with_nested)
30
+ sha256 = Digest::SHA256.hexdigest(query)
31
+ context = { extensions: { "persistedQuery" => { "sha256Hash" => sha256 } } }
32
+
33
+ # warmup
34
+ GraphqlSchema.execute(query, context: context)
35
+
36
+ x.report("#{field_count} fields#{" (nested)" if with_nested}") do
37
+ GraphqlSchema.execute(query, context: context)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ require "bundler/inline"
2
+
3
+ gemfile do
4
+ source "https://rubygems.org"
5
+ gem "graphql", "1.12.4"
6
+ end
7
+
8
+ $:.push File.expand_path("../lib", __dir__)
9
+
10
+ require "benchmark"
11
+ require "graphql/persisted_queries"
12
+ require_relative "helpers"
13
+
14
+ class GraphqlSchema < GraphQL::Schema
15
+ query QueryType
16
+ end
17
+
18
+ GraphqlSchema.to_definition
19
+
20
+ puts "Plain schema:"
21
+ puts
22
+
23
+ Benchmark.bm(28) do |x|
24
+ [false, true].each do |with_nested|
25
+ FIELD_COUNTS.each do |field_count|
26
+ query = generate_query(field_count, with_nested)
27
+
28
+ x.report("#{field_count} fields#{" (nested)" if with_nested}") do
29
+ GraphqlSchema.execute(query)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -25,6 +25,9 @@ class GraphqlSchema < GraphQL::Schema
25
25
  use GraphQL::PersistedQueries,
26
26
  store: :redis,
27
27
  redis_client: ConnectionPool.new { Redis.new(url: "redis://127.0.0.2:2214/7") }
28
+ # or with ENV["REDIS_URL"]
29
+ use GraphQL::PersistedQueries,
30
+ store: :redis
28
31
  end
29
32
  ```
30
33
 
@@ -0,0 +1,75 @@
1
+ # Compiled queries benchmarks
2
+
3
+ The name of benchmark consists of a field count and optional "nested" label. In case of non–nested one we just generate a query with that field count, e.g. `2 fields` means:
4
+
5
+ ```gql
6
+ query {
7
+ field1
8
+ field2
9
+ }
10
+ ```
11
+
12
+ In case of "nested" benchmark we also put a list of fields to each top–level field, e.g. `2 fields (nested)` means:
13
+
14
+ ```gql
15
+ query {
16
+ field1 {
17
+ field1
18
+ field2
19
+ }
20
+ field2 {
21
+ field1
22
+ field2
23
+ }
24
+ }
25
+ ```
26
+
27
+ Field resolver just returns a string, so real–world tests might be way slower because of IO.
28
+
29
+ Here are the results:
30
+
31
+ ```
32
+ Plain schema:
33
+
34
+ user system total real
35
+ 10 fields 0.001061 0.000039 0.001100 ( 0.001114)
36
+ 50 fields 0.001658 0.000003 0.001661 ( 0.001661)
37
+ 100 fields 0.004587 0.000026 0.004613 ( 0.004614)
38
+ 200 fields 0.006447 0.000016 0.006463 ( 0.006476)
39
+ 300 fields 0.024493 0.000073 0.024566 ( 0.024614)
40
+ 10 fields (nested) 0.003061 0.000043 0.003104 ( 0.003109)
41
+ 50 fields (nested) 0.056927 0.000995 0.057922 ( 0.057997)
42
+ 100 fields (nested) 0.245235 0.001336 0.246571 ( 0.246727)
43
+ 200 fields (nested) 0.974444 0.006531 0.980975 ( 0.981810)
44
+ 300 fields (nested) 2.175855 0.012773 2.188628 ( 2.190130)
45
+
46
+ Schema with persisted queries:
47
+
48
+ user system total real
49
+ 10 fields 0.000606 0.000007 0.000613 ( 0.000607)
50
+ 50 fields 0.001855 0.000070 0.001925 ( 0.001915)
51
+ 100 fields 0.003239 0.000009 0.003248 ( 0.003239)
52
+ 200 fields 0.007542 0.000009 0.007551 ( 0.007551)
53
+ 300 fields 0.014975 0.000237 0.015212 ( 0.015318)
54
+ 10 fields (nested) 0.002992 0.000068 0.003060 ( 0.003049)
55
+ 50 fields (nested) 0.062314 0.000274 0.062588 ( 0.062662)
56
+ 100 fields (nested) 0.256404 0.000865 0.257269 ( 0.257419)
57
+ 200 fields (nested) 0.978408 0.007437 0.985845 ( 0.986579)
58
+ 300 fields (nested) 2.263338 0.010994 2.274332 ( 2.275967)
59
+
60
+ Schema with compiled queries:
61
+
62
+ user system total real
63
+ 10 fields 0.000526 0.000009 0.000535 ( 0.000530)
64
+ 50 fields 0.001280 0.000012 0.001292 ( 0.001280)
65
+ 100 fields 0.002292 0.000004 0.002296 ( 0.002286)
66
+ 200 fields 0.005462 0.000001 0.005463 ( 0.005463)
67
+ 300 fields 0.014229 0.000121 0.014350 ( 0.014348)
68
+ 10 fields (nested) 0.002027 0.000069 0.002096 ( 0.002104)
69
+ 50 fields (nested) 0.029933 0.000087 0.030020 ( 0.030040)
70
+ 100 fields (nested) 0.133933 0.000502 0.134435 ( 0.134756)
71
+ 200 fields (nested) 0.495052 0.003545 0.498597 ( 0.499452)
72
+ 300 fields (nested) 1.041463 0.005130 1.046593 ( 1.047137)
73
+ ```
74
+
75
+ Results gathered from my MacBook Pro Mid 2014 (2,5 GHz Quad-Core Intel Core i7, 16 GB 1600 MHz DDR3).
@@ -1,5 +1,5 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- gem "graphql", "~> 1.8.0"
3
+ gem "graphql", "~> 1.11.0"
4
4
 
5
5
  gemspec path: "../"
@@ -1,5 +1,5 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- gem "graphql", "~> 1.9.0"
3
+ gem "graphql", "~> 1.12.0"
4
4
 
5
5
  gemspec path: "../"
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "graphql", "~> 1.12.4"
4
+
5
+ gemspec path: "../"
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.required_ruby_version = ">= 2.3"
24
24
 
25
- spec.add_dependency "graphql", ">= 1.8"
25
+ spec.add_dependency "graphql", ">= 1.10"
26
26
 
27
27
  spec.add_development_dependency "rspec", "~> 3.9"
28
28
  spec.add_development_dependency "rake", ">= 10.0"
@@ -1,17 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "graphql/persisted_queries/resolver_helpers"
4
+ require "graphql/persisted_queries/errors"
3
5
  require "graphql/persisted_queries/error_handlers"
4
6
  require "graphql/persisted_queries/schema_patch"
5
7
  require "graphql/persisted_queries/store_adapters"
6
8
  require "graphql/persisted_queries/version"
7
9
  require "graphql/persisted_queries/builder_helpers"
8
10
 
11
+ require "graphql/persisted_queries/compiled_queries/resolver"
12
+ require "graphql/persisted_queries/compiled_queries/multiplex_patch"
13
+ require "graphql/persisted_queries/compiled_queries/query_patch"
14
+
9
15
  module GraphQL
10
16
  # Plugin definition
11
17
  module PersistedQueries
18
+ # rubocop:disable Metrics/MethodLength
12
19
  def self.use(schema_defn, **options)
13
20
  schema = schema_defn.is_a?(Class) ? schema_defn : schema_defn.target
14
- SchemaPatch.patch(schema)
21
+
22
+ compiled_queries = options.delete(:compiled_queries)
23
+ SchemaPatch.patch(schema, compiled_queries)
24
+ configure_compiled_queries if compiled_queries
15
25
 
16
26
  schema.hash_generator = options.delete(:hash_generator) || :sha256
17
27
 
@@ -25,5 +35,18 @@ module GraphQL
25
35
  store = options.delete(:store) || :memory
26
36
  schema.configure_persisted_query_store(store, **options)
27
37
  end
38
+ # rubocop:enable Metrics/MethodLength
39
+
40
+ def self.configure_compiled_queries
41
+ if Gem::Dependency.new("graphql", "< 1.12.0").match?("graphql", GraphQL::VERSION)
42
+ raise ArgumentError, "compiled_queries are not supported for graphql-ruby < 1.12.0"
43
+ end
44
+
45
+ GraphQL::Execution::Multiplex.singleton_class.prepend(
46
+ GraphQL::PersistedQueries::CompiledQueries::MultiplexPatch
47
+ )
48
+
49
+ GraphQL::Query.prepend(GraphQL::PersistedQueries::CompiledQueries::QueryPatch)
50
+ end
28
51
  end
29
52
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module PersistedQueries
5
+ module CompiledQueries
6
+ # Patches GraphQL::Execution::Multiplex to support compiled queries
7
+ module MultiplexPatch
8
+ if Gem::Dependency.new("graphql", ">= 1.12.4").match?("graphql", GraphQL::VERSION)
9
+ def begin_query(results, idx, query, multiplex)
10
+ return super unless query.persisted_query_not_found?
11
+
12
+ results[idx] = add_not_found_error(query)
13
+ end
14
+ else
15
+ def begin_query(query, multiplex)
16
+ return super unless query.persisted_query_not_found?
17
+
18
+ add_not_found_error(query)
19
+ end
20
+ end
21
+
22
+ def add_not_found_error(query)
23
+ query.context.errors.clear
24
+ query.context.errors << GraphQL::ExecutionError.new("PersistedQueryNotFound")
25
+ GraphQL::Execution::Multiplex::NO_OPERATION
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module PersistedQueries
5
+ module CompiledQueries
6
+ # Patches GraphQL::Query to support compiled queries
7
+ module QueryPatch
8
+ def persisted_query_not_found?
9
+ @persisted_query_not_found
10
+ end
11
+
12
+ def prepare_ast
13
+ return super unless @context[:extensions]
14
+
15
+ @document = resolver.fetch
16
+ not_loaded_document = @document.nil?
17
+
18
+ @persisted_query_not_found = not_loaded_document && query_string.nil?
19
+
20
+ super.tap do
21
+ resolver.persist(query_string, @document) if not_loaded_document && query_string
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def resolver
28
+ @resolver ||= Resolver.new(@schema, @context[:extensions])
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module PersistedQueries
5
+ module CompiledQueries
6
+ # Fetches and persists compiled query
7
+ class Resolver
8
+ include GraphQL::PersistedQueries::ResolverHelpers
9
+
10
+ def initialize(schema, extensions)
11
+ @schema = schema
12
+ @extensions = extensions
13
+ end
14
+
15
+ def fetch
16
+ return if hash.nil?
17
+
18
+ with_error_handling do
19
+ compiled_query = @schema.persisted_query_store.fetch_query(hash, compiled_query: true)
20
+ Marshal.load(compiled_query) if compiled_query # rubocop:disable Security/MarshalLoad
21
+ end
22
+ end
23
+
24
+ def persist(query_string, compiled_query)
25
+ return if hash.nil?
26
+
27
+ validate_hash!(query_string)
28
+
29
+ with_error_handling do
30
+ @schema.persisted_query_store.save_query(
31
+ hash, Marshal.dump(compiled_query), compiled_query: true
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module PersistedQueries
5
+ # Raised when persisted query is not found in the storage
6
+ class NotFound < StandardError
7
+ def message
8
+ "PersistedQueryNotFound"
9
+ end
10
+ end
11
+
12
+ # Raised when provided hash is not matched with query
13
+ class WrongHash < StandardError
14
+ def message
15
+ "Wrong hash was passed"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -33,7 +33,7 @@ module GraphQL
33
33
  return unless extensions
34
34
 
35
35
  query_params[:query] = Resolver.new(extensions, @schema).resolve(query_params[:query])
36
- rescue Resolver::NotFound, Resolver::WrongHash => e
36
+ rescue GraphQL::PersistedQueries::NotFound, GraphQL::PersistedQueries::WrongHash => e
37
37
  values = { "errors" => [{ "message" => e.message }] }
38
38
  query = GraphQL::Query.new(@schema, query_params[:query])
39
39
  results[pos] = GraphQL::Query::Result.new(query: query, values: values)
@@ -4,54 +4,32 @@ module GraphQL
4
4
  module PersistedQueries
5
5
  # Fetches or stores query string in the storage
6
6
  class Resolver
7
- # Raised when persisted query is not found in the storage
8
- class NotFound < StandardError
9
- def message
10
- "PersistedQueryNotFound"
11
- end
12
- end
13
-
14
- # Raised when provided hash is not matched with query
15
- class WrongHash < StandardError
16
- def message
17
- "Wrong hash was passed"
18
- end
19
- end
7
+ include GraphQL::PersistedQueries::ResolverHelpers
20
8
 
21
9
  def initialize(extensions, schema)
22
10
  @extensions = extensions
23
11
  @schema = schema
24
12
  end
25
13
 
26
- def resolve(query_str)
27
- return query_str if hash.nil?
14
+ def resolve(query_string)
15
+ return query_string if hash.nil?
28
16
 
29
- if query_str
30
- persist_query(query_str)
17
+ if query_string
18
+ persist_query(query_string)
31
19
  else
32
- query_str = with_error_handling { @schema.persisted_query_store.fetch_query(hash) }
33
- raise NotFound if query_str.nil?
20
+ query_string = with_error_handling { @schema.persisted_query_store.fetch_query(hash) }
21
+ raise GraphQL::PersistedQueries::NotFound if query_string.nil?
34
22
  end
35
23
 
36
- query_str
24
+ query_string
37
25
  end
38
26
 
39
27
  private
40
28
 
41
- def with_error_handling
42
- yield
43
- rescue StandardError => e
44
- @schema.persisted_query_error_handler.call(e)
45
- end
46
-
47
- def persist_query(query_str)
48
- raise WrongHash if @schema.hash_generator_proc.call(query_str) != hash
49
-
50
- with_error_handling { @schema.persisted_query_store.save_query(hash, query_str) }
51
- end
29
+ def persist_query(query_string)
30
+ validate_hash!(query_string)
52
31
 
53
- def hash
54
- @hash ||= @extensions.dig("persistedQuery", "sha256Hash")
32
+ with_error_handling { @schema.persisted_query_store.save_query(hash, query_string) }
55
33
  end
56
34
  end
57
35
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module PersistedQueries
5
+ # Helper functions for resolvers
6
+ module ResolverHelpers
7
+ module_function
8
+
9
+ def with_error_handling
10
+ yield
11
+ rescue StandardError => e
12
+ @schema.persisted_query_error_handler.call(e)
13
+ end
14
+
15
+ def validate_hash!(query_string)
16
+ return if @schema.hash_generator_proc.call(query_string) == hash
17
+
18
+ raise GraphQL::PersistedQueries::WrongHash
19
+ end
20
+
21
+ def hash
22
+ @hash ||= @extensions.dig("persistedQuery", "sha256Hash")
23
+ end
24
+ end
25
+ end
26
+ end
@@ -10,13 +10,23 @@ module GraphQL
10
10
  # Patches GraphQL::Schema to support persisted queries
11
11
  module SchemaPatch
12
12
  class << self
13
- def patch(schema)
14
- schema.singleton_class.class_eval { alias_method :multiplex_original, :multiplex }
13
+ def patch(schema, compiled_queries)
15
14
  schema.singleton_class.prepend(SchemaPatch)
15
+
16
+ return if compiled_queries
17
+
18
+ schema.singleton_class.class_eval { alias_method :multiplex_original, :multiplex }
19
+ schema.singleton_class.prepend(MultiplexPatch)
20
+ end
21
+ end
22
+
23
+ # Patches GraphQL::Schema to override multiplex (not needed for compiled queries)
24
+ module MultiplexPatch
25
+ def multiplex(queries, **kwargs)
26
+ MultiplexResolver.new(self, queries, **kwargs).resolve
16
27
  end
17
28
  end
18
29
 
19
- attr_reader :persisted_query_store, :hash_generator_proc, :persisted_query_error_handler
20
30
  attr_writer :persisted_queries_tracing_enabled
21
31
 
22
32
  def configure_persisted_query_store(store, **options)
@@ -25,6 +35,14 @@ module GraphQL
25
35
  end
26
36
  end
27
37
 
38
+ def persisted_query_store
39
+ @persisted_query_store ||= find_inherited_value(:persisted_query_store)
40
+ end
41
+
42
+ def persisted_query_error_handler
43
+ @persisted_query_error_handler ||= find_inherited_value(:persisted_query_error_handler)
44
+ end
45
+
28
46
  def configure_persisted_query_error_handler(handler)
29
47
  @persisted_query_error_handler = ErrorHandlers.build(handler)
30
48
  end
@@ -33,22 +51,17 @@ module GraphQL
33
51
  @hash_generator_proc = HashGeneratorBuilder.new(hash_generator).build
34
52
  end
35
53
 
36
- def verify_http_method=(verify)
37
- return unless verify
38
-
39
- if graphql10?
40
- query_analyzer(prepare_analyzer)
41
- else
42
- query_analyzers << prepare_analyzer
43
- end
54
+ def hash_generator_proc
55
+ @hash_generator_proc ||= find_inherited_value(:hash_generator_proc)
44
56
  end
45
57
 
46
- def persisted_queries_tracing_enabled?
47
- @persisted_queries_tracing_enabled
58
+ def verify_http_method=(verify)
59
+ query_analyzer(prepare_analyzer) if verify
48
60
  end
49
61
 
50
- def multiplex(queries, **kwargs)
51
- MultiplexResolver.new(self, queries, **kwargs).resolve
62
+ def persisted_queries_tracing_enabled?
63
+ @persisted_queries_tracing_enabled ||=
64
+ find_inherited_value(:persisted_queries_tracing_enabled?)
52
65
  end
53
66
 
54
67
  def tracer(name)
@@ -62,12 +75,8 @@ module GraphQL
62
75
 
63
76
  private
64
77
 
65
- def graphql10?
66
- Gem::Dependency.new("graphql", ">= 1.10.0").match?("graphql", GraphQL::VERSION)
67
- end
68
-
69
78
  def prepare_analyzer
70
- if graphql10? && using_ast_analysis?
79
+ if using_ast_analysis?
71
80
  require "graphql/persisted_queries/analyzers/http_method_ast_analyzer"
72
81
  Analyzers::HttpMethodAstAnalyzer
73
82
  else
@@ -12,15 +12,18 @@ module GraphQL
12
12
  @name = :base
13
13
  end
14
14
 
15
- def fetch_query(hash)
16
- fetch(hash).tap do |result|
15
+ def fetch_query(hash, compiled_query: false)
16
+ key = build_key(hash, compiled_query)
17
+
18
+ fetch(key).tap do |result|
17
19
  event = result ? "cache_hit" : "cache_miss"
18
20
  trace("fetch_query.#{event}", adapter: @name)
19
21
  end
20
22
  end
21
23
 
22
- def save_query(hash, query)
23
- trace("save_query", adapter: @name) { save(hash, query) }
24
+ def save_query(hash, query, compiled_query: false)
25
+ key = build_key(hash, compiled_query)
26
+ trace("save_query", adapter: @name) { save(key, query) }
24
27
  end
25
28
 
26
29
  protected
@@ -41,6 +44,13 @@ module GraphQL
41
44
  yield
42
45
  end
43
46
  end
47
+
48
+ private
49
+
50
+ def build_key(hash, compiled_query)
51
+ key = "#{RUBY_ENGINE}-#{RUBY_VERSION}:#{GraphQL::VERSION}:#{hash}"
52
+ compiled_query ? "compiled:#{key}" : key
53
+ end
44
54
  end
45
55
  end
46
56
  end
@@ -10,7 +10,7 @@ module GraphQL
10
10
  DEFAULT_EXPIRATION = 24 * 60 * 60
11
11
  DEFAULT_NAMESPACE = "graphql-persisted-query"
12
12
 
13
- def initialize(redis_client:, expiration: nil, namespace: nil)
13
+ def initialize(redis_client: {}, expiration: nil, namespace: nil)
14
14
  @redis_proc = build_redis_proc(redis_client)
15
15
  @expiration = expiration || DEFAULT_EXPIRATION
16
16
  @namespace = namespace || DEFAULT_NAMESPACE
@@ -8,7 +8,7 @@ module GraphQL
8
8
  DEFAULT_REDIS_ADAPTER_CLASS = RedisStoreAdapter
9
9
  DEFAULT_MEMORY_ADAPTER_CLASS = MemoryStoreAdapter
10
10
 
11
- def initialize(redis_client:, expiration: nil, namespace: nil, redis_adapter_class: nil,
11
+ def initialize(redis_client: {}, expiration: nil, namespace: nil, redis_adapter_class: nil,
12
12
  memory_adapter_class: nil)
13
13
  redis_adapter_class ||= DEFAULT_REDIS_ADAPTER_CLASS
14
14
  memory_adapter_class ||= DEFAULT_MEMORY_ADAPTER_CLASS
@@ -18,7 +18,7 @@ module GraphQL
18
18
  expiration: expiration,
19
19
  namespace: namespace
20
20
  )
21
- @memory_adapter = memory_adapter_class.new(nil)
21
+ @memory_adapter = memory_adapter_class.new
22
22
  @name = :redis_with_local_cache
23
23
  end
24
24
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module PersistedQueries
5
- VERSION = "1.1.1"
5
+ VERSION = "1.2.4"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-persisted_queries
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - DmitryTsepelev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-12-03 00:00:00.000000000 Z
11
+ date: 2021-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.8'
19
+ version: '1.10'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.8'
26
+ version: '1.10'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rspec
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -124,16 +124,22 @@ files:
124
124
  - LICENSE.txt
125
125
  - README.md
126
126
  - Rakefile
127
+ - benchmark/compiled_queries.rb
128
+ - benchmark/helpers.rb
129
+ - benchmark/persisted_queries.rb
130
+ - benchmark/plain_gql.rb
127
131
  - bin/console
128
132
  - bin/setup
129
133
  - docs/alternative_stores.md
134
+ - docs/compiled_queries_benchmark.md
130
135
  - docs/error_handling.md
131
136
  - docs/hash.md
132
137
  - docs/http_cache.md
133
138
  - docs/tracing.md
134
139
  - gemfiles/graphql_1_10.gemfile
135
- - gemfiles/graphql_1_8.gemfile
136
- - gemfiles/graphql_1_9.gemfile
140
+ - gemfiles/graphql_1_11.gemfile
141
+ - gemfiles/graphql_1_12_0.gemfile
142
+ - gemfiles/graphql_1_12_4.gemfile
137
143
  - gemfiles/graphql_master.gemfile
138
144
  - graphql-persisted_queries.gemspec
139
145
  - lib/graphql/persisted_queries.rb
@@ -141,12 +147,17 @@ files:
141
147
  - lib/graphql/persisted_queries/analyzers/http_method_ast_analyzer.rb
142
148
  - lib/graphql/persisted_queries/analyzers/http_method_validator.rb
143
149
  - lib/graphql/persisted_queries/builder_helpers.rb
150
+ - lib/graphql/persisted_queries/compiled_queries/multiplex_patch.rb
151
+ - lib/graphql/persisted_queries/compiled_queries/query_patch.rb
152
+ - lib/graphql/persisted_queries/compiled_queries/resolver.rb
144
153
  - lib/graphql/persisted_queries/error_handlers.rb
145
154
  - lib/graphql/persisted_queries/error_handlers/base_error_handler.rb
146
155
  - lib/graphql/persisted_queries/error_handlers/default_error_handler.rb
156
+ - lib/graphql/persisted_queries/errors.rb
147
157
  - lib/graphql/persisted_queries/hash_generator_builder.rb
148
158
  - lib/graphql/persisted_queries/multiplex_resolver.rb
149
159
  - lib/graphql/persisted_queries/resolver.rb
160
+ - lib/graphql/persisted_queries/resolver_helpers.rb
150
161
  - lib/graphql/persisted_queries/schema_patch.rb
151
162
  - lib/graphql/persisted_queries/store_adapters.rb
152
163
  - lib/graphql/persisted_queries/store_adapters/base_store_adapter.rb