@envelop/response-cache 4.0.8 → 4.0.9-alpha-20230505095410-8ff5ea11
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.
- package/LICENSE +21 -0
- package/README.md +49 -35
- package/cjs/cache.js +0 -2
- package/cjs/plugin.js +93 -86
- package/esm/cache.js +0 -1
- package/esm/plugin.js +92 -86
- package/package.json +3 -3
- package/typings/cache.d.cts +1 -1
- package/typings/cache.d.ts +1 -1
- package/typings/plugin.d.cts +2 -1
- package/typings/plugin.d.ts +2 -1
- package/cjs/cache-document-str.js +0 -26
- package/esm/cache-document-str.js +0 -21
- package/typings/cache-document-str.d.cts +0 -4
- package/typings/cache-document-str.d.ts +0 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Dotan Simha
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
- Customize cache entry time to live based on fields and types within the execution result.
|
|
5
5
|
- Automatically invalidate the cache based on mutation selection sets.
|
|
6
6
|
- Customize invalidation through the cache api (e.g. listen to a database write log).
|
|
7
|
-
- Implement your own global cache (e.g. using another key/value store) by implementing the `Cache`
|
|
7
|
+
- Implement your own global cache (e.g. using another key/value store) by implementing the `Cache`
|
|
8
|
+
interface.
|
|
8
9
|
|
|
9
10
|
[Check out the GraphQL Response Cache Guide for more information](https://envelop.dev/docs/guides/adding-a-graphql-response-cache)
|
|
10
11
|
|
|
11
|
-
> Watch
|
|
12
|
+
> Watch
|
|
13
|
+
> [Episode #34 of `graphql.wtf`](https://graphql.wtf/episodes/34-response-cache-plugin-with-envelop)
|
|
14
|
+
> for a quick introduction to using Response Cache plugin with Envelop:
|
|
12
15
|
|
|
13
16
|
<iframe
|
|
14
17
|
width="100%"
|
|
@@ -38,7 +41,7 @@ When configuring the `useResponseCache`, you can choose the type of cache:
|
|
|
38
41
|
The in-memory LRU cache is used by default.
|
|
39
42
|
|
|
40
43
|
```ts
|
|
41
|
-
import {
|
|
44
|
+
import { execute, parse, specifiedRules, subscribe, validate } from 'graphql'
|
|
42
45
|
import { envelop, useEngine } from '@envelop/core'
|
|
43
46
|
import { useResponseCache } from '@envelop/response-cache'
|
|
44
47
|
|
|
@@ -57,9 +60,9 @@ const getEnveloped = envelop({
|
|
|
57
60
|
Or, you may create the in-memory LRU cache explicitly.
|
|
58
61
|
|
|
59
62
|
```ts
|
|
60
|
-
import {
|
|
63
|
+
import { execute, parse, specifiedRules, subscribe, validate } from 'graphql'
|
|
61
64
|
import { envelop, useEngine } from '@envelop/core'
|
|
62
|
-
import {
|
|
65
|
+
import { createInMemoryCache, useResponseCache } from '@envelop/response-cache'
|
|
63
66
|
|
|
64
67
|
const cache = createInMemoryCache()
|
|
65
68
|
|
|
@@ -75,12 +78,13 @@ const getEnveloped = envelop({
|
|
|
75
78
|
})
|
|
76
79
|
```
|
|
77
80
|
|
|
78
|
-
> Note: The in-memory LRU cache is not suitable for serverless deployments. Instead, consider the
|
|
81
|
+
> Note: The in-memory LRU cache is not suitable for serverless deployments. Instead, consider the
|
|
82
|
+
> Redis cache provided by `@envelop/response-cache-redis`.
|
|
79
83
|
|
|
80
84
|
### Cache based on session/user
|
|
81
85
|
|
|
82
86
|
```ts
|
|
83
|
-
import {
|
|
87
|
+
import { execute, parse, specifiedRules, subscribe, validate } from 'graphql'
|
|
84
88
|
import { envelop, useEngine } from '@envelop/core'
|
|
85
89
|
import { useResponseCache } from '@envelop/response-cache'
|
|
86
90
|
|
|
@@ -106,8 +110,11 @@ yarn add @envelop/response-cache-redis
|
|
|
106
110
|
In order to use the Redis cache, you need to:
|
|
107
111
|
|
|
108
112
|
- Create a Redis database
|
|
109
|
-
- Collect the connection settings (or its connection string), e.g., `host`, `port`, `username`,
|
|
110
|
-
|
|
113
|
+
- Collect the connection settings (or its connection string), e.g., `host`, `port`, `username`,
|
|
114
|
+
`password`, `tls`, etc.
|
|
115
|
+
- Create and configure a [Redis client](https://github.com/luin/ioredis) with your
|
|
116
|
+
[connection settings](https://github.com/luin/ioredis/blob/master/API.md#Redis) and any
|
|
117
|
+
[additional options](https://github.com/luin/ioredis/blob/master/API.md#new_Redis_new)
|
|
111
118
|
- Create an instance of the Redis Cache and set to the `useResponseCache` plugin options
|
|
112
119
|
|
|
113
120
|
```ts
|
|
@@ -143,14 +150,15 @@ const getEnveloped = envelop({
|
|
|
143
150
|
})
|
|
144
151
|
```
|
|
145
152
|
|
|
146
|
-
> Note: In the Recipes below, be sure to provide your Redis `cache` instance with
|
|
153
|
+
> Note: In the Recipes below, be sure to provide your Redis `cache` instance with
|
|
154
|
+
> `useResponseCache({ cache })`.
|
|
147
155
|
|
|
148
156
|
## Recipes
|
|
149
157
|
|
|
150
158
|
### Cache with maximum TTL
|
|
151
159
|
|
|
152
160
|
```ts
|
|
153
|
-
import {
|
|
161
|
+
import { execute, parse, subscribe, validate } from 'graphql'
|
|
154
162
|
import { envelop } from '@envelop/core'
|
|
155
163
|
import { useResponseCache } from '@envelop/response-cache'
|
|
156
164
|
|
|
@@ -169,12 +177,13 @@ const getEnveloped = envelop({
|
|
|
169
177
|
})
|
|
170
178
|
```
|
|
171
179
|
|
|
172
|
-
> Note: Setting `ttl: 0` will disable TTL for all types. You can use that if you wish to disable
|
|
180
|
+
> Note: Setting `ttl: 0` will disable TTL for all types. You can use that if you wish to disable
|
|
181
|
+
> caching for all type, and then enable caching for specific types using `ttlPerType`.
|
|
173
182
|
|
|
174
183
|
### Cache with custom TTL per object type
|
|
175
184
|
|
|
176
185
|
```ts
|
|
177
|
-
import {
|
|
186
|
+
import { execute, parse, subscribe, validate } from 'graphql'
|
|
178
187
|
import { envelop } from '@envelop/core'
|
|
179
188
|
import { useResponseCache } from '@envelop/response-cache'
|
|
180
189
|
|
|
@@ -199,7 +208,7 @@ const getEnveloped = envelop({
|
|
|
199
208
|
### Cache with custom TTL per schema coordinate
|
|
200
209
|
|
|
201
210
|
```ts
|
|
202
|
-
import {
|
|
211
|
+
import { execute, parse, subscribe, validate } from 'graphql'
|
|
203
212
|
import { envelop } from '@envelop/core'
|
|
204
213
|
import { useResponseCache } from '@envelop/response-cache'
|
|
205
214
|
|
|
@@ -224,7 +233,7 @@ const getEnveloped = envelop({
|
|
|
224
233
|
### Disable cache based on session/user
|
|
225
234
|
|
|
226
235
|
```ts
|
|
227
|
-
import {
|
|
236
|
+
import { execute, parse, subscribe, validate } from 'graphql'
|
|
228
237
|
import { envelop } from '@envelop/core'
|
|
229
238
|
import { useResponseCache } from '@envelop/response-cache'
|
|
230
239
|
|
|
@@ -247,21 +256,22 @@ const getEnveloped = envelop({
|
|
|
247
256
|
|
|
248
257
|
### Customize if result should be cached
|
|
249
258
|
|
|
250
|
-
You can define a custom function used to check if a query operation execution result should be
|
|
259
|
+
You can define a custom function used to check if a query operation execution result should be
|
|
260
|
+
cached.
|
|
251
261
|
|
|
252
262
|
```ts
|
|
253
263
|
type ShouldCacheResultFunction = (params: { result: ExecutionResult }) => boolean
|
|
254
264
|
```
|
|
255
265
|
|
|
256
|
-
This is useful for advanced use-cases. E.g. if you want to
|
|
257
|
-
cache results with certain error types.
|
|
266
|
+
This is useful for advanced use-cases. E.g. if you want to cache results with certain error types.
|
|
258
267
|
|
|
259
|
-
By default, the `defaultShouldCacheResult` function is used which never caches any query operation
|
|
268
|
+
By default, the `defaultShouldCacheResult` function is used which never caches any query operation
|
|
269
|
+
execution results that includes any errors (unexpected, EnvelopError, or GraphQLError).
|
|
260
270
|
|
|
261
271
|
```ts
|
|
262
|
-
import {
|
|
272
|
+
import { execute, parse, subscribe, validate } from 'graphql'
|
|
263
273
|
import { envelop } from '@envelop/core'
|
|
264
|
-
import {
|
|
274
|
+
import { ShouldCacheResultFunction, useResponseCache } from '@envelop/response-cache'
|
|
265
275
|
|
|
266
276
|
export const defaultShouldCacheResult: ShouldCacheResultFunction = (params): Boolean => {
|
|
267
277
|
// cache any query operation execution result
|
|
@@ -286,12 +296,13 @@ const getEnveloped = envelop({
|
|
|
286
296
|
|
|
287
297
|
### Cache Introspection query operations
|
|
288
298
|
|
|
289
|
-
By default introspection query operations are not cached. In case you want to cache them you can do
|
|
299
|
+
By default introspection query operations are not cached. In case you want to cache them you can do
|
|
300
|
+
so with the `ttlPerSchemaCoordinate` parameter.
|
|
290
301
|
|
|
291
302
|
**Infinite caching**
|
|
292
303
|
|
|
293
304
|
```ts
|
|
294
|
-
import {
|
|
305
|
+
import { execute, parse, subscribe, validate } from 'graphql'
|
|
295
306
|
import { envelop } from '@envelop/core'
|
|
296
307
|
import { useResponseCache } from '@envelop/response-cache'
|
|
297
308
|
|
|
@@ -315,7 +326,7 @@ const getEnveloped = envelop({
|
|
|
315
326
|
**TTL caching**
|
|
316
327
|
|
|
317
328
|
```ts
|
|
318
|
-
import {
|
|
329
|
+
import { execute, parse, subscribe, validate } from 'graphql'
|
|
319
330
|
import { envelop } from '@envelop/core'
|
|
320
331
|
import { useResponseCache } from '@envelop/response-cache'
|
|
321
332
|
|
|
@@ -339,7 +350,7 @@ const getEnveloped = envelop({
|
|
|
339
350
|
### Cache with maximum TTL
|
|
340
351
|
|
|
341
352
|
```ts
|
|
342
|
-
import {
|
|
353
|
+
import { execute, parse, subscribe, validate } from 'graphql'
|
|
343
354
|
import { envelop } from '@envelop/core'
|
|
344
355
|
import { useResponseCache } from '@envelop/response-cache'
|
|
345
356
|
|
|
@@ -361,7 +372,7 @@ const getEnveloped = envelop({
|
|
|
361
372
|
### Customize the fields that are used for building the cache ID
|
|
362
373
|
|
|
363
374
|
```ts
|
|
364
|
-
import {
|
|
375
|
+
import { execute, parse, subscribe, validate } from 'graphql'
|
|
365
376
|
import { envelop } from '@envelop/core'
|
|
366
377
|
import { useResponseCache } from '@envelop/response-cache'
|
|
367
378
|
|
|
@@ -385,7 +396,7 @@ const getEnveloped = envelop({
|
|
|
385
396
|
### Disable automatic cache invalidation via mutations
|
|
386
397
|
|
|
387
398
|
```ts
|
|
388
|
-
import {
|
|
399
|
+
import { execute, parse, subscribe, validate } from 'graphql'
|
|
389
400
|
import { envelop } from '@envelop/core'
|
|
390
401
|
import { useResponseCache } from '@envelop/response-cache'
|
|
391
402
|
|
|
@@ -409,9 +420,9 @@ const getEnveloped = envelop({
|
|
|
409
420
|
### Invalidate Cache based on custom logic
|
|
410
421
|
|
|
411
422
|
```ts
|
|
412
|
-
import {
|
|
423
|
+
import { execute, parse, subscribe, validate } from 'graphql'
|
|
413
424
|
import { envelop } from '@envelop/core'
|
|
414
|
-
import {
|
|
425
|
+
import { createInMemoryCache, useResponseCache } from '@envelop/response-cache'
|
|
415
426
|
import { emitter } from './eventEmitter'
|
|
416
427
|
|
|
417
428
|
// we create our cache instance, which allows calling all methods on it
|
|
@@ -446,9 +457,9 @@ emitter.on('invalidate', resource => {
|
|
|
446
457
|
### Customize how cache ids are built
|
|
447
458
|
|
|
448
459
|
```ts
|
|
449
|
-
import {
|
|
460
|
+
import { execute, parse, subscribe, validate } from 'graphql'
|
|
450
461
|
import { envelop } from '@envelop/core'
|
|
451
|
-
import {
|
|
462
|
+
import { createInMemoryCache, useResponseCache } from '@envelop/response-cache'
|
|
452
463
|
import { emitter } from './eventEmitter'
|
|
453
464
|
|
|
454
465
|
// we create our cache instance, which allows calling all methods on it
|
|
@@ -476,10 +487,11 @@ const getEnveloped = envelop({
|
|
|
476
487
|
|
|
477
488
|
### Expose cache metadata via extensions
|
|
478
489
|
|
|
479
|
-
For debugging or monitoring it might be useful to know whether a response got served from the cache
|
|
490
|
+
For debugging or monitoring it might be useful to know whether a response got served from the cache
|
|
491
|
+
or not.
|
|
480
492
|
|
|
481
493
|
```ts
|
|
482
|
-
import {
|
|
494
|
+
import { execute, parse, subscribe, validate } from 'graphql'
|
|
483
495
|
import { envelop } from '@envelop/core'
|
|
484
496
|
|
|
485
497
|
const getEnveloped = envelop({
|
|
@@ -498,10 +510,12 @@ const getEnveloped = envelop({
|
|
|
498
510
|
})
|
|
499
511
|
```
|
|
500
512
|
|
|
501
|
-
This option will attach the following fields to the execution result if set to true (or
|
|
513
|
+
This option will attach the following fields to the execution result if set to true (or
|
|
514
|
+
`process.env["NODE_ENV"]` is `"development"`).
|
|
502
515
|
|
|
503
516
|
- `extension.responseCache.hit` - Whether the result was served form the cache or not
|
|
504
|
-
- `extension.responseCache.invalidatedEntities` - Entities that got invalidated by a mutation
|
|
517
|
+
- `extension.responseCache.invalidatedEntities` - Entities that got invalidated by a mutation
|
|
518
|
+
operation
|
|
505
519
|
|
|
506
520
|
#### Examples:
|
|
507
521
|
|
package/cjs/cache.js
CHANGED
package/cjs/plugin.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.useResponseCache = exports.defaultShouldCacheResult = exports.defaultBuildResponseCacheKey = void 0;
|
|
3
|
+
exports.useResponseCache = exports.defaultGetDocumentString = exports.defaultShouldCacheResult = exports.defaultBuildResponseCacheKey = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
+
const fast_json_stable_stringify_1 = tslib_1.__importDefault(require("fast-json-stable-stringify"));
|
|
6
|
+
const graphql_1 = require("graphql");
|
|
5
7
|
const core_1 = require("@envelop/core");
|
|
6
8
|
const utils_1 = require("@graphql-tools/utils");
|
|
7
|
-
const graphql_1 = require("graphql");
|
|
8
|
-
const fast_json_stable_stringify_1 = tslib_1.__importDefault(require("fast-json-stable-stringify"));
|
|
9
|
-
const in_memory_cache_js_1 = require("./in-memory-cache.js");
|
|
10
9
|
const hash_sha256_js_1 = require("./hash-sha256.js");
|
|
11
|
-
const
|
|
10
|
+
const in_memory_cache_js_1 = require("./in-memory-cache.js");
|
|
12
11
|
/**
|
|
13
12
|
* Default function used for building the response cache key.
|
|
14
13
|
* It is exported here for advanced use-cases. E.g. if you want to short circuit and serve responses from the cache on a global level in order to completely by-pass the GraphQL flow.
|
|
@@ -37,55 +36,63 @@ const defaultShouldCacheResult = (params) => {
|
|
|
37
36
|
return true;
|
|
38
37
|
};
|
|
39
38
|
exports.defaultShouldCacheResult = defaultShouldCacheResult;
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
function defaultGetDocumentString(executionArgs) {
|
|
40
|
+
return (0, core_1.getDocumentString)(executionArgs.document, graphql_1.print);
|
|
41
|
+
}
|
|
42
|
+
exports.defaultGetDocumentString = defaultGetDocumentString;
|
|
43
|
+
const originalDocumentMap = new WeakMap();
|
|
44
|
+
const addTypeNameToDocument = (0, utils_1.memoize1)(function addTypeNameToDocument(document) {
|
|
45
|
+
let documentChanged = false;
|
|
46
|
+
const newDocument = (0, graphql_1.visit)(document, {
|
|
47
|
+
SelectionSet(node) {
|
|
48
|
+
if (!node.selections.some(selection => selection.kind === graphql_1.Kind.FIELD && selection.name.value === '__typename')) {
|
|
49
|
+
documentChanged = true;
|
|
50
|
+
return {
|
|
51
|
+
...node,
|
|
52
|
+
selections: [
|
|
53
|
+
{
|
|
54
|
+
kind: graphql_1.Kind.FIELD,
|
|
55
|
+
name: {
|
|
56
|
+
kind: graphql_1.Kind.NAME,
|
|
57
|
+
value: '__typename',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
...node.selections,
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return node;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
if (documentChanged) {
|
|
68
|
+
originalDocumentMap.set(newDocument, document);
|
|
69
|
+
}
|
|
70
|
+
return newDocument;
|
|
71
|
+
});
|
|
72
|
+
function useResponseCache({ cache = (0, in_memory_cache_js_1.createInMemoryCache)(), ttl: globalTtl = Infinity, session, enabled, ignoredTypes = [], ttlPerType = {}, ttlPerSchemaCoordinate = {}, idFields = ['id'], invalidateViaMutation = true, buildResponseCacheKey = exports.defaultBuildResponseCacheKey, getDocumentString = defaultGetDocumentString, shouldCacheResult = exports.defaultShouldCacheResult, includeExtensionMetadata = typeof process !== 'undefined'
|
|
73
|
+
? // eslint-disable-next-line dot-notation
|
|
74
|
+
process.env['NODE_ENV'] === 'development'
|
|
75
|
+
: false, }) {
|
|
43
76
|
const ignoredTypesMap = new Set(ignoredTypes);
|
|
44
77
|
// never cache Introspections
|
|
45
78
|
ttlPerSchemaCoordinate = { 'Query.__schema': 0, ...ttlPerSchemaCoordinate };
|
|
46
79
|
return {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
onParse() {
|
|
81
|
+
return ({ result, replaceParseResult }) => {
|
|
82
|
+
if (result.kind === graphql_1.Kind.DOCUMENT) {
|
|
83
|
+
const newDocument = addTypeNameToDocument(result);
|
|
84
|
+
replaceParseResult(newDocument);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
51
87
|
},
|
|
52
88
|
async onExecute(onExecuteParams) {
|
|
53
|
-
let documentChanged = false;
|
|
54
|
-
const newDocument = (0, graphql_1.visit)(onExecuteParams.args.document, {
|
|
55
|
-
SelectionSet(node) {
|
|
56
|
-
if (!node.selections.some(selection => selection.kind === graphql_1.Kind.FIELD && selection.name.value === '__typename')) {
|
|
57
|
-
documentChanged = true;
|
|
58
|
-
return {
|
|
59
|
-
...node,
|
|
60
|
-
selections: [
|
|
61
|
-
{
|
|
62
|
-
kind: graphql_1.Kind.FIELD,
|
|
63
|
-
name: {
|
|
64
|
-
kind: graphql_1.Kind.NAME,
|
|
65
|
-
value: '__typename',
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
...node.selections,
|
|
69
|
-
],
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
return node;
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
if (documentChanged) {
|
|
76
|
-
onExecuteParams.setExecuteFn(function typeNameAddedExecute() {
|
|
77
|
-
return onExecuteParams.executeFn({
|
|
78
|
-
...onExecuteParams.args,
|
|
79
|
-
document: newDocument,
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
89
|
const identifier = new Map();
|
|
84
90
|
const types = new Set();
|
|
85
91
|
let currentTtl;
|
|
86
92
|
let skip = false;
|
|
87
93
|
const processResult = (result) => (0, utils_1.visitResult)(result, {
|
|
88
|
-
document: onExecuteParams.args.document
|
|
94
|
+
document: originalDocumentMap.get(onExecuteParams.args.document) ??
|
|
95
|
+
onExecuteParams.args.document,
|
|
89
96
|
variables: onExecuteParams.args.variableValues,
|
|
90
97
|
operationName: onExecuteParams.args.operationName ?? undefined,
|
|
91
98
|
rootValue: onExecuteParams.args.rootValue,
|
|
@@ -100,25 +107,23 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
|
|
|
100
107
|
return val;
|
|
101
108
|
},
|
|
102
109
|
get(_, fieldName) {
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
};
|
|
121
|
-
}
|
|
110
|
+
if (fieldName === '__typename') {
|
|
111
|
+
typenameCalled++;
|
|
112
|
+
}
|
|
113
|
+
if (fieldName === '__leave' &&
|
|
114
|
+
/**
|
|
115
|
+
* The visitResult function is called for each field in the selection set.
|
|
116
|
+
* But visitResult function looks for __typename field visitor even if it is not there in the document
|
|
117
|
+
* So it calls __typename field visitor twice if it is also in the selection set.
|
|
118
|
+
* That's why we need to count the number of times it is called.
|
|
119
|
+
*
|
|
120
|
+
* Default call of __typename https://github.com/ardatan/graphql-tools/blob/master/packages/utils/src/visitResult.ts#L277
|
|
121
|
+
* Call for the field node https://github.com/ardatan/graphql-tools/blob/master/packages/utils/src/visitResult.ts#L272
|
|
122
|
+
*/ typenameCalled < 2) {
|
|
123
|
+
return (root) => {
|
|
124
|
+
delete root.__typename;
|
|
125
|
+
return root;
|
|
126
|
+
};
|
|
122
127
|
}
|
|
123
128
|
if (ignoredTypesMap.has(typename)) {
|
|
124
129
|
skip = true;
|
|
@@ -141,31 +146,33 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
|
|
|
141
146
|
});
|
|
142
147
|
},
|
|
143
148
|
}));
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
149
|
+
if (invalidateViaMutation !== false) {
|
|
150
|
+
const operationAST = (0, graphql_1.getOperationAST)(onExecuteParams.args.document, onExecuteParams.args.operationName);
|
|
151
|
+
if (operationAST?.operation === 'mutation') {
|
|
152
|
+
return {
|
|
153
|
+
onExecuteDone({ result, setResult }) {
|
|
154
|
+
if ((0, core_1.isAsyncIterable)(result)) {
|
|
155
|
+
// eslint-disable-next-line no-console
|
|
156
|
+
console.warn('[useResponseCache] AsyncIterable returned from execute is currently unsupported.');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const processedResult = processResult(result);
|
|
160
|
+
cache.invalidate(identifier.values());
|
|
161
|
+
if (includeExtensionMetadata) {
|
|
162
|
+
setResult({
|
|
163
|
+
...processedResult,
|
|
164
|
+
extensions: {
|
|
165
|
+
...processedResult.extensions,
|
|
166
|
+
responseCache: {
|
|
167
|
+
...processedResult.extensions?.responseCache,
|
|
168
|
+
invalidatedEntities: Array.from(identifier.values()),
|
|
169
|
+
},
|
|
163
170
|
},
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
169
176
|
}
|
|
170
177
|
const cacheKey = await buildResponseCacheKey({
|
|
171
178
|
documentString: getDocumentString(onExecuteParams.args),
|
package/esm/cache.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/esm/plugin.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { isAsyncIterable } from '@envelop/core';
|
|
2
|
-
import { visitResult } from '@graphql-tools/utils';
|
|
3
|
-
import { visit, TypeInfo, visitWithTypeInfo, getOperationAST, Kind } from 'graphql';
|
|
4
1
|
import jsonStableStringify from 'fast-json-stable-stringify';
|
|
5
|
-
import {
|
|
2
|
+
import { getOperationAST, Kind, print, TypeInfo, visit, visitWithTypeInfo, } from 'graphql';
|
|
3
|
+
import { getDocumentString, isAsyncIterable, } from '@envelop/core';
|
|
4
|
+
import { memoize1, visitResult } from '@graphql-tools/utils';
|
|
6
5
|
import { hashSHA256 } from './hash-sha256.js';
|
|
7
|
-
import {
|
|
6
|
+
import { createInMemoryCache } from './in-memory-cache.js';
|
|
8
7
|
/**
|
|
9
8
|
* Default function used for building the response cache key.
|
|
10
9
|
* It is exported here for advanced use-cases. E.g. if you want to short circuit and serve responses from the cache on a global level in order to completely by-pass the GraphQL flow.
|
|
@@ -31,55 +30,62 @@ export const defaultShouldCacheResult = (params) => {
|
|
|
31
30
|
}
|
|
32
31
|
return true;
|
|
33
32
|
};
|
|
34
|
-
export function
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
export function defaultGetDocumentString(executionArgs) {
|
|
34
|
+
return getDocumentString(executionArgs.document, print);
|
|
35
|
+
}
|
|
36
|
+
const originalDocumentMap = new WeakMap();
|
|
37
|
+
const addTypeNameToDocument = memoize1(function addTypeNameToDocument(document) {
|
|
38
|
+
let documentChanged = false;
|
|
39
|
+
const newDocument = visit(document, {
|
|
40
|
+
SelectionSet(node) {
|
|
41
|
+
if (!node.selections.some(selection => selection.kind === Kind.FIELD && selection.name.value === '__typename')) {
|
|
42
|
+
documentChanged = true;
|
|
43
|
+
return {
|
|
44
|
+
...node,
|
|
45
|
+
selections: [
|
|
46
|
+
{
|
|
47
|
+
kind: Kind.FIELD,
|
|
48
|
+
name: {
|
|
49
|
+
kind: Kind.NAME,
|
|
50
|
+
value: '__typename',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
...node.selections,
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return node;
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
if (documentChanged) {
|
|
61
|
+
originalDocumentMap.set(newDocument, document);
|
|
62
|
+
}
|
|
63
|
+
return newDocument;
|
|
64
|
+
});
|
|
65
|
+
export function useResponseCache({ cache = createInMemoryCache(), ttl: globalTtl = Infinity, session, enabled, ignoredTypes = [], ttlPerType = {}, ttlPerSchemaCoordinate = {}, idFields = ['id'], invalidateViaMutation = true, buildResponseCacheKey = defaultBuildResponseCacheKey, getDocumentString = defaultGetDocumentString, shouldCacheResult = defaultShouldCacheResult, includeExtensionMetadata = typeof process !== 'undefined'
|
|
66
|
+
? // eslint-disable-next-line dot-notation
|
|
67
|
+
process.env['NODE_ENV'] === 'development'
|
|
68
|
+
: false, }) {
|
|
37
69
|
const ignoredTypesMap = new Set(ignoredTypes);
|
|
38
70
|
// never cache Introspections
|
|
39
71
|
ttlPerSchemaCoordinate = { 'Query.__schema': 0, ...ttlPerSchemaCoordinate };
|
|
40
72
|
return {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
73
|
+
onParse() {
|
|
74
|
+
return ({ result, replaceParseResult }) => {
|
|
75
|
+
if (result.kind === Kind.DOCUMENT) {
|
|
76
|
+
const newDocument = addTypeNameToDocument(result);
|
|
77
|
+
replaceParseResult(newDocument);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
45
80
|
},
|
|
46
81
|
async onExecute(onExecuteParams) {
|
|
47
|
-
let documentChanged = false;
|
|
48
|
-
const newDocument = visit(onExecuteParams.args.document, {
|
|
49
|
-
SelectionSet(node) {
|
|
50
|
-
if (!node.selections.some(selection => selection.kind === Kind.FIELD && selection.name.value === '__typename')) {
|
|
51
|
-
documentChanged = true;
|
|
52
|
-
return {
|
|
53
|
-
...node,
|
|
54
|
-
selections: [
|
|
55
|
-
{
|
|
56
|
-
kind: Kind.FIELD,
|
|
57
|
-
name: {
|
|
58
|
-
kind: Kind.NAME,
|
|
59
|
-
value: '__typename',
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
...node.selections,
|
|
63
|
-
],
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
return node;
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
if (documentChanged) {
|
|
70
|
-
onExecuteParams.setExecuteFn(function typeNameAddedExecute() {
|
|
71
|
-
return onExecuteParams.executeFn({
|
|
72
|
-
...onExecuteParams.args,
|
|
73
|
-
document: newDocument,
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
82
|
const identifier = new Map();
|
|
78
83
|
const types = new Set();
|
|
79
84
|
let currentTtl;
|
|
80
85
|
let skip = false;
|
|
81
86
|
const processResult = (result) => visitResult(result, {
|
|
82
|
-
document: onExecuteParams.args.document
|
|
87
|
+
document: originalDocumentMap.get(onExecuteParams.args.document) ??
|
|
88
|
+
onExecuteParams.args.document,
|
|
83
89
|
variables: onExecuteParams.args.variableValues,
|
|
84
90
|
operationName: onExecuteParams.args.operationName ?? undefined,
|
|
85
91
|
rootValue: onExecuteParams.args.rootValue,
|
|
@@ -94,25 +100,23 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
|
|
|
94
100
|
return val;
|
|
95
101
|
},
|
|
96
102
|
get(_, fieldName) {
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
};
|
|
115
|
-
}
|
|
103
|
+
if (fieldName === '__typename') {
|
|
104
|
+
typenameCalled++;
|
|
105
|
+
}
|
|
106
|
+
if (fieldName === '__leave' &&
|
|
107
|
+
/**
|
|
108
|
+
* The visitResult function is called for each field in the selection set.
|
|
109
|
+
* But visitResult function looks for __typename field visitor even if it is not there in the document
|
|
110
|
+
* So it calls __typename field visitor twice if it is also in the selection set.
|
|
111
|
+
* That's why we need to count the number of times it is called.
|
|
112
|
+
*
|
|
113
|
+
* Default call of __typename https://github.com/ardatan/graphql-tools/blob/master/packages/utils/src/visitResult.ts#L277
|
|
114
|
+
* Call for the field node https://github.com/ardatan/graphql-tools/blob/master/packages/utils/src/visitResult.ts#L272
|
|
115
|
+
*/ typenameCalled < 2) {
|
|
116
|
+
return (root) => {
|
|
117
|
+
delete root.__typename;
|
|
118
|
+
return root;
|
|
119
|
+
};
|
|
116
120
|
}
|
|
117
121
|
if (ignoredTypesMap.has(typename)) {
|
|
118
122
|
skip = true;
|
|
@@ -135,31 +139,33 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
|
|
|
135
139
|
});
|
|
136
140
|
},
|
|
137
141
|
}));
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
142
|
+
if (invalidateViaMutation !== false) {
|
|
143
|
+
const operationAST = getOperationAST(onExecuteParams.args.document, onExecuteParams.args.operationName);
|
|
144
|
+
if (operationAST?.operation === 'mutation') {
|
|
145
|
+
return {
|
|
146
|
+
onExecuteDone({ result, setResult }) {
|
|
147
|
+
if (isAsyncIterable(result)) {
|
|
148
|
+
// eslint-disable-next-line no-console
|
|
149
|
+
console.warn('[useResponseCache] AsyncIterable returned from execute is currently unsupported.');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const processedResult = processResult(result);
|
|
153
|
+
cache.invalidate(identifier.values());
|
|
154
|
+
if (includeExtensionMetadata) {
|
|
155
|
+
setResult({
|
|
156
|
+
...processedResult,
|
|
157
|
+
extensions: {
|
|
158
|
+
...processedResult.extensions,
|
|
159
|
+
responseCache: {
|
|
160
|
+
...processedResult.extensions?.responseCache,
|
|
161
|
+
invalidatedEntities: Array.from(identifier.values()),
|
|
162
|
+
},
|
|
157
163
|
},
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
163
169
|
}
|
|
164
170
|
const cacheKey = await buildResponseCacheKey({
|
|
165
171
|
documentString: getDocumentString(onExecuteParams.args),
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@envelop/response-cache",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.9-alpha-20230505095410-8ff5ea11",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"peerDependencies": {
|
|
6
|
-
"@envelop/core": "
|
|
6
|
+
"@envelop/core": "3.0.7-alpha-20230505095410-8ff5ea11",
|
|
7
7
|
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@graphql-tools/utils": "^8.8.0",
|
|
11
11
|
"@whatwg-node/fetch": "^0.8.0",
|
|
12
|
-
"lru-cache": "^6.0.0",
|
|
13
12
|
"fast-json-stable-stringify": "^2.1.0",
|
|
13
|
+
"lru-cache": "^6.0.0",
|
|
14
14
|
"tslib": "^2.5.0"
|
|
15
15
|
},
|
|
16
16
|
"repository": {
|
package/typings/cache.d.cts
CHANGED
package/typings/cache.d.ts
CHANGED
package/typings/plugin.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Plugin, Maybe, ExecutionResult, ObjMap } from '@envelop/core';
|
|
2
1
|
import { ExecutionArgs } from 'graphql';
|
|
2
|
+
import { ExecutionResult, Maybe, ObjMap, Plugin } from '@envelop/core';
|
|
3
3
|
import type { Cache } from './cache.cjs';
|
|
4
4
|
/**
|
|
5
5
|
* Function for building the response cache key based on the input parameters
|
|
@@ -120,6 +120,7 @@ export declare const defaultBuildResponseCacheKey: BuildResponseCacheKeyFunction
|
|
|
120
120
|
* By default, results with errors (unexpected, EnvelopError, or GraphQLError) are not cached.
|
|
121
121
|
*/
|
|
122
122
|
export declare const defaultShouldCacheResult: ShouldCacheResultFunction;
|
|
123
|
+
export declare function defaultGetDocumentString(executionArgs: ExecutionArgs): string;
|
|
123
124
|
export declare type ResponseCacheExtensions = {
|
|
124
125
|
hit: true;
|
|
125
126
|
} | {
|
package/typings/plugin.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Plugin, Maybe, ExecutionResult, ObjMap } from '@envelop/core';
|
|
2
1
|
import { ExecutionArgs } from 'graphql';
|
|
2
|
+
import { ExecutionResult, Maybe, ObjMap, Plugin } from '@envelop/core';
|
|
3
3
|
import type { Cache } from './cache.js';
|
|
4
4
|
/**
|
|
5
5
|
* Function for building the response cache key based on the input parameters
|
|
@@ -120,6 +120,7 @@ export declare const defaultBuildResponseCacheKey: BuildResponseCacheKeyFunction
|
|
|
120
120
|
* By default, results with errors (unexpected, EnvelopError, or GraphQLError) are not cached.
|
|
121
121
|
*/
|
|
122
122
|
export declare const defaultShouldCacheResult: ShouldCacheResultFunction;
|
|
123
|
+
export declare function defaultGetDocumentString(executionArgs: ExecutionArgs): string;
|
|
123
124
|
export declare type ResponseCacheExtensions = {
|
|
124
125
|
hit: true;
|
|
125
126
|
} | {
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.defaultGetDocumentString = exports.useCacheDocumentString = void 0;
|
|
4
|
-
const graphql_1 = require("graphql");
|
|
5
|
-
const documentStringByDocument = new WeakMap();
|
|
6
|
-
function useCacheDocumentString() {
|
|
7
|
-
return {
|
|
8
|
-
onParse({ params: { source } }) {
|
|
9
|
-
return function onParseEnd({ result }) {
|
|
10
|
-
if (result != null && !(result instanceof Error)) {
|
|
11
|
-
documentStringByDocument.set(result, source.toString());
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
exports.useCacheDocumentString = useCacheDocumentString;
|
|
18
|
-
function defaultGetDocumentString(executionArgs) {
|
|
19
|
-
let documentString = documentStringByDocument.get(executionArgs.document);
|
|
20
|
-
if (documentString == null) {
|
|
21
|
-
documentString = (0, graphql_1.print)(executionArgs.document);
|
|
22
|
-
documentStringByDocument.set(executionArgs.document, documentString);
|
|
23
|
-
}
|
|
24
|
-
return documentString;
|
|
25
|
-
}
|
|
26
|
-
exports.defaultGetDocumentString = defaultGetDocumentString;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { print } from 'graphql';
|
|
2
|
-
const documentStringByDocument = new WeakMap();
|
|
3
|
-
export function useCacheDocumentString() {
|
|
4
|
-
return {
|
|
5
|
-
onParse({ params: { source } }) {
|
|
6
|
-
return function onParseEnd({ result }) {
|
|
7
|
-
if (result != null && !(result instanceof Error)) {
|
|
8
|
-
documentStringByDocument.set(result, source.toString());
|
|
9
|
-
}
|
|
10
|
-
};
|
|
11
|
-
},
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
export function defaultGetDocumentString(executionArgs) {
|
|
15
|
-
let documentString = documentStringByDocument.get(executionArgs.document);
|
|
16
|
-
if (documentString == null) {
|
|
17
|
-
documentString = print(executionArgs.document);
|
|
18
|
-
documentStringByDocument.set(executionArgs.document, documentString);
|
|
19
|
-
}
|
|
20
|
-
return documentString;
|
|
21
|
-
}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { ExecutionArgs } from 'graphql';
|
|
2
|
-
import { Plugin } from '@envelop/core';
|
|
3
|
-
export declare function useCacheDocumentString<PluginContext extends Record<string, any> = {}>(): Plugin<PluginContext>;
|
|
4
|
-
export declare function defaultGetDocumentString(executionArgs: ExecutionArgs): string;
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { ExecutionArgs } from 'graphql';
|
|
2
|
-
import { Plugin } from '@envelop/core';
|
|
3
|
-
export declare function useCacheDocumentString<PluginContext extends Record<string, any> = {}>(): Plugin<PluginContext>;
|
|
4
|
-
export declare function defaultGetDocumentString(executionArgs: ExecutionArgs): string;
|