From 3f03b8c1fa7005b37371e1cc401c3a03334a4f7a Mon Sep 17 00:00:00 2001 From: Tim Leslie Date: Thu, 15 Jul 2021 10:34:41 +1000 Subject: [PATCH] Cosmetic core changes before error handling (#6104) --- .changeset/bright-turkeys-add.md | 5 + examples/custom-field/CHANGELOG.md | 3 +- .../src/lib/core/mutations/access-control.ts | 116 ++++++--------- .../src/lib/core/mutations/create-update.ts | 140 +++++++++--------- .../keystone/src/lib/core/mutations/delete.ts | 21 ++- .../keystone/src/lib/core/mutations/hooks.ts | 3 + .../keystone/src/lib/core/mutations/index.ts | 65 +++----- .../nested-mutation-many-input-resolvers.ts | 66 +++++---- .../nested-mutation-one-input-resolvers.ts | 48 +++--- .../keystone/src/lib/core/queries/index.ts | 12 +- .../src/lib/core/queries/output-field.ts | 58 +++----- .../src/lib/core/queries/resolvers.ts | 57 +++---- .../keystone/src/lib/core/types-for-lists.ts | 34 ++--- .../keystone/src/lib/core/where-inputs.ts | 5 +- tests/admin-ui-tests/CHANGELOG.md | 1 + tests/test-projects/basic/CHANGELOG.md | 1 + 16 files changed, 280 insertions(+), 355 deletions(-) create mode 100644 .changeset/bright-turkeys-add.md diff --git a/.changeset/bright-turkeys-add.md b/.changeset/bright-turkeys-add.md new file mode 100644 index 00000000000..81d5e20e415 --- /dev/null +++ b/.changeset/bright-turkeys-add.md @@ -0,0 +1,5 @@ +--- +'@keystone-next/keystone': patch +--- + +Cosmetic changes to the core code in preparation for improvements to the error handling logic. diff --git a/examples/custom-field/CHANGELOG.md b/examples/custom-field/CHANGELOG.md index 79057d6c412..f94e8140063 100644 --- a/examples/custom-field/CHANGELOG.md +++ b/examples/custom-field/CHANGELOG.md @@ -1,9 +1,8 @@ # @keystone-next/example-custom-field ## 0.0.1 -### Patch Changes - +### Patch Changes - [#6087](https://github.com/keystonejs/keystone/pull/6087) [`139d7a8de`](https://github.com/keystonejs/keystone/commit/139d7a8def263d40c0d1d5353d2744842d9a0951) Thanks [@JedWatson](https://github.com/JedWatson)! - Move source code from the `packages-next` to the `packages` directory. diff --git a/packages/keystone/src/lib/core/mutations/access-control.ts b/packages/keystone/src/lib/core/mutations/access-control.ts index 4a819ca10f8..e085f0f1d11 100644 --- a/packages/keystone/src/lib/core/mutations/access-control.ts +++ b/packages/keystone/src/lib/core/mutations/access-control.ts @@ -22,6 +22,8 @@ export async function getAccessControlledItemForDelete( inputFilter: UniqueInputFilter ): Promise { const itemId = await getStringifiedItemIdFromUniqueWhereInput(filter, list.listKey, context); + + // List access: pass 1 const access = await validateNonCreateListAccessControl({ access: list.access.delete, args: { context, listKey: list.listKey, operation: 'delete', session: context.session, itemId }, @@ -29,6 +31,8 @@ export async function getAccessControlledItemForDelete( if (access === false) { throw accessDeniedError('mutation'); } + + // List access: pass 2 const prismaModel = getPrismaModelForList(context.prisma, list.listKey); let where: PrismaFilter = mapUniqueWhereToWhere( list, @@ -41,37 +45,8 @@ export async function getAccessControlledItemForDelete( if (item === null) { throw accessDeniedError('mutation'); } - return item; -} -export async function checkFieldAccessControlForUpdate( - list: InitialisedList, - context: KeystoneContext, - originalInput: Record, - item: Record -) { - const results = await Promise.all( - Object.keys(originalInput).map(fieldKey => { - const field = list.fields[fieldKey]; - return validateFieldAccessControl({ - access: field.access.update, - args: { - context, - fieldKey, - listKey: list.listKey, - operation: 'update', - originalInput, - session: context.session, - item, - itemId: item.id.toString(), - }, - }); - }) - ); - - if (results.some(canAccess => !canAccess)) { - throw accessDeniedError('mutation'); - } + return item; } export async function getAccessControlledItemForUpdate( @@ -83,33 +58,51 @@ export async function getAccessControlledItemForUpdate( const prismaModel = getPrismaModelForList(context.prisma, list.listKey); const resolvedUniqueWhere = await resolveUniqueWhereInput(uniqueWhere, list.fields, context); const itemId = await getStringifiedItemIdFromUniqueWhereInput(uniqueWhere, list.listKey, context); + const args = { + context, + itemId, + listKey: list.listKey, + operation: 'update' as const, + originalInput: update, + session: context.session, + }; + + // List access: pass 1 const accessControl = await validateNonCreateListAccessControl({ access: list.access.update, - args: { - context, - itemId, - listKey: list.listKey, - operation: 'update', - originalInput: update, - session: context.session, - }, + args, }); if (accessControl === false) { throw accessDeniedError('mutation'); } + + // List access: pass 2 const uniqueWhereInWhereForm = mapUniqueWhereToWhere(list, resolvedUniqueWhere); const item = await prismaModel.findFirst({ where: accessControl === true ? uniqueWhereInWhereForm - : { - AND: [uniqueWhereInWhereForm, await resolveWhereInput(accessControl, list)], - }, + : { AND: [uniqueWhereInWhereForm, await resolveWhereInput(accessControl, list)] }, }); if (!item) { throw accessDeniedError('mutation'); } - await checkFieldAccessControlForUpdate(list, context, update, item); + + // Field access + const results = await Promise.all( + Object.keys(update).map(fieldKey => { + const field = list.fields[fieldKey]; + return validateFieldAccessControl({ + access: field.access.update, + args: { ...args, fieldKey, item }, + }); + }) + ); + + if (results.some(canAccess => !canAccess)) { + throw accessDeniedError('mutation'); + } + return item; } @@ -118,40 +111,27 @@ export async function applyAccessControlForCreate( context: KeystoneContext, originalInput: Record ) { - const result = await validateCreateListAccessControl({ - access: list.access.create, - args: { - context, - listKey: list.listKey, - operation: 'create', - originalInput, - session: context.session, - }, - }); + const args = { + context, + listKey: list.listKey, + operation: 'create' as const, + originalInput, + session: context.session, + }; + + // List access + const result = await validateCreateListAccessControl({ access: list.access.create, args }); if (!result) { throw accessDeniedError('mutation'); } - await checkFieldAccessControlForCreate(list, context, originalInput); -} -async function checkFieldAccessControlForCreate( - list: InitialisedList, - context: KeystoneContext, - originalInput: Record -) { + // Field access const results = await Promise.all( Object.keys(originalInput).map(fieldKey => { const field = list.fields[fieldKey]; return validateFieldAccessControl({ access: field.access.create, - args: { - context, - fieldKey, - listKey: list.listKey, - operation: 'create', - originalInput, - session: context.session, - }, + args: { fieldKey, ...args }, }); }) ); @@ -170,7 +150,7 @@ async function getStringifiedItemIdFromUniqueWhereInput( return uniqueWhere.id; } try { - const item = await context.sudo().lists[listKey].findOne({ where: uniqueWhere as any }); + const item = await context.sudo().lists[listKey].findOne({ where: uniqueWhere }); return item.id; } catch (err) { throw accessDeniedError('mutation'); diff --git a/packages/keystone/src/lib/core/mutations/create-update.ts b/packages/keystone/src/lib/core/mutations/create-update.ts index 40ea29c7fbd..5e876bc2ded 100644 --- a/packages/keystone/src/lib/core/mutations/create-update.ts +++ b/packages/keystone/src/lib/core/mutations/create-update.ts @@ -30,7 +30,9 @@ export class NestedMutationState { list: InitialisedList ): Promise<{ kind: 'connect'; id: IdType } | { kind: 'create'; data: Record }> { const { afterChange, data } = await createOneState({ data: input }, list, this.#context); + const item = await getPrismaModelForList(this.#context.prisma, list.listKey).create({ data }); + this.#afterChanges.push(() => afterChange(item)); return { kind: 'connect' as const, id: item.id as any }; } @@ -39,39 +41,14 @@ export class NestedMutationState { } } -export function createMany( - { data }: { data: Record[] }, - list: InitialisedList, - context: KeystoneContext, - provider: DatabaseProvider -) { - const writeLimit = pLimit(provider === 'sqlite' ? 1 : Infinity); - return data.map(async rawData => { - const { afterChange, data } = await createOneState({ data: rawData }, list, context); - const item = await writeLimit(() => - getPrismaModelForList(context.prisma, list.listKey).create({ data }) - ); - await afterChange(item); - return item; - }); -} - export async function createOneState( { data: rawData }: { data: Record }, list: InitialisedList, context: KeystoneContext ) { await applyAccessControlForCreate(list, context, rawData); - const { data, afterChange } = await resolveInputForCreateOrUpdate( - list, - context, - rawData, - undefined - ); - return { - data, - afterChange, - }; + + return resolveInputForCreateOrUpdate(list, context, rawData, undefined); } export async function createOne( @@ -80,29 +57,28 @@ export async function createOne( context: KeystoneContext ) { const { afterChange, data } = await createOneState(args, list, context); + const item = await getPrismaModelForList(context.prisma, list.listKey).create({ data }); + await afterChange(item); return item; } -export function updateMany( - { data }: { data: { where: Record; data: Record }[] }, +export function createMany( + { data }: { data: Record[] }, list: InitialisedList, context: KeystoneContext, provider: DatabaseProvider ) { const writeLimit = pLimit(provider === 'sqlite' ? 1 : Infinity); - return data.map(async ({ data: rawData, where: rawUniqueWhere }) => { - const item = await getAccessControlledItemForUpdate(list, context, rawUniqueWhere, rawData); - const { afterChange, data } = await resolveInputForCreateOrUpdate(list, context, rawData, item); - const updatedItem = await writeLimit(() => - getPrismaModelForList(context.prisma, list.listKey).update({ - where: { id: item.id }, - data, - }) + return data.map(async rawData => { + const { afterChange, data } = await createOneState({ data: rawData }, list, context); + + const item = await writeLimit(() => + getPrismaModelForList(context.prisma, list.listKey).create({ data }) ); - afterChange(updatedItem); - return updatedItem; + await afterChange(item); + return item; }); } @@ -127,6 +103,24 @@ export async function updateOne( return updatedItem; } +export function updateMany( + { data }: { data: { where: Record; data: Record }[] }, + list: InitialisedList, + context: KeystoneContext, + provider: DatabaseProvider +) { + const writeLimit = pLimit(provider === 'sqlite' ? 1 : Infinity); + return data.map(async ({ data: rawData, where: rawUniqueWhere }) => { + const item = await getAccessControlledItemForUpdate(list, context, rawUniqueWhere, rawData); + const { afterChange, data } = await resolveInputForCreateOrUpdate(list, context, rawData, item); + const updatedItem = await writeLimit(() => + getPrismaModelForList(context.prisma, list.listKey).update({ where: { id: item.id }, data }) + ); + await afterChange(updatedItem); + return updatedItem; + }); +} + async function resolveInputForCreateOrUpdate( list: InitialisedList, context: KeystoneContext, @@ -139,7 +133,9 @@ async function resolveInputForCreateOrUpdate( await promiseAllRejectWithAllErrors( Object.entries(list.fields).map(async ([fieldKey, field]) => { const inputConfig = field.input?.[operation]; + let input = originalInput[fieldKey]; + // Apply default values if ( operation === 'create' && input === undefined && @@ -150,13 +146,15 @@ async function resolveInputForCreateOrUpdate( ? await field.__legacy.defaultValue({ originalInput, context }) : field.__legacy.defaultValue; } + + // Resolve field type input resolvers const resolved = inputConfig?.resolve ? await inputConfig.resolve( input, context, (() => { if (field.dbField.kind !== 'relation') { - return undefined as any; + return undefined; } const target = `${list.listKey}.${fieldKey}<${field.dbField.list}>`; const foreignList = list.lists[field.dbField.list]; @@ -168,28 +166,31 @@ async function resolveInputForCreateOrUpdate( foreignList, target ); + } else { + return resolveRelateToManyForUpdateInput( + nestedMutationState, + context, + foreignList, + target + ); + } + } else { + if (operation === 'create') { + return resolveRelateToOneForCreateInput( + nestedMutationState, + context, + foreignList, + target + ); + } else { + return resolveRelateToOneForUpdateInput( + nestedMutationState, + context, + foreignList, + target + ); } - return resolveRelateToManyForUpdateInput( - nestedMutationState, - context, - foreignList, - target - ); - } - if (operation === 'create') { - return resolveRelateToOneForCreateInput( - nestedMutationState, - context, - foreignList, - target - ); } - return resolveRelateToOneForUpdateInput( - nestedMutationState, - context, - foreignList, - target - ); })() ) : input; @@ -198,6 +199,7 @@ async function resolveInputForCreateOrUpdate( ) ); + // Resolve input hooks resolvedData = await resolveInputHook( list, context, @@ -207,6 +209,7 @@ async function resolveInputForCreateOrUpdate( existingItem ); + // Check isRequired await validationHook(list.listKey, operation, originalInput, addValidationError => { for (const [fieldKey, field] of Object.entries(list.fields)) { // yes, this is a massive hack, it's just to make image and file fields work well enough @@ -232,6 +235,7 @@ async function resolveInputForCreateOrUpdate( } }); + // Field validation hooks const args = { context, listKey: list.listKey, @@ -252,12 +256,18 @@ async function resolveInputForCreateOrUpdate( ); }); + // List validation hooks await validationHook(list.listKey, operation, originalInput, async addValidationError => { await list.hooks.validateInput?.({ ...args, addValidationError }); }); + + // Run beforeChange hooks const originalInputKeys = new Set(Object.keys(originalInput)); const shouldCallFieldLevelSideEffectHook = (fieldKey: string) => originalInputKeys.has(fieldKey); await runSideEffectOnlyHook(list, 'beforeChange', args, shouldCallFieldLevelSideEffectHook); + + // Return the full resolved input (ready for prisma level operation), + // and the afterChange hook to be applied return { data: flattenMultiDbFields(list.fields, resolvedData), afterChange: async (updatedItem: ItemRootValue) => { @@ -311,19 +321,13 @@ async function resolveInputHook( if (field.hooks.resolveInput === undefined) { return [fieldKey, resolvedData[fieldKey]]; } - const value = await field.hooks.resolveInput({ - ...args, - fieldPath: fieldKey, - }); + const value = await field.hooks.resolveInput({ ...args, fieldPath: fieldKey }); return [fieldKey, value]; }) ) ); if (list.hooks.resolveInput) { - resolvedData = (await list.hooks.resolveInput({ - ...args, - resolvedData, - })) as any; + resolvedData = (await list.hooks.resolveInput({ ...args, resolvedData })) as any; } return resolvedData; } diff --git a/packages/keystone/src/lib/core/mutations/delete.ts b/packages/keystone/src/lib/core/mutations/delete.ts index 3791a30bd0f..4c97e959634 100644 --- a/packages/keystone/src/lib/core/mutations/delete.ts +++ b/packages/keystone/src/lib/core/mutations/delete.ts @@ -15,12 +15,15 @@ export function deleteMany( const writeLimit = pLimit(provider === 'sqlite' ? 1 : Infinity); return where.map(async where => { const { afterDelete, existingItem } = await processDelete(list, context, where); + await writeLimit(() => getPrismaModelForList(context.prisma, list.listKey).delete({ where: { id: existingItem.id }, }) ); + afterDelete(); + return existingItem; }); } @@ -31,38 +34,42 @@ export async function deleteOne( context: KeystoneContext ) { const { afterDelete, existingItem } = await processDelete(list, context, where); + const item = await getPrismaModelForList(context.prisma, list.listKey).delete({ where: { id: existingItem.id }, }); + await afterDelete(); + return item; } -export async function processDelete( +async function processDelete( list: InitialisedList, context: KeystoneContext, filter: UniqueInputFilter ) { + // Access control const existingItem = await getAccessControlledItemForDelete(list, context, filter, filter); + // Field validation const hookArgs = { operation: 'delete' as const, listKey: list.listKey, context, existingItem }; await validationHook(list.listKey, 'delete', undefined, async addValidationError => { await promiseAllRejectWithAllErrors( - Object.entries(list.fields).map(async ([fieldKey, field]) => { - await field.hooks.validateDelete?.({ - ...hookArgs, - addValidationError, - fieldPath: fieldKey, - }); + Object.entries(list.fields).map(async ([fieldPath, field]) => { + await field.hooks.validateDelete?.({ ...hookArgs, addValidationError, fieldPath }); }) ); }); + // List validation await validationHook(list.listKey, 'delete', undefined, async addValidationError => { await list.hooks.validateDelete?.({ ...hookArgs, addValidationError }); }); + // Before delete await runSideEffectOnlyHook(list, 'beforeDelete', hookArgs, () => true); + return { existingItem, afterDelete: async () => { diff --git a/packages/keystone/src/lib/core/mutations/hooks.ts b/packages/keystone/src/lib/core/mutations/hooks.ts index 04edbb16272..abac1b057af 100644 --- a/packages/keystone/src/lib/core/mutations/hooks.ts +++ b/packages/keystone/src/lib/core/mutations/hooks.ts @@ -52,6 +52,7 @@ export async function runSideEffectOnlyHook< args: Args, shouldRunFieldLevelHook: (fieldKey: string) => boolean ) { + // Field hooks await promiseAllRejectWithAllErrors( Object.entries(list.fields).map(async ([fieldKey, field]) => { if (shouldRunFieldLevelHook(fieldKey)) { @@ -59,5 +60,7 @@ export async function runSideEffectOnlyHook< } }) ); + + // List hooks await list.hooks[hookName]?.(args); } diff --git a/packages/keystone/src/lib/core/mutations/index.ts b/packages/keystone/src/lib/core/mutations/index.ts index 39d5f22e3aa..b8bca544946 100644 --- a/packages/keystone/src/lib/core/mutations/index.ts +++ b/packages/keystone/src/lib/core/mutations/index.ts @@ -3,12 +3,12 @@ import { InitialisedList } from '../types-for-lists'; import * as createAndUpdate from './create-update'; import * as deletes from './delete'; -// this is not a thing that i really agree with but it's to make the behaviour consistent with old keystone -// basically, old keystone uses Promise.allSettled and then after that maps that into promises that resolve and reject, +// This is not a thing that I really agree with but it's to make the behaviour consistent with old keystone. +// Basically, old keystone uses Promise.allSettled and then after that maps that into promises that resolve and reject, // whereas the new stuff is just like "here are some promises" with no guarantees about the order they will be settled in. -// that doesn't matter when they all resolve successfully because the order they resolve successfully in -// doesn't affect anything, if some reject though, the order that they reject in will be the order in the errors array -// and some of our tests rely on the order of the graphql errors array. they shouldn't, but they do. +// That doesn't matter when they all resolve successfully because the order they resolve successfully in +// doesn't affect anything, If some reject though, the order that they reject in will be the order in the errors array +// and some of our tests rely on the order of the graphql errors array. They shouldn't, but they do. function promisesButSettledWhenAllSettledAndInOrder[]>(promises: T): T { const resultsPromise = Promise.allSettled(promises); return promises.map(async (_, i) => { @@ -22,14 +22,9 @@ function promisesButSettledWhenAllSettledAndInOrder[] export function getMutationsForList(list: InitialisedList, provider: DatabaseProvider) { const names = getGqlNames(list); - const createOneArgs = { - data: schema.arg({ - type: list.types.create, - }), - }; const createOne = schema.field({ type: list.types.output, - args: createOneArgs, + args: { data: schema.arg({ type: list.types.create }) }, description: ` Create a single ${list.listKey} item.`, resolve(_rootVal, { data }, context) { return createAndUpdate.createOne({ data: data ?? {} }, list, context); @@ -38,18 +33,11 @@ export function getMutationsForList(list: InitialisedList, provider: DatabasePro const createManyInput = schema.inputObject({ name: names.createManyInputName, - fields: { - data: schema.arg({ type: list.types.create }), - }, + fields: { data: schema.arg({ type: list.types.create }) }, }); - const createMany = schema.field({ type: schema.list(list.types.output), - args: { - data: schema.arg({ - type: schema.list(createManyInput), - }), - }, + args: { data: schema.arg({ type: schema.list(createManyInput) }) }, description: ` Create multiple ${list.listKey} items.`, resolve(_rootVal, args, context) { return promisesButSettledWhenAllSettledAndInOrder( @@ -63,17 +51,12 @@ export function getMutationsForList(list: InitialisedList, provider: DatabasePro }, }); - const updateOneArgs = { - id: schema.arg({ - type: schema.nonNull(schema.ID), - }), - data: schema.arg({ - type: list.types.update, - }), - }; const updateOne = schema.field({ type: list.types.output, - args: updateOneArgs, + args: { + id: schema.arg({ type: schema.nonNull(schema.ID) }), + data: schema.arg({ type: list.types.update }), + }, description: ` Update a single ${list.listKey} item by ID.`, resolve(_rootVal, { data, id }, context) { return createAndUpdate.updateOne({ data: data ?? {}, where: { id } }, list, context); @@ -82,16 +65,14 @@ export function getMutationsForList(list: InitialisedList, provider: DatabasePro const updateManyInput = schema.inputObject({ name: names.updateManyInputName, - fields: updateOneArgs, + fields: { + id: schema.arg({ type: schema.nonNull(schema.ID) }), + data: schema.arg({ type: list.types.update }), + }, }); - const updateMany = schema.field({ type: schema.list(list.types.output), - args: { - data: schema.arg({ - type: schema.list(updateManyInput), - }), - }, + args: { data: schema.arg({ type: schema.list(updateManyInput) }) }, description: ` Update multiple ${list.listKey} items by ID.`, resolve(_rootVal, { data }, context) { return promisesButSettledWhenAllSettledAndInOrder( @@ -111,11 +92,7 @@ export function getMutationsForList(list: InitialisedList, provider: DatabasePro const deleteOne = schema.field({ type: list.types.output, - args: { - id: schema.arg({ - type: schema.nonNull(schema.ID), - }), - }, + args: { id: schema.arg({ type: schema.nonNull(schema.ID) }) }, description: ` Delete a single ${list.listKey} item by ID.`, resolve(rootVal, { id }, context) { return deletes.deleteOne({ where: { id } }, list, context); @@ -124,11 +101,7 @@ export function getMutationsForList(list: InitialisedList, provider: DatabasePro const deleteMany = schema.field({ type: schema.list(list.types.output), - args: { - ids: schema.arg({ - type: schema.list(schema.nonNull(schema.ID)), - }), - }, + args: { ids: schema.arg({ type: schema.list(schema.nonNull(schema.ID)) }) }, description: ` Delete multiple ${list.listKey} items by ID.`, resolve(rootVal, { ids }, context) { return promisesButSettledWhenAllSettledAndInOrder( diff --git a/packages/keystone/src/lib/core/mutations/nested-mutation-many-input-resolvers.ts b/packages/keystone/src/lib/core/mutations/nested-mutation-many-input-resolvers.ts index 442cd112e28..5290016afa9 100644 --- a/packages/keystone/src/lib/core/mutations/nested-mutation-many-input-resolvers.ts +++ b/packages/keystone/src/lib/core/mutations/nested-mutation-many-input-resolvers.ts @@ -6,22 +6,13 @@ import { NestedMutationState } from './create-update'; const isNotNull = (arg: T): arg is Exclude => arg !== null; -export function resolveRelateToManyForCreateInput( - nestedMutationState: NestedMutationState, - context: KeystoneContext, - foreignList: InitialisedList, - target: string -) { - return async ( - value: schema.InferValueFromArg> - ) => { - if (value == null) { - return undefined; - } - assertValidManyOperation(value, target); - return resolveCreateAndConnect(value, nestedMutationState, context, foreignList, target); - }; -} +type _CreateValueType = schema.InferValueFromArg< + schema.Arg +>; + +type _UpdateValueType = schema.InferValueFromArg< + schema.Arg +>; async function getDisconnects( uniqueWheres: (UniqueInputFilter | null)[], @@ -33,7 +24,7 @@ async function getDisconnects( uniqueWheres.map(async filter => { if (filter === null) return []; try { - await context.sudo().db.lists[foreignList.listKey].findOne({ where: filter as any }); + await context.sudo().db.lists[foreignList.listKey].findOne({ where: filter }); } catch (err) { return []; } @@ -49,37 +40,38 @@ function getConnects( foreignList: InitialisedList ): Promise[] { return uniqueWhere.map(async filter => { - await context.db.lists[foreignList.listKey].findOne({ where: filter as any }); + await context.db.lists[foreignList.listKey].findOne({ where: filter }); return resolveUniqueWhereInput(filter, foreignList.fields, context); }); } async function resolveCreateAndConnect( - value: Exclude< - schema.InferValueFromArg>, - null | undefined - >, + value: Exclude<_UpdateValueType, null | undefined>, nestedMutationState: NestedMutationState, context: KeystoneContext, foreignList: InitialisedList, target: string ) { + // Perform queries for the connections const connects = Promise.allSettled( getConnects((value.connect || []).filter(isNotNull), context, foreignList) ); + + // Perform nested mutations for the creations const creates = Promise.allSettled( (value.create || []).filter(isNotNull).map(x => nestedMutationState.create(x, foreignList)) ); const [connectResult, createResult] = await Promise.all([connects, creates]); + // Collect all the errors const errors = [...connectResult.filter(isRejected), ...createResult.filter(isRejected)].map( x => x.reason ); - if (errors.length) { throw new Error(`Unable to create and/or connect ${errors.length} ${target}`); } + const result = { connect: connectResult.filter(isFulfilled).map(x => x.value), create: [] as Record[], @@ -88,20 +80,17 @@ async function resolveCreateAndConnect( for (const createData of createResult.filter(isFulfilled).map(x => x.value)) { if (createData.kind === 'create') { result.create.push(createData.data); - } - if (createData.kind === 'connect') { + } else if (createData.kind === 'connect') { result.connect.push({ id: createData.id }); } } + // Perform queries for the connections return result; } function assertValidManyOperation( - val: Exclude< - schema.InferValueFromArg>, - undefined | null - >, + val: Exclude<_UpdateValueType, undefined | null>, target: string ) { if ( @@ -114,15 +103,28 @@ function assertValidManyOperation( } } +export function resolveRelateToManyForCreateInput( + nestedMutationState: NestedMutationState, + context: KeystoneContext, + foreignList: InitialisedList, + target: string +) { + return async (value: _CreateValueType) => { + if (value == null) { + return undefined; + } + assertValidManyOperation(value, target); + return resolveCreateAndConnect(value, nestedMutationState, context, foreignList, target); + }; +} + export function resolveRelateToManyForUpdateInput( nestedMutationState: NestedMutationState, context: KeystoneContext, foreignList: InitialisedList, target: string ) { - return async ( - value: schema.InferValueFromArg> - ) => { + return async (value: _UpdateValueType) => { if (value == null) { return undefined; } diff --git a/packages/keystone/src/lib/core/mutations/nested-mutation-one-input-resolvers.ts b/packages/keystone/src/lib/core/mutations/nested-mutation-one-input-resolvers.ts index 23c51e4f660..29e6fbbffb5 100644 --- a/packages/keystone/src/lib/core/mutations/nested-mutation-one-input-resolvers.ts +++ b/packages/keystone/src/lib/core/mutations/nested-mutation-one-input-resolvers.ts @@ -3,11 +3,15 @@ import { resolveUniqueWhereInput } from '../where-inputs'; import { InitialisedList } from '../types-for-lists'; import { NestedMutationState } from './create-update'; +type _CreateValueType = schema.InferValueFromArg< + schema.Arg +>; +type _UpdateValueType = schema.InferValueFromArg< + schema.Arg> +>; + async function handleCreateAndUpdate( - value: Exclude< - schema.InferValueFromArg>, - null | undefined - >, + value: Exclude<_CreateValueType, null | undefined>, nestedMutationState: NestedMutationState, context: KeystoneContext, foreignList: InitialisedList, @@ -15,18 +19,17 @@ async function handleCreateAndUpdate( ) { if (value.connect) { try { - await context.db.lists[foreignList.listKey].findOne({ where: value.connect as any }); + await context.db.lists[foreignList.listKey].findOne({ where: value.connect }); } catch (err) { throw new Error(`Unable to connect a ${target}`); } - return { - connect: await resolveUniqueWhereInput(value.connect, foreignList.fields, context), - }; - } - if (value.create) { + const connect = await resolveUniqueWhereInput(value.connect, foreignList.fields, context); + return { connect }; + } else if (value.create) { const createInput = value.create; let create = await (async () => { try { + // Perform the nested create operation return await nestedMutationState.create(createInput, foreignList); } catch (err) { throw new Error(`Unable to create a ${target}`); @@ -35,8 +38,9 @@ async function handleCreateAndUpdate( if (create.kind === 'connect') { return { connect: { id: create.id } }; + } else { + return { create: create.data }; } - return { create: create.data }; } } @@ -46,9 +50,7 @@ export function resolveRelateToOneForCreateInput( foreignList: InitialisedList, target: string ) { - return async ( - value: schema.InferValueFromArg> - ) => { + return async (value: _CreateValueType) => { if (value == null) { return undefined; } @@ -66,31 +68,25 @@ export function resolveRelateToOneForUpdateInput( foreignList: InitialisedList, target: string ) { - return async ( - value: schema.InferValueFromArg< - schema.Arg> - > - ) => { + return async (value: _UpdateValueType) => { if (value == null) { return undefined; } + if (value.connect && value.create) { throw new Error(`Nested mutation operation invalid for ${target}`); } + if (value.connect || value.create) { return handleCreateAndUpdate(value, nestedMutationState, context, foreignList, target); - } - if (value.disconnect) { + } else if (value.disconnect) { try { - await context - .sudo() - .db.lists[foreignList.listKey].findOne({ where: value.disconnect as any }); + await context.sudo().db.lists[foreignList.listKey].findOne({ where: value.disconnect }); } catch (err) { return; } return { disconnect: true }; - } - if (value.disconnectAll) { + } else if (value.disconnectAll) { return { disconnect: true }; } }; diff --git a/packages/keystone/src/lib/core/queries/index.ts b/packages/keystone/src/lib/core/queries/index.ts index b3a01926438..2e741fdd0c9 100644 --- a/packages/keystone/src/lib/core/queries/index.ts +++ b/packages/keystone/src/lib/core/queries/index.ts @@ -9,16 +9,13 @@ export function getQueriesForList(list: InitialisedList) { const findOne = schema.field({ type: list.types.output, - args: { - where: schema.arg({ - type: schema.nonNull(list.types.uniqueWhere), - }), - }, + args: { where: schema.arg({ type: schema.nonNull(list.types.uniqueWhere) }) }, description: ` Search for the ${list.listKey} item with the matching ID.`, async resolve(_rootVal, args, context) { return queries.findOne(args, list, context); }, }); + const findMany = schema.field({ type: schema.list(schema.nonNull(list.types.output)), args: list.types.findManyArgs, @@ -27,11 +24,10 @@ export function getQueriesForList(list: InitialisedList) { return queries.findMany(args, list, context, info); }, }); + const countQuery = schema.field({ type: schema.Int, - args: { - where: schema.arg({ type: schema.nonNull(list.types.where), defaultValue: {} }), - }, + args: { where: schema.arg({ type: schema.nonNull(list.types.where), defaultValue: {} }) }, async resolve(_rootVal, args, context, info) { const count = await queries.count(args, list, context); if (info && info.cacheControl && list.cacheHint) { diff --git a/packages/keystone/src/lib/core/queries/output-field.ts b/packages/keystone/src/lib/core/queries/output-field.ts index f7bedf12f44..0a104bd5d49 100644 --- a/packages/keystone/src/lib/core/queries/output-field.ts +++ b/packages/keystone/src/lib/core/queries/output-field.ts @@ -23,12 +23,6 @@ import { } from '../utils'; import { findMany, findManyFilter } from './resolvers'; -function assert(condition: boolean): asserts condition { - if (!condition) { - throw new Error('failed assert'); - } -} - function getRelationVal( dbField: ResolvedRelationDBField, id: IdType, @@ -37,15 +31,14 @@ function getRelationVal( info: GraphQLResolveInfo ) { const oppositeDbField = foreignList.resolvedDbFields[dbField.field]; - assert(oppositeDbField.kind === 'relation'); + if (oppositeDbField.kind !== 'relation') throw new Error('failed assert'); const relationFilter = { [dbField.field]: oppositeDbField.mode === 'many' ? { some: { id } } : { id }, }; if (dbField.mode === 'many') { return { - findMany: async (args: FindManyArgsValue) => { - return findMany(args, foreignList, context, info, relationFilter); - }, + findMany: async (args: FindManyArgsValue) => + findMany(args, foreignList, context, info, relationFilter), count: async ({ where, search, first, skip }: FindManyArgsValue) => { const filter = await findManyFilter(foreignList, context, where, search); if (filter === false) { @@ -70,29 +63,24 @@ function getRelationVal( return count; }, }; - } - - return async () => { - const access = await validateNonCreateListAccessControl({ - access: foreignList.access.read, - args: { - context, - listKey: dbField.list, - operation: 'read', - session: context.session, - }, - }); - if (access === false) { - throw accessDeniedError('query'); - } + } else { + return async () => { + const access = await validateNonCreateListAccessControl({ + access: foreignList.access.read, + args: { context, listKey: dbField.list, operation: 'read', session: context.session }, + }); + if (access === false) { + throw accessDeniedError('query'); + } - return getPrismaModelForList(context.prisma, dbField.list).findFirst({ - where: - access === true - ? relationFilter - : { AND: [relationFilter, await resolveWhereInput(access, foreignList)] }, - }); - }; + return getPrismaModelForList(context.prisma, dbField.list).findFirst({ + where: + access === true + ? relationFilter + : { AND: [relationFilter, await resolveWhereInput(access, foreignList)] }, + }); + }; + } } function getValueForDBField( @@ -114,8 +102,9 @@ function getValueForDBField( } if (dbField.kind === 'relation') { return getRelationVal(dbField, id, lists[dbField.list], context, info); + } else { + return rootVal[fieldPath] as any; } - return rootVal[fieldPath] as any; } export function outputTypeField( @@ -164,8 +153,9 @@ export function outputTypeField( if (output.resolve) { return output.resolve({ value, item: rootVal }, args, context, info); + } else { + return value; } - return value; }, }); } diff --git a/packages/keystone/src/lib/core/queries/resolvers.ts b/packages/keystone/src/lib/core/queries/resolvers.ts index ea34251deb8..cc53c8282cb 100644 --- a/packages/keystone/src/lib/core/queries/resolvers.ts +++ b/packages/keystone/src/lib/core/queries/resolvers.ts @@ -25,21 +25,14 @@ export async function findManyFilter( ): Promise { const access = await validateNonCreateListAccessControl({ access: list.access.read, - args: { - context, - listKey: list.listKey, - operation: 'read', - session: context.session, - }, + args: { context, listKey: list.listKey, operation: 'read', session: context.session }, }); if (!access) { return false; } let resolvedWhere = await resolveWhereInput(where || {}, list); if (typeof access === 'object') { - resolvedWhere = { - AND: [resolvedWhere, await resolveWhereInput(access, list)], - }; + resolvedWhere = { AND: [resolvedWhere, await resolveWhereInput(access, list)] }; } return list.applySearchField(resolvedWhere, search); @@ -78,17 +71,14 @@ async function findOneFilter( ) { const access = await validateNonCreateListAccessControl({ access: list.access.read, - args: { - context, - listKey: list.listKey, - operation: 'read', - session: context.session, - }, + args: { context, listKey: list.listKey, operation: 'read', session: context.session }, }); if (access === false) { return false; } + let resolvedUniqueWhere = await resolveUniqueWhereInput(where, list.fields, context); + const wherePrismaFilter = mapUniqueWhereToWhere(list, resolvedUniqueWhere); return access === true ? wherePrismaFilter @@ -120,29 +110,27 @@ export async function findMany( info: GraphQLResolveInfo, extraFilter?: PrismaFilter ): Promise { - const [resolvedWhere, orderBy] = await Promise.all([ - findManyFilter(list, context, where || {}, search), - resolveOrderBy(rawOrderBy, sortBy, list, context), - ]); + const orderBy = await resolveOrderBy(rawOrderBy, sortBy, list, context); + applyEarlyMaxResults(first, list); + const resolvedWhere = await findManyFilter(list, context, where || {}, search); if (resolvedWhere === false) { throw accessDeniedError('query'); } + const results = await getPrismaModelForList(context.prisma, list.listKey).findMany({ where: extraFilter === undefined ? resolvedWhere : { AND: [resolvedWhere, extraFilter] }, orderBy, take: first ?? undefined, skip, }); + applyMaxResults(results, list, context); + if (info.cacheControl && list.cacheHint) { info.cacheControl.setCacheHint( - list.cacheHint({ - results, - operationName: info.operation.name?.value, - meta: false, - }) as any + list.cacheHint({ results, operationName: info.operation.name?.value, meta: false }) as any ); } return results; @@ -165,16 +153,14 @@ async function resolveOrderBy( } const fieldKey = keys[0]; - const value = orderBySelection[fieldKey]; - if (value === null) { throw new Error('null cannot be passed as an order direction'); } const field = list.fields[fieldKey]; - const resolveOrderBy = field.input!.orderBy!.resolve; - const resolvedValue = resolveOrderBy ? await resolveOrderBy(value, context) : value; + const resolve = field.input!.orderBy!.resolve; + const resolvedValue = resolve ? await resolve(value, context) : value; if (field.dbField.kind === 'multi') { const keys = Object.keys(resolvedValue); if (keys.length !== 1) { @@ -186,17 +172,17 @@ async function resolveOrderBy( return { [getDBFieldKeyForFieldOnMultiField(fieldKey, innerKey)]: resolvedValue[innerKey], }; + } else { + return { [fieldKey]: resolvedValue }; } - return { [fieldKey]: resolvedValue }; }) ) ).concat( - sortBy?.map(sort => { - if (sort.endsWith('_DESC')) { - return { [sort.slice(0, -'_DESC'.length)]: 'desc' }; - } - return { [sort.slice(0, -'_ASC'.length)]: 'asc' }; - }) || [] + sortBy?.map(sort => + sort.endsWith('_DESC') + ? { [sort.slice(0, -'_DESC'.length)]: 'desc' } + : { [sort.slice(0, -'_ASC'.length)]: 'asc' } + ) || [] ); } @@ -209,6 +195,7 @@ export async function count( if (resolvedWhere === false) { throw accessDeniedError('query'); } + return getPrismaModelForList(context.prisma, list.listKey).count({ where: resolvedWhere, }); diff --git a/packages/keystone/src/lib/core/types-for-lists.ts b/packages/keystone/src/lib/core/types-for-lists.ts index 5466ac919dd..cf1b0a21c08 100644 --- a/packages/keystone/src/lib/core/types-for-lists.ts +++ b/packages/keystone/src/lib/core/types-for-lists.ts @@ -113,12 +113,8 @@ export function initialiseLists( const { fields } = lists[listKey]; return Object.assign( { - AND: schema.arg({ - type: schema.list(schema.nonNull(where)), - }), - OR: schema.arg({ - type: schema.list(schema.nonNull(where)), - }), + AND: schema.arg({ type: schema.list(schema.nonNull(where)) }), + OR: schema.arg({ type: schema.list(schema.nonNull(where)) }), }, ...Object.values(fields).map(field => field.access.read === false ? {} : field.__legacy?.filters?.fields ?? {} @@ -167,20 +163,12 @@ export function initialiseLists( }); const findManyArgs: FindManyArgs = { - where: schema.arg({ - type: schema.nonNull(where), - defaultValue: {}, - }), - search: schema.arg({ - type: schema.String, - }), + where: schema.arg({ type: schema.nonNull(where), defaultValue: {} }), + search: schema.arg({ type: schema.String }), sortBy: schema.arg({ type: schema.list( schema.nonNull( - schema.enum({ - name: names.listSortName, - values: schema.enumValues(['bad']), - }) + schema.enum({ name: names.listSortName, values: schema.enumValues(['bad']) }) ) ), deprecationReason: 'sortBy has been deprecated in favour of orderBy', @@ -190,13 +178,8 @@ export function initialiseLists( defaultValue: [], }), // TODO: non-nullable when max results is specified in the list with the default of max results - first: schema.arg({ - type: schema.Int, - }), - skip: schema.arg({ - type: schema.nonNull(schema.Int), - defaultValue: 0, - }), + first: schema.arg({ type: schema.Int }), + skip: schema.arg({ type: schema.nonNull(schema.Int), defaultValue: 0 }), }; const relateToMany = schema.inputObject({ @@ -351,8 +334,9 @@ export function initialiseLists( ]; }) ); + } else { + return field.__legacy?.filters?.impls ?? {}; } - return field.__legacy?.filters?.impls ?? {}; }) ), applySearchField: (filter, search) => { diff --git a/packages/keystone/src/lib/core/where-inputs.ts b/packages/keystone/src/lib/core/where-inputs.ts index 5fd4c3b33b2..3228f89e657 100644 --- a/packages/keystone/src/lib/core/where-inputs.ts +++ b/packages/keystone/src/lib/core/where-inputs.ts @@ -42,10 +42,7 @@ export async function resolveUniqueWhereInput( throw new Error(`The unique value provided in a unique where input must not be null`); } const resolver = fields[key].input!.uniqueWhere!.resolve; - const resolvedVal = resolver ? await resolver(val, context) : val; - return { - [key]: resolvedVal, - }; + return { [key]: resolver ? await resolver(val, context) : val }; } export async function resolveWhereInput( diff --git a/tests/admin-ui-tests/CHANGELOG.md b/tests/admin-ui-tests/CHANGELOG.md index 1e357112af1..52389150f89 100644 --- a/tests/admin-ui-tests/CHANGELOG.md +++ b/tests/admin-ui-tests/CHANGELOG.md @@ -1,6 +1,7 @@ # @keystone-next/admin-ui-tests ## 0.0.2 + ### Patch Changes - Updated dependencies [[`38b78f2ae`](https://github.com/keystonejs/keystone/commit/38b78f2aeaf4c5d8176a1751ad8cb5a7acce2790), [`5f3d407d7`](https://github.com/keystonejs/keystone/commit/5f3d407d79171f04ae877e8eaed9a7f9d5671705), [`139d7a8de`](https://github.com/keystonejs/keystone/commit/139d7a8def263d40c0d1d5353d2744842d9a0951)]: diff --git a/tests/test-projects/basic/CHANGELOG.md b/tests/test-projects/basic/CHANGELOG.md index 7dd7fc5494a..0d9fc5294fc 100644 --- a/tests/test-projects/basic/CHANGELOG.md +++ b/tests/test-projects/basic/CHANGELOG.md @@ -1,6 +1,7 @@ # @keystone-next/test-projects-basic ## 0.0.1 + ### Patch Changes - Updated dependencies [[`38b78f2ae`](https://github.com/keystonejs/keystone/commit/38b78f2aeaf4c5d8176a1751ad8cb5a7acce2790), [`139d7a8de`](https://github.com/keystonejs/keystone/commit/139d7a8def263d40c0d1d5353d2744842d9a0951), [`279403cb0`](https://github.com/keystonejs/keystone/commit/279403cb0b4bffb946763c9a7ef71be57478eeb3), [`253df44c2`](https://github.com/keystonejs/keystone/commit/253df44c2f8d6535a6425b2593eaed5380433d57), [`253df44c2`](https://github.com/keystonejs/keystone/commit/253df44c2f8d6535a6425b2593eaed5380433d57), [`f482db633`](https://github.com/keystonejs/keystone/commit/f482db6332e54a1d5cd469e2805b99b544208e83), [`c536b478f`](https://github.com/keystonejs/keystone/commit/c536b478fc89f2d933cddf8533e7d88030540a63)]: