Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add compatibility with @cacheControl directive #1864

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 57 additions & 3 deletions packages/plugins/response-cache/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import jsonStableStringify from 'fast-json-stable-stringify';
import {
ConstDirectiveNode,
ConstValueNode,
DocumentNode,
ExecutionArgs,
getOperationAST,
IntValueNode,
Kind,
print,
SelectionSetNode,
Expand Down Expand Up @@ -317,7 +320,25 @@ export function useResponseCache<PluginContext extends Record<string, any> = {}>
}

types.add(typename);
const cacheControlDirective = getDirectiveArg(
onExecuteParams.args.schema.getType(typename),
'cacheControl',
'maxAge',
);
if (cacheControlDirective) {
if (cacheControlDirective.kind !== Kind.INT) {
throw new Error(
'cacheControl directive maxAge argument must be an integer',
);
}
currentTtl = calculateTtl(Number(cacheControlDirective.value), currentTtl);
}
if (typename in ttlPerType) {
if (cacheControlDirective) {
throw new Error(
`cacheControl directive and ttlPerType cannot be used together for type ${typename}`,
);
}
currentTtl = calculateTtl(ttlPerType[typename], currentTtl);
}

Expand Down Expand Up @@ -407,12 +428,31 @@ export function useResponseCache<PluginContext extends Record<string, any> = {}>
onExecuteParams.args.document,
visitWithTypeInfo(typeInfo, {
Field(fieldNode) {
let maybeTtl: number | undefined = undefined;
const parentType = typeInfo.getParentType();
const maybeCacheControlDirective = getDirectiveArg(
typeInfo.getFieldDef(),
'cacheControl',
'maxAge',
);

if (maybeCacheControlDirective) {
if (maybeCacheControlDirective.kind !== Kind.INT) {
throw new Error('maxAge argument must be an integer');
}
currentTtl = calculateTtl(
Number((maybeCacheControlDirective as IntValueNode).value),
currentTtl,
);
}

if (parentType) {
const schemaCoordinate = `${parentType.name}.${fieldNode.name.value}`;
const maybeTtl = ttlPerSchemaCoordinate[schemaCoordinate];
if (maybeTtl !== undefined) {
currentTtl = calculateTtl(maybeTtl, currentTtl);
if (schemaCoordinate in ttlPerSchemaCoordinate) {
if (maybeCacheControlDirective) {
throw new Error("Can't set ttl for a field with cacheControl directive");
}
currentTtl = calculateTtl(ttlPerSchemaCoordinate[schemaCoordinate]!, currentTtl);
}
}
},
Expand Down Expand Up @@ -487,3 +527,17 @@ function calculateTtl(typeTtl: number, currentTtl: number | undefined): number {
}
return typeTtl;
}

function getDirectiveArg(
type: Maybe<{ astNode: Maybe<{ directives?: readonly ConstDirectiveNode[] }> }>,
directiveName: string,
argName: string,
): ConstValueNode | undefined {
return type?.astNode?.directives
?.find(directive => directive.name.value === directiveName)
?.arguments?.find(arg => arg.name.value === argName)?.value;
}

export const responseCacheDirectiveTypeDefs = /* GraphQL */ `
directive @cacheControl(maxAge: Int!) on FIELD_DEFINITION | OBJECT
`;
95 changes: 90 additions & 5 deletions packages/plugins/response-cache/test/response-cache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { makeExecutableSchema } from '@graphql-tools/schema';
import {
createInMemoryCache,
defaultBuildResponseCacheKey,
responseCacheDirectiveTypeDefs,
useResponseCache,
} from '../src/index.js';

Expand Down Expand Up @@ -100,6 +101,91 @@ describe('useResponseCache', () => {
expect(spy).toHaveBeenCalledTimes(2);
});

it('custom ttl per type by schema directive 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 */ `
${responseCacheDirectiveTypeDefs}

type Query {
users: [User!]!
}

type User @cacheControl(maxAge: 200) {
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,
}),
],
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(() => [
{
Expand Down Expand Up @@ -962,7 +1048,7 @@ describe('useResponseCache', () => {
expect(spy).toHaveBeenCalledTimes(2);
});

it('custom ttl can be specified per schema coordinate and will be used over the default ttl for caching a query operation execution result if included in the operation document', async () => {
it('custom ttl can be specified per static schema directive and will be used over the default ttl for caching a query operation execution result if included in the operation document', async () => {
jest.useFakeTimers();
const spy = jest.fn(() => [
{
Expand All @@ -989,8 +1075,10 @@ describe('useResponseCache', () => {

const schema = makeExecutableSchema({
typeDefs: /* GraphQL */ `
${responseCacheDirectiveTypeDefs}

type Query {
users: [User!]!
users: [User!]! @cacheControl(maxAge: 200)
}

type User {
Expand All @@ -1017,9 +1105,6 @@ describe('useResponseCache', () => {
useResponseCache({
session: () => null,
ttl: 500,
ttlPerSchemaCoordinate: {
'Query.users': 200,
},
}),
],
schema,
Expand Down