graphql-persisted_queries 0.4.0 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +28 -145
- data/docs/alternative_stores.md +80 -0
- data/docs/error_handling.md +26 -0
- data/docs/hash.md +17 -0
- data/docs/http_cache.md +49 -0
- data/docs/tracing.md +51 -0
- data/lib/graphql/persisted_queries.rb +2 -1
- data/lib/graphql/persisted_queries/analyzers/http_method_analyzer.rb +22 -0
- data/lib/graphql/persisted_queries/analyzers/http_method_ast_analyzer.rb +19 -0
- data/lib/graphql/persisted_queries/analyzers/http_method_validator.rb +20 -0
- data/lib/graphql/persisted_queries/multiplex_resolver.rb +3 -2
- data/lib/graphql/persisted_queries/schema_patch.rb +37 -6
- data/lib/graphql/persisted_queries/store_adapters/base_store_adapter.rb +30 -3
- data/lib/graphql/persisted_queries/store_adapters/memcached_store_adapter.rb +5 -2
- data/lib/graphql/persisted_queries/store_adapters/memory_store_adapter.rb +5 -2
- data/lib/graphql/persisted_queries/store_adapters/redis_store_adapter.rb +5 -2
- data/lib/graphql/persisted_queries/store_adapters/redis_with_local_cache_store_adapter.rb +13 -2
- data/lib/graphql/persisted_queries/version.rb +1 -1
- metadata +10 -3
- data/lib/graphql/persisted_queries/http_method_analyzer.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a54ca19186e396ff8a0148b3fc272ed6980dd62e150ca027620d51c105373cb8
|
4
|
+
data.tar.gz: 5c49f66f61b87648f08259fb93e3f810a656b24fdc4f6ecbcdeed5e2f2b9db8f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 36d204d8c8874a85c454a0e74884ac750b538cc993ade9e1d70b038e089934012beb515d9aa8d2fc8a5e4ed90c9a7a4e3dce975fef24919271d7472d2e158b77
|
7
|
+
data.tar.gz: 15f63fca919296e2bdf7624a4eaefd78a05415f17839d6ef5cd56881ba3b8c05e68f9929ccc93b0b503383226607d8dce34a8b11bb9ed99064eabb088f81c3fd
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,26 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 1.0.2 (2020-06-29)
|
6
|
+
|
7
|
+
- [PR#35](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/35) fix args for GraphQL::Query::Result ([@ogidow ][])
|
8
|
+
|
9
|
+
## 1.0.1 (2020-06-25)
|
10
|
+
|
11
|
+
- [PR#34](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/34) Return GraphQL::Query::Result when raise error ([@ogidow ][])
|
12
|
+
|
13
|
+
## 🥳 1.0.0 (2020-03-31)
|
14
|
+
|
15
|
+
- [PR#30](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/30) **BREAKING CHANGE** Move extenstions to the query context ([@DmitryTsepelev][])
|
16
|
+
|
17
|
+
## 0.5.1 (2020-03-18)
|
18
|
+
|
19
|
+
- [PR#33](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/33) Support AST analyzers ([@DmitryTsepelev][])
|
20
|
+
|
21
|
+
## 0.5.0 (2020-03-12)
|
22
|
+
|
23
|
+
- [PR#29](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/29) Instrumentation via graphql-ruby's tracer feature ([@bmorton][])
|
24
|
+
|
5
25
|
## 0.4.0 (2020-02-26)
|
6
26
|
|
7
27
|
- [PR#26](https://github.com/DmitryTsepelev/graphql-ruby-persisted_queries/pull/26) Add Memcached store ([@JanStevens][])
|
@@ -35,3 +55,4 @@
|
|
35
55
|
[@DmitryTsepelev]: https://github.com/DmitryTsepelev
|
36
56
|
[@bmorton]: https://github.com/bmorton
|
37
57
|
[@JanStevens]: https://github.com/JanStevens
|
58
|
+
[@ogidow]: https://github.com/ogidow
|
data/README.md
CHANGED
@@ -14,11 +14,9 @@
|
|
14
14
|
</a>
|
15
15
|
</p>
|
16
16
|
|
17
|
-
##
|
17
|
+
## Getting started
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
2. Install and configure [apollo-link-persisted-queries](https://github.com/apollographql/apollo-link-persisted-queries):
|
19
|
+
First of all, install and configure [apollo-link-persisted-queries](https://github.com/apollographql/apollo-link-persisted-queries) on the front–end side:
|
22
20
|
|
23
21
|
```js
|
24
22
|
import { createPersistedQueryLink } from "apollo-link-persisted-queries";
|
@@ -35,7 +33,7 @@ const client = new ApolloClient({
|
|
35
33
|
});
|
36
34
|
```
|
37
35
|
|
38
|
-
|
36
|
+
Add the gem to your Gemfile `gem 'graphql-persisted_queries'` and add the plugin to your schema class:
|
39
37
|
|
40
38
|
```ruby
|
41
39
|
class GraphqlSchema < GraphQL::Schema
|
@@ -43,170 +41,55 @@ class GraphqlSchema < GraphQL::Schema
|
|
43
41
|
end
|
44
42
|
```
|
45
43
|
|
46
|
-
|
47
|
-
|
48
|
-
```ruby
|
49
|
-
GraphqlSchema.execute(
|
50
|
-
params[:query],
|
51
|
-
variables: ensure_hash(params[:variables]),
|
52
|
-
context: {},
|
53
|
-
operation_name: params[:operationName],
|
54
|
-
extensions: ensure_hash(params[:extensions])
|
55
|
-
)
|
56
|
-
```
|
57
|
-
|
58
|
-
5. Run the app! 🔥
|
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.
|
63
|
-
In this case you need to use `GraphqlSchema.multiplex(queries)` instead of `#execute`. The gem supports it too, no action required!
|
64
|
-
|
65
|
-
## Alternative stores
|
66
|
-
|
67
|
-
All the queries are stored in memory by default, but you can easily switch to _redis_ or _memcached_:
|
68
|
-
|
69
|
-
```ruby
|
70
|
-
class GraphqlSchema < GraphQL::Schema
|
71
|
-
use GraphQL::PersistedQueries, store: :redis, redis_client: { redis_url: ENV["MY_REDIS_URL"] }
|
72
|
-
end
|
73
|
-
```
|
74
|
-
|
75
|
-
### Redis
|
76
|
-
|
77
|
-
If you have `ENV["REDIS_URL"]` configured – you don't need to pass it explicitly. Also, you can pass `:redis_host`, `:redis_port` and `:redis_db_name`
|
78
|
-
inside the `:redis_client` hash to build the URL from scratch or pass the configured `Redis` or `ConnectionPool` object:
|
79
|
-
|
80
|
-
```ruby
|
81
|
-
class GraphqlSchema < GraphQL::Schema
|
82
|
-
use GraphQL::PersistedQueries,
|
83
|
-
store: :redis,
|
84
|
-
redis_client: { redis_host: "127.0.0.2", redis_port: "2214", redis_db_name: "7" }
|
85
|
-
# or
|
86
|
-
use GraphQL::PersistedQueries,
|
87
|
-
store: :redis,
|
88
|
-
redis_client: Redis.new(url: "redis://127.0.0.2:2214/7")
|
89
|
-
# or
|
90
|
-
use GraphQL::PersistedQueries,
|
91
|
-
store: :redis,
|
92
|
-
redis_client: ConnectionPool.new { Redis.new(url: "redis://127.0.0.2:2214/7") }
|
93
|
-
end
|
94
|
-
```
|
95
|
-
|
96
|
-
You can also pass options for expiration and namespace to override the defaults:
|
97
|
-
|
98
|
-
```ruby
|
99
|
-
class GraphqlSchema < GraphQL::Schema
|
100
|
-
use GraphQL::PersistedQueries,
|
101
|
-
store: :redis,
|
102
|
-
redis_client: { redis_url: ENV["MY_REDIS_URL"] },
|
103
|
-
expiration: 172800, # optional, default is 24 hours
|
104
|
-
namespace: "my-custom-namespace" # optional, default is "graphql-persisted-query"
|
105
|
-
end
|
106
|
-
```
|
107
|
-
|
108
|
-
### Memcached
|
109
|
-
|
110
|
-
If you have `ENV["MEMCACHE_SERVERS"]` configured - you don't need to pass it explicitly. Also, you can pass `:memcached_host` and `:memcached_port`
|
111
|
-
inside the `:dalli_client` hash to build the server name from scratch or pass the configured `Dalli::Client` object:
|
44
|
+
**Heads up!** If you've already switched to interpreter mode and and AST analyzers—make sure AST plugin is added _before_ `GraphQL::PersistedQueries`:
|
112
45
|
|
113
46
|
```ruby
|
114
47
|
class GraphqlSchema < GraphQL::Schema
|
115
|
-
use GraphQL::
|
116
|
-
|
117
|
-
|
118
|
-
# or
|
119
|
-
use GraphQL::PersistedQueries,
|
120
|
-
store: :memcached,
|
121
|
-
dalli_client: Dalli::Client.new("127.0.0.2:11211")
|
122
|
-
# or
|
123
|
-
use GraphQL::PersistedQueries,
|
124
|
-
store: :memcached,
|
125
|
-
dalli_client: { memcached_url: "127.0.0.2:11211" }
|
48
|
+
use GraphQL::Execution::Interpreter
|
49
|
+
use GraphQL::Analysis::AST
|
50
|
+
use GraphQL::PersistedQueries
|
126
51
|
end
|
127
52
|
```
|
128
53
|
|
129
|
-
|
130
|
-
Any additional argument inside `dalli_client` will be forwarded to `Dalli::Client.new`.
|
131
|
-
Following example configures Dalli `pool_size` and `compress` options:
|
54
|
+
Pass `:extensions` argument as part of a `context` to all calls of `GraphqlSchema#execute`, usually it happens in `GraphqlController`, `GraphqlChannel` and tests:
|
132
55
|
|
133
56
|
```ruby
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
expiration: 172800, # optional, default is 24 hours
|
143
|
-
namespace: "my-custom-namespace" # optional, default is "graphql-persisted-query"
|
144
|
-
end
|
57
|
+
GraphqlSchema.execute(
|
58
|
+
params[:query],
|
59
|
+
variables: ensure_hash(params[:variables]),
|
60
|
+
context: {
|
61
|
+
extensions: ensure_hash(params[:extensions])
|
62
|
+
},
|
63
|
+
operation_name: params[:operationName]
|
64
|
+
)
|
145
65
|
```
|
146
66
|
|
147
|
-
|
148
|
-
|
149
|
-
We currently support a few different stores that can be configured out of the box:
|
67
|
+
You're all set!
|
150
68
|
|
151
|
-
|
152
|
-
- `: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.
|
153
|
-
- `: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.
|
154
|
-
- `:memcached`: This store will allow you to share a Memcached cache across all instances of your GraphQL application. The client is implemented with the Dalli gem.
|
69
|
+
## Advanced usage
|
155
70
|
|
156
|
-
|
157
|
-
|
158
|
-
[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:
|
159
|
-
|
160
|
-
```ruby
|
161
|
-
class GraphqlSchema < GraphQL::Schema
|
162
|
-
use GraphQL::PersistedQueries, hash_generator: :md5
|
163
|
-
end
|
164
|
-
```
|
165
|
-
|
166
|
-
If string or symbol is passed – the gem would try to find the class in the `Digest` namespace. Altenatively, you can pass a lambda, e.g.:
|
71
|
+
All the queries are stored in memory by default, but you can easily switch to another storage (e.g., _redis_:
|
167
72
|
|
168
73
|
```ruby
|
169
74
|
class GraphqlSchema < GraphQL::Schema
|
170
|
-
use GraphQL::PersistedQueries,
|
75
|
+
use GraphQL::PersistedQueries, store: :redis, redis_client: { redis_url: ENV["MY_REDIS_URL"] }
|
171
76
|
end
|
172
77
|
```
|
173
78
|
|
174
|
-
|
175
|
-
|
176
|
-
You may optionally specify an object that will be called whenever an error occurs while attempting to resolve or save a query. This will give you the opportunity to both handle (e.g. graceful Redis failure) and/or log the error. By default, errors will be raised when a failure occurs within a `StoreAdapter`.
|
79
|
+
We currently support `memory`, `redis`, `redis_with_local_cache` and `memcached` out of the box. The detailed documentation can be found [here](docs/alternative_stores.md).
|
177
80
|
|
178
|
-
|
81
|
+
When the error occurs, the gem tries to not interrupt the regular flow of the app (e.g., when something is wrong with the storage, it will just answer that persisted query is not found). You can add a [custom](docs/error_handling.md) error handler and try to fix the problem or just log it.
|
179
82
|
|
180
|
-
|
181
|
-
class GracefulRedisErrorHandler < GraphQL::PersistedQueries::ErrorHandlers::BaseErrorHandler
|
182
|
-
def call(error)
|
183
|
-
case error
|
184
|
-
when Redis::BaseError
|
185
|
-
# Treat Redis errors as a cache miss, but you should log the error into
|
186
|
-
# your instrumentation framework here.
|
187
|
-
else
|
188
|
-
raise error
|
189
|
-
end
|
190
|
-
|
191
|
-
# Return nothing to ensure handled errors are treated as cache misses
|
192
|
-
return
|
193
|
-
end
|
194
|
-
end
|
83
|
+
Since our queries are slim now, we can switch back to HTTP GET, you can find a [guide](docs/http_cache.md) here.
|
195
84
|
|
196
|
-
|
197
|
-
use GraphQL::PersistedQueries, error_handler: GracefulRedisErrorHandler.new
|
198
|
-
end
|
199
|
-
```
|
85
|
+
[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!
|
200
86
|
|
201
|
-
|
87
|
+
[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.
|
202
88
|
|
203
|
-
|
204
|
-
1. Change the way link is initialized on front-end side (`createPersistedQueryLink({ useGETForHashedQueries: true })`);
|
205
|
-
2. Register a new route `get "/graphql", to: "graphql#execute"`;
|
206
|
-
3. Put the request object to the GraphQL context in the controller `GraphqlSchema.execute(query, variables: variables, context: { request: request })`;
|
207
|
-
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).
|
89
|
+
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).
|
208
90
|
|
209
|
-
|
91
|
+
> 📖 Read more about the gem internals: [Persisted queries in GraphQL:
|
92
|
+
Slim down Apollo requests to your Ruby application](https://evilmartians.com/chronicles/persisted-queries-in-graphql-slim-down-apollo-requests-to-your-ruby-application)
|
210
93
|
|
211
94
|
## Contributing
|
212
95
|
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# Alternative stores
|
2
|
+
|
3
|
+
We currently support a few different stores that can be configured out of the box:
|
4
|
+
|
5
|
+
- `: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.
|
6
|
+
- `: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.
|
7
|
+
- `: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.
|
8
|
+
- `:memcached`: This store will allow you to share a Memcached cache across all instances of your GraphQL application. The client is implemented with the Dalli gem.
|
9
|
+
|
10
|
+
## Redis
|
11
|
+
|
12
|
+
If you have `ENV["REDIS_URL"]` configured – you don't need to pass it explicitly. Also, you can pass `:redis_host`, `:redis_port` and `:redis_db_name`
|
13
|
+
inside the `:redis_client` hash to build the URL from scratch or pass the configured `Redis` or `ConnectionPool` object:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
class GraphqlSchema < GraphQL::Schema
|
17
|
+
use GraphQL::PersistedQueries,
|
18
|
+
store: :redis,
|
19
|
+
redis_client: { redis_host: "127.0.0.2", redis_port: "2214", redis_db_name: "7" }
|
20
|
+
# or
|
21
|
+
use GraphQL::PersistedQueries,
|
22
|
+
store: :redis,
|
23
|
+
redis_client: Redis.new(url: "redis://127.0.0.2:2214/7")
|
24
|
+
# or
|
25
|
+
use GraphQL::PersistedQueries,
|
26
|
+
store: :redis,
|
27
|
+
redis_client: ConnectionPool.new { Redis.new(url: "redis://127.0.0.2:2214/7") }
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
You can also pass options for expiration and namespace to override the defaults:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class GraphqlSchema < GraphQL::Schema
|
35
|
+
use GraphQL::PersistedQueries,
|
36
|
+
store: :redis,
|
37
|
+
redis_client: { redis_url: ENV["MY_REDIS_URL"] },
|
38
|
+
expiration: 172800, # optional, default is 24 hours
|
39
|
+
namespace: "my-custom-namespace" # optional, default is "graphql-persisted-query"
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
## Memcached
|
44
|
+
|
45
|
+
If you have `ENV["MEMCACHE_SERVERS"]` configured - you don't need to pass it explicitly. Also, you can pass `:memcached_host` and `:memcached_port`
|
46
|
+
inside the `:dalli_client` hash to build the server name from scratch or pass the configured `Dalli::Client` object:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
class GraphqlSchema < GraphQL::Schema
|
50
|
+
use GraphQL::PersistedQueries,
|
51
|
+
store: :memcached,
|
52
|
+
dalli_client: { memcached_host: "127.0.0.2", memcached_port: "11211" }
|
53
|
+
# or
|
54
|
+
use GraphQL::PersistedQueries,
|
55
|
+
store: :memcached,
|
56
|
+
dalli_client: Dalli::Client.new("127.0.0.2:11211")
|
57
|
+
# or
|
58
|
+
use GraphQL::PersistedQueries,
|
59
|
+
store: :memcached,
|
60
|
+
dalli_client: { memcached_url: "127.0.0.2:11211" }
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
You can also pass options for `expiration` and `namespace` to override the defaults.
|
65
|
+
Any additional argument inside `dalli_client` will be forwarded to `Dalli::Client.new`.
|
66
|
+
Following example configures Dalli `pool_size` and `compress` options:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
class GraphqlSchema < GraphQL::Schema
|
70
|
+
use GraphQL::PersistedQueries,
|
71
|
+
store: :memcached,
|
72
|
+
dalli_client: {
|
73
|
+
memcached_url: "127.0.0.2:11211",
|
74
|
+
pool_size: 5,
|
75
|
+
compress: true
|
76
|
+
},
|
77
|
+
expiration: 172800, # optional, default is 24 hours
|
78
|
+
namespace: "my-custom-namespace" # optional, default is "graphql-persisted-query"
|
79
|
+
end
|
80
|
+
```
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Error handling
|
2
|
+
|
3
|
+
You may optionally specify an object that will be called whenever an error occurs while attempting to resolve or save a query. This will give you the opportunity to both handle (e.g. graceful Redis failure) and/or log the error. By default, errors will be raised when a failure occurs within a `StoreAdapter`.
|
4
|
+
|
5
|
+
An error handler can be a proc or an implementation of `GraphQL::PersistedQueries::ErrorHandlers::BaseErrorHandler`. Here's an example for treating Redis failures as cache misses:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
class GracefulRedisErrorHandler < GraphQL::PersistedQueries::ErrorHandlers::BaseErrorHandler
|
9
|
+
def call(error)
|
10
|
+
case error
|
11
|
+
when Redis::BaseError
|
12
|
+
# Treat Redis errors as a cache miss, but you should log the error into
|
13
|
+
# your instrumentation framework here.
|
14
|
+
else
|
15
|
+
raise error
|
16
|
+
end
|
17
|
+
|
18
|
+
# Return nothing to ensure handled errors are treated as cache misses
|
19
|
+
return
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class GraphqlSchema < GraphQL::Schema
|
24
|
+
use GraphQL::PersistedQueries, error_handler: GracefulRedisErrorHandler.new
|
25
|
+
end
|
26
|
+
```
|
data/docs/hash.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Alternative hash functions
|
2
|
+
|
3
|
+
[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:
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
class GraphqlSchema < GraphQL::Schema
|
7
|
+
use GraphQL::PersistedQueries, hash_generator: :md5
|
8
|
+
end
|
9
|
+
```
|
10
|
+
|
11
|
+
If string or symbol is passed – the gem would try to find the class in the `Digest` namespace. Altenatively, you can pass a lambda, e.g.:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
class GraphqlSchema < GraphQL::Schema
|
15
|
+
use GraphQL::PersistedQueries, hash_generator: proc { |_value| "super_safe_hash!!!" }
|
16
|
+
end
|
17
|
+
```
|
data/docs/http_cache.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# GET requests and HTTP cache
|
2
|
+
|
3
|
+
Using `GET` requests for persisted queries allows you to enable HTTP caching (e.g., turn on CDN).
|
4
|
+
|
5
|
+
Firstly, turn on the `useGETForHashedQueries` parameter on the front-end side:
|
6
|
+
|
7
|
+
```js
|
8
|
+
import { createPersistedQueryLink } from "apollo-link-persisted-queries";
|
9
|
+
import { createHttpLink } from "apollo-link-http";
|
10
|
+
import { InMemoryCache } from "apollo-cache-inmemory";
|
11
|
+
import ApolloClient from "apollo-client";
|
12
|
+
|
13
|
+
|
14
|
+
// use this with Apollo Client
|
15
|
+
const link = createPersistedQueryLink({ useGETForHashedQueries: true }).concat(createHttpLink({ uri: "/graphql" }));
|
16
|
+
const client = new ApolloClient({
|
17
|
+
cache: new InMemoryCache(),
|
18
|
+
link: link,
|
19
|
+
});
|
20
|
+
```
|
21
|
+
|
22
|
+
Register a new route in `routes.rb`:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
get "/graphql", to: "graphql#execute"
|
26
|
+
```
|
27
|
+
|
28
|
+
Put the request object to the GraphQL context everywhere you execute GraphQL queries:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
GraphqlSchema.execute(
|
32
|
+
query,
|
33
|
+
variables: ensure_hash(params[:variables]),
|
34
|
+
context: {
|
35
|
+
extensions: ensure_hash(params[:extensions]),
|
36
|
+
request: request
|
37
|
+
}
|
38
|
+
)
|
39
|
+
```
|
40
|
+
|
41
|
+
Turn the `verify_http_method` option when configuring the plugin to enforce using `POST` requests for performing mutations (otherwise the error `Mutations cannot be performed via HTTP GET` will be returned):
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
class GraphqlSchema < GraphQL::Schema
|
45
|
+
use GraphQL::PersistedQueries, verify_http_method: true
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
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.
|
data/docs/tracing.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# Tracing
|
2
|
+
|
3
|
+
Tracing is an experimental feature that, when enabled, uses the tracing system defined in `graphql-ruby` to surface these events:
|
4
|
+
|
5
|
+
* `persisted_queries.fetch_query.cache_hit` - Triggered when a store adapter successfully looks up a hash and finds a query.
|
6
|
+
* `persisted_queries.fetch_query.cache_miss` - Triggered when a store adapter attempts to look up a hash but cannot find it.
|
7
|
+
* `persisted_queries.save_query` - Triggered when a store adapter persists a query.
|
8
|
+
|
9
|
+
All events include a metadata hash as their `data` parameter. This hash currently only includes the name of the adapter that triggered the event.
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
Tracing must be opted into via the plugin configuration for the events to trigger. Once they are enabled, any tracer that is defined on the schema will get the following events yielded to them. An example configuration will look similar to this:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
class GraphqlSchema < GraphQL::Schema
|
17
|
+
use GraphQL::PersistedQueries, tracing: true
|
18
|
+
tracer MyPersistedQueriesTracer
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
Tracers in this plugin integrate with the `GraphQL::Tracing` feature in [`graphql-ruby`](https://github.com/rmosolgo/graphql-ruby). The same tracers are used for tracing events directly from `graphql-ruby` and this plugin. The [guide on "Tracing"](https://graphql-ruby.org/queries/tracing.html) in `graphql-ruby` has implementation details, but an example tracer would look similar to this:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
class MyPersistedQueriesTracer
|
26
|
+
def self.trace(key, data)
|
27
|
+
yield.tap do |result|
|
28
|
+
# Note: this tracer will get called for these persisted queries events as
|
29
|
+
# well as all events traced by the graphql-ruby gem.
|
30
|
+
case key
|
31
|
+
when "persisted_queries.fetch_query.cache_hit"
|
32
|
+
# data = { adapter: :redis }
|
33
|
+
# result = nil
|
34
|
+
# increment a counter metric to track cache hits
|
35
|
+
when "persisted_queries.fetch_query.cache_miss"
|
36
|
+
# data = { adapter: :redis }
|
37
|
+
# result = nil
|
38
|
+
# increment a counter metric to track cache misses
|
39
|
+
when "persisted_queries.save_query"
|
40
|
+
# data = { adapter: :redis }
|
41
|
+
# result = return value from method call
|
42
|
+
# increment a counter metric to track saved queries
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
## Be aware of tracers-as-notifications
|
50
|
+
|
51
|
+
A word of caution about the `cache_hit` and `cache_miss` events: they yield an empty block. The `GraphQL::Tracing` feature typically wraps around the code performing the event. The `save_query` event works this way, too -- the block that is yielded is essentially the `StoreAdapter#save` method. This means you can add timing instrumentation for this call. However, the `cache_hit` and `cache_miss` events are simply event notifications and do not wrap any code. This means that they won't yield anything meaningful and they can't be timed.
|
@@ -5,7 +5,6 @@ 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"
|
9
8
|
|
10
9
|
module GraphQL
|
11
10
|
# Plugin definition
|
@@ -21,6 +20,8 @@ module GraphQL
|
|
21
20
|
error_handler = options.delete(:error_handler) || :default
|
22
21
|
schema.configure_persisted_query_error_handler(error_handler)
|
23
22
|
|
23
|
+
schema.persisted_queries_tracing_enabled = options.delete(:tracing)
|
24
|
+
|
24
25
|
store = options.delete(:store) || :memory
|
25
26
|
schema.configure_persisted_query_store(store, options)
|
26
27
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module PersistedQueries
|
5
|
+
module Analyzers
|
6
|
+
# Verifies that mutations are not executed using GET requests
|
7
|
+
class HttpMethodAnalyzer
|
8
|
+
def initial_value(query)
|
9
|
+
{ query: query }
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(memo, _visit_type, _irep_node)
|
13
|
+
memo
|
14
|
+
end
|
15
|
+
|
16
|
+
def final_value(memo)
|
17
|
+
HttpMethodValidator.new(memo[:query]).perform
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module PersistedQueries
|
5
|
+
module Analyzers
|
6
|
+
# Verifies that mutations are not executed using GET requests
|
7
|
+
class HttpMethodAstAnalyzer < GraphQL::Analysis::AST::Analyzer
|
8
|
+
def initialize(query)
|
9
|
+
super
|
10
|
+
@query = query
|
11
|
+
end
|
12
|
+
|
13
|
+
def result
|
14
|
+
HttpMethodValidator.new(@query).perform
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module PersistedQueries
|
5
|
+
module Analyzers
|
6
|
+
# Verifies that mutations are not executed using GET requests
|
7
|
+
class HttpMethodValidator
|
8
|
+
def initialize(query)
|
9
|
+
@query = query
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform
|
13
|
+
return if !@query.context[:request]&.get? || !@query.mutation?
|
14
|
+
|
15
|
+
GraphQL::AnalysisError.new("Mutations cannot be performed via HTTP GET")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -29,12 +29,13 @@ module GraphQL
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def resolve_persisted_query(query_params, pos)
|
32
|
-
extensions = query_params.
|
32
|
+
extensions = query_params.dig(:context, :extensions)
|
33
33
|
return unless extensions
|
34
34
|
|
35
35
|
query_params[:query] = Resolver.new(extensions, @schema).resolve(query_params[:query])
|
36
36
|
rescue Resolver::NotFound, Resolver::WrongHash => e
|
37
|
-
|
37
|
+
values = { "errors" => [{ "message" => e.message }] }
|
38
|
+
results[pos] = GraphQL::Query::Result.new(query: GraphQL::Query.new(@schema, query_params[:query]), values: values)
|
38
39
|
end
|
39
40
|
|
40
41
|
def perform_multiplex
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require "graphql/persisted_queries/hash_generator_builder"
|
4
4
|
require "graphql/persisted_queries/resolver"
|
5
5
|
require "graphql/persisted_queries/multiplex_resolver"
|
6
|
+
require "graphql/persisted_queries/analyzers/http_method_validator"
|
6
7
|
|
7
8
|
module GraphQL
|
8
9
|
module PersistedQueries
|
@@ -16,9 +17,12 @@ module GraphQL
|
|
16
17
|
end
|
17
18
|
|
18
19
|
attr_reader :persisted_query_store, :hash_generator_proc, :persisted_query_error_handler
|
20
|
+
attr_writer :persisted_queries_tracing_enabled
|
19
21
|
|
20
22
|
def configure_persisted_query_store(store, options)
|
21
|
-
@persisted_query_store = StoreAdapters.build(store, options)
|
23
|
+
@persisted_query_store = StoreAdapters.build(store, options).tap do |adapter|
|
24
|
+
adapter.tracers = tracers if persisted_queries_tracing_enabled?
|
25
|
+
end
|
22
26
|
end
|
23
27
|
|
24
28
|
def configure_persisted_query_error_handler(handler)
|
@@ -32,18 +36,45 @@ module GraphQL
|
|
32
36
|
def verify_http_method=(verify)
|
33
37
|
return unless verify
|
34
38
|
|
35
|
-
|
36
|
-
|
37
|
-
if Gem::Dependency.new("graphql", ">= 1.10.0").match?("graphql", GraphQL::VERSION)
|
38
|
-
query_analyzer(analyzer)
|
39
|
+
if graphql10?
|
40
|
+
query_analyzer(prepare_analyzer)
|
39
41
|
else
|
40
|
-
query_analyzers <<
|
42
|
+
query_analyzers << prepare_analyzer
|
41
43
|
end
|
42
44
|
end
|
43
45
|
|
46
|
+
def persisted_queries_tracing_enabled?
|
47
|
+
@persisted_queries_tracing_enabled
|
48
|
+
end
|
49
|
+
|
44
50
|
def multiplex(queries, **kwargs)
|
45
51
|
MultiplexResolver.new(self, queries, kwargs).resolve
|
46
52
|
end
|
53
|
+
|
54
|
+
def tracer(name)
|
55
|
+
super.tap do
|
56
|
+
# Since tracers can be set before *and* after our plugin hooks in,
|
57
|
+
# we need to set tracers both when this plugin gets initialized
|
58
|
+
# and any time a tracer is added after initialization
|
59
|
+
persisted_query_store.tracers = tracers if persisted_queries_tracing_enabled?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def graphql10?
|
66
|
+
Gem::Dependency.new("graphql", ">= 1.10.0").match?("graphql", GraphQL::VERSION)
|
67
|
+
end
|
68
|
+
|
69
|
+
def prepare_analyzer
|
70
|
+
if graphql10? && using_ast_analysis?
|
71
|
+
require "graphql/persisted_queries/analyzers/http_method_ast_analyzer"
|
72
|
+
Analyzers::HttpMethodAstAnalyzer
|
73
|
+
else
|
74
|
+
require "graphql/persisted_queries/analyzers/http_method_analyzer"
|
75
|
+
Analyzers::HttpMethodAnalyzer.new
|
76
|
+
end
|
77
|
+
end
|
47
78
|
end
|
48
79
|
end
|
49
80
|
end
|
@@ -5,15 +5,42 @@ module GraphQL
|
|
5
5
|
module StoreAdapters
|
6
6
|
# Base class for all store adapters
|
7
7
|
class BaseStoreAdapter
|
8
|
-
|
8
|
+
include GraphQL::Tracing::Traceable
|
9
|
+
attr_writer :tracers
|
9
10
|
|
10
|
-
def
|
11
|
+
def initialize(_options)
|
12
|
+
@name = :base
|
13
|
+
end
|
14
|
+
|
15
|
+
def fetch_query(hash)
|
16
|
+
fetch(hash).tap do |result|
|
17
|
+
event = result ? "cache_hit" : "cache_miss"
|
18
|
+
trace("fetch_query.#{event}", adapter: @name)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def save_query(hash, query)
|
23
|
+
trace("save_query", adapter: @name) { save(hash, query) }
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def fetch(_hash)
|
11
29
|
raise NotImplementedError
|
12
30
|
end
|
13
31
|
|
14
|
-
def
|
32
|
+
def save(_hash, _query)
|
15
33
|
raise NotImplementedError
|
16
34
|
end
|
35
|
+
|
36
|
+
def trace(key, metadata)
|
37
|
+
if @tracers
|
38
|
+
key = "persisted_queries.#{key}"
|
39
|
+
block_given? ? super : super {}
|
40
|
+
elsif block_given?
|
41
|
+
yield
|
42
|
+
end
|
43
|
+
end
|
17
44
|
end
|
18
45
|
end
|
19
46
|
end
|
@@ -14,13 +14,16 @@ module GraphQL
|
|
14
14
|
@dalli_proc = build_dalli_proc(dalli_client)
|
15
15
|
@expiration = expiration || DEFAULT_EXPIRATION
|
16
16
|
@namespace = namespace || DEFAULT_NAMESPACE
|
17
|
+
@name = :memcached
|
17
18
|
end
|
18
19
|
|
19
|
-
|
20
|
+
protected
|
21
|
+
|
22
|
+
def fetch(hash)
|
20
23
|
@dalli_proc.call { |dalli| dalli.get(key_for(hash)) }
|
21
24
|
end
|
22
25
|
|
23
|
-
def
|
26
|
+
def save(hash, query)
|
24
27
|
@dalli_proc.call { |dalli| dalli.set(key_for(hash), query, @expiration) }
|
25
28
|
end
|
26
29
|
|
@@ -7,13 +7,16 @@ module GraphQL
|
|
7
7
|
class MemoryStoreAdapter < BaseStoreAdapter
|
8
8
|
def initialize(_options)
|
9
9
|
@storage = {}
|
10
|
+
@name = :memory
|
10
11
|
end
|
11
12
|
|
12
|
-
|
13
|
+
protected
|
14
|
+
|
15
|
+
def fetch(hash)
|
13
16
|
@storage[hash]
|
14
17
|
end
|
15
18
|
|
16
|
-
def
|
19
|
+
def save(hash, query)
|
17
20
|
@storage[hash] = query
|
18
21
|
end
|
19
22
|
end
|
@@ -14,13 +14,16 @@ module GraphQL
|
|
14
14
|
@redis_proc = build_redis_proc(redis_client)
|
15
15
|
@expiration = expiration || DEFAULT_EXPIRATION
|
16
16
|
@namespace = namespace || DEFAULT_NAMESPACE
|
17
|
+
@name = :redis
|
17
18
|
end
|
18
19
|
|
19
|
-
|
20
|
+
protected
|
21
|
+
|
22
|
+
def fetch(hash)
|
20
23
|
@redis_proc.call { |redis| redis.get(key_for(hash)) }
|
21
24
|
end
|
22
25
|
|
23
|
-
def
|
26
|
+
def save(hash, query)
|
24
27
|
@redis_proc.call { |redis| redis.set(key_for(hash), query, ex: @expiration) }
|
25
28
|
end
|
26
29
|
|
@@ -19,9 +19,20 @@ module GraphQL
|
|
19
19
|
namespace: namespace
|
20
20
|
)
|
21
21
|
@memory_adapter = memory_adapter_class.new(nil)
|
22
|
+
@name = :redis_with_local_cache
|
22
23
|
end
|
23
24
|
|
24
|
-
|
25
|
+
# We don't need to implement our own traces for this adapter since the
|
26
|
+
# underlying adapters will emit the proper events for us. However,
|
27
|
+
# since tracers can be defined at any time, we need to pass them through.
|
28
|
+
def tracers=(tracers)
|
29
|
+
@memory_adapter.tracers = tracers
|
30
|
+
@redis_adapter.tracers = tracers
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def fetch(hash)
|
25
36
|
result = @memory_adapter.fetch_query(hash)
|
26
37
|
result ||= begin
|
27
38
|
inner_result = @redis_adapter.fetch_query(hash)
|
@@ -31,7 +42,7 @@ module GraphQL
|
|
31
42
|
result
|
32
43
|
end
|
33
44
|
|
34
|
-
def
|
45
|
+
def save(hash, query)
|
35
46
|
@redis_adapter.save_query(hash, query)
|
36
47
|
@memory_adapter.save_query(hash, query)
|
37
48
|
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: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- DmitryTsepelev
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-06-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -125,18 +125,25 @@ files:
|
|
125
125
|
- Rakefile
|
126
126
|
- bin/console
|
127
127
|
- bin/setup
|
128
|
+
- docs/alternative_stores.md
|
129
|
+
- docs/error_handling.md
|
130
|
+
- docs/hash.md
|
131
|
+
- docs/http_cache.md
|
132
|
+
- docs/tracing.md
|
128
133
|
- gemfiles/graphql_1_10.gemfile
|
129
134
|
- gemfiles/graphql_1_8.gemfile
|
130
135
|
- gemfiles/graphql_1_9.gemfile
|
131
136
|
- gemfiles/graphql_master.gemfile
|
132
137
|
- graphql-persisted_queries.gemspec
|
133
138
|
- lib/graphql/persisted_queries.rb
|
139
|
+
- lib/graphql/persisted_queries/analyzers/http_method_analyzer.rb
|
140
|
+
- lib/graphql/persisted_queries/analyzers/http_method_ast_analyzer.rb
|
141
|
+
- lib/graphql/persisted_queries/analyzers/http_method_validator.rb
|
134
142
|
- lib/graphql/persisted_queries/builder_helpers.rb
|
135
143
|
- lib/graphql/persisted_queries/error_handlers.rb
|
136
144
|
- lib/graphql/persisted_queries/error_handlers/base_error_handler.rb
|
137
145
|
- lib/graphql/persisted_queries/error_handlers/default_error_handler.rb
|
138
146
|
- lib/graphql/persisted_queries/hash_generator_builder.rb
|
139
|
-
- lib/graphql/persisted_queries/http_method_analyzer.rb
|
140
147
|
- lib/graphql/persisted_queries/multiplex_resolver.rb
|
141
148
|
- lib/graphql/persisted_queries/resolver.rb
|
142
149
|
- lib/graphql/persisted_queries/schema_patch.rb
|
@@ -1,29 +0,0 @@
|
|
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
|