Skip to content

Commit

Permalink
cache control: don't apply default max age until after resolver runs
Browse files Browse the repository at this point in the history
This also gives resolvers the ability to differentiate between a
`maxAge` that was explicitly set but happens to equal the default
`maxAge` and one that was not explicitly set.

Fixes #5488. Helpful for apollo/federation#870.
  • Loading branch information
glasser committed Jul 16, 2021
1 parent b8d8828 commit f1c1732
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The version headers in this history reflect the versions of Apollo Server itself

## vNEXT

- `apollo-server-core`: The default `maxAge` (which defaults to 0) for a field should only be applied if no dynamic cache control hint is set. Specifically, if you call the (new in 3.0.0) function `info.cacheControl.cacheHint.restrict({ maxAge: 60 })`, it should set `maxAge` to 60 even if the default max age is lower. (This bug fix is the behavior that was intended for 3.0.0, and primarily affects the behavior of functions added in Apollo Server 3. This does mean that checking `info.cacheControl.cacheHint` now only shows explicitly-set `maxAge` and not the default, but this seems like it will be helpful since it lets you differentiate between the two similar circumstances.) [PR #5492](https://github.com/apollographql/apollo-server/pull/5492)
- `apollo-server-lambda`: Fix TypeScript types for `context` function. (In 3.0.0, the TS types for the `context` function were accidentally inherited from `apollo-server-express` instead of using the correct Lambda-specific types). [PR #5481](https://github.com/apollographql/apollo-server/pull/5481)

## v3.0.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,50 @@ describe('dynamic cache control', () => {
expect(hints).toStrictEqual(new Map([['droid', { maxAge: 60 }]]));
});

it('can use restrict to set the maxAge for a field', async () => {
const typeDefs = `
type Query {
droid(id: ID!): Droid
}
type Droid {
id: ID!
name: String!
}
`;

const resolvers: GraphQLResolvers = {
Query: {
droid: (_source, _args, _context, { cacheControl }) => {
cacheControl.cacheHint.restrict({ maxAge: 60 });
return {
id: 2001,
name: 'R2-D2',
};
},
},
};

const schema = makeExecutableSchemaWithCacheControlSupport({
typeDefs,
resolvers,
});

const hints = await collectCacheControlHints(
schema,
`
query {
droid(id: 2001) {
name
}
}
`,
{ defaultMaxAge: 10 },
);

expect(hints).toStrictEqual(new Map([['droid', { maxAge: 60 }]]));
});

it('should set the scope for a field from a dynamic cache hint', async () => {
const typeDefs = `
type Query {
Expand Down
51 changes: 29 additions & 22 deletions packages/apollo-server-core/src/plugin/cacheControl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,28 +186,6 @@ export function ApolloServerPluginCacheControl(
fieldPolicy.replace(fieldAnnotation);
}

// If this field returns a composite type or is a root field and
// we haven't seen an explicit maxAge hint, set the maxAge to 0
// (uncached) or the default if specified in the constructor.
// (Non-object fields by default are assumed to inherit their
// cacheability from their parents. But on the other hand, while
// root non-object fields can get explicit hints from their
// definition on the Query/Mutation object, if that doesn't exist
// then there's no parent field that would assign the default
// maxAge, so we do it here.)
//
// You can disable this on a non-root field by writing
// `@cacheControl(inheritMaxAge: true)` on it. If you do this,
// then its children will be treated like root paths, since there
// is no parent maxAge to inherit.
if (
fieldPolicy.maxAge === undefined &&
((isCompositeType(targetType) && !inheritMaxAge) ||
!info.path.prev)
) {
fieldPolicy.restrict({ maxAge: defaultMaxAge });
}

info.cacheControl = {
setCacheHint: (dynamicHint: CacheHint) => {
fieldPolicy.replace(dynamicHint);
Expand All @@ -221,6 +199,35 @@ export function ApolloServerPluginCacheControl(
// "undo" the effect on overallCachePolicy of a static hint that
// gets refined by a dynamic hint.
return () => {
// If this field returns a composite type or is a root field and
// we haven't seen an explicit maxAge hint, set the maxAge to 0
// (uncached) or the default if specified in the constructor.
// (Non-object fields by default are assumed to inherit their
// cacheability from their parents. But on the other hand, while
// root non-object fields can get explicit hints from their
// definition on the Query/Mutation object, if that doesn't
// exist then there's no parent field that would assign the
// default maxAge, so we do it here.)
//
// You can disable this on a non-root field by writing
// `@cacheControl(inheritMaxAge: true)` on it. If you do this,
// then its children will be treated like root paths, since
// there is no parent maxAge to inherit.
//
// We do this in the end hook so that dynamic cache control
// prevents it from happening (eg,
// `info.cacheControl.cacheHint.restrict({maxAge: 60})` should
// work rather than doing nothing because we've already set the
// max age to the default of 0). This also lets resolvers assume
// any hint in `info.cacheControl.cacheHint` was explicitly set.
if (
fieldPolicy.maxAge === undefined &&
((isCompositeType(targetType) && !inheritMaxAge) ||
!info.path.prev)
) {
fieldPolicy.restrict({ maxAge: defaultMaxAge });
}

if (__testing__cacheHints && isRestricted(fieldPolicy)) {
const path = responsePathAsArray(info.path).join('.');
if (__testing__cacheHints.has(path)) {
Expand Down

0 comments on commit f1c1732

Please sign in to comment.