@envelop/response-cache 3.0.0-alpha-9e9dfbb.0 → 3.0.0-alpha-e41b2ae.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,35 +38,37 @@ When configuring the `useResponseCache`, you can choose the type of cache:
38
38
  The in-memory LRU cache is used by default.
39
39
 
40
40
  ```ts
41
- import { envelop } from '@envelop/core';
42
- import { useResponseCache } from '@envelop/response-cache';
41
+ import { envelop } from '@envelop/core'
42
+ import { useResponseCache } from '@envelop/response-cache'
43
43
 
44
44
  const getEnveloped = envelop({
45
45
  plugins: [
46
46
  // ... other plugins ...
47
47
  useResponseCache({
48
48
  // use global cache for all operations
49
- session: () => null,
50
- }),
51
- ],
52
- });
49
+ session: () => null
50
+ })
51
+ ]
52
+ })
53
53
  ```
54
54
 
55
55
  Or, you may create the in-memory LRU cache explicitly.
56
56
 
57
57
  ```ts
58
- import { envelop } from '@envelop/core';
59
- import { useResponseCache, createInMemoryCache } from '@envelop/response-cache';
58
+ import { envelop } from '@envelop/core'
59
+ import { useResponseCache, createInMemoryCache } from '@envelop/response-cache'
60
60
 
61
- const cache = createInMemoryCache();
61
+ const cache = createInMemoryCache()
62
62
 
63
63
  const getEnveloped = envelop({
64
64
  plugins: [
65
65
  // ... other plugins ...
66
- useResponseCache({ cache }),
67
- session: () => null, // use global cache for all operations
68
- ],
69
- });
66
+ useResponseCache({
67
+ cache,
68
+ session: () => null // use global cache for all operations
69
+ })
70
+ ]
71
+ })
70
72
  ```
71
73
 
72
74
  > Note: The in-memory LRU cache is not suitable for serverless deployments. Instead, consider the Redis cache provided by `@envelop/response-cache-redis`.
@@ -74,8 +76,8 @@ const getEnveloped = envelop({
74
76
  ### Cache based on session/user
75
77
 
76
78
  ```ts
77
- import { envelop } from '@envelop/core';
78
- import { useResponseCache } from '@envelop/response-cache';
79
+ import { envelop } from '@envelop/core'
80
+ import { useResponseCache } from '@envelop/response-cache'
79
81
 
80
82
  const getEnveloped = envelop({
81
83
  plugins: [
@@ -83,10 +85,10 @@ const getEnveloped = envelop({
83
85
  useResponseCache({
84
86
  ttl: 2000,
85
87
  // context is the GraphQL context used for execution
86
- session: context => String(context.user?.id),
87
- }),
88
- ],
89
- });
88
+ session: context => String(context.user?.id)
89
+ })
90
+ ]
91
+ })
90
92
  ```
91
93
 
92
94
  ### Redis Cache
@@ -103,31 +105,31 @@ In order to use the Redis cache, you need to:
103
105
  - Create an instance of the Redis Cache and set to the `useResponseCache` plugin options
104
106
 
105
107
  ```ts
106
- import { envelop } from '@envelop/core';
107
- import { useResponseCache } from '@envelop/response-cache';
108
- import { createRedisCache } from '@envelop/response-cache-redis';
108
+ import { envelop } from '@envelop/core'
109
+ import { useResponseCache } from '@envelop/response-cache'
110
+ import { createRedisCache } from '@envelop/response-cache-redis'
109
111
 
110
- import Redis from 'ioredis';
112
+ import Redis from 'ioredis'
111
113
 
112
114
  const redis = new Redis({
113
115
  host: 'my-redis-db.example.com',
114
116
  port: '30652',
115
- password: '1234567890',
116
- });
117
+ password: '1234567890'
118
+ })
117
119
 
118
- const redis = new Redis('rediss://:1234567890@my-redis-db.example.com:30652');
120
+ const redis = new Redis('rediss://:1234567890@my-redis-db.example.com:30652')
119
121
 
120
- const cache = createRedisCache({ redis });
122
+ const cache = createRedisCache({ redis })
121
123
 
122
124
  const getEnveloped = envelop({
123
125
  plugins: [
124
126
  // ... other plugins ...
125
127
  useResponseCache({
126
128
  cache,
127
- session: () => null, // use global cache for all operations
128
- }),
129
- ],
130
- });
129
+ session: () => null // use global cache for all operations
130
+ })
131
+ ]
132
+ })
131
133
  ```
132
134
 
133
135
  > Note: In the Recipes below, be sure to provide your Redis `cache` instance with `useResponseCache({ cache })`.
@@ -137,18 +139,18 @@ const getEnveloped = envelop({
137
139
  ### Cache with maximum TTL
138
140
 
139
141
  ```ts
140
- import { envelop } from '@envelop/core';
141
- import { useResponseCache } from '@envelop/response-cache';
142
+ import { envelop } from '@envelop/core'
143
+ import { useResponseCache } from '@envelop/response-cache'
142
144
 
143
145
  const getEnveloped = envelop({
144
146
  plugins: [
145
147
  // ... other plugins ...
146
148
  useResponseCache({
147
149
  ttl: 2000, // cached execution results become stale after 2 seconds
148
- session: () => null, // use global cache for all operations
149
- }),
150
- ],
151
- });
150
+ session: () => null // use global cache for all operations
151
+ })
152
+ ]
153
+ })
152
154
  ```
153
155
 
154
156
  > 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`.
@@ -156,8 +158,8 @@ const getEnveloped = envelop({
156
158
  ### Cache with custom TTL per object type
157
159
 
158
160
  ```ts
159
- import { envelop } from '@envelop/core';
160
- import { useResponseCache } from '@envelop/response-cache';
161
+ import { envelop } from '@envelop/core'
162
+ import { useResponseCache } from '@envelop/response-cache'
161
163
 
162
164
  const getEnveloped = envelop({
163
165
  plugins: [
@@ -166,18 +168,18 @@ const getEnveloped = envelop({
166
168
  ttl: 2000,
167
169
  ttlPerType: {
168
170
  // cached execution results that contain a `Stock` object become stale after 500ms
169
- Stock: 500,
170
- },
171
- }),
172
- ],
173
- });
171
+ Stock: 500
172
+ }
173
+ })
174
+ ]
175
+ })
174
176
  ```
175
177
 
176
178
  ### Cache with custom TTL per schema coordinate
177
179
 
178
180
  ```ts
