diff --git a/.changeset/twenty-camels-wonder.md b/.changeset/twenty-camels-wonder.md new file mode 100644 index 0000000000..80b0baad4c --- /dev/null +++ b/.changeset/twenty-camels-wonder.md @@ -0,0 +1,25 @@ +--- +'@envelop/response-cache': minor +--- + +Deprecate `ttlPerType` in favor of `ttlPerSchemaCoordinate`, for a more streamlined API + +## Migration instructions + +If you where using `ttlPerType`, you can merge the object into the `ttlPerSchemaCoordinate`, the +syntax doesn't change. + +```diff +useResponseCache({ + session: null, +- ttlPerType: { +- User: 10_000, +- Profile: 600_000, +- }, + ttlPerSchemaCoordinate: { + 'Query.me': 0 ++ User: 10_000, ++ Profile: 600_000, + } +}) +``` diff --git a/packages/plugins/response-cache/src/plugin.ts b/packages/plugins/response-cache/src/plugin.ts index 007e38dfed..679c4eaeb3 100644 --- a/packages/plugins/response-cache/src/plugin.ts +++ b/packages/plugins/response-cache/src/plugin.ts @@ -68,9 +68,7 @@ export type UseResponseCacheParameter */ ttl?: number; /** - * Overwrite the ttl for query operations whose execution result contains a specific object type. - * Useful if the occurrence of a object time in the execution result should reduce or increase the TTL of the query operation. - * The TTL per type is always favored over the global TTL. + * @deprecated Use `ttlPerSchemaCoordinate` instead. */ ttlPerType?: Record; /** @@ -308,7 +306,7 @@ export function useResponseCache = {}> session, enabled, ignoredTypes = [], - ttlPerType = {}, + ttlPerType, ttlPerSchemaCoordinate = {}, scopePerSchemaCoordinate = {}, idFields = ['id'], @@ -329,6 +327,16 @@ export function useResponseCache = {}> // never cache Introspections ttlPerSchemaCoordinate = { 'Query.__schema': 0, ...ttlPerSchemaCoordinate }; + if (ttlPerType) { + // eslint-disable-next-line no-console + console.warn( + '[useResponseCache] `ttlForType` is deprecated. To migrate, merge it with `ttlForSchemaCoordinate` option', + ); + for (const [typeName, ttl] of Object.entries(ttlPerType)) { + ttlPerSchemaCoordinate[typeName] = ttl; + } + } + const documentMetadataOptions = { queries: { invalidateViaMutation, ttlPerSchemaCoordinate }, mutations: { invalidateViaMutation }, // remove ttlPerSchemaCoordinate for mutations to skip TTL calculation @@ -366,7 +374,7 @@ export function useResponseCache = {}> ) as unknown as CacheControlDirective[] | undefined; cacheControlAnnotations?.forEach(cacheControl => { if (cacheControl.maxAge != null) { - ttlPerType[type.name] = cacheControl.maxAge * 1000; + ttlPerSchemaCoordinate[type.name] = cacheControl.maxAge * 1000; } if (cacheControl.scope) { scopePerSchemaCoordinate[type.name] = cacheControl.scope; @@ -461,8 +469,8 @@ export function useResponseCache = {}> } types.add(entity.typename); - if (entity.typename in ttlPerType) { - const maybeTtl = ttlPerType[entity.typename] as unknown; + if (entity.typename in ttlPerSchemaCoordinate) { + const maybeTtl = ttlPerSchemaCoordinate[entity.typename] as unknown; currentTtl = calculateTtl(maybeTtl, currentTtl); } if (entity.id != null) { @@ -473,8 +481,8 @@ export function useResponseCache = {}> if (fieldData == null || (Array.isArray(fieldData) && fieldData.length === 0)) { const inferredTypes = typePerSchemaCoordinateMap.get(`${entity.typename}.${fieldName}`); inferredTypes?.forEach(inferredType => { - if (inferredType in ttlPerType) { - const maybeTtl = ttlPerType[inferredType] as unknown; + if (inferredType in ttlPerSchemaCoordinate) { + const maybeTtl = ttlPerSchemaCoordinate[inferredType] as unknown; currentTtl = calculateTtl(maybeTtl, currentTtl); } identifier.set(inferredType, { typename: inferredType }); diff --git a/packages/plugins/response-cache/test/response-cache.spec.ts b/packages/plugins/response-cache/test/response-cache.spec.ts index ab9d297e96..14d2e6984d 100644 --- a/packages/plugins/response-cache/test/response-cache.spec.ts +++ b/packages/plugins/response-cache/test/response-cache.spec.ts @@ -27,7 +27,7 @@ import { describe('useResponseCache', () => { beforeEach(() => jest.useRealTimers()); - it('custom ttl per type is used instead of the global ttl - only enable caching for a specific type when the global ttl is 0', async () => { + it('deprecated ttlForType still work until removed', async () => { jest.useFakeTimers(); const spy = jest.fn(() => [ { @@ -113,6 +113,92 @@ describe('useResponseCache', () => { expect(spy).toHaveBeenCalledTimes(2); }); + it('custom ttl per type is used instead of the global ttl - only enable caching for a specific type when the global ttl is 0', async () => { + jest.useFakeTimers(); + const spy = jest.fn(() => [ + { + id: 1, + name: 'User 1', + comments: [ + { + id: 1, + text: 'Comment 1 of User 1', + }, + ], + }, + { + id: 2, + name: 'User 2', + comments: [ + { + id: 2, + text: 'Comment 2 of User 2', + }, + ], + }, + ]); + + const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + users: [User!]! + } + + type User { + id: ID! + name: String! + comments: [Comment!]! + recentComment: Comment + } + + type Comment { + id: ID! + text: String! + } + `, + resolvers: { + Query: { + users: spy, + }, + }, + }); + + const testInstance = createTestkit( + [ + useResponseCache({ + session: () => null, + ttl: 0, + ttlPerSchemaCoordinate: { + User: 200, + }, + }), + ], + schema, + ); + + const query = /* GraphQL */ ` + query test { + users { + id + name + comments { + id + text + } + } + } + `; + + await testInstance.execute(query); + await testInstance.execute(query); + expect(spy).toHaveBeenCalledTimes(1); + await testInstance.execute(query); + expect(spy).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(201); + await testInstance.execute(query); + expect(spy).toHaveBeenCalledTimes(2); + }); + it('reuses the cache if the same query operation is executed in sequence without a TTL', async () => { const spy = jest.fn(() => [ {