diff --git a/packages-next/keystone/src/lib/createKeystone.ts b/packages-next/keystone/src/lib/createKeystone.ts index 26de13f8f54..b4531db5642 100644 --- a/packages-next/keystone/src/lib/createKeystone.ts +++ b/packages-next/keystone/src/lib/createKeystone.ts @@ -70,7 +70,10 @@ export function createKeystone( Object.entries(lists).forEach(([key, { fields, graphql, access, hooks, description, db }]) => { keystone.createList(key, { fields: Object.fromEntries( - Object.entries(fields).map(([key, { type, config }]: any) => [key, { type, ...config }]) + Object.entries(fields).map(([key, { type, config }]: any) => [ + key, + { type, cacheHint: config.graphql?.cacheHint, ...config }, + ]) ), access, queryLimits: graphql?.queryLimits, @@ -79,6 +82,7 @@ export function createKeystone( itemQueryName: graphql?.itemQueryName, hooks, adapterConfig: db, + cacheHint: graphql?.cacheHint, // FIXME: Unsupported options: Need to work which of these we want to support with backwards // compatibility options. // adminDoc @@ -89,7 +93,6 @@ export function createKeystone( // singular // plural // path - // cacheHint // plugins }); }); diff --git a/packages-next/types/src/base.ts b/packages-next/types/src/base.ts index 41a8b7e3550..800e74c1c43 100644 --- a/packages-next/types/src/base.ts +++ b/packages-next/types/src/base.ts @@ -1,3 +1,4 @@ +import type { CacheHint } from 'apollo-cache-control'; import type { KeystoneContext } from './context'; import type { BaseGeneratedListTypes, GqlNames } from './utils'; @@ -16,6 +17,7 @@ export type BaseKeystone = { itemQueryName?: string; hooks?: Record; adapterConfig?: { searchField?: string }; + cacheHint?: ((args: any) => CacheHint) | CacheHint; } ) => BaseKeystoneList; connect: (args?: any) => Promise; diff --git a/packages-next/types/src/config/lists.ts b/packages-next/types/src/config/lists.ts index 79161cca7fb..3d4abeea21f 100644 --- a/packages-next/types/src/config/lists.ts +++ b/packages-next/types/src/config/lists.ts @@ -1,3 +1,4 @@ +import type { CacheHint } from 'apollo-cache-control'; import { AdminMetaRootVal } from '../admin-meta'; import type { BaseGeneratedListTypes, MaybePromise, JSONValue } from '../utils'; import type { ListHooks } from './hooks'; @@ -194,6 +195,11 @@ export type FieldConfig = { fieldMode?: MaybeItemFunction<'edit' | 'read' | 'hidden'>; }; }; + graphql?: { + cacheHint?: + | ((args: { results: any[]; operationName: string; meta: boolean }) => CacheHint) + | CacheHint; + }; }; export type MaybeSessionFunction = @@ -209,7 +215,7 @@ export type MaybeItemFunction = export type ListGraphQLConfig = { // was previously top-level cacheHint - // cacheHint?: CacheHint; + cacheHint?: ((args: any) => CacheHint) | CacheHint; /** * The description added to the GraphQL schema * @default listConfig.description @@ -232,5 +238,3 @@ export type ListDBConfig = { */ searchField?: string; }; - -// export type CacheHint = { scope: 'PRIVATE' | 'PUBLIC'; maxAge: number }; diff --git a/packages-next/types/src/core.ts b/packages-next/types/src/core.ts index 4fb2b6f0a89..97bc939371b 100644 --- a/packages-next/types/src/core.ts +++ b/packages-next/types/src/core.ts @@ -1,4 +1,5 @@ import { IncomingMessage, ServerResponse } from 'http'; +import type { GraphQLResolveInfo } from 'graphql'; import type { GqlNames, MaybePromise } from './utils'; import type { KeystoneContext, SessionContext } from './context'; @@ -24,7 +25,12 @@ export type SessionImplementation = { ): Promise>; }; -export type GraphQLResolver = (root: any, args: any, context: KeystoneContext) => any; +export type GraphQLResolver = ( + root: any, + args: any, + context: KeystoneContext, + info: GraphQLResolveInfo +) => any; export type GraphQLSchemaExtension = { typeDefs: string; diff --git a/tests/api-tests/package.json b/tests/api-tests/package.json index 35e298492b8..0b8f9d59bde 100644 --- a/tests/api-tests/package.json +++ b/tests/api-tests/package.json @@ -37,6 +37,7 @@ "@keystone-next/test-utils-legacy": "^14.0.0", "@keystone-next/types": "^15.0.0", "@keystone-next/utils-legacy": "^7.0.0", + "apollo-cache-control": "^0.11.6", "express": "^4.17.1" } } diff --git a/tests/api-tests/queries/cache-hints.test.js b/tests/api-tests/queries/cache-hints.test.ts similarity index 74% rename from tests/api-tests/queries/cache-hints.test.js rename to tests/api-tests/queries/cache-hints.test.ts index a3584ca718f..8d5204daa99 100644 --- a/tests/api-tests/queries/cache-hints.test.js +++ b/tests/api-tests/queries/cache-hints.test.ts @@ -1,93 +1,91 @@ -const { Integer, Text, Relationship } = require('@keystone-next/fields-legacy'); -const { +import { CacheScope } from 'apollo-cache-control'; +import { text, relationship, integer } from '@keystone-next/fields'; +import { multiAdapterRunners, - setupServer, + setupFromConfig, networkedGraphqlRequest, -} = require('@keystone-next/test-utils-legacy'); -const { createItems } = require('@keystone-next/server-side-graphql-client-legacy'); - -function setupKeystone(adapterName) { - return setupServer({ + testConfig, + AdapterName, +} from '@keystone-next/test-utils-legacy'; +// @ts-ignore +import { createItems } from '@keystone-next/server-side-graphql-client-legacy'; +import { list, createSchema, graphQLSchemaExtension } from '@keystone-next/keystone/schema'; +import { KeystoneContext } from '@keystone-next/types'; + +function setupKeystone(adapterName: AdapterName) { + return setupFromConfig({ adapterName, - createLists: keystone => { - keystone.createList('Post', { - fields: { - title: { type: Text }, - author: { type: Relationship, ref: 'User.posts', many: true }, - }, - cacheHint: { - scope: 'PUBLIC', - maxAge: 100, - }, - }); - - keystone.createList('User', { - fields: { - name: { - type: Text, - cacheHint: { - maxAge: 80, - }, + config: testConfig({ + lists: createSchema({ + Post: list({ + fields: { + title: text(), + author: relationship({ ref: 'User.posts', many: true }), }, - favNumber: { - type: Integer, - cacheHint: { - maxAge: 10, - scope: 'PRIVATE', + graphql: { + cacheHint: { scope: CacheScope.Public, maxAge: 100 }, + }, + }), + User: list({ + fields: { + name: text({ + graphql: { cacheHint: { maxAge: 80 } }, + }), + favNumber: integer({ + graphql: { cacheHint: { maxAge: 10, scope: CacheScope.Private } }, + }), + posts: relationship({ ref: 'Post.author', many: true }), + }, + graphql: { + cacheHint: ({ results, operationName, meta }) => { + if (meta) { + return { scope: CacheScope.Public, maxAge: 90 }; + } + if (operationName === 'complexQuery') { + return { maxAge: 1 }; + } + if (results.length === 0) { + return { maxAge: 5 }; + } + return { maxAge: 100 }; }, }, - posts: { type: Relationship, ref: 'Post.author', many: true }, - }, - cacheHint: ({ results, operationName, meta }) => { - if (meta) { - return { - scope: 'PUBLIC', - maxAge: 90, - }; + }), + }), + extendGraphqlSchema: graphQLSchemaExtension({ + typeDefs: ` + type MyType { + original: Int + double: Float } - if (operationName === 'complexQuery') { - return { - maxAge: 1, - }; + + type Mutation { + triple(x: Int): Int } - if (results.length === 0) { - return { - maxAge: 5, - }; + + type Query { + double(x: Int): MyType } - return { - maxAge: 100, - }; + `, + resolvers: { + Query: { + double: (root, { x }, context, info) => { + info.cacheControl.setCacheHint({ scope: CacheScope.Public, maxAge: 100 }); + return { original: x, double: 2.0 * x }; + }, + }, + Mutation: { + triple: (root, { x }) => 3 * x, + }, }, - }); - - // These should be added to the system and tested when we implement cacheHints - // keystone.extendGraphQLSchema({ - // types: [{ type: 'type MyType { original: Int, double: Float }' }], - // queries: [ - // { - // schema: 'double(x: Int): MyType', - // resolver: (_, { x }) => ({ original: x, double: 2.0 * x }), - // cacheHint: { - // scope: 'PUBLIC', - // maxAge: 100, - // }, - // }, - // ], - // mutations: [ - // { - // schema: 'triple(x: Int): Int', - // resolver: (_, { x }) => 3 * x, - // }, - // ], - // }); - }, + }), + }), }); } -const addFixtures = async keystone => { +const addFixtures = async (context: KeystoneContext) => { const users = await createItems({ - keystone, + context, listKey: 'User', items: [ { data: { name: 'Jess', favNumber: 1 } }, @@ -97,7 +95,7 @@ const addFixtures = async keystone => { }); const posts = await createItems({ - keystone, + context, listKey: 'Post', items: [ { data: { author: { connect: [{ id: users[0].id }] }, title: 'One author' } }, @@ -124,8 +122,8 @@ multiAdapterRunners().map(({ runner, adapterName }) => describe('cache hints', () => { test( 'users', - runner(setupKeystone, async ({ keystone, app }) => { - await addFixtures(keystone); + runner(setupKeystone, async ({ context, app }) => { + await addFixtures(context); // Basic query let { data, errors, res } = await networkedGraphqlRequest({ @@ -230,8 +228,8 @@ multiAdapterRunners().map(({ runner, adapterName }) => test( 'posts', - runner(setupKeystone, async ({ keystone, app }) => { - await addFixtures(keystone); + runner(setupKeystone, async ({ context, app }) => { + await addFixtures(context); // The Post list has a static cache hint // Basic query @@ -327,8 +325,8 @@ multiAdapterRunners().map(({ runner, adapterName }) => test( 'mutations', - runner(setupKeystone, async ({ keystone, app }) => { - const { posts } = await addFixtures(keystone); + runner(setupKeystone, async ({ context, app }) => { + const { posts } = await addFixtures(context); // Mutation responses shouldn't be cached. // Here's a smoke test to make sure they still work. @@ -354,8 +352,8 @@ multiAdapterRunners().map(({ runner, adapterName }) => // eslint-disable-next-line jest/no-disabled-tests test.skip( 'extendGraphQLSchemaQueries', - runner(setupKeystone, async ({ keystone, app }) => { - await addFixtures(keystone); + runner(setupKeystone, async ({ context, app }) => { + await addFixtures(context); // Basic query let { data, errors, res } = await networkedGraphqlRequest({ @@ -380,8 +378,8 @@ multiAdapterRunners().map(({ runner, adapterName }) => // eslint-disable-next-line jest/no-disabled-tests test.skip( 'extendGraphQLSchemaMutations', - runner(setupKeystone, async ({ keystone, app }) => { - await addFixtures(keystone); + runner(setupKeystone, async ({ context, app }) => { + await addFixtures(context); // Mutation responses shouldn't be cached. // Here's a smoke test to make sure they still work.