179
- import { envelop } from '@envelop/core';
180
- import { useResponseCache } from '@envelop/response-cache';
181
+ import { envelop } from '@envelop/core'
182
+ import { useResponseCache } from '@envelop/response-cache'
181
183
 
182
184
  const getEnveloped = envelop({
183
185
  plugins: [
@@ -186,18 +188,18 @@ const getEnveloped = envelop({
186
188
  ttl: 2000,
187
189
  ttlPerSchemaCoordinate: {
188
190
  // cached execution results that select the `Query.user` field become stale after 100ms
189
- 'Query.rocketCoordinates': 100,
190
- },
191
- }),
192
- ],
193
- });
191
+ 'Query.rocketCoordinates': 100
192
+ }
193
+ })
194
+ ]
195
+ })
194
196
  ```
195
197
 
196
198
  ### Disable cache based on session/user
197
199
 
198
200
  ```ts
199
- import { envelop } from '@envelop/core';
200
- import { useResponseCache } from '@envelop/response-cache';
201
+ import { envelop } from '@envelop/core'
202
+ import { useResponseCache } from '@envelop/response-cache'
201
203
 
202
204
  const getEnveloped = envelop({
203
205
  plugins: [
@@ -206,10 +208,10 @@ const getEnveloped = envelop({
206
208
  ttl: 2000,
207
209
  // context is the GraphQL context used for execution
208
210
  enabled: context => context.user?.role !== 'admin',
209
- session: () => null,
210
- }),
211
- ],
212
- });
211
+ session: () => null
212
+ })
213
+ ]
214
+ })
213
215
  ```
214
216
 
215
217
  ### Customize if result should be cached
@@ -217,7 +219,7 @@ const getEnveloped = envelop({
217
219
  You can define a custom function used to check if a query operation execution result should be cached.
218
220
 
219
221
  ```ts
220
- type ShouldCacheResultFunction = (params: { result: ExecutionResult }) => Boolean;
222
+ type ShouldCacheResultFunction = (params: { result: ExecutionResult }) => boolean
221
223
  ```
222
224
 
223
225
  This is useful for advanced use-cases. E.g. if you want to
@@ -226,24 +228,24 @@ cache results with certain error types.
226
228
  By default, the `defaultShouldCacheResult` function is used which never caches any query operation execution results that includes any errors (unexpected, EnvelopError, or GraphQLError).
227
229
 
228
230
  ```ts
229
- import { envelop } from '@envelop/core';
230
- import { useResponseCache, ShouldCacheResultFunction } from '@envelop/response-cache';
231
+ import { envelop } from '@envelop/core'
232
+ import { useResponseCache, ShouldCacheResultFunction } from '@envelop/response-cache'
231
233
 
232
234
  export const defaultShouldCacheResult: ShouldCacheResultFunction = (params): Boolean => {
233
235
  // cache any query operation execution result
234
236
  // even if it includes errors
235
- return true;
236
- };
237
+ return true
238
+ }
237
239
 
238
240
  const getEnveloped = envelop({
239
241
  plugins: [
240
242
  // ... other plugins ...
241
243
  useResponseCache({
242
- shouldCacheResult = myCustomShouldCacheResult,
243
- session: () => null,
244
- }),
245
- ],
246
- });
244
+ shouldCacheResult: myCustomShouldCacheResult,
245
+ session: () => null
246
+ })
247
+ ]
248
+ })
247
249
  ```
248
250
 
249
251
  ### Cache Introspection query operations
@@ -253,63 +255,63 @@ By default introspection query operations are not cached. In case you want to ca
253
255
  **Infinite caching**
254
256
 
255
257
  ```ts
256
- import { envelop } from '@envelop/core';
257
- import { useResponseCache } from '@envelop/response-cache';
258
+ import { envelop } from '@envelop/core'
259
+ import { useResponseCache } from '@envelop/response-cache'
258
260
 
259
261
  const getEnveloped = envelop({
260
262
  plugins: [
261
263
  // ... other plugins ...
262
264
  useResponseCache({
263
265
  ttlPerSchemaCoordinate: {
264
- 'Query.__schema': undefined, // cache infinitely
266
+ 'Query.__schema': undefined // cache infinitely
265
267
  },
266
- session: () => null,
267
- }),
268
- ],
269
- });
268
+ session: () => null
269
+ })
270
+ ]
271
+ })
270
272
  ```
271
273
 
272
274
  **TTL caching**
273
275
 
274
276
  ```ts
275
- import { envelop } from '@envelop/core';
276
- import { useResponseCache } from '@envelop/response-cache';
277
+ import { envelop } from '@envelop/core'
278
+ import { useResponseCache } from '@envelop/response-cache'
277
279
 
278
280
  const getEnveloped = envelop({
279
281
  plugins: [
280
282
  // ... other plugins ...
281
283
  useResponseCache({
282
284
  ttlPerSchemaCoordinate: {
283
- 'Query.__schema': 10_000, // cache for 10 seconds
285
+ 'Query.__schema': 10_000 // cache for 10 seconds
284
286
  },
285
- session: () => null,
286
- }),
287
- ],
288
- });
287
+ session: () => null
288
+ })
289
+ ]
290
+ })
289
291
  ```
290
292
 
291
293
  ### Cache with maximum TTL
292
294
 
293
295
  ```ts
294
- import { envelop } from '@envelop/core';
295
- import { useResponseCache } from '@envelop/response-cache';
296
+ import { envelop } from '@envelop/core'
297
+ import { useResponseCache } from '@envelop/response-cache'
296
298
 
297
299
  const getEnveloped = envelop({
298
300
  plugins: [
299
301
  // ... other plugins ...
300
302
  useResponseCache({
301
303
  ttl: 2000, // cached execution results become stale after 2 seconds
302
- session: () => null,
303
- }),
304
- ],
305
- });
304
+ session: () => null
305
+ })
306
+ ]
307
+ })
306
308
  ```
307
309
 
308
310
  ### Customize the fields that are used for building the cache ID
309
311
 
310
312
  ```ts
311
- import { envelop } from '@envelop/core';
312
- import { useResponseCache } from '@envelop/response-cache';
313
+ import { envelop } from '@envelop/core'
314
+ import { useResponseCache } from '@envelop/response-cache'
313
315
 
314
316
  const getEnveloped = envelop({
315
317
  plugins: [
@@ -318,17 +320,17 @@ const getEnveloped = envelop({
318
320
  ttl: 2000,
319
321
  // use the `_id` instead of `id` field.
320
322
  idFields: ['_id'],
321
- session: () => null,
322
- }),
323
- ],
324
- });
323
+ session: () => null
324
+ })
325
+ ]
326
+ })
325
327
  ```
326
328
 
327
329
  ### Disable automatic cache invalidation via mutations
328
330
 
329
331
  ```ts
330
- import { envelop } from '@envelop/core';
331
- import { useResponseCache } from '@envelop/response-cache';
332
+ import { envelop } from '@envelop/core'
333
+ import { useResponseCache } from '@envelop/response-cache'
332
334
 
333
335
  const getEnveloped = envelop({
334
336
  plugins: [
@@ -337,21 +339,21 @@ const getEnveloped = envelop({
337
339
  ttl: 2000,
338
340
  // some might prefer invalidating based on a database write log
339
341
  invalidateViaMutation: false,
340
- session: () => null,
341
- }),
342
- ],
343
- });
342
+ session: () => null
343
+ })
344
+ ]
345
+ })
344
346
  ```
345
347
 
346
348
  ### Invalidate Cache based on custom logic
347
349
 
348
350
  ```ts
349
- import { envelop } from '@envelop/core';
350
- import { useResponseCache, createInMemoryCache } from '@envelop/response-cache';
351
- import { emitter } from './eventEmitter';
351
+ import { envelop } from '@envelop/core'
352
+ import { useResponseCache, createInMemoryCache } from '@envelop/response-cache'
353
+ import { emitter } from './eventEmitter'
352
354
 
353
355
  // we create our cache instance, which allows calling all methods on it
354
- const cache = createInMemoryCache();
356
+ const cache = createInMemoryCache()
355
357
 
356
358
  const getEnveloped = envelop({
357
359
  plugins: [
@@ -360,33 +362,33 @@ const getEnveloped = envelop({
360
362
  ttl: 2000,
361
363
  // we pass the cache instance to the request.
362
364
  cache,
363
- session: () => null,
364
- }),
365
- ],
366
- });
365
+ session: () => null
366
+ })
367
+ ]
368
+ })
367
369
 
368
370
  emitter.on('invalidate', resource => {
369
371
  cache.invalidate([
370
372
  {
371
373
  typename: resource.type,
372
- id: resource.id,
373
- },
374
- ]);
375
- });
374
+ id: resource.id
375
+ }
376
+ ])
377
+ })
376
378
  ```
377
379
 
378
380
  ### Customize how cache ids are built
379
381
 
380
382
  ```ts
381
- import { envelop } from '@envelop/core';
382
- import { useResponseCache, createInMemoryCache } from '@envelop/response-cache';
383
- import { emitter } from './eventEmitter';
383
+ import { envelop } from '@envelop/core'
384
+ import { useResponseCache, createInMemoryCache } from '@envelop/response-cache'
385
+ import { emitter } from './eventEmitter'
384
386
 
385
387
  // we create our cache instance, which allows calling all methods on it
386
388
  const cache = createInMemoryCache({
387
389
  // in relay we have global unique ids, no need to use `typename:id`
388
- makeId: (typename, id) => id ?? typename,
389
- });
390
+ makeId: (typename, id) => id ?? typename
391
+ })
390
392
 
391
393
  const getEnveloped = envelop({
392
394
  plugins: [
@@ -395,10 +397,10 @@ const getEnveloped = envelop({
395
397
  ttl: 2000,
396
398
  // we pass the cache instance to the request.
397
399
  cache,
398
- session: () => null,
399
- }),
400
- ],
401
- });
400
+ session: () => null
401
+ })
402
+ ]
403
+ })
402
404
  ```
403
405
 
404
406
  ### Expose cache metadata via extensions
@@ -412,10 +414,10 @@ const getEnveloped = envelop({
412
414
  useResponseCache({
413
415
  ttl: 2000,
414
416
  includeExtensionMetadata: true,
415
- session: () => null,
416
- }),
417
- ],
418
- });
417
+ session: () => null
418
+ })
419
+ ]
420
+ })
419
421
  ```
420
422
 
421
423
  This option will attach the following fields to the execution result if set to true (or `process.env["NODE_ENV"]` is `"development"`).
package/cjs/plugin.js CHANGED
@@ -9,7 +9,6 @@ const fast_json_stable_stringify_1 = tslib_1.__importDefault(require("fast-json-
9
9
  const in_memory_cache_js_1 = require("./in-memory-cache.js");
10
10
  const hashSHA256_js_1 = require("./hashSHA256.js");
11
11
  const cache_document_str_js_1 = require("./cache-document-str.js");
12
- const contextSymbol = Symbol('responseCache');
13
12
  /**
14
13
  * Default function used for building the response cache key.
15
14
  * 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.
@@ -44,7 +43,6 @@ exports.defaultShouldCacheResult = defaultShouldCacheResult;
44
43
  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,
45
44
  // eslint-disable-next-line dot-notation
46
45
  includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_ENV'] === 'development' : false, }) {
47
- const appliedTransform = Symbol('responseCache.appliedTransform');
48
46
  const ignoredTypesMap = new Set(ignoredTypes);
49
47
  // never cache Introspections
50
48
  ttlPerSchemaCoordinate = { 'Query.__schema': 0, ...ttlPerSchemaCoordinate };
@@ -54,38 +52,106 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
54
52
  addPlugin((0, cache_document_str_js_1.useCacheDocumentString)());
55
53
  }
56
54
  },
57
- onSchemaChange({ schema, replaceSchema }) {
55
+ async onExecute(onExecuteParams) {
58
56
  var _a;
59
- // @ts-expect-error See https://github.com/graphql/graphql-js/pull/3511 - remove this comments once merged
60
- if (((_a = schema.extensions) === null || _a === void 0 ? void 0 : _a[appliedTransform]) === true) {
61
- return;
57
+ let documentChanged = false;
58
+ const newDocument = (0, graphql_1.visit)(onExecuteParams.args.document, {
59
+ SelectionSet(node) {
60
+ if (!node.selections.some(selection => selection.kind === graphql_1.Kind.FIELD && selection.name.value === '__typename')) {
61
+ documentChanged = true;
62
+ return {
63
+ ...node,
64
+ selections: [
65
+ {
66
+ kind: graphql_1.Kind.FIELD,
67
+ name: {
68
+ kind: graphql_1.Kind.NAME,
69
+ value: '__typename',
70
+ },
71
+ },
72
+ ...node.selections,
73
+ ],
74
+ };
75
+ }
76
+ return node;
77
+ },
78
+ });
79
+ if (documentChanged) {
80
+ onExecuteParams.setExecuteFn(function typeNameAddedExecute() {
81
+ return onExecuteParams.executeFn({
82
+ ...onExecuteParams.args,
83
+ document: newDocument,
84
+ });
85
+ });
62
86
  }
63
- const patchedSchema = applyResponseCacheLogic(schema, idFields);
64
- patchedSchema.extensions = {
65
- ...patchedSchema.extensions,
66
- [appliedTransform]: true,
67
- };
68
- replaceSchema(patchedSchema);
69
- },
70
- async onExecute(ctx) {
71
- var _a;
72
87
  const identifier = new Map();
73
88
  const types = new Set();
74
- const context = {
75
- identifier,
76
- types,
77
- currentTtl: undefined,
78
- ttlPerType,
79
- ignoredTypesMap,
80
- skip: false,
89
+ let currentTtl;
90
+ let skip = false;
91
+ const processResult = (result) => {
92
+ var _a;
93
+ return (0, utils_1.visitResult)(result, {
94
+ document: onExecuteParams.args.document,
95
+ variables: onExecuteParams.args.variableValues,
96
+ operationName: (_a = onExecuteParams.args.operationName) !== null && _a !== void 0 ? _a : undefined,
97
+ rootValue: onExecuteParams.args.rootValue,
98
+ context: onExecuteParams.args.contextValue,
99
+ }, onExecuteParams.args.schema, new Proxy({}, {
100
+ get(_, typename) {
101
+ let typenameCalled = 0;
102
+ return new Proxy((val) => val, {
103
+ // Needed for leaf values such as scalars, enums etc
104
+ // They don't have fields so visitResult expects functions for those
105
+ apply(_, __, [val]) {
106
+ return val;
107
+ },
108
+ get(_, fieldName) {
109
+ if (documentChanged) {
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
+ */
123
+ if (typenameCalled < 2) {
124
+ return (root) => {
125
+ delete root.__typename;
126
+ return root;
127
+ };
128
+ }
129
+ }
130
+ }
131
+ if (idFields.includes(fieldName)) {
132
+ if (ignoredTypesMap.has(typename)) {
133
+ skip = true;
134
+ }
135
+ if (skip === true) {
136
+ return;
137
+ }
138
+ return (id) => {
139
+ identifier.set(`${typename}:${id}`, { typename, id });
140
+ types.add(typename);
141
+ if (typename in ttlPerType) {
142
+ currentTtl = calculateTtl(ttlPerType[typename], currentTtl);
143
+ }
144
+ return id;
145
+ };
146
+ }
147
+ return undefined;
148
+ },
149
+ });
150
+ },
151
+ }));
81
152
  };
