From c14fa5cdd01a1948c719240797bd4a68a08f12bb Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Mon, 13 Feb 2023 18:30:33 +1100 Subject: [PATCH] Add sudo extension for extendGraphqlSchema (#8298) Co-authored-by: Daniel Cousens --- .changeset/blue-papaya-straws.md | 5 + .changeset/six-papayas-bob.md | 5 + .../schema.graphql | 1 + .../schema.ts | 18 +++ packages/auth/src/index.ts | 15 +- packages/auth/src/schema.ts | 6 +- packages/core/src/lib/core/graphql-schema.ts | 87 ----------- packages/core/src/lib/createGraphQLSchema.ts | 143 +++++++++++++++--- packages/core/src/lib/createSystem.ts | 5 +- 9 files changed, 164 insertions(+), 121 deletions(-) create mode 100644 .changeset/blue-papaya-straws.md create mode 100644 .changeset/six-papayas-bob.md delete mode 100644 packages/core/src/lib/core/graphql-schema.ts diff --git a/.changeset/blue-papaya-straws.md b/.changeset/blue-papaya-straws.md new file mode 100644 index 00000000000..935666eedb6 --- /dev/null +++ b/.changeset/blue-papaya-straws.md @@ -0,0 +1,5 @@ +--- +'@keystone-6/auth': patch +--- + +Fixes `isFilterable: false` throwing an error for identity fields diff --git a/.changeset/six-papayas-bob.md b/.changeset/six-papayas-bob.md new file mode 100644 index 00000000000..91ddeb503c0 --- /dev/null +++ b/.changeset/six-papayas-bob.md @@ -0,0 +1,5 @@ +--- +'@keystone-6/core': minor +--- + +Adds a `sudo` GraphQL extension for the `GraphQLSchema` passed to `extendGraphqlSchema`; enabling developers to determine if they are extending the sudo GraphQL schema diff --git a/examples/extend-graphql-schema-graphql-ts/schema.graphql b/examples/extend-graphql-schema-graphql-ts/schema.graphql index 86ad77c6999..1dcaec091c3 100644 --- a/examples/extend-graphql-schema-graphql-ts/schema.graphql +++ b/examples/extend-graphql-schema-graphql-ts/schema.graphql @@ -13,6 +13,7 @@ type Post { enum PostStatusType { draft published + banned } scalar DateTime @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc3339#section-5.6") diff --git a/examples/extend-graphql-schema-graphql-ts/schema.ts b/examples/extend-graphql-schema-graphql-ts/schema.ts index ddc4b2d30dc..32419adfffd 100644 --- a/examples/extend-graphql-schema-graphql-ts/schema.ts +++ b/examples/extend-graphql-schema-graphql-ts/schema.ts @@ -13,6 +13,7 @@ export const lists: Lists = { options: [ { label: 'Draft', value: 'draft' }, { label: 'Published', value: 'published' }, + { label: 'Banned', value: 'banned' }, ], }), content: text(), @@ -63,6 +64,7 @@ export const extendGraphqlSchema = graphql.extend(base => { }), }, }); + return { mutation: { publishPost: graphql.field({ @@ -81,6 +83,22 @@ export const extendGraphqlSchema = graphql.extend(base => { }); }, }), + + // only add this mutation for a sudo Context (this is not usable from the API) + ...(base.schema.extensions.sudo + ? { + banPost: graphql.field({ + type: base.object('Post'), + args: { id: graphql.arg({ type: graphql.nonNull(graphql.ID) }) }, + resolve(source, { id }, context: Context) { + return context.db.Post.updateOne({ + where: { id }, + data: { status: 'banned' }, + }); + }, + }), + } + : {}), }, query: { recentPosts: graphql.field({ diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 9e9078c1a19..3c44c630e81 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -115,7 +115,7 @@ export function createAuth({ * * Must be added to the extendGraphqlSchema config. Can be composed. */ - const extendGraphqlSchema = getSchemaExtension({ + const authExtendGraphqlSchema = getSchemaExtension({ identityField, listKey, secretField, @@ -253,6 +253,10 @@ export function createAuth({ return session !== undefined; } + function defaultExtendGraphqlSchema(schema: T) { + return schema; + } + /** * withAuth * @@ -292,8 +296,9 @@ export function createAuth({ if (!keystoneConfig.session) throw new TypeError('Missing .session configuration'); const session = withItemData(keystoneConfig.session); - const existingExtendGraphQLSchema = keystoneConfig.extendGraphqlSchema; + const { extendGraphqlSchema = defaultExtendGraphqlSchema } = keystoneConfig; const listConfig = keystoneConfig.lists[listKey]; + return { ...keystoneConfig, ui, @@ -302,9 +307,9 @@ export function createAuth({ ...keystoneConfig.lists, [listKey]: { ...listConfig, fields: { ...listConfig.fields, ...fields } }, }, - extendGraphqlSchema: existingExtendGraphQLSchema - ? schema => existingExtendGraphQLSchema(extendGraphqlSchema(schema)) - : extendGraphqlSchema, + extendGraphqlSchema: schema => { + return extendGraphqlSchema(authExtendGraphqlSchema(schema)); + }, }; }; diff --git a/packages/auth/src/schema.ts b/packages/auth/src/schema.ts index 2476d2698a5..ddccf975b68 100644 --- a/packages/auth/src/schema.ts +++ b/packages/auth/src/schema.ts @@ -1,4 +1,4 @@ -import { ExtendGraphqlSchema, getGqlNames } from '@keystone-6/core/types'; +import { getGqlNames } from '@keystone-6/core/types'; import { assertObjectType, @@ -58,13 +58,14 @@ export const getSchemaExtension = ({ passwordResetLink?: AuthTokenTypeConfig; magicAuthLink?: AuthTokenTypeConfig; sessionData: string; -}): ExtendGraphqlSchema => +}) => graphql.extend(base => { const uniqueWhereInputType = assertInputObjectType( base.schema.getType(`${listKey}WhereUniqueInput`) ); const identityFieldOnUniqueWhere = uniqueWhereInputType.getFields()[identityField]; if ( + base.schema.extensions.sudo && identityFieldOnUniqueWhere?.type !== GraphQLString && identityFieldOnUniqueWhere?.type !== GraphQLID ) { @@ -75,6 +76,7 @@ export const getSchemaExtension = ({ `to the field at ${listKey}.${identityField}` ); } + const baseSchema = getBaseAuthSchema({ identityField, listKey, diff --git a/packages/core/src/lib/core/graphql-schema.ts b/packages/core/src/lib/core/graphql-schema.ts deleted file mode 100644 index f1c917bcbf0..00000000000 --- a/packages/core/src/lib/core/graphql-schema.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { GraphQLNamedType, GraphQLSchema } from 'graphql'; -import { graphql } from '../..'; -import { InitialisedList } from './types-for-lists'; - -import { getMutationsForList } from './mutations'; -import { getQueriesForList } from './queries'; - -export function getGraphQLSchema( - lists: Record, - extraFields: { - mutation: Record>; - query: Record>; - } -) { - const query = graphql.object()({ - name: 'Query', - fields: Object.assign( - {}, - ...Object.values(lists).map(list => getQueriesForList(list)), - extraFields.query - ), - }); - - const updateManyByList: Record> = {}; - - const mutation = graphql.object()({ - name: 'Mutation', - fields: Object.assign( - {}, - ...Object.values(lists).map(list => { - const { mutations, updateManyInput } = getMutationsForList(list); - updateManyByList[list.listKey] = updateManyInput; - return mutations; - }), - extraFields.mutation - ), - }); - const graphQLSchema = new GraphQLSchema({ - query: query.graphQLType, - mutation: mutation.graphQLType, - // not about behaviour, only ordering - types: [...collectTypes(lists, updateManyByList), mutation.graphQLType], - }); - return graphQLSchema; -} - -function collectTypes( - lists: Record, - updateManyByList: Record> -) { - const collectedTypes: GraphQLNamedType[] = []; - for (const list of Object.values(lists)) { - const { isEnabled } = list.graphql; - if (!isEnabled.type) continue; - // adding all of these types explicitly isn't strictly necessary but we do it to create a certain order in the schema - collectedTypes.push(list.types.output.graphQLType); - if (isEnabled.query || isEnabled.update || isEnabled.delete) { - collectedTypes.push(list.types.uniqueWhere.graphQLType); - } - if (isEnabled.query) { - for (const field of Object.values(list.fields)) { - if ( - isEnabled.query && - field.graphql.isEnabled.read && - field.unreferencedConcreteInterfaceImplementations - ) { - // this _IS_ actually necessary since they aren't implicitly referenced by other types, unlike the types above - collectedTypes.push( - ...field.unreferencedConcreteInterfaceImplementations.map(x => x.graphQLType) - ); - } - } - collectedTypes.push(list.types.where.graphQLType); - collectedTypes.push(list.types.orderBy.graphQLType); - } - if (isEnabled.update) { - collectedTypes.push(list.types.update.graphQLType); - collectedTypes.push(updateManyByList[list.listKey].graphQLType); - } - if (isEnabled.create) { - collectedTypes.push(list.types.create.graphQLType); - } - } - // this is not necessary, just about ordering - collectedTypes.push(graphql.JSON.graphQLType); - return collectedTypes; -} diff --git a/packages/core/src/lib/createGraphQLSchema.ts b/packages/core/src/lib/createGraphQLSchema.ts index cd86a8d0549..f1a7f85840a 100644 --- a/packages/core/src/lib/createGraphQLSchema.ts +++ b/packages/core/src/lib/createGraphQLSchema.ts @@ -1,41 +1,134 @@ +import { GraphQLNamedType, GraphQLSchema } from 'graphql'; + import type { KeystoneConfig } from '../types'; import { KeystoneMeta } from '../admin-ui/system/adminMetaSchema'; import { graphql } from '../types/schema'; import { AdminMetaRootVal } from '../admin-ui/system/createAdminMeta'; import { InitialisedList } from './core/types-for-lists'; -import { getGraphQLSchema } from './core/graphql-schema'; -export function createGraphQLSchema( - config: KeystoneConfig, +import { getMutationsForList } from './core/mutations'; +import { getQueriesForList } from './core/queries'; + +function getGraphQLSchema( lists: Record, - adminMeta: AdminMetaRootVal + extraFields: { + mutation: Record>; + query: Record>; + }, + sudo: boolean ) { - // Start with the core keystone graphQL schema - let graphQLSchema = getGraphQLSchema(lists, { - mutation: config.session - ? { - endSession: graphql.field({ - type: graphql.nonNull(graphql.Boolean), - async resolve(rootVal, args, context) { - if (context.sessionStrategy) { - await context.sessionStrategy.end({ context }); - } - return true; - }, - }), - } - : {}, - query: { - keystone: graphql.field({ - type: graphql.nonNull(KeystoneMeta), - resolve: () => ({ adminMeta }), + const query = graphql.object()({ + name: 'Query', + fields: Object.assign( + {}, + ...Object.values(lists).map(list => getQueriesForList(list)), + extraFields.query + ), + }); + + const updateManyByList: Record> = {}; + + const mutation = graphql.object()({ + name: 'Mutation', + fields: Object.assign( + {}, + ...Object.values(lists).map(list => { + const { mutations, updateManyInput } = getMutationsForList(list); + updateManyByList[list.listKey] = updateManyInput; + return mutations; }), + extraFields.mutation + ), + }); + + return new GraphQLSchema({ + query: query.graphQLType, + mutation: mutation.graphQLType, + // not about behaviour, only ordering + types: [...collectTypes(lists, updateManyByList), mutation.graphQLType], + extensions: { + sudo, }, }); +} + +function collectTypes( + lists: Record, + updateManyByList: Record> +) { + const collectedTypes: GraphQLNamedType[] = []; + for (const list of Object.values(lists)) { + const { isEnabled } = list.graphql; + if (!isEnabled.type) continue; + // adding all of these types explicitly isn't strictly necessary but we do it to create a certain order in the schema + collectedTypes.push(list.types.output.graphQLType); + if (isEnabled.query || isEnabled.update || isEnabled.delete) { + collectedTypes.push(list.types.uniqueWhere.graphQLType); + } + if (isEnabled.query) { + for (const field of Object.values(list.fields)) { + if ( + isEnabled.query && + field.graphql.isEnabled.read && + field.unreferencedConcreteInterfaceImplementations + ) { + // this _IS_ actually necessary since they aren't implicitly referenced by other types, unlike the types above + collectedTypes.push( + ...field.unreferencedConcreteInterfaceImplementations.map(x => x.graphQLType) + ); + } + } + collectedTypes.push(list.types.where.graphQLType); + collectedTypes.push(list.types.orderBy.graphQLType); + } + if (isEnabled.update) { + collectedTypes.push(list.types.update.graphQLType); + collectedTypes.push(updateManyByList[list.listKey].graphQLType); + } + if (isEnabled.create) { + collectedTypes.push(list.types.create.graphQLType); + } + } + // this is not necessary, just about ordering + collectedTypes.push(graphql.JSON.graphQLType); + return collectedTypes; +} + +export function createGraphQLSchema( + config: KeystoneConfig, + lists: Record, + adminMeta: AdminMetaRootVal, + sudo: boolean +) { + const graphQLSchema = getGraphQLSchema( + lists, + { + mutation: config.session + ? { + endSession: graphql.field({ + type: graphql.nonNull(graphql.Boolean), + async resolve(rootVal, args, context) { + if (context.sessionStrategy) { + await context.sessionStrategy.end({ context }); + } + return true; + }, + }), + } + : {}, + query: { + keystone: graphql.field({ + type: graphql.nonNull(KeystoneMeta), + resolve: () => ({ adminMeta }), + }), + }, + }, + sudo + ); - // Merge in the user defined graphQL API + // merge in the user defined graphQL API if (config.extendGraphqlSchema) { - graphQLSchema = config.extendGraphqlSchema(graphQLSchema); + return config.extendGraphqlSchema(graphQLSchema); } return graphQLSchema; diff --git a/packages/core/src/lib/createSystem.ts b/packages/core/src/lib/createSystem.ts index fabf34a5907..325a8928f11 100644 --- a/packages/core/src/lib/createSystem.ts +++ b/packages/core/src/lib/createSystem.ts @@ -58,15 +58,16 @@ function getSudoGraphQLSchema(config: KeystoneConfig) { }) ), }; + const lists = initialiseLists(transformedConfig); const adminMeta = createAdminMeta(transformedConfig, lists); - return createGraphQLSchema(transformedConfig, lists, adminMeta); + return createGraphQLSchema(transformedConfig, lists, adminMeta, true); } export function createSystem(config: KeystoneConfig) { const lists = initialiseLists(config); const adminMeta = createAdminMeta(config, lists); - const graphQLSchema = createGraphQLSchema(config, lists, adminMeta); + const graphQLSchema = createGraphQLSchema(config, lists, adminMeta, false); const sudoGraphQLSchema = getSudoGraphQLSchema(config); return {