@envelop/response-cache 4.0.8 → 4.0.9-alpha-20230505095813-2dc6c6f3

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 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` interface.
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 [Episode #34 of `graphql.wtf`](https://graphql.wtf/episodes/34-response-cache-plugin-with-envelop) for a quick introduction to using Response Cache plugin with Envelop:
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 { parse, validate, specifiedRules, execute, subscribe } from 'graphql'
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 { parse, validate, specifiedRules, execute, subscribe } from 'graphql'
63
+ import { execute, parse, specifiedRules, subscribe, validate } from 'graphql'
61
64
  import { envelop, useEngine } from '@envelop/core'
62
- import { useResponseCache, createInMemoryCache } from '@envelop/response-cache'
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 Redis cache provided by `@envelop/response-cache-redis`.
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 { parse, validate, specifiedRules, execute, subscribe } from 'graphql'
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`, `password`, `tls`, etc.
110
- - Create and configure a [Redis client](https://github.com/luin/ioredis) with your [connection settings](https://github.com/luin/ioredis/blob/master/API.md#Redis) and any [additional options](https://github.com/luin/ioredis/blob/master/API.md#new_Redis_new)
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 `useResponseCache({ cache })`.
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 { parse, validate, execute, subscribe } from 'graphql'
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 caching for all type, and then enable caching for specific types using `ttlPerType`.
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 { parse, validate, execute, subscribe } from 'graphql'
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 { parse, validate, execute, subscribe } from 'graphql'
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 { parse, validate, execute, subscribe } from 'graphql'
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 cached.
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 execution results that includes any errors (unexpected, EnvelopError, or GraphQLError).
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 { parse, validate, execute, subscribe } from 'graphql'
272
+ import { execute, parse, subscribe, validate } from 'graphql'
263
273
  import { envelop } from '@envelop/core'
264
- import { useResponseCache, ShouldCacheResultFunction } from '@envelop/response-cache'
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 so with the `ttlPerSchemaCoordinate` parameter.
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 { parse, validate, execute, subscribe } from 'graphql'
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 { parse, validate, execute, subscribe } from 'graphql'
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 { parse, validate, execute, subscribe } from 'graphql'
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 { parse, validate, execute, subscribe } from 'graphql'
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 { parse, validate, execute, subscribe } from 'graphql'
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 { parse, validate, execute, subscribe } from 'graphql'
423
+ import { execute, parse, subscribe, validate } from 'graphql'
413
424
  import { envelop } from '@envelop/core'
414
- import { useResponseCache, createInMemoryCache } from '@envelop/response-cache'
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 { parse, validate, execute, subscribe } from 'graphql'
460
+ import { execute, parse, subscribe, validate } from 'graphql'
450
461
  import { envelop } from '@envelop/core'
451
- import { useResponseCache, createInMemoryCache } from '@envelop/response-cache'
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 or not.
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 { parse, validate, execute, subscribe } from 'graphql'
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 `process.env["NODE_ENV"]` is `"development"`).
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 operation
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
@@ -1,2 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
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 cache_document_str_js_1 = require("./cache-document-str.js");
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 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 = cache_document_str_js_1.defaultGetDocumentString, shouldCacheResult = exports.defaultShouldCacheResult,
41
- // eslint-disable-next-line dot-notation
42
- includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_ENV'] === 'development' : false, }) {
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
- onPluginInit({ addPlugin }) {
48
- if (getDocumentString === cache_document_str_js_1.defaultGetDocumentString) {
49
- addPlugin((0, cache_document_str_js_1.useCacheDocumentString)());
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 (documentChanged) {
104
- if (fieldName === '__typename') {
105
- typenameCalled++;
106
- }
107
- if (fieldName === '__leave' &&
108
- /**
109
- * The visitResult function is called for each field in the selection set.
110
- * But visitResult function looks for __typename field visitor even if it is not there in the document
111
- * So it calls __typename field visitor twice if it is also in the selection set.
112
- * That's why we need to count the number of times it is called.
113
- *
114
- * Default call of __typename https://github.com/ardatan/graphql-tools/blob/master/packages/utils/src/visitResult.ts#L277
115
- * Call for the field node https://github.com/ardatan/graphql-tools/blob/master/packages/utils/src/visitResult.ts#L272
116
- */ typenameCalled < 2) {
117
- return (root) => {
118
- delete root.__typename;
119
- return root;
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
- const operationAST = (0, graphql_1.getOperationAST)(onExecuteParams.args.document, onExecuteParams.args.operationName);
145
- if (invalidateViaMutation !== false && operationAST?.operation === 'mutation') {
146
- return {
147
- onExecuteDone({ result, setResult }) {
148
- if ((0, core_1.isAsyncIterable)(result)) {
149
- // eslint-disable-next-line no-console
150
- console.warn('[useResponseCache] AsyncIterable returned from execute is currently unsupported.');
151
- return;
152
- }
153
- const processedResult = processResult(result);
154
- cache.invalidate(identifier.values());
155
- if (includeExtensionMetadata) {
156
- setResult({
157
- ...processedResult,
158
- extensions: {
159
- ...processedResult.extensions,
160
- responseCache: {
161
- ...processedResult.extensions?.responseCache,
162
- invalidatedEntities: Array.from(identifier.values()),
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 { createInMemoryCache } from './in-memory-cache.js';
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 { defaultGetDocumentString, useCacheDocumentString } from './cache-document-str.js';
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 useResponseCache({ cache = createInMemoryCache(), ttl: globalTtl = Infinity, session, enabled, ignoredTypes = [], ttlPerType = {}, ttlPerSchemaCoordinate = {}, idFields = ['id'], invalidateViaMutation = true, buildResponseCacheKey = defaultBuildResponseCacheKey, getDocumentString = defaultGetDocumentString, shouldCacheResult = defaultShouldCacheResult,
35
- // eslint-disable-next-line dot-notation
36
- includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_ENV'] === 'development' : false, }) {
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
- onPluginInit({ addPlugin }) {
42
- if (getDocumentString === defaultGetDocumentString) {
43
- addPlugin(useCacheDocumentString());
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 (documentChanged) {
98
- if (fieldName === '__typename') {
99
- typenameCalled++;
100
- }
101
- if (fieldName === '__leave' &&
102
- /**
103
- * The visitResult function is called for each field in the selection set.
104
- * But visitResult function looks for __typename field visitor even if it is not there in the document
105
- * So it calls __typename field visitor twice if it is also in the selection set.
106
- * That's why we need to count the number of times it is called.
107
- *
108
- * Default call of __typename https://github.com/ardatan/graphql-tools/blob/master/packages/utils/src/visitResult.ts#L277
109
- * Call for the field node https://github.com/ardatan/graphql-tools/blob/master/packages/utils/src/visitResult.ts#L272
110
- */ typenameCalled < 2) {
111
- return (root) => {
112
- delete root.__typename;
113
- return root;
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
- const operationAST = getOperationAST(onExecuteParams.args.document, onExecuteParams.args.operationName);
139
- if (invalidateViaMutation !== false && operationAST?.operation === 'mutation') {
140
- return {
141
- onExecuteDone({ result, setResult }) {
142
- if (isAsyncIterable(result)) {
143
- // eslint-disable-next-line no-console
144
- console.warn('[useResponseCache] AsyncIterable returned from execute is currently unsupported.');
145
- return;
146
- }
147
- const processedResult = processResult(result);
148
- cache.invalidate(identifier.values());
149
- if (includeExtensionMetadata) {
150
- setResult({
151
- ...processedResult,
152
- extensions: {
153
- ...processedResult.extensions,
154
- responseCache: {
155
- ...processedResult.extensions?.responseCache,
156
- invalidatedEntities: Array.from(identifier.values()),
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.8",
3
+ "version": "4.0.9-alpha-20230505095813-2dc6c6f3",
4
4
  "sideEffects": false,
5
5
  "peerDependencies": {
6
- "@envelop/core": "^3.0.6",
6
+ "@envelop/core": "3.0.7-alpha-20230505095813-2dc6c6f3",
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": {
@@ -1,5 +1,5 @@
1
- import type { Maybe, PromiseOrValue } from '@envelop/core';
2
1
  import type { ExecutionResult } from 'graphql';
2
+ import type { Maybe, PromiseOrValue } from '@envelop/core';
3
3
  export declare type CacheEntityRecord = {
4
4
  typename: string;
5
5
  id?: number | string;
@@ -1,5 +1,5 @@
1
- import type { Maybe, PromiseOrValue } from '@envelop/core';
2
1
  import type { ExecutionResult } from 'graphql';
2
+ import type { Maybe, PromiseOrValue } from '@envelop/core';
3
3
  export declare type CacheEntityRecord = {
4
4
  typename: string;
5
5
  id?: number | string;
@@ -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
  } | {
@@ -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;