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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12531a6f0099e3220f5ea0bf400b1ccd23f2e6f3161d91d54c05ae404d3e1532
4
- data.tar.gz: 4660b422f801db343ce2f8e04ea21576ae500d7d9519f87399300e957f555160
3
+ metadata.gz: a54ca19186e396ff8a0148b3fc272ed6980dd62e150ca027620d51c105373cb8
4
+ data.tar.gz: 5c49f66f61b87648f08259fb93e3f810a656b24fdc4f6ecbcdeed5e2f2b9db8f
5
5
  SHA512:
6
- metadata.gz: c2d317723d55e67aebc607624e32e7052c53f378e8dd4584d161102167f5f44c221580518ab912a88db3e8b82381455ba77b20aefce5904602159c33bd23b88c
7
- data.tar.gz: b831aeb69392cdd662fcf285cc8f48051feb3cd3703db862bd4434cfdbb67acd4b85cf70ae64cbcee61f7df969cbd913c152517af4a7da2b7121ae99fd0282a4
6
+ metadata.gz: 36d204d8c8874a85c454a0e74884ac750b538cc993ade9e1d70b038e089934012beb515d9aa8d2fc8a5e4ed90c9a7a4e3dce975fef24919271d7472d2e158b77
7
+ data.tar.gz: 15f63fca919296e2bdf7624a4eaefd78a05415f17839d6ef5cd56881ba3b8c05e68f9929ccc93b0b503383226607d8dce34a8b11bb9ed99064eabb088f81c3fd
@@ -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
- ## Installation
17
+ ## Getting started
18
18
 
19
- 1. Add the gem to your Gemfile `gem 'graphql-persisted_queries'`
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
- 3. Add plugin to the schema:
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
- 4. Pass `:extensions` argument to all calls of `GraphqlSchema#execute` (start with `GraphqlController` and `GraphqlChannel`)
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::PersistedQueries,
116
- store: :memcached,
117
- dalli_client: { memcached_host: "127.0.0.2", memcached_port: "11211" }
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
- You can also pass options for `expiration` and `namespace` to override the defaults.
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
- class GraphqlSchema < GraphQL::Schema
135
- use GraphQL::PersistedQueries,
136
- store: :memcached,
137
- dalli_client: {
138
- memcached_url: "127.0.0.2:11211",
139
- pool_size: 5,
140
- compress: true
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
- ### Supported stores
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
- - `: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.
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
- ## Alternative hash functions
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, hash_generator: proc { |_value| "super_safe_hash!!!" }
75
+ use GraphQL::PersistedQueries, store: :redis, redis_client: { redis_url: ENV["MY_REDIS_URL"] }
171
76
  end
172
77
  ```
173
78
 
174
- ## Error handling
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
- 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:
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
- ```ruby
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
- class GraphqlSchema < GraphQL::Schema
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
- ## GET requests and HTTP cache
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
- Using `GET` requests for persisted queries allows you to enable HTTP caching (e.g., turn on CDN). This is how to turn them on:
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
- 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.
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
+ ```
@@ -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
+ ```
@@ -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.
@@ -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.delete(:extensions)
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
- results[pos] = { "errors" => [{ "message" => e.message }] }
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
- analyzer = HttpMethodAnalyzer.new
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 << analyzer
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
- def initialize(_options); end
8
+ include GraphQL::Tracing::Traceable
9
+ attr_writer :tracers
9
10
 
10
- def fetch_query(_hash)
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 save_query(_hash, _query)
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
- def fetch_query(hash)
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 save_query(hash, query)
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
- def fetch_query(hash)
13
+ protected
14
+
15
+ def fetch(hash)
13
16
  @storage[hash]
14
17
  end
15
18
 
16
- def save_query(hash, query)
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
- def fetch_query(hash)
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 save_query(hash, query)
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
- def fetch_query(hash)
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 save_query(hash, query)
45
+ def save(hash, query)
35
46
  @redis_adapter.save_query(hash, query)
36
47
  @memory_adapter.save_query(hash, query)
37
48
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module PersistedQueries
5
- VERSION = "0.4.0"
5
+ VERSION = "1.0.2"
6
6
  end
7
7
  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.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-02-26 00:00:00.000000000 Z
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