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 +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:
|