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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +20 -2
- data/lib/graphql/persisted_queries/http_method_analyzer.rb +29 -0
- data/lib/graphql/persisted_queries/multiplex_resolver.rb +49 -0
- data/lib/graphql/persisted_queries/resolver.rb +6 -8
- data/lib/graphql/persisted_queries/schema_patch.rb +20 -8
- data/lib/graphql/persisted_queries/store_adapters/redis_with_local_cache_store_adapter.rb +45 -0
- data/lib/graphql/persisted_queries/store_adapters.rb +1 -0
- data/lib/graphql/persisted_queries/version.rb +1 -1
- data/lib/graphql/persisted_queries.rb +10 -4
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f36403d19c66dd6b6627c571cd87744b7da8a1d27f47bbf1f1dd5f32357ae4b1
|
4
|
+
data.tar.gz: 3f47b64e03c5bba2f498455e53aa136e20955ef7b4045a4088b0c19903ebde43
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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).
|
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,
|
21
|
+
def initialize(extensions, schema)
|
22
22
|
@extensions = extensions
|
23
|
-
@
|
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 { @
|
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
|
-
@
|
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 { @
|
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
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
32
|
-
|
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
|
@@ -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,
|
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.
|
17
|
-
|
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.
|
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
|
+
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:
|