graphql-persisted_queries 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3121fbd184f715130c9f9f1956dcd89c24d52507d23ac0b35a408a9d53d54e72
4
- data.tar.gz: 7e4b32a6b5d0fda8d9b135a435bca92587749ccb61664f37092c593ef4b04eb0
3
+ metadata.gz: f36403d19c66dd6b6627c571cd87744b7da8a1d27f47bbf1f1dd5f32357ae4b1
4
+ data.tar.gz: 3f47b64e03c5bba2f498455e53aa136e20955ef7b4045a4088b0c19903ebde43
5
5
  SHA512:
6
- metadata.gz: a42f753183b5d8aa8c0d8f321a988b61375ad6ae828ef9c232e91f4f20adb5da824b6b095cb034af805e11039f5c7122d52add4cdb4caa36bb873335a85e5847
7
- data.tar.gz: 539d3e7da1bed8a470555b2b411cd52a6631663e7f3927e0c9252988488800c205bfe10f4eb03b3baa34899e286420b9d743f411fefc54c9dc091a1406d75a96
6
+ metadata.gz: 50c222c3023ed556d6c3d46d49c2495e676c149fb8a887b623d76beb04b805dcb53bfb23dd1ed151675e7f2216282c99903b784161f13d7ff65de5837240fdc6
7
+ data.tar.gz: d9e359c4258a466bbd71fc8aa9873d0a100e878fba185b213e1305fc950c21ea75133e04104822a2f24f828ada9b4982e17f5ed4b068fdcfef055bc4098ac2b5
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.3.0 (2020-02-21)
6
+
7
+ - [PR#24](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/24) Add multiplex support ([@DmitryTsepelev][])
8
+ - [PR#23](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/23) Adapter for Redis-backed in-memory store ([@bmorton][])
9
+ - [PR#22](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/22) Add `verify_http_method` option restricting mutations to be performed via `GET` requests ([@DmitryTsepelev][])
10
+
5
11
  ## 0.2.0 (2020-02-11)
6
12
 
7
13
  - [PR#17](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/17) Allow an optional custom error handler so that implementors can control failure scenarios when query resolution fails ([@bmorton][])
data/README.md CHANGED
@@ -57,6 +57,10 @@ GraphqlSchema.execute(
57
57
 
58
58
  5. Run the app! 🔥
59
59
 
60
+ ## Usage with BatchLink
61
+
62
+ It's possible to group queries using [batch-link](https://www.apollographql.com/docs/link/links/batch-http/) and send them as a single HTTP request. In this case you need to use `GraphqlSchema.multiplex(queries)` instead of `#execute`. The gem supports it too, no action required!
63
+
60
64
  ## Alternative stores
61
65
 
62
66
  All the queries are stored in memory by default, but you can easily switch to _redis_:
@@ -97,6 +101,14 @@ class GraphqlSchema < GraphQL::Schema
97
101
  end
98
102
  ```
99
103
 
104
+ ### Supported stores
105
+
106
+ We currently support a few different stores that can be configured out of the box:
107
+
108
+ - `:memory`: This is the default in-memory store and is great for getting started, but will require each instance to cache results independently which can result in lots of ["new query path"](https://blog.apollographql.com/improve-graphql-performance-with-automatic-persisted-queries-c31d27b8e6ea) requests.
109
+ - `:redis`: This store will allow you to share a Redis cache across all instances of your GraphQL application so that each instance doesn't have to ask the client for the query again if it hasn't seen it yet.
110
+ - `:redis_with_local_cache`: This store combines both the `:memory` and `:redis` approaches so that we can reduce the number of network requests we make while mitigating the independent cache issue. This adapter is configured identically to the `:redis` store.
111
+
100
112
  ## Alternative hash functions
101
113
 
102
114
  [apollo-link-persisted-queries](https://github.com/apollographql/apollo-link-persisted-queries) uses _SHA256_ by default so this gem uses it as a default too, but if you want to override it – you can use `:hash_generator` option:
@@ -144,11 +156,17 @@ end
144
156
 
145
157
  ## GET requests and HTTP cache
146
158
 
147
- Using `GET` requests for persisted queries allows you to enable HTTP caching (e.g., turn on CDN). In order to make it work you should change the way link is initialized on front-end side (`createPersistedQueryLink({ useGETForHashedQueries: true })`) and register a new route `get "/graphql", to: "graphql#execute"`.
159
+ Using `GET` requests for persisted queries allows you to enable HTTP caching (e.g., turn on CDN). This is how to turn them on:
160
+ 1. Change the way link is initialized on front-end side (`createPersistedQueryLink({ useGETForHashedQueries: true })`);
161
+ 2. Register a new route `get "/graphql", to: "graphql#execute"`;
162
+ 3. Put the request object to the GraphQL context in the controller `GraphqlSchema.execute(query, variables: variables, context: { request: request })`;
163
+ 4. Turn the `verify_http_method` option on (`use GraphQL::PersistedQueries, verify_http_method: true`) to enforce using `POST` requests for performing mutations (otherwise the error `Mutations cannot be performed via HTTP GET` will be returned).
164
+
165
+ HTTP method verification is important, because when mutations are allowed via `GET` requests, it's easy to perform an attack by sending the link containing mutation to a signed in user.
148
166
 
149
167
  ## Contributing
150
168
 
151
- Bug reports and pull requests are welcome on GitHub at https://github.com/DmitryTsepelev/graphql-persisted_queries.
169
+ Bug reports and pull requests are welcome on GitHub at https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries.
152
170
 
153
171
  ## License
154
172
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module PersistedQueries
5
+ # Verifies that mutations are not executed using GET requests
6
+ class HttpMethodAnalyzer
7
+ def analyze?(query)
8
+ query.context[:request]
9
+ end
10
+
11
+ def initial_value(query)
12
+ {
13
+ get_request: query.context[:request]&.get?,
14
+ mutation: query.mutation?
15
+ }
16
+ end
17
+
18
+ def call(memo, _visit_type, _irep_node)
19
+ memo
20
+ end
21
+
22
+ def final_value(memo)
23
+ return if !memo[:get_request] || !memo[:mutation]
24
+
25
+ GraphQL::AnalysisError.new("Mutations cannot be performed via HTTP GET")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module PersistedQueries
5
+ # Resolves multiplex query
6
+ class MultiplexResolver
7
+ def initialize(schema, queries, kwargs)
8
+ @schema = schema
9
+ @queries = queries
10
+ @kwargs = kwargs
11
+ end
12
+
13
+ def resolve
14
+ resolve_persisted_queries
15
+ perform_multiplex
16
+ results
17
+ end
18
+
19
+ private
20
+
21
+ def results
22
+ @results ||= Array.new(@queries.count)
23
+ end
24
+
25
+ def resolve_persisted_queries
26
+ @queries.each_with_index do |query_params, i|
27
+ resolve_persisted_query(query_params, i)
28
+ end
29
+ end
30
+
31
+ def resolve_persisted_query(query_params, pos)
32
+ extensions = query_params.delete(:extensions)
33
+ return unless extensions
34
+
35
+ query_params[:query] = Resolver.new(extensions, @schema).resolve(query_params[:query])
36
+ rescue Resolver::NotFound, Resolver::WrongHash => e
37
+ results[pos] = { "errors" => [{ "message" => e.message }] }
38
+ end
39
+
40
+ def perform_multiplex
41
+ resolve_idx = (0...@queries.count).select { |i| results[i].nil? }
42
+ multiplex_result = @schema.multiplex_original(
43
+ resolve_idx.map { |i| @queries.at(i) }, @kwargs
44
+ )
45
+ resolve_idx.each_with_index { |res_i, mult_i| results[res_i] = multiplex_result[mult_i] }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -18,11 +18,9 @@ module GraphQL
18
18
  end
19
19
  end
20
20
 
21
- def initialize(extensions, store, hash_generator_proc, error_handler)
21
+ def initialize(extensions, schema)
22
22
  @extensions = extensions
23
- @store = store
24
- @hash_generator_proc = hash_generator_proc
25
- @error_handler = error_handler
23
+ @schema = schema
26
24
  end
27
25
 
28
26
  def resolve(query_str)
@@ -31,7 +29,7 @@ module GraphQL
31
29
  if query_str
32
30
  persist_query(query_str)
33
31
  else
34
- query_str = with_error_handling { @store.fetch_query(hash) }
32
+ query_str = with_error_handling { @schema.persisted_query_store.fetch_query(hash) }
35
33
  raise NotFound if query_str.nil?
36
34
  end
37
35
 
@@ -43,13 +41,13 @@ module GraphQL
43
41
  def with_error_handling
44
42
  yield
45
43
  rescue StandardError => e
46
- @error_handler.call(e)
44
+ @schema.persisted_query_error_handler.call(e)
47
45
  end
48
46
 
49
47
  def persist_query(query_str)
50
- raise WrongHash if @hash_generator_proc.call(query_str) != hash
48
+ raise WrongHash if @schema.hash_generator_proc.call(query_str) != hash
51
49
 
52
- with_error_handling { @store.save_query(hash, query_str) }
50
+ with_error_handling { @schema.persisted_query_store.save_query(hash, query_str) }
53
51
  end
54
52
 
55
53
  def hash
@@ -2,11 +2,19 @@
2
2
 
3
3
  require "graphql/persisted_queries/hash_generator_builder"
4
4
  require "graphql/persisted_queries/resolver"
5
+ require "graphql/persisted_queries/multiplex_resolver"
5
6
 
6
7
  module GraphQL
7
8
  module PersistedQueries
8
9
  # Patches GraphQL::Schema to support persisted queries
9
10
  module SchemaPatch
11
+ class << self
12
+ def patch(schema)
13
+ schema.singleton_class.class_eval { alias_method :multiplex_original, :multiplex }
14
+ schema.singleton_class.prepend(SchemaPatch)
15
+ end
16
+ end
17
+
10
18
  attr_reader :persisted_query_store, :hash_generator_proc, :persisted_query_error_handler
11
19
 
12
20
  def configure_persisted_query_store(store, options)
@@ -21,16 +29,20 @@ module GraphQL
21
29
  @hash_generator_proc = HashGeneratorBuilder.new(hash_generator).build
22
30
  end
23
31
 
24
- def execute(query_str = nil, **kwargs)
25
- if (extensions = kwargs.delete(:extensions))
26
- resolver = Resolver.new(extensions, persisted_query_store, hash_generator_proc,
27
- persisted_query_error_handler)
28
- query_str = resolver.resolve(query_str)
32
+ def verify_http_method=(verify)
33
+ return unless verify
34
+
35
+ analyzer = HttpMethodAnalyzer.new
36
+
37
+ if Gem::Dependency.new("graphql", ">= 1.10.0").match?("graphql", GraphQL::VERSION)
38
+ query_analyzer(analyzer)
39
+ else
40
+ query_analyzers << analyzer
29
41
  end
42
+ end
30
43
 
31
- super
32
- rescue Resolver::NotFound, Resolver::WrongHash => e
33
- { errors: [{ message: e.message }] }
44
+ def multiplex(queries, **kwargs)
45
+ MultiplexResolver.new(self, queries, kwargs).resolve
34
46
  end
35
47
  end
36
48
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module PersistedQueries
5
+ module StoreAdapters
6
+ # Memory adapter for storing persisted queries
7
+ class RedisWithLocalCacheStoreAdapter < BaseStoreAdapter
8
+ DEFAULT_REDIS_ADAPTER_CLASS = RedisStoreAdapter
9
+ DEFAULT_MEMORY_ADAPTER_CLASS = MemoryStoreAdapter
10
+
11
+ def initialize(redis_client:, expiration: nil, namespace: nil, redis_adapter_class: nil,
12
+ memory_adapter_class: nil)
13
+ redis_adapter_class ||= DEFAULT_REDIS_ADAPTER_CLASS
14
+ memory_adapter_class ||= DEFAULT_MEMORY_ADAPTER_CLASS
15
+
16
+ @redis_adapter = redis_adapter_class.new(
17
+ redis_client: redis_client,
18
+ expiration: expiration,
19
+ namespace: namespace
20
+ )
21
+ @memory_adapter = memory_adapter_class.new(nil)
22
+ end
23
+
24
+ def fetch_query(hash)
25
+ result = @memory_adapter.fetch_query(hash)
26
+ result ||= begin
27
+ inner_result = @redis_adapter.fetch_query(hash)
28
+ @memory_adapter.save_query(hash, inner_result) if inner_result
29
+ inner_result
30
+ end
31
+ result
32
+ end
33
+
34
+ def save_query(hash, query)
35
+ @redis_adapter.save_query(hash, query)
36
+ @memory_adapter.save_query(hash, query)
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :redis_adapter, :memory_adapter
42
+ end
43
+ end
44
+ end
45
+ end
@@ -3,6 +3,7 @@
3
3
  require "graphql/persisted_queries/store_adapters/base_store_adapter"
4
4
  require "graphql/persisted_queries/store_adapters/memory_store_adapter"
5
5
  require "graphql/persisted_queries/store_adapters/redis_store_adapter"
6
+ require "graphql/persisted_queries/store_adapters/redis_with_local_cache_store_adapter"
6
7
 
7
8
  module GraphQL
8
9
  module PersistedQueries
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module PersistedQueries
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
@@ -5,17 +5,23 @@ require "graphql/persisted_queries/schema_patch"
5
5
  require "graphql/persisted_queries/store_adapters"
6
6
  require "graphql/persisted_queries/version"
7
7
  require "graphql/persisted_queries/builder_helpers"
8
+ require "graphql/persisted_queries/http_method_analyzer"
8
9
 
9
10
  module GraphQL
10
11
  # Plugin definition
11
12
  module PersistedQueries
12
- def self.use(schema_defn, store: :memory, hash_generator: :sha256,
13
- error_handler: :default, **options)
13
+ def self.use(schema_defn, options = {})
14
14
  schema = schema_defn.is_a?(Class) ? schema_defn : schema_defn.target
15
+ SchemaPatch.patch(schema)
15
16
 
16
- schema.singleton_class.prepend(SchemaPatch)
17
- schema.hash_generator = hash_generator
17
+ schema.hash_generator = options.delete(:hash_generator) || :sha256
18
+
19
+ schema.verify_http_method = options.delete(:verify_http_method)
20
+
21
+ error_handler = options.delete(:error_handler) || :default
18
22
  schema.configure_persisted_query_error_handler(error_handler)
23
+
24
+ store = options.delete(:store) || :memory
19
25
  schema.configure_persisted_query_store(store, options)
20
26
  end
21
27
  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: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DmitryTsepelev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-02-11 00:00:00.000000000 Z
11
+ date: 2020-02-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -122,6 +122,8 @@ files:
122
122
  - lib/graphql/persisted_queries/error_handlers/base_error_handler.rb
123
123
  - lib/graphql/persisted_queries/error_handlers/default_error_handler.rb
124
124
  - lib/graphql/persisted_queries/hash_generator_builder.rb
125
+ - lib/graphql/persisted_queries/http_method_analyzer.rb
126
+ - lib/graphql/persisted_queries/multiplex_resolver.rb
125
127
  - lib/graphql/persisted_queries/resolver.rb
126
128
  - lib/graphql/persisted_queries/schema_patch.rb
127
129
  - lib/graphql/persisted_queries/store_adapters.rb
@@ -129,6 +131,7 @@ files:
129
131
  - lib/graphql/persisted_queries/store_adapters/memory_store_adapter.rb
130
132
  - lib/graphql/persisted_queries/store_adapters/redis_client_builder.rb
131
133
  - lib/graphql/persisted_queries/store_adapters/redis_store_adapter.rb
134
+ - lib/graphql/persisted_queries/store_adapters/redis_with_local_cache_store_adapter.rb
132
135
  - lib/graphql/persisted_queries/version.rb
133
136
  homepage: https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries
134
137
  licenses: