graphql-persisted_queries 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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: