@envelop/response-cache 3.0.0-alpha-0d610c4.0 → 3.0.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"`).
@@ -6,10 +6,10 @@ const sha256Browser = (bytes) => {
6
6
  const hash = webCrypto.subtle.digest({ name: 'SHA-256' }, bytes);
7
7
  return new Promise((resolve, reject) => {
8
8
  Promise.resolve(hash)
9
- .then(function (result) {
9
+ .then(result => {
10
10
  resolve(new Uint8Array(result));
11
11
  })
12
- .catch(function (error) {
12
+ .catch(error => {
13
13
  reject(error);
14
14
  });
15
15
  });
package/cjs/plugin.js CHANGED
@@ -7,16 +7,15 @@ const utils_1 = require("@graphql-tools/utils");
7
7
  const graphql_1 = require("graphql");
8
8
  const fast_json_stable_stringify_1 = tslib_1.__importDefault(require("fast-json-stable-stringify"));
9
9
  const in_memory_cache_js_1 = require("./in-memory-cache.js");
10
- const hashSHA256_js_1 = require("./hashSHA256.js");
10
+ const hash_sha256_js_1 = require("./hash-sha256.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.
16
15
  */
17
16
  const defaultBuildResponseCacheKey = params => {
18
17
  var _a, _b, _c;
19
- return (0, hashSHA256_js_1.hashSHA256)([
18
+ return (0, hash_sha256_js_1.hashSHA256)([
20
19
  params.documentString,
21
20
  (_a = params.operationName) !== null && _a !== void 0 ? _a : '',
22
21
  (0, fast_json_stable_stringify_1.default)((_b = params.variableValues) !== null && _b !== void 0 ? _b : {}),
@@ -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,104 @@ 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
+ */ typenameCalled < 2) {
123
+ return (root) => {
124
+ delete root.__typename;
125
+ return root;
126
+ };
127
+ }
128
+ }
129
+ if (idFields.includes(fieldName)) {
130
+ if (ignoredTypesMap.has(typename)) {
131
+ skip = true;
132
+ }
133
+ if (skip === true) {
134
+ return;
135
+ }
136
+ return (id) => {
137
+ identifier.set(`${typename}:${id}`, { typename, id });
138
+ types.add(typename);
139
+ if (typename in ttlPerType) {
140
+ currentTtl = calculateTtl(ttlPerType[typename], currentTtl);
141
+ }
142
+ return id;
143
+ };
144
+ }
145
+ return undefined;
146
+ },
147
+ });
148
+ },
149
+ }));
81
150
  };
82
- ctx.extendContext({
83
- [contextSymbol]: context,
84
- });
85
- if (isMutation(ctx.args.document)) {
86
- if (invalidateViaMutation === false) {
87
- return;
88
- }
151
+ const operationAST = (0, graphql_1.getOperationAST)(onExecuteParams.args.document, onExecuteParams.args.operationName);
152
+ if (invalidateViaMutation !== false && (operationAST === null || operationAST === void 0 ? void 0 : operationAST.operation) === 'mutation') {
89
153
  return {
90
154
  onExecuteDone({ result, setResult }) {
91
155
  if ((0, core_1.isAsyncIterable)(result)) {
@@ -93,14 +157,15 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
93
157
  console.warn('[useResponseCache] AsyncIterable returned from execute is currently unsupported.');
94
158
  return;
95
159
  }
96
- cache.invalidate(context.identifier.values());
160
+ const processedResult = processResult(result);
161
+ cache.invalidate(identifier.values());
97
162
  if (includeExtensionMetadata) {
98
163
  setResult({
99
- ...result,
164
+ ...processedResult,
100
165
  extensions: {
101
- ...result.extensions,
166
+ ...processedResult.extensions,
102
167
  responseCache: {
103
- invalidatedEntities: Array.from(context.identifier.values()),
168
+ invalidatedEntities: Array.from(identifier.values()),
104
169
  },
105
170
  },
106
171
  });
@@ -109,16 +174,16 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
109
174
  };
110
175
  }
111
176
  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),
177
+ documentString: getDocumentString(onExecuteParams.args),
178
+ variableValues: onExecuteParams.args.variableValues,
179
+ operationName: onExecuteParams.args.operationName,
180
+ sessionId: session(onExecuteParams.args.contextValue),
116
181
  });
117
- if (((_a = enabled === null || enabled === void 0 ? void 0 : enabled(ctx.args.contextValue)) !== null && _a !== void 0 ? _a : true) === true) {
182
+ if (((_a = enabled === null || enabled === void 0 ? void 0 : enabled(onExecuteParams.args.contextValue)) !== null && _a !== void 0 ? _a : true) === true) {
118
183
  const cachedResponse = await cache.get(operationId);
119
184
  if (cachedResponse != null) {
120
185
  if (includeExtensionMetadata) {
121
- ctx.setResultAndStopExecution({
186
+ onExecuteParams.setResultAndStopExecution({
122
187
  ...cachedResponse,
123
188
  extensions: {
124
189
  responseCache: {
@@ -128,21 +193,21 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
128
193
  });
129
194
  }
130
195
  else {
131
- ctx.setResultAndStopExecution(cachedResponse);
196
+ onExecuteParams.setResultAndStopExecution(cachedResponse);
132
197
  }
133
198
  return;
134
199
  }
135
200
  }
136
201
  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, {
202
+ const typeInfo = new graphql_1.TypeInfo(onExecuteParams.args.schema);
203
+ (0, graphql_1.visit)(onExecuteParams.args.document, (0, graphql_1.visitWithTypeInfo)(typeInfo, {
139
204
  Field(fieldNode) {
140
205
  const parentType = typeInfo.getParentType();
141
206
  if (parentType) {
142
207
  const schemaCoordinate = `${parentType.name}.${fieldNode.name.value}`;
143
208
  const maybeTtl = ttlPerSchemaCoordinate[schemaCoordinate];
144
209
  if (maybeTtl !== undefined) {
145
- context.currentTtl = calculateTtl(maybeTtl, context.currentTtl);
210
+ currentTtl = calculateTtl(maybeTtl, currentTtl);
146
211
  }
147
212
  }
148
213
  },
@@ -150,24 +215,24 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
150
215
  }
151
216
  return {
152
217
  onExecuteDone({ result, setResult }) {
153
- var _a;
154
218
  if ((0, core_1.isAsyncIterable)(result)) {
155
219
  // eslint-disable-next-line no-console
156
220
  console.warn('[useResponseCache] AsyncIterable returned from execute is currently unsupported.');
157
221
  return;
158
222
  }
159
- if (context.skip) {
223
+ const processedResult = processResult(result);
224
+ if (skip) {
160
225
  return;
161
226
  }
162
- if (!shouldCacheResult({ result })) {
227
+ if (!shouldCacheResult({ result: processedResult })) {
163
228
  return;
164
229
  }
165
230
  // we only use the global ttl if no currentTtl has been determined.
166
- const finalTtl = (_a = context.currentTtl) !== null && _a !== void 0 ? _a : globalTtl;
231
+ const finalTtl = currentTtl !== null && currentTtl !== void 0 ? currentTtl : globalTtl;
167
232
  if (finalTtl === 0) {
168
233
  if (includeExtensionMetadata) {
169
234
  setResult({
170
- ...result,
235
+ ...processedResult,
171
236
  extensions: {
172
237
  responseCache: {
173
238
  hit: false,
@@ -178,10 +243,10 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
178
243
  }
179
244
  return;
180
245
  }
181
- cache.set(operationId, result, identifier.values(), finalTtl);
246
+ cache.set(operationId, processedResult, identifier.values(), finalTtl);
182
247
  if (includeExtensionMetadata) {
183
248
  setResult({
184
- ...result,
249
+ ...processedResult,
185
250
  extensions: {
186
251
  responseCache: {
187
252
  hit: false,
@@ -197,59 +262,9 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
197
262
  };
198
263
  }
199
264
  exports.useResponseCache = useResponseCache;
200
- function isOperationDefinition(node) {
201
- return (node === null || node === void 0 ? void 0 : node.kind) === 'OperationDefinition';
202
- }
203
265
  function calculateTtl(typeTtl, currentTtl) {
204
266
  if (typeof currentTtl === 'number') {
205
267
  return Math.min(currentTtl, typeTtl);
206
268
  }
207
269
  return typeTtl;
208
270
  }
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
- }
@@ -3,10 +3,10 @@ const sha256Browser = (bytes) => {
3
3
  const hash = webCrypto.subtle.digest({ name: 'SHA-256' }, bytes);
4
4
  return new Promise((resolve, reject) => {
5
5
  Promise.resolve(hash)
6
- .then(function (result) {
6
+ .then(result => {
7
7
  resolve(new Uint8Array(result));
8
8
  })
9
- .catch(function (error) {
9
+ .catch(error => {
10
10
  reject(error);
11
11
  });
12
12
  });
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
- import { hashSHA256 } from './hashSHA256.js';
6
+ import { hashSHA256 } from './hash-sha256.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,104 @@ 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
+ */ typenameCalled < 2) {
117
+ return (root) => {
118
+ delete root.__typename;
119
+ return root;
120
+ };
121
+ }
122
+ }
123
+ if (idFields.includes(fieldName)) {
124
+ if (ignoredTypesMap.has(typename)) {
125
+ skip = true;
126
+ }
127
+ if (skip === true) {
128
+ return;
129
+ }
130
+ return (id) => {
131
+ identifier.set(`${typename}:${id}`, { typename, id });
132
+ types.add(typename);
133
+ if (typename in ttlPerType) {
134
+ currentTtl = calculateTtl(ttlPerType[typename], currentTtl);
135
+ }
136
+ return id;
137
+ };
138
+ }
139
+ return undefined;
140
+ },
141
+ });
142
+ },
143
+ }));
75
144
  };
76
- ctx.extendContext({
77
- [contextSymbol]: context,
78
- });
79
- if (isMutation(ctx.args.document)) {
80
- if (invalidateViaMutation === false) {
81
- return;
82
- }
145
+ const operationAST = getOperationAST(onExecuteParams.args.document, onExecuteParams.args.operationName);
146
+ if (invalidateViaMutation !== false && (operationAST === null || operationAST === void 0 ? void 0 : operationAST.operation) === 'mutation') {
83
147
  return {
84
148
  onExecuteDone({ result, setResult }) {
85
149
  if (isAsyncIterable(result)) {
@@ -87,14 +151,15 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
87
151
  console.warn('[useResponseCache] AsyncIterable returned from execute is currently unsupported.');
88
152
  return;
89
153
  }
90
- cache.invalidate(context.identifier.values());
154
+ const processedResult = processResult(result);
155
+ cache.invalidate(identifier.values());
91
156
  if (includeExtensionMetadata) {
92
157
  setResult({
93
- ...result,
158
+ ...processedResult,
94
159
  extensions: {
95
- ...result.extensions,
160
+ ...processedResult.extensions,
96
161
  responseCache: {
97
- invalidatedEntities: Array.from(context.identifier.values()),
162
+ invalidatedEntities: Array.from(identifier.values()),
98
163
  },
99
164
  },
100
165
  });
@@ -103,16 +168,16 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
103
168
  };
104
169
  }
105
170
  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),
171
+ documentString: getDocumentString(onExecuteParams.args),
172
+ variableValues: onExecuteParams.args.variableValues,
173
+ operationName: onExecuteParams.args.operationName,
174
+ sessionId: session(onExecuteParams.args.contextValue),
110
175
  });
111
- if (((_a = enabled === null || enabled === void 0 ? void 0 : enabled(ctx.args.contextValue)) !== null && _a !== void 0 ? _a : true) === true) {
176
+ if (((_a = enabled === null || enabled === void 0 ? void 0 : enabled(onExecuteParams.args.contextValue)) !== null && _a !== void 0 ? _a : true) === true) {
112
177
  const cachedResponse = await cache.get(operationId);
113
178
  if (cachedResponse != null) {
114
179
  if (includeExtensionMetadata) {
115
- ctx.setResultAndStopExecution({
180
+ onExecuteParams.setResultAndStopExecution({
116
181
  ...cachedResponse,
117
182
  extensions: {
118
183
  responseCache: {
@@ -122,21 +187,21 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
122
187
  });
123
188
  }
124
189
  else {
125
- ctx.setResultAndStopExecution(cachedResponse);
190
+ onExecuteParams.setResultAndStopExecution(cachedResponse);
126
191
  }
127
192
  return;
128
193
  }
129
194
  }
130
195
  if (ttlPerSchemaCoordinate) {
131
- const typeInfo = new TypeInfo(ctx.args.schema);
132
- visit(ctx.args.document, visitWithTypeInfo(typeInfo, {
196
+ const typeInfo = new TypeInfo(onExecuteParams.args.schema);
197
+ visit(onExecuteParams.args.document, visitWithTypeInfo(typeInfo, {
133
198
  Field(fieldNode) {
134
199
  const parentType = typeInfo.getParentType();
135
200
  if (parentType) {
136
201
  const schemaCoordinate = `${parentType.name}.${fieldNode.name.value}`;
137
202
  const maybeTtl = ttlPerSchemaCoordinate[schemaCoordinate];
138
203
  if (maybeTtl !== undefined) {
139
- context.currentTtl = calculateTtl(maybeTtl, context.currentTtl);
204
+ currentTtl = calculateTtl(maybeTtl, currentTtl);
140
205
  }
141
206
  }
142
207
  },
@@ -144,24 +209,24 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
144
209
  }
145
210
  return {
146
211
  onExecuteDone({ result, setResult }) {
147
- var _a;
148
212
  if (isAsyncIterable(result)) {
149
213
  // eslint-disable-next-line no-console
150
214
  console.warn('[useResponseCache] AsyncIterable returned from execute is currently unsupported.');
151
215
  return;
152
216
  }
153
- if (context.skip) {
217
+ const processedResult = processResult(result);
218
+ if (skip) {
154
219
  return;
155
220
  }
156
- if (!shouldCacheResult({ result })) {
221
+ if (!shouldCacheResult({ result: processedResult })) {
157
222
  return;
158
223
  }
159
224
  // we only use the global ttl if no currentTtl has been determined.
160
- const finalTtl = (_a = context.currentTtl) !== null && _a !== void 0 ? _a : globalTtl;
225
+ const finalTtl = currentTtl !== null && currentTtl !== void 0 ? currentTtl : globalTtl;
161
226
  if (finalTtl === 0) {
162
227
  if (includeExtensionMetadata) {
163
228
  setResult({
164
- ...result,
229
+ ...processedResult,
165
230
  extensions: {
166
231
  responseCache: {
167
232
  hit: false,
@@ -172,10 +237,10 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
172
237
  }
173
238
  return;
174
239
  }
175
- cache.set(operationId, result, identifier.values(), finalTtl);
240
+ cache.set(operationId, processedResult, identifier.values(), finalTtl);
176
241
  if (includeExtensionMetadata) {
177
242
  setResult({
178
- ...result,
243
+ ...processedResult,
179
244
  extensions: {
180
245
  responseCache: {
181
246
  hit: false,
@@ -190,59 +255,9 @@ includeExtensionMetadata = typeof process !== 'undefined' ? process.env['NODE_EN
190
255
  },
191
256
  };
192
257
  }
193
- function isOperationDefinition(node) {
194
- return (node === null || node === void 0 ? void 0 : node.kind) === 'OperationDefinition';
195
- }
196
258
  function calculateTtl(typeTtl, currentTtl) {
197
259
  if (typeof currentTtl === 'number') {
198
260
  return Math.min(currentTtl, typeTtl);
199
261
  }
200
262
  return typeTtl;
201
263
  }
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-0d610c4.0",
3
+ "version": "3.0.0",
4
4
  "sideEffects": false,
5
5
  "peerDependencies": {
6
6
  "@envelop/core": "^2.4.0",
File without changes