82
- ctx.extendContext({
83
- [contextSymbol]: context,
84
- });
85
- if (isMutation(ctx.args.document)) {
86
- if (invalidateViaMutation === false) {
87
- return;
88
- }
153
+ const operationAST = (0, graphql_1.getOperationAST)(onExecuteParams.args.document, onExecuteParams.args.operationName);
154
+ if (invalidateViaMutation !== false && (operationAST === null || operationAST === void 0 ? void 0 : operationAST.operation) === 'mutation') {
89
155
  return {
90
156
  onExecuteDone({ result, setResult }) {
91
157
  if ((0, core_1.isAsyncIterable)(result)) {
@@ -93,14 +159,15 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
93
159
  console.warn('[useResponseCache] AsyncIterable returned from execute is currently unsupported.');
94
160
  return;
95
161
  }
96
- cache.invalidate(context.identifier.values());
162
+ const processedResult = processResult(result);
163
+ cache.invalidate(identifier.values());
97
164
  if (includeExtensionMetadata) {
98
165
  setResult({
99
- ...result,
166
+ ...processedResult,
100
167
  extensions: {
101
- ...result.extensions,
168
+ ...processedResult.extensions,
102
169
  responseCache: {
103
- invalidatedEntities: Array.from(context.identifier.values()),
170
+ invalidatedEntities: Array.from(identifier.values()),
104
171
  },
105
172
  },
106
173
  });
@@ -109,16 +176,16 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
109
176
  };
110
177
  }
111
178
  const operationId = await buildResponseCacheKey({
112
- documentString: getDocumentString(ctx.args),
113
- variableValues: ctx.args.variableValues,
114
- operationName: ctx.args.operationName,
115
- sessionId: session(ctx.args.contextValue),
179
+ documentString: getDocumentString(onExecuteParams.args),
180
+ variableValues: onExecuteParams.args.variableValues,
181
+ operationName: onExecuteParams.args.operationName,
182
+ sessionId: session(onExecuteParams.args.contextValue),
116
183
  });
117
- if (((_a = enabled === null || enabled === void 0 ? void 0 : enabled(ctx.args.contextValue)) !== null && _a !== void 0 ? _a : true) === true) {
184
+ if (((_a = enabled === null || enabled === void 0 ? void 0 : enabled(onExecuteParams.args.contextValue)) !== null && _a !== void 0 ? _a : true) === true) {
118
185
  const cachedResponse = await cache.get(operationId);
119
186
  if (cachedResponse != null) {
120
187
  if (includeExtensionMetadata) {
121
- ctx.setResultAndStopExecution({
188
+ onExecuteParams.setResultAndStopExecution({
122
189
  ...cachedResponse,
123
190
  extensions: {
124
191
  responseCache: {
@@ -128,21 +195,21 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
128
195
  });
129
196
  }
130
197
  else {
131
- ctx.setResultAndStopExecution(cachedResponse);
198
+ onExecuteParams.setResultAndStopExecution(cachedResponse);
132
199
  }
133
200
  return;
134
201
  }
135
202
  }
136
203
  if (ttlPerSchemaCoordinate) {
137
- const typeInfo = new graphql_1.TypeInfo(ctx.args.schema);
138
- (0, graphql_1.visit)(ctx.args.document, (0, graphql_1.visitWithTypeInfo)(typeInfo, {
204
+ const typeInfo = new graphql_1.TypeInfo(onExecuteParams.args.schema);
205
+ (0, graphql_1.visit)(onExecuteParams.args.document, (0, graphql_1.visitWithTypeInfo)(typeInfo, {
139
206
  Field(fieldNode) {
140
207
  const parentType = typeInfo.getParentType();
141
208
  if (parentType) {
142
209
  const schemaCoordinate = `${parentType.name}.${fieldNode.name.value}`;
143
210
  const maybeTtl = ttlPerSchemaCoordinate[schemaCoordinate];
144
211
  if (maybeTtl !== undefined) {
145
- context.currentTtl = calculateTtl(maybeTtl, context.currentTtl);
212
+ currentTtl = calculateTtl(maybeTtl, currentTtl);
146
213
  }
147
214
  }
148
215
  },
@@ -150,24 +217,24 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
150
217
  }
151
218
  return {
152
219
  onExecuteDone({ result, setResult }) {
153
- var _a;
154
220
  if ((0, core_1.isAsyncIterable)(result)) {
155
221
  // eslint-disable-next-line no-console
156
222
  console.warn('[useResponseCache] AsyncIterable returned from execute is currently unsupported.');
157
223
  return;
158
224
  }
159
- if (context.skip) {
225
+ const processedResult = processResult(result);
226
+ if (skip) {
160
227
  return;
161
228
  }
162
- if (!shouldCacheResult({ result })) {
229
+ if (!shouldCacheResult({ result: processedResult })) {
163
230
  return;
164
231
  }
165
232
  // we only use the global ttl if no currentTtl has been determined.
166
- const finalTtl = (_a = context.currentTtl) !== null && _a !== void 0 ? _a : globalTtl;
233
+ const finalTtl = currentTtl !== null && currentTtl !== void 0 ? currentTtl : globalTtl;
167
234
  if (finalTtl === 0) {
168
235
  if (includeExtensionMetadata) {
169
236
  setResult({
170
- ...result,
237
+ ...processedResult,
171
238
  extensions: {
172
239
  responseCache: {
173
240
  hit: false,
@@ -178,10 +245,10 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
178
245
  }
179
246
  return;
180
247
  }
181
- cache.set(operationId, result, identifier.values(), finalTtl);
248
+ cache.set(operationId, processedResult, identifier.values(), finalTtl);
182
249
  if (includeExtensionMetadata) {
183
250
  setResult({
184
- ...result,
251
+ ...processedResult,
185
252
  extensions: {
186
253
  responseCache: {
187
254
  hit: false,
@@ -197,59 +264,9 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
197
264
  };
198
265
  }
199
266
  exports.useResponseCache = useResponseCache;
200
- function isOperationDefinition(node) {
201
- return (node === null || node === void 0 ? void 0 : node.kind) === 'OperationDefinition';
202
- }
203
267
  function calculateTtl(typeTtl, currentTtl) {
204
268
  if (typeof currentTtl === 'number') {
205
269
  return Math.min(currentTtl, typeTtl);
206
270
  }
207
271
  return typeTtl;
208
272
  }
209
- function isMutation(doc) {
210
- return doc.definitions.find(isOperationDefinition).operation === 'mutation';
211
- }
212
- function runWith(input, callback) {
213
- if (input instanceof Promise) {
214
- input.then(callback, () => undefined);
215
- }
216
- else {
217
- callback(input);
218
- }
219
- }
220
- function applyResponseCacheLogic(schema, idFieldNames) {
221
- return (0, utils_1.mapSchema)(schema, {
222
- [utils_1.MapperKind.OBJECT_FIELD](fieldConfig, fieldName, typename) {
223
- if (idFieldNames.includes(fieldName)) {
224
- return {
225
- ...fieldConfig,
226
- resolve(src, args, context, info) {
227
- var _a;
228
- const result = ((_a = fieldConfig.resolve) !== null && _a !== void 0 ? _a : graphql_1.defaultFieldResolver)(src, args, context, info);
229
- runWith(result, (id) => {
230
- const ctx = context[contextSymbol];
231
- if (ctx !== undefined) {
232
- if (ctx.ignoredTypesMap.has(typename)) {
233
- ctx.skip = true;
234
- }
235
- if (ctx.skip === true) {
236
- ctx.skip = true;
237
- return;
238
- }
239
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
240
- // @ts-ignore TODO: investigate what to do if id is something unexpected
241
- ctx.identifier.set(`${typename}:${id}`, { typename, id });
242
- ctx.types.add(typename);
243
- if (typename in ctx.ttlPerType) {
244
- ctx.currentTtl = calculateTtl(ctx.ttlPerType[typename], ctx.currentTtl);
245
- }
246
- }
247
- });
248
- return result;
249
- },
250
- };
251
- }
252
- return fieldConfig;
253
- },
254
- });
255
- }
package/esm/plugin.js CHANGED
@@ -1,11 +1,10 @@
1
1
  import { isAsyncIterable } from '@envelop/core';
2
- import { MapperKind, mapSchema } from '@graphql-tools/utils';
3
- import { visit, TypeInfo, visitWithTypeInfo, defaultFieldResolver, } from 'graphql';
2
+ import { visitResult } from '@graphql-tools/utils';
3
+ import { visit, TypeInfo, visitWithTypeInfo, getOperationAST, Kind, } from 'graphql';
4
4
  import jsonStableStringify from 'fast-json-stable-stringify';
5
5
  import { createInMemoryCache } from './in-memory-cache.js';
6
6
  import { hashSHA256 } from './hashSHA256.js';
7
7
  import { defaultGetDocumentString, useCacheDocumentString } from './cache-document-str.js';
8
- const contextSymbol = Symbol('responseCache');
9
8
  /**
10
9
  * Default function used for building the response cache key.
11
10
  * 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.
@@ -38,7 +37,6 @@ export const defaultShouldCacheResult = (params) => {
38
37
  export function useResponseCache({ cache = createInMemoryCache(), ttl: globalTtl = Infinity, session, enabled, ignoredTypes = [], ttlPerType = {}, ttlPerSchemaCoordinate = {}, idFields = ['id'], invalidateViaMutation = true, buildResponseCacheKey = defaultBuildResponseCacheKey, getDocumentString = defaultGetDocumentString, shouldCacheResult = defaultShouldCacheResult,
39
38
  // eslint-disable-next-line dot-notation
40
39
  includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_ENV'] === 'development' : false, }) {
41
- const appliedTransform = Symbol('responseCache.appliedTransform');
42
40
  const ignoredTypesMap = new Set(ignoredTypes);
43
41
  // never cache Introspections
44
42
  ttlPerSchemaCoordinate = { 'Query.__schema': 0, ...ttlPerSchemaCoordinate };
@@ -48,38 +46,106 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
48
46
  addPlugin(useCacheDocumentString());
49
47
  }
50
48
  },
51
- onSchemaChange({ schema, replaceSchema }) {
49
+ async onExecute(onExecuteParams) {
52
50
  var _a;
53
- // @ts-expect-error See https://github.com/graphql/graphql-js/pull/3511 - remove this comments once merged
54
- if (((_a = schema.extensions) === null || _a === void 0 ? void 0 : _a[appliedTransform]) === true) {
55
- return;
51
+ let documentChanged = false;
52
+ const newDocument = visit(onExecuteParams.args.document, {
53
+ SelectionSet(node) {
54
+ if (!node.selections.some(selection => selection.kind === Kind.FIELD && selection.name.value === '__typename')) {
55
+ documentChanged = true;
56
+ return {
57
+ ...node,
58
+ selections: [
59
+ {
60
+ kind: Kind.FIELD,
61
+ name: {
62
+ kind: Kind.NAME,
63
+ value: '__typename',
64
+ },
65
+ },
66
+ ...node.selections,
67
+ ],
68
+ };
69
+ }
70
+ return node;
71
+ },
72
+ });
73
+ if (documentChanged) {
74
+ onExecuteParams.setExecuteFn(function typeNameAddedExecute() {
75
+ return onExecuteParams.executeFn({
76
+ ...onExecuteParams.args,
77
+ document: newDocument,
78
+ });
79
+ });
56
80
  }
57
- const patchedSchema = applyResponseCacheLogic(schema, idFields);
58
- patchedSchema.extensions = {
59
- ...patchedSchema.extensions,
60
- [appliedTransform]: true,
61
- };
62
- replaceSchema(patchedSchema);
63
- },
64
- async onExecute(ctx) {
65
- var _a;
66
81
  const identifier = new Map();
67
82
  const types = new Set();
68
- const context = {
69
- identifier,
70
- types,
71
- currentTtl: undefined,
72
- ttlPerType,
73
- ignoredTypesMap,
74
- skip: false,
83
+ let currentTtl;
84
+ let skip = false;
85
+ const processResult = (result) => {
86
+ var _a;
87
+ return visitResult(result, {
88
+ document: onExecuteParams.args.document,
89
+ variables: onExecuteParams.args.variableValues,
90
+ operationName: (_a = onExecuteParams.args.operationName) !== null && _a !== void 0 ? _a : undefined,
91
+ rootValue: onExecuteParams.args.rootValue,
92
+ context: onExecuteParams.args.contextValue,
93
+ }, onExecuteParams.args.schema, new Proxy({}, {
94
+ get(_, typename) {
95
+ let typenameCalled = 0;
96
+ return new Proxy((val) => val, {
97
+ // Needed for leaf values such as scalars, enums etc
98
+ // They don't have fields so visitResult expects functions for those
99
+ apply(_, __, [val]) {
100
+ return val;
101
+ },
102
+ 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
+ */
117
+ if (typenameCalled < 2) {
118
+ return (root) => {
119
+ delete root.__typename;
120
+ return root;
121
+ };
122
+ }
123
+ }
124
+ }
125
+ if (idFields.includes(fieldName)) {
126
+ if (ignoredTypesMap.has(typename)) {
127
+ skip = true;
128
+ }
129
+ if (skip === true) {
130
+ return;
131
+ }
132
+ return (id) => {
133
+ identifier.set(`${typename}:${id}`, { typename, id });
134
+ types.add(typename);
135
+ if (typename in ttlPerType) {
136
+ currentTtl = calculateTtl(ttlPerType[typename], currentTtl);
137
+ }
138
+ return id;
139
+ };
140
+ }
141
+ return undefined;
142
+ },
143
+ });
144
+ },
145
+ }));
75
146
  };
76
- ctx.extendContext({
77
- [contextSymbol]: context,
78
- });
79
- if (isMutation(ctx.args.document)) {
80
- if (invalidateViaMutation === false) {
81
- return;
82
- }
147
+ const operationAST = getOperationAST(onExecuteParams.args.document, onExecuteParams.args.operationName);
148
+ if (invalidateViaMutation !== false && (operationAST === null || operationAST === void 0 ? void 0 : operationAST.operation) === 'mutation') {
83
149
  return {
84
150
  onExecuteDone({ result, setResult }) {
85
151
  if (isAsyncIterable(result)) {
@@ -87,14 +153,15 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
87
153
  console.warn('[useResponseCache] AsyncIterable returned from execute is currently unsupported.');
88
154
  return;
89
155
  }
90
- cache.invalidate(context.identifier.values());
156
+ const processedResult = processResult(result);
157
+ cache.invalidate(identifier.values());
91
158
  if (includeExtensionMetadata) {
92
159
  setResult({
93
- ...result,
160
+ ...processedResult,
94
161
  extensions: {
95
- ...result.extensions,
162
+ ...processedResult.extensions,
96
163
  responseCache: {
97
- invalidatedEntities: Array.from(context.identifier.values()),
164
+ invalidatedEntities: Array.from(identifier.values()),
98
165
  },
99
166
  },
100
167
  });
@@ -103,16 +170,16 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
103
170
  };
104
171
  }
105
172
  const operationId = await buildResponseCacheKey({
106
- documentString: getDocumentString(ctx.args),
107
- variableValues: ctx.args.variableValues,
108
- operationName: ctx.args.operationName,
109
- sessionId: session(ctx.args.contextValue),
173
+ documentString: getDocumentString(onExecuteParams.args),
174
+ variableValues: onExecuteParams.args.variableValues,
175
+ operationName: onExecuteParams.args.operationName,
176
+ sessionId: session(onExecuteParams.args.contextValue),
110
177
  });
111
- if (((_a = enabled === null || enabled === void 0 ? void 0 : enabled(ctx.args.contextValue)) !== null && _a !== void 0 ? _a : true) === true) {
178
+ if (((_a = enabled === null || enabled === void 0 ? void 0 : enabled(onExecuteParams.args.contextValue)) !== null && _a !== void 0 ? _a : true) === true) {
112
179
  const cachedResponse = await cache.get(operationId);
113
180
  if (cachedResponse != null) {
114
181
  if (includeExtensionMetadata) {
115
- ctx.setResultAndStopExecution({
182
+ onExecuteParams.setResultAndStopExecution({
116
183
  ...cachedResponse,
117
184
  extensions: {
118
185
  responseCache: {
@@ -122,21 +189,21 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
122
189
  });
123
190
  }
124
191
  else {
125
- ctx.setResultAndStopExecution(cachedResponse);
192
+ onExecuteParams.setResultAndStopExecution(cachedResponse);
126
193
  }
127
194
  return;
128
195
  }
129
196
  }
130
197
  if (ttlPerSchemaCoordinate) {
131
- const typeInfo = new TypeInfo(ctx.args.schema);
132
- visit(ctx.args.document, visitWithTypeInfo(typeInfo, {
198
+ const typeInfo = new TypeInfo(onExecuteParams.args.schema);
199
+ visit(onExecuteParams.args.document, visitWithTypeInfo(typeInfo, {
133
200
  Field(fieldNode) {
134
201
  const parentType = typeInfo.getParentType();
135
202
  if (parentType) {
136
203
  const schemaCoordinate = `${parentType.name}.${fieldNode.name.value}`;
137
204
  const maybeTtl = ttlPerSchemaCoordinate[schemaCoordinate];
138
205
  if (maybeTtl !== undefined) {
139
- context.currentTtl = calculateTtl(maybeTtl, context.currentTtl);
206
+ currentTtl = calculateTtl(maybeTtl, currentTtl);
140
207
  }
141
208
  }
142
209
  },
@@ -144,24 +211,24 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
144
211
  }
145
212
  return {
146
213
  onExecuteDone({ result, setResult }) {
147
- var _a;
148
214
  if (isAsyncIterable(result)) {
149
215
  // eslint-disable-next-line no-console
150
216
  console.warn('[useResponseCache] AsyncIterable returned from execute is currently unsupported.');
151
217
  return;
152
218
  }
153
- if (context.skip) {
219
+ const processedResult = processResult(result);
220
+ if (skip) {
154
221
  return;
155
222
  }
156
- if (!shouldCacheResult({ result })) {
223
+ if (!shouldCacheResult({ result: processedResult })) {
157
224
  return;
158
225
  }
159
226
  // we only use the global ttl if no currentTtl has been determined.
160
- const finalTtl = (_a = context.currentTtl) !== null && _a !== void 0 ? _a : globalTtl;
227
+ const finalTtl = currentTtl !== null && currentTtl !== void 0 ? currentTtl : globalTtl;
161
228
  if (finalTtl === 0) {
162
229
  if (includeExtensionMetadata) {
163
230
  setResult({
164
- ...result,
231
+ ...processedResult,
165
232
  extensions: {
166
233
  responseCache: {
167
234
  hit: false,
@@ -172,10 +239,10 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
172
239
  }
173
240
  return;
174
241
  }
175
- cache.set(operationId, result, identifier.values(), finalTtl);
242
+ cache.set(operationId, processedResult, identifier.values(), finalTtl);
176
243
  if (includeExtensionMetadata) {
177
244
  setResult({
178
- ...result,
245
+ ...processedResult,
179
246
  extensions: {
180
247
  responseCache: {
181
248
  hit: false,
@@ -190,59 +257,9 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
190
257
  },
191
258
  };
192
259
  }
193
- function isOperationDefinition(node) {
194
- return (node === null || node === void 0 ? void 0 : node.kind) === 'OperationDefinition';
195
- }
196
260
  function calculateTtl(typeTtl, currentTtl) {
197
261
  if (typeof currentTtl === 'number') {
198
262
  return Math.min(currentTtl, typeTtl);
199
263
  }
200
264
  return typeTtl;
201
265
  }
202
- function isMutation(doc) {
203
- return doc.definitions.find(isOperationDefinition).operation === 'mutation';
204
- }
205
- function runWith(input, callback) {
206
- if (input instanceof Promise) {
207
- input.then(callback, () => undefined);
208
- }
209
- else {
210
- callback(input);
211
- }
212
- }
213
- function applyResponseCacheLogic(schema, idFieldNames) {
214
- return mapSchema(schema, {
215
- [MapperKind.OBJECT_FIELD](fieldConfig, fieldName, typename) {
216
- if (idFieldNames.includes(fieldName)) {
217
- return {
218
- ...fieldConfig,
219
- resolve(src, args, context, info) {
220
- var _a;
221
- const result = ((_a = fieldConfig.resolve) !== null && _a !== void 0 ? _a : defaultFieldResolver)(src, args, context, info);
222
- runWith(result, (id) => {
223
- const ctx = context[contextSymbol];
224
- if (ctx !== undefined) {
225
- if (ctx.ignoredTypesMap.has(typename)) {
226
- ctx.skip = true;
227
- }
228
- if (ctx.skip === true) {
229
- ctx.skip = true;
230
- return;
231
- }
232
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
233
- // @ts-ignore TODO: investigate what to do if id is something unexpected
234
- ctx.identifier.set(`${typename}:${id}`, { typename, id });
235
- ctx.types.add(typename);
236
- if (typename in ctx.ttlPerType) {
237
- ctx.currentTtl = calculateTtl(ctx.ttlPerType[typename], ctx.currentTtl);
238
- }
239
- }
240
- });
241
- return result;
242
- },
243
- };
244
- }
245
- return fieldConfig;
246
- },
247
- });
248
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envelop/response-cache",
3
- "version": "3.0.0-alpha-9e9dfbb.0",
3
+ "version": "3.0.0-alpha-e41b2ae.0",
4
4
  "sideEffects": false,
5
5
  "peerDependencies": {
6
6
  "@envelop/core": "^2.4.0",
@@ -45,6 +45,20 @@ export declare type UseResponseCacheParameter<C = any> = {
45
45
  * Return `null` or `undefined` to mark the session as public/global.
46
46
  * Creates a global session by default.
47
47
  * @param context GraphQL Context
48
+ *
49
+ * **Global Example:**
50
+ * ```ts
51
+ * useResponseCache({
52
+ * session: () => null,
53
+ * });
54
+ * ```
55
+ *
56
+ * **User Specific with global fallback example:**
57
+ * ```ts
58
+ * useResponseCache({
59
+ * session: (context) => context.user?.id ?? null,
60
+ * });
61
+ * ```
48
62
  */
49
63
  session(context: C): string | undefined | null;
50
64
  /**