diff --git a/.changeset/chilled-moons-walk.md b/.changeset/chilled-moons-walk.md new file mode 100644 index 00000000000..6a5464f1977 --- /dev/null +++ b/.changeset/chilled-moons-walk.md @@ -0,0 +1,6 @@ +--- +"@keystone-6/core": major +--- + +Removed deprecated list and field hooks. +Fixed field hooks of all the in built types diff --git a/examples/custom-field/2-stars-field/index.ts b/examples/custom-field/2-stars-field/index.ts index abf8b22df3f..932b6538a76 100644 --- a/examples/custom-field/2-stars-field/index.ts +++ b/examples/custom-field/2-stars-field/index.ts @@ -37,13 +37,22 @@ export const stars = ...config.hooks, // We use the `validateInput` hook to ensure that the user doesn't set an out of range value. // This hook is the key difference on the backend between the stars field type and the integer field type. - async validateInput (args) { - const val = args.resolvedData[meta.fieldKey] - if (!(val == null || (val >= 0 && val <= maxStars))) { - args.addValidationError(`The value must be within the range of 0-${maxStars}`) + validate: { + async create (args) { + const val = args.resolvedData[meta.fieldKey] + if (!(val == null || (val >= 0 && val <= maxStars))) { + args.addValidationError(`The value must be within the range of 0-${maxStars}`) + } + await config.hooks?.validate?.create?.(args) + }, + async update (args) { + const val = args.resolvedData[meta.fieldKey] + if (!(val == null || (val >= 0 && val <= maxStars))) { + args.addValidationError(`The value must be within the range of 0-${maxStars}`) + } + await config.hooks?.validate?.update?.(args) } - await config.hooks?.validateInput?.(args) - }, + } }, // all of these inputs are optional if they don't make sense for a particular field type input: { diff --git a/examples/custom-field/schema.ts b/examples/custom-field/schema.ts index ac2899f29a1..2bdaf10ce40 100644 --- a/examples/custom-field/schema.ts +++ b/examples/custom-field/schema.ts @@ -20,31 +20,58 @@ export const lists = { }, hooks: { - resolveInput: async ({ resolvedData, operation, inputData, item, fieldKey }) => { - console.log('Post.content.hooks.resolveInput', { + resolveInput: { + create: async ({ resolvedData, operation, inputData, item, fieldKey }) => { + console.log('Post.content.hooks.resolveInput.create', { + resolvedData, + operation, + inputData, + item, + fieldKey, + }) + return resolvedData[fieldKey] + }, + update: async ({ resolvedData, operation, inputData, item, fieldKey }) => { + console.log('Post.content.hooks.resolveInput.update', { + resolvedData, + operation, + inputData, + item, + fieldKey, + }) + return resolvedData[fieldKey] + }, + }, + validate: { + create: async ({ resolvedData, - operation, inputData, item, + addValidationError, fieldKey, - }) - return resolvedData[fieldKey] - }, - - validateInput: async ({ - resolvedData, - inputData, - item, - addValidationError, - fieldKey, - }) => { - console.log('Post.content.hooks.validateInput', { + }) => { + console.log('Post.content.hooks.validateInput.create', { + resolvedData, + inputData, + item, + fieldKey, + }) + }, + update: async ({ resolvedData, inputData, item, + addValidationError, fieldKey, - }) - }, + }) => { + console.log('Post.content.hooks.validateInput.update', { + resolvedData, + inputData, + item, + fieldKey, + }) + }, + } }, }), rating: stars({ @@ -100,13 +127,22 @@ export const lists = { }, }, - validateInput: async ({ resolvedData, operation, inputData, item, addValidationError }) => { - console.log('Post.hooks.validateInput', { resolvedData, operation, inputData, item }) + validate: { + create: async ({ resolvedData, operation, inputData, item, addValidationError }) => { + console.log('Post.hooks.validateInput.create', { resolvedData, operation, inputData, item }) - if (Math.random() > 0.95) { - addValidationError('oh oh, try again, this is part of the example') - } - }, + if (Math.random() > 0.95) { + addValidationError('oh oh, try again, this is part of the example') + } + }, + update: async ({ resolvedData, operation, inputData, item, addValidationError }) => { + console.log('Post.hooks.validateInput.update', { resolvedData, operation, inputData, item }) + + if (Math.random() > 0.95) { + addValidationError('oh oh, try again, this is part of the example') + } + }, + } }, }), } satisfies Lists diff --git a/examples/custom-output-paths/schema.ts b/examples/custom-output-paths/schema.ts index e81eb845ad0..b3d5b8835a4 100644 --- a/examples/custom-output-paths/schema.ts +++ b/examples/custom-output-paths/schema.ts @@ -13,25 +13,46 @@ export const lists = { }, hooks: { - afterOperation: async ({ context }) => { - const posts = (await context.db.Post.findMany({ - where: { - title: { equals: 'Home' }, - }, - - // we use Typescript's satisfies here as way to ensure that - // this is the contextualised type - you don't need this - // - // it is helpful for us to check that the example is not - // broken by code changes - // - - // TODO: FIXME, babel and pnpm issues - })) as readonly { title: string, content: string }[] - // })) satisfies readonly { title: string; content: string }[]; - - console.log(posts) - }, + afterOperation: { + create: async ({ context }) => { + const posts = (await context.db.Post.findMany({ + where: { + title: { equals: 'Home' }, + }, + + // we use Typescript's satisfies here as way to ensure that + // this is the contextualised type - you don't need this + // + // it is helpful for us to check that the example is not + // broken by code changes + // + + // TODO: FIXME, babel and pnpm issues + })) as readonly { title: string, content: string }[] + // })) satisfies readonly { title: string; content: string }[]; + + console.log(posts) + }, + update: async ({ context }) => { + const posts = (await context.db.Post.findMany({ + where: { + title: { equals: 'Home' }, + }, + + // we use Typescript's satisfies here as way to ensure that + // this is the contextualised type - you don't need this + // + // it is helpful for us to check that the example is not + // broken by code changes + // + + // TODO: FIXME, babel and pnpm issues + })) as readonly { title: string, content: string }[] + // })) satisfies readonly { title: string; content: string }[]; + + console.log(posts) + }, + } }, }), } satisfies Lists diff --git a/examples/custom-session-invalidation/README.md b/examples/custom-session-invalidation/README.md index 3c2a0101f0b..e07d2839d8c 100644 --- a/examples/custom-session-invalidation/README.md +++ b/examples/custom-session-invalidation/README.md @@ -37,11 +37,19 @@ We add one new field, `passwordChangedAt`, to the `Person` list. Setting the `pa passwordChangedAt: timestamp({ access: () => false, hooks: { - resolveInput: ({ resolvedData }) => { - if (resolvedData.password) { - return new Date(); - } - return; + resolveInput: { + create: ({ resolvedData }) => { + if (resolvedData.password) { + return new Date(); + } + return; + }, + update: ({ resolvedData }) => { + if (resolvedData.password) { + return new Date(); + } + return; + }, }, }, ui: { diff --git a/examples/default-values/schema.ts b/examples/default-values/schema.ts index 839dee0e55b..9d6fd54dc14 100644 --- a/examples/default-values/schema.ts +++ b/examples/default-values/schema.ts @@ -17,17 +17,30 @@ export const lists = { { label: 'High', value: 'high' }, ], hooks: { - resolveInput ({ resolvedData, inputData }) { - if (inputData.priority === null) { - // default to high if "urgent" is in the label - if (inputData.label && inputData.label.toLowerCase().includes('urgent')) { - return 'high' - } else { - return 'low' + resolveInput: { + create ({ resolvedData, inputData }) { + if (inputData.priority === null) { + // default to high if "urgent" is in the label + if (inputData.label && inputData.label.toLowerCase().includes('urgent')) { + return 'high' + } else { + return 'low' + } } - } - return resolvedData.priority - }, + return resolvedData.priority + }, + update ({ resolvedData, inputData }) { + if (inputData.priority === null) { + // default to high if "urgent" is in the label + if (inputData.label && inputData.label.toLowerCase().includes('urgent')) { + return 'high' + } else { + return 'low' + } + } + return resolvedData.priority + }, + } }, }), @@ -39,33 +52,58 @@ export const lists = { many: false, hooks: { // dynamic default: if unassigned, find an anonymous user and assign the task to them - async resolveInput ({ context, operation, resolvedData }) { - if (resolvedData.assignedTo === null) { - const [user] = await context.db.Person.findMany({ - where: { name: { equals: 'Anonymous' } }, - }) - - if (user) { - return { connect: { id: user.id } } + resolveInput: { + async create ({ context, operation, resolvedData }) { + if (resolvedData.assignedTo === null) { + const [user] = await context.db.Person.findMany({ + where: { name: { equals: 'Anonymous' } }, + }) + + if (user) { + return { connect: { id: user.id } } + } } - } - - return resolvedData.assignedTo - }, + + return resolvedData.assignedTo + }, + async update ({ context, operation, resolvedData }) { + if (resolvedData.assignedTo === null) { + const [user] = await context.db.Person.findMany({ + where: { name: { equals: 'Anonymous' } }, + }) + + if (user) { + return { connect: { id: user.id } } + } + } + + return resolvedData.assignedTo + }, + } }, }), // dynamic default: we set the due date to be 7 days in the future finishBy: timestamp({ hooks: { - resolveInput ({ resolvedData, inputData, operation }) { - if (inputData.finishBy == null) { - const date = new Date() - date.setUTCDate(new Date().getUTCDate() + 7) - return date - } - return resolvedData.finishBy - }, + resolveInput: { + create ({ resolvedData, inputData, operation }) { + if (inputData.finishBy == null) { + const date = new Date() + date.setUTCDate(new Date().getUTCDate() + 7) + return date + } + return resolvedData.finishBy + }, + update ({ resolvedData, inputData, operation }) { + if (inputData.finishBy == null) { + const date = new Date() + date.setUTCDate(new Date().getUTCDate() + 7) + return date + } + return resolvedData.finishBy + }, + } }, }), diff --git a/examples/extend-graphql-subscriptions/schema.ts b/examples/extend-graphql-subscriptions/schema.ts index ce54e31ff53..db417b85ee2 100644 --- a/examples/extend-graphql-subscriptions/schema.ts +++ b/examples/extend-graphql-subscriptions/schema.ts @@ -12,17 +12,30 @@ export const lists = { access: allowAll, hooks: { // this hook publishes posts to the 'POST_UPDATED' channel when a post mutated - afterOperation: async ({ item }) => { - // WARNING: passing this item directly to pubSub bypasses any contextual access control - // if you want access control, you need to use a different architecture - // - // tl;dr Keystone access filters are not respected in this scenario - console.log('POST_UPDATED', { id: item?.id }) - - pubSub.publish('POST_UPDATED', { - postUpdated: item, - }) - }, + afterOperation: { + create: async ({ item }) => { + // WARNING: passing this item directly to pubSub bypasses any contextual access control + // if you want access control, you need to use a different architecture + // + // tl;dr Keystone access filters are not respected in this scenario + console.log('POST_UPDATED', { id: item?.id }) + + pubSub.publish('POST_UPDATED', { + postUpdated: item, + }) + }, + update: async ({ item }) => { + // WARNING: passing this item directly to pubSub bypasses any contextual access control + // if you want access control, you need to use a different architecture + // + // tl;dr Keystone access filters are not respected in this scenario + console.log('POST_UPDATED', { id: item?.id }) + + pubSub.publish('POST_UPDATED', { + postUpdated: item, + }) + }, + } }, fields: { title: text({ validation: { isRequired: true } }), diff --git a/examples/field-groups/schema.ts b/examples/field-groups/schema.ts index ad73009af9c..0cd551166b7 100644 --- a/examples/field-groups/schema.ts +++ b/examples/field-groups/schema.ts @@ -26,15 +26,12 @@ export const lists = { // for this example, we are going to use a hook for fun // defaultValue: { kind: 'now' } hooks: { - resolveInput: ({ context, operation, resolvedData }) => { - // TODO: text should allow you to prevent a defaultValue, then Prisma create could be non-null - // if (operation === 'create') return resolvedData.title.replace(/ /g, '-').toLowerCase() - if (operation === 'create') { - return resolvedData.title?.replace(/ /g, '-').toLowerCase() - } - - return resolvedData.slug - }, + resolveInput: { + create: ({ context, operation, resolvedData }) => { + // TODO: text should allow you to prevent a defaultValue, then Prisma create could be non-null + return resolvedData.title?.replace(/ /g, '-').toLowerCase() + }, + } }, }), }, diff --git a/examples/hooks/schema.ts b/examples/hooks/schema.ts index 7b5472397ed..a03f5668488 100644 --- a/examples/hooks/schema.ts +++ b/examples/hooks/schema.ts @@ -78,18 +78,10 @@ export const lists = { // defaultValue: { kind: 'now' } hooks: { - resolveInput: ({ context, operation, resolvedData }) => { - if (operation === 'create') return new Date() - return resolvedData.createdAt - }, - }, - - // TODO: this would be nice - // hooks: { - // resolveInput: { - // create: () => new Date() - // } - // } + resolveInput: { + create: () => new Date() + } + } }), updatedBy: text({ ...readOnly }), @@ -102,18 +94,10 @@ export const lists = { // }, hooks: { - resolveInput: ({ context, operation, resolvedData }) => { - if (operation === 'update') return new Date() - return resolvedData.updatedAt - }, - }, - - // TODO: this would be nice - // hooks: { - // resolveInput: { - // update: () => new Date() - // } - // } + resolveInput: { + update: () => new Date() + } + } }), }, }), @@ -131,29 +115,45 @@ export const lists = { return resolvedData }, }, - validateInput: ({ context, operation, inputData, addValidationError }) => { - const { title, content } = inputData + validate: { + create: ({ inputData, addValidationError }) => { + const { title, content } = inputData + + + // an example of a content filter, the prevents the title or content containing the word "Profanity" + if (/profanity/i.test(title)) return addValidationError('Unacceptable title') + if (/profanity/i.test(content)) return addValidationError('Unacceptable content') + }, + update: ({ inputData, addValidationError }) => { + const { title, content } = inputData - if (operation === 'update' && 'feedback' in inputData) { - const { feedback } = inputData - if (/profanity/i.test(feedback ?? '')) return addValidationError('Unacceptable feedback') - } + if ('feedback' in inputData) { + const { feedback } = inputData + if (/profanity/i.test(feedback ?? '')) return addValidationError('Unacceptable feedback') + } - // an example of a content filter, the prevents the title or content containing the word "Profanity" - if (/profanity/i.test(title)) return addValidationError('Unacceptable title') - if (/profanity/i.test(content)) return addValidationError('Unacceptable content') - }, - validateDelete: ({ context, item, addValidationError }) => { - const { preventDelete } = item - - // an example of a content filter, the prevents the title or content containing the word "Profanity" - if (preventDelete) return addValidationError('Cannot delete Post, preventDelete is true') + // an example of a content filter, the prevents the title or content containing the word "Profanity" + if (/profanity/i.test(title)) return addValidationError('Unacceptable title') + if (/profanity/i.test(content)) return addValidationError('Unacceptable content') + }, + delete: ({ context, item, addValidationError }) => { + const { preventDelete } = item + + // an example of a content filter, the prevents the title or content containing the word "Profanity" + if (preventDelete) return addValidationError('Cannot delete Post, preventDelete is true') + }, }, - - beforeOperation: ({ item, resolvedData, operation }) => { - console.log(`Post beforeOperation.${operation}`, resolvedData) + beforeOperation: { + create: ({ item, resolvedData, operation }) => { + console.log(`Post beforeOperation.${operation}`, resolvedData) + }, + update: ({ item, resolvedData, operation }) => { + console.log(`Post beforeOperation.${operation}`, resolvedData) + }, + delete: ({ item, operation }) => { + console.log(`Post beforeOperation.${operation}`, item) + }, }, - afterOperation: { create: ({ inputData, item }) => { console.log(`Post afterOperation.create`, inputData, '->', item) @@ -162,7 +162,6 @@ export const lists = { update: ({ originalItem, item }) => { console.log(`Post afterOperation.update`, originalItem, '->', item) }, - delete: ({ originalItem }) => { console.log(`Post afterOperation.delete`, originalItem, '-> deleted') }, diff --git a/examples/reuse/schema.ts b/examples/reuse/schema.ts index 6cc07c52e6f..ee021c52367 100644 --- a/examples/reuse/schema.ts +++ b/examples/reuse/schema.ts @@ -55,18 +55,23 @@ function trackingByHooks< // FieldKey extends 'createdBy' | 'updatedBy' // TODO: refined types for the return types > (immutable: boolean = false): FieldHooks { return { - async resolveInput ({ context, operation, resolvedData, item, fieldKey }) { - if (operation === 'update') { + resolveInput: { + async create ({ context, operation, resolvedData, item, fieldKey }) { + // TODO: refined types for the return types + // FIXME: CommonFieldConfig need not always be generalised + return `${context.req?.socket.remoteAddress} (${context.req?.headers['user-agent']})` as any + }, + async update ({ context, operation, resolvedData, item, fieldKey }) { if (immutable) return undefined // show we have refined types for compatible item.* fields if (isTrue(item.completed) && resolvedData.completed !== false) return undefined - } - - // TODO: refined types for the return types - // FIXME: CommonFieldConfig need not always be generalised - return `${context.req?.socket.remoteAddress} (${context.req?.headers['user-agent']})` as any - }, + + // TODO: refined types for the return types + // FIXME: CommonFieldConfig need not always be generalised + return `${context.req?.socket.remoteAddress} (${context.req?.headers['user-agent']})` as any + }, + } } } @@ -76,18 +81,23 @@ function trackingAtHooks< > (immutable: boolean = false): FieldHooks { return { // TODO: switch to operation routing when supported for fields - async resolveInput ({ context, operation, resolvedData, item, fieldKey }) { - if (operation === 'update') { + resolveInput: { + async create ({ context, operation, resolvedData, item, fieldKey }) { + // TODO: refined types for the return types + // FIXME: CommonFieldConfig need not always be generalised + return new Date() as any + }, + async update ({ context, operation, resolvedData, item, fieldKey }) { if (immutable) return undefined // show we have refined types for compatible item.* fields if (isTrue(item.completed) && resolvedData.completed !== false) return undefined - } - // TODO: refined types for the return types - // FIXME: CommonFieldConfig need not always be generalised - return new Date() as any - }, + // TODO: refined types for the return types + // FIXME: CommonFieldConfig need not always be generalised + return new Date() as any + }, + } } } diff --git a/examples/usecase-relationship-union/schema.ts b/examples/usecase-relationship-union/schema.ts index efda8ca3385..980b8c8f355 100644 --- a/examples/usecase-relationship-union/schema.ts +++ b/examples/usecase-relationship-union/schema.ts @@ -80,31 +80,34 @@ export const lists = { }, hooks: { - validateInput: async ({ operation, inputData, addValidationError }) => { - if (operation === 'create') { - const { post, link } = inputData - const values = [post, link].filter(x => x?.connect ?? x?.create) - if (values.length === 0) { - return addValidationError('A relationship is required') - } - if (values.length > 1) { - return addValidationError('Only one relationship at a time') - } - } - - if (operation === 'update') { - const { post, link } = inputData - if ([post, link].some(x => x?.disconnect)) { - return addValidationError('Cannot change relationship type') + validate: { + create: async ({ operation, inputData, addValidationError }) => { + if (operation === 'create') { + const { post, link } = inputData + const values = [post, link].filter(x => x?.connect ?? x?.create) + if (values.length === 0) { + return addValidationError('A relationship is required') + } + if (values.length > 1) { + return addValidationError('Only one relationship at a time') + } } - - const values = [post, link].filter(x => x?.connect ?? x?.create) - if (values.length > 1) { - return addValidationError('Only one relationship at a time') + }, + update: async ({ operation, inputData, addValidationError }) => { + if (operation === 'update') { + const { post, link } = inputData + if ([post, link].some(x => x?.disconnect)) { + return addValidationError('Cannot change relationship type') + } + + const values = [post, link].filter(x => x?.connect ?? x?.create) + if (values.length > 1) { + return addValidationError('Only one relationship at a time') + } + + // TODO: prevent item from changing types with implicit disconnect } - - // TODO: prevent item from changing types with implicit disconnect - } + }, }, resolveInput: { update: async ({ context, operation, resolvedData }) => { diff --git a/examples/usecase-roles/schema.ts b/examples/usecase-roles/schema.ts index 246e0786604..3b8873b95c7 100644 --- a/examples/usecase-roles/schema.ts +++ b/examples/usecase-roles/schema.ts @@ -69,15 +69,17 @@ export const lists = { }, }, hooks: { - resolveInput ({ operation, resolvedData, context }) { - if (operation === 'create' && !resolvedData.assignedTo && context.session) { - // Always default new todo items to the current user; this is important because users - // without canManageAllTodos don't see this field when creating new items - return { connect: { id: context.session.itemId } } + resolveInput: { + create ({ operation, resolvedData, context }) { + if (!resolvedData.assignedTo && context.session) { + // Always default new todo items to the current user; this is important because users + // without canManageAllTodos don't see this field when creating new items + return { connect: { id: context.session.itemId } } + } + return resolvedData.assignedTo } - return resolvedData.assignedTo }, - }, + } }), }, }), diff --git a/examples/usecase-versioning/schema.ts b/examples/usecase-versioning/schema.ts index 46ba7cca800..8f3637961e9 100644 --- a/examples/usecase-versioning/schema.ts +++ b/examples/usecase-versioning/schema.ts @@ -27,11 +27,12 @@ export const lists = { }, }, hooks: { - resolveInput: async ({ resolvedData, operation, item }) => { - if (operation === 'create') return resolvedData.version - if (resolvedData.version !== item.version) throw new Error('Out of sync') - - return item.version + 1 + resolveInput: { + update: async ({ resolvedData, operation, item }) => { + if (resolvedData.version !== item.version) throw new Error('Out of sync') + + return item.version + 1 + }, }, }, }), diff --git a/packages/core/src/fields/resolve-hooks.ts b/packages/core/src/fields/resolve-hooks.ts index 68b21281498..44e47573210 100644 --- a/packages/core/src/fields/resolve-hooks.ts +++ b/packages/core/src/fields/resolve-hooks.ts @@ -1,15 +1,6 @@ -import { - type BaseListTypeInfo, - type FieldHooks, - type MaybePromise -} from '../types' +import { type MaybePromise } from '../types' -// force new syntax for built-in fields -// and block hooks from using resolveInput, they should use GraphQL resolvers -export type InternalFieldHooks = - Omit, 'validateInput' | 'validateDelete' | 'resolveInput'> - -function merge < +export function merge < R, A extends (r: R) => MaybePromise, B extends (r: R) => MaybePromise @@ -20,44 +11,3 @@ function merge < await b?.(args) } } - -/** @deprecated, TODO: remove in breaking change */ -function resolveValidateHooks ({ - validate, - validateInput, - validateDelete -}: FieldHooks): Exclude['validate'], (...args: any) => any> | undefined { - if (!validate && !validateInput && !validateDelete) return - return { - create: merge(validateInput, typeof validate === 'function' ? validate : validate?.create), - update: merge(validateInput, typeof validate === 'function' ? validate : validate?.update), - delete: merge(validateDelete, typeof validate === 'function' ? validate : validate?.delete), - } -} - -export function mergeFieldHooks ( - builtin?: InternalFieldHooks, - hooks?: FieldHooks, -) { - if (hooks === undefined) return builtin - if (builtin === undefined) return hooks - - const builtinValidate = resolveValidateHooks(builtin) - const hooksValidate = resolveValidateHooks(hooks) - return { - ...hooks, - // WARNING: beforeOperation is _after_ a user beforeOperation hook, TODO: this is align with user expectations about when "operations" happen - // our *Operation hooks are built-in, and should happen nearest to the database - beforeOperation: merge(hooks.beforeOperation, builtin.beforeOperation), - afterOperation: merge(builtin.afterOperation, hooks.afterOperation), - validate: (builtinValidate || hooksValidate) ? { - create: merge(builtinValidate?.create, hooksValidate?.create), - update: merge(builtinValidate?.update, hooksValidate?.update), - delete: merge(builtinValidate?.delete, hooksValidate?.delete) - } : undefined, - - // TODO: remove in breaking change - validateInput: undefined, // prevent continuation - validateDelete: undefined, // prevent continuation - } satisfies FieldHooks -} diff --git a/packages/core/src/fields/types/bigInt/index.ts b/packages/core/src/fields/types/bigInt/index.ts index ff25772a15b..92c5b0837f3 100644 --- a/packages/core/src/fields/types/bigInt/index.ts +++ b/packages/core/src/fields/types/bigInt/index.ts @@ -11,7 +11,7 @@ import { resolveDbNullable, makeValidateHook } from '../../non-null-graphql' -import { mergeFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type BigIntFieldConfig = CommonFieldConfig & { @@ -124,7 +124,14 @@ export function bigInt (config: BigIntFi extendPrismaSchema: config.db?.extendPrismaSchema, })({ ...config, - hooks: mergeFieldHooks({ validate }, config.hooks), + hooks: { + ...config.hooks, + validate: { + ...config.hooks?.validate, + create: merge(validate, config.hooks?.validate?.create), + update: merge(validate, config.hooks?.validate?.update), + } + }, input: { uniqueWhere: isIndexed === 'unique' ? { arg: graphql.arg({ type: graphql.BigInt }) } : undefined, where: { diff --git a/packages/core/src/fields/types/calendarDay/index.ts b/packages/core/src/fields/types/calendarDay/index.ts index 4e541516ca5..471e2187a63 100644 --- a/packages/core/src/fields/types/calendarDay/index.ts +++ b/packages/core/src/fields/types/calendarDay/index.ts @@ -9,7 +9,7 @@ import { type CalendarDayFieldMeta } from './views' import { graphql } from '../../..' import { filters } from '../../filters' import { makeValidateHook } from '../../non-null-graphql' -import { mergeFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type CalendarDayFieldConfig = CommonFieldConfig & { @@ -74,7 +74,14 @@ export function calendarDay (config: Cal nativeType: usesNativeDateType ? 'Date' : undefined, })({ ...config, - hooks: mergeFieldHooks({ validate }, config.hooks), + hooks: { + ...config.hooks, + validate: { + ...config.hooks?.validate, + create: merge(validate, config.hooks?.validate?.create), + update: merge(validate, config.hooks?.validate?.update), + }, + }, input: { uniqueWhere: isIndexed === 'unique' diff --git a/packages/core/src/fields/types/decimal/index.ts b/packages/core/src/fields/types/decimal/index.ts index 134164a5524..a7410971e2b 100644 --- a/packages/core/src/fields/types/decimal/index.ts +++ b/packages/core/src/fields/types/decimal/index.ts @@ -11,7 +11,7 @@ import { graphql } from '../../..' import { filters } from '../../filters' import { type DecimalFieldMeta } from './views' import { makeValidateHook } from '../../non-null-graphql' -import { mergeFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type DecimalFieldConfig = CommonFieldConfig & { @@ -141,7 +141,14 @@ export function decimal (config: Decimal return fieldType(dbField)({ ...config, - hooks: mergeFieldHooks({ validate }, config.hooks), + hooks: { + ...config.hooks, + validate: { + ...config.hooks?.validate, + create: merge(validate, config.hooks?.validate?.create), + update: merge(validate, config.hooks?.validate?.update), + }, + }, input: { uniqueWhere: isIndexed === 'unique' ? { arg: graphql.arg({ type: graphql.Decimal }) } : undefined, diff --git a/packages/core/src/fields/types/file/index.ts b/packages/core/src/fields/types/file/index.ts index 78882b594b1..6cb528a74c8 100644 --- a/packages/core/src/fields/types/file/index.ts +++ b/packages/core/src/fields/types/file/index.ts @@ -7,7 +7,7 @@ import { fieldType, } from '../../../types' import { graphql } from '../../..' -import { mergeFieldHooks, type InternalFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type FileFieldConfig = CommonFieldConfig & { @@ -63,23 +63,20 @@ export function file (config: FileFieldC throw Error("isIndexed: 'unique' is not a supported option for field type file") } - const hooks: InternalFieldHooks = {} - if (!storage.preserve) { - hooks.beforeOperation = async function (args) { - if (args.operation === 'update' || args.operation === 'delete') { - const filenameKey = `${fieldKey}_filename` - const filename = args.item[filenameKey] + async function beforeOperationResolver (args: any) { + if (args.operation === 'update' || args.operation === 'delete') { + const filenameKey = `${fieldKey}_filename` + const filename = args.item[filenameKey] - // this will occur on an update where a file already existed but has been - // changed, or on a delete, where there is no longer an item - if ( - (args.operation === 'delete' || - typeof args.resolvedData[fieldKey].filename === 'string' || - args.resolvedData[fieldKey].filename === null) && - typeof filename === 'string' - ) { - await args.context.files(config.storage).deleteAtSource(filename) - } + // this will occur on an update where a file already existed but has been + // changed, or on a delete, where there is no longer an item + if ( + (args.operation === 'delete' || + typeof args.resolvedData[fieldKey].filename === 'string' || + args.resolvedData[fieldKey].filename === null) && + typeof filename === 'string' + ) { + await args.context.files(config.storage).deleteAtSource(filename) } } } @@ -93,7 +90,16 @@ export function file (config: FileFieldC }, })({ ...config, - hooks: mergeFieldHooks(hooks, config.hooks), + hooks: storage.preserve + ? config.hooks + : { + ...config.hooks, + beforeOperation: { + ...config.hooks?.beforeOperation, + update: merge(config.hooks?.beforeOperation?.update, beforeOperationResolver), + delete: merge(config.hooks?.beforeOperation?.delete, beforeOperationResolver), + }, + }, input: { create: { arg: inputArg, diff --git a/packages/core/src/fields/types/float/index.ts b/packages/core/src/fields/types/float/index.ts index 400d3272cf4..82c9145b077 100644 --- a/packages/core/src/fields/types/float/index.ts +++ b/packages/core/src/fields/types/float/index.ts @@ -9,7 +9,7 @@ import { import { graphql } from '../../..' import { filters } from '../../filters' import { makeValidateHook } from '../../non-null-graphql' -import { mergeFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type FloatFieldConfig = CommonFieldConfig & { @@ -87,7 +87,14 @@ export function float (config: FloatFiel extendPrismaSchema: config.db?.extendPrismaSchema, })({ ...config, - hooks: mergeFieldHooks({ validate }, config.hooks), + hooks: { + ...config.hooks, + validate: { + ...config.hooks?.validate, + create: merge(validate, config.hooks?.validate?.create), + update: merge(validate, config.hooks?.validate?.update), + }, + }, input: { uniqueWhere: isIndexed === 'unique' ? { arg: graphql.arg({ type: graphql.Float }) } : undefined, where: { diff --git a/packages/core/src/fields/types/image/index.ts b/packages/core/src/fields/types/image/index.ts index 7877902c6d9..520139f5a4a 100644 --- a/packages/core/src/fields/types/image/index.ts +++ b/packages/core/src/fields/types/image/index.ts @@ -9,7 +9,7 @@ import { } from '../../../types' import { graphql } from '../../..' import { SUPPORTED_IMAGE_EXTENSIONS } from './utils' -import { mergeFieldHooks, type InternalFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type ImageFieldConfig = CommonFieldConfig & { @@ -88,27 +88,24 @@ export function image (config: ImageFiel throw Error("isIndexed: 'unique' is not a supported option for field type image") } - const hooks: InternalFieldHooks = {} - if (!storage.preserve) { - hooks.beforeOperation = async (args) => { - if (args.operation === 'update' || args.operation === 'delete') { - const idKey = `${fieldKey}_id` - const id = args.item[idKey] - const extensionKey = `${fieldKey}_extension` - const extension = args.item[extensionKey] - - // This will occur on an update where an image already existed but has been - // changed, or on a delete, where there is no longer an item - if ( - (args.operation === 'delete' || - typeof args.resolvedData[fieldKey].id === 'string' || - args.resolvedData[fieldKey].id === null) && - typeof id === 'string' && - typeof extension === 'string' && - isValidImageExtension(extension) - ) { - await args.context.images(config.storage).deleteAtSource(id, extension) - } + async function beforeOperationResolver (args: any) { + if (args.operation === 'update' || args.operation === 'delete') { + const idKey = `${fieldKey}_id` + const id = args.item[idKey] + const extensionKey = `${fieldKey}_extension` + const extension = args.item[extensionKey] + + // This will occur on an update where an image already existed but has been + // changed, or on a delete, where there is no longer an item + if ( + (args.operation === 'delete' || + typeof args.resolvedData[fieldKey].id === 'string' || + args.resolvedData[fieldKey].id === null) && + typeof id === 'string' && + typeof extension === 'string' && + isValidImageExtension(extension) + ) { + await args.context.images(config.storage).deleteAtSource(id, extension) } } } @@ -125,7 +122,16 @@ export function image (config: ImageFiel }, })({ ...config, - hooks: mergeFieldHooks(hooks, config.hooks), + hooks: storage.preserve + ? config.hooks + : { + ...config.hooks, + beforeOperation: { + ...config.hooks?.beforeOperation, + update: merge(config.hooks?.beforeOperation?.update, beforeOperationResolver), + delete: merge(config.hooks?.beforeOperation?.delete, beforeOperationResolver), + }, + }, input: { create: { arg: inputArg, diff --git a/packages/core/src/fields/types/integer/index.ts b/packages/core/src/fields/types/integer/index.ts index 43e6a3499f6..9ac09e3ecf0 100644 --- a/packages/core/src/fields/types/integer/index.ts +++ b/packages/core/src/fields/types/integer/index.ts @@ -11,7 +11,7 @@ import { resolveDbNullable, makeValidateHook } from '../../non-null-graphql' -import { mergeFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type IntegerFieldConfig = CommonFieldConfig & { @@ -121,7 +121,14 @@ export function integer (config: Integer extendPrismaSchema: config.db?.extendPrismaSchema, })({ ...config, - hooks: mergeFieldHooks({ validate }, config.hooks), + hooks: { + ...config.hooks, + validate: { + ...config.hooks?.validate, + create: merge(validate, config.hooks?.validate?.create), + update: merge(validate, config.hooks?.validate?.update), + }, + }, input: { uniqueWhere: isIndexed === 'unique' ? { arg: graphql.arg({ type: graphql.Int }) } : undefined, where: { diff --git a/packages/core/src/fields/types/multiselect/index.ts b/packages/core/src/fields/types/multiselect/index.ts index bb5614cb8f7..4eca54f780a 100644 --- a/packages/core/src/fields/types/multiselect/index.ts +++ b/packages/core/src/fields/types/multiselect/index.ts @@ -9,7 +9,7 @@ import { } from '../../../types' import { graphql } from '../../..' import { makeValidateHook } from '../../non-null-graphql' -import { mergeFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type MultiselectFieldConfig = CommonFieldConfig & @@ -108,7 +108,14 @@ export function multiselect ( { ...config, __ksTelemetryFieldTypeName: '@keystone-6/multiselect', - hooks: mergeFieldHooks({ validate }, config.hooks), + hooks: { + ...config.hooks, + validate: { + ...config.hooks?.validate, + create: merge(validate, config.hooks?.validate?.create), + update: merge(validate, config.hooks?.validate?.update), + }, + }, views: '@keystone-6/core/fields/types/multiselect/views', getAdminMeta: () => ({ options: transformedConfig.options, diff --git a/packages/core/src/fields/types/password/index.ts b/packages/core/src/fields/types/password/index.ts index 3c189aa4900..24d75f0e7a7 100644 --- a/packages/core/src/fields/types/password/index.ts +++ b/packages/core/src/fields/types/password/index.ts @@ -11,7 +11,7 @@ import { import { graphql } from '../../..' import { type PasswordFieldMeta } from './views' import { makeValidateHook } from '../../non-null-graphql' -import { mergeFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type PasswordFieldConfig = CommonFieldConfig & { @@ -137,7 +137,14 @@ export function password (config: Passwo extendPrismaSchema: config.db?.extendPrismaSchema, })({ ...config, - hooks: mergeFieldHooks({ validate }, config.hooks), + hooks: { + ...config.hooks, + validate: { + ...config.hooks?.validate, + create: merge(validate, config.hooks?.validate?.create), + update: merge(validate, config.hooks?.validate?.update), + }, + }, input: { where: mode === 'required' diff --git a/packages/core/src/fields/types/relationship/index.ts b/packages/core/src/fields/types/relationship/index.ts index 1bd1de69f9e..f0326602117 100644 --- a/packages/core/src/fields/types/relationship/index.ts +++ b/packages/core/src/fields/types/relationship/index.ts @@ -107,7 +107,7 @@ export type RelationshipFieldConfig = } & (OneDbConfig | ManyDbConfig) & (SelectDisplayConfig | CardsDisplayConfig | CountDisplayConfig) -export function relationship ({ +export function relationship ({ ref, ...config }: RelationshipFieldConfig): FieldTypeFunc { diff --git a/packages/core/src/fields/types/select/index.ts b/packages/core/src/fields/types/select/index.ts index eb3d14543a0..c8d5a24d437 100644 --- a/packages/core/src/fields/types/select/index.ts +++ b/packages/core/src/fields/types/select/index.ts @@ -10,7 +10,7 @@ import { import { graphql } from '../../..' import { filters } from '../../filters' import { makeValidateHook } from '../../non-null-graphql' -import { mergeFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type SelectFieldConfig = CommonFieldConfig & @@ -95,7 +95,14 @@ export function select (config: SelectFi ...config, mode, ui, - hooks: mergeFieldHooks({ validate }, config.hooks), + hooks: { + ...config.hooks, + validate: { + ...config.hooks?.validate, + create: merge(validate, config.hooks?.validate?.create), + update: merge(validate, config.hooks?.validate?.update), + }, + }, __ksTelemetryFieldTypeName: '@keystone-6/select', views: '@keystone-6/core/fields/types/select/views', getAdminMeta: () => ({ diff --git a/packages/core/src/fields/types/text/index.ts b/packages/core/src/fields/types/text/index.ts index 426908a2763..bf77f75b2ae 100644 --- a/packages/core/src/fields/types/text/index.ts +++ b/packages/core/src/fields/types/text/index.ts @@ -8,7 +8,7 @@ import { import { graphql } from '../../..' import { makeValidateHook } from '../../non-null-graphql' import { filters } from '../../filters' -import { mergeFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type TextFieldConfig = CommonFieldConfig & { @@ -142,7 +142,14 @@ export function text ( extendPrismaSchema: config.db?.extendPrismaSchema, })({ ...config, - hooks: mergeFieldHooks({ validate }, config.hooks), + hooks: { + ...config.hooks, + validate: { + ...config.hooks?.validate, + create: merge(validate, config.hooks?.validate?.create), + update: merge(validate, config.hooks?.validate?.update), + }, + }, input: { uniqueWhere: isIndexed === 'unique' ? { arg: graphql.arg({ type: graphql.String }) } : undefined, where: { diff --git a/packages/core/src/fields/types/timestamp/index.ts b/packages/core/src/fields/types/timestamp/index.ts index d2cdd55d8ac..75cd6c85be5 100644 --- a/packages/core/src/fields/types/timestamp/index.ts +++ b/packages/core/src/fields/types/timestamp/index.ts @@ -8,7 +8,7 @@ import { import { graphql } from '../../..' import { filters } from '../../filters' import { makeValidateHook } from '../../non-null-graphql' -import { mergeFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' import { type TimestampFieldMeta } from './views' export type TimestampFieldConfig = @@ -73,7 +73,14 @@ export function timestamp ( extendPrismaSchema: config.db?.extendPrismaSchema, })({ ...config, - hooks: mergeFieldHooks({ validate }, config.hooks), + hooks: { + ...config.hooks, + validate: { + ...config.hooks?.validate, + create: merge(validate, config.hooks?.validate?.create), + update: merge(validate, config.hooks?.validate?.update), + }, + }, input: { uniqueWhere: isIndexed === 'unique' ? { arg: graphql.arg({ type: graphql.DateTime }) } : undefined, where: { diff --git a/packages/core/src/lib/core/initialise-lists.ts b/packages/core/src/lib/core/initialise-lists.ts index 5bb774b5edc..d5936912df9 100644 --- a/packages/core/src/lib/core/initialise-lists.ts +++ b/packages/core/src/lib/core/initialise-lists.ts @@ -244,78 +244,27 @@ function defaultListHooksResolveInput ({ resolvedData }: { resolvedData: any }) return resolvedData } -function parseListHooksResolveInput (f: ListHooks['resolveInput']) { - if (typeof f === 'function') { - return { - create: f, - update: f, - } - } - - const { - create = defaultListHooksResolveInput, - update = defaultListHooksResolveInput - } = f ?? {} - return { create, update } -} - -function parseListHooksValidate (f: ListHooks['validate']) { - if (typeof f === 'function') { - return { - create: f, - update: f, - delete: f, - } - } - - const { - create = defaultOperationHook, - update = defaultOperationHook, - delete: delete_ = defaultOperationHook, - } = f ?? {} - return { create, update, delete: delete_ } -} - -function parseListHooksBeforeOperation (f: ListHooks['beforeOperation']) { - if (typeof f === 'function') { - return { - create: f, - update: f, - delete: f, - } - } - - const { - create = defaultOperationHook, - update = defaultOperationHook, - delete: _delete = defaultOperationHook, - } = f ?? {} - return { create, update, delete: _delete } -} - -function parseListHooksAfterOperation (f: ListHooks['afterOperation']) { - if (typeof f === 'function') { - return { - create: f, - update: f, - delete: f, - } - } - - const { - create = defaultOperationHook, - update = defaultOperationHook, - delete: _delete = defaultOperationHook, - } = f ?? {} - return { create, update, delete: _delete } -} - function parseListHooks (hooks: ListHooks): ResolvedListHooks { return { - resolveInput: parseListHooksResolveInput(hooks.resolveInput), - validate: parseListHooksValidate(hooks.validate), - beforeOperation: parseListHooksBeforeOperation(hooks.beforeOperation), - afterOperation: parseListHooksAfterOperation(hooks.afterOperation), + resolveInput: { + create: hooks.resolveInput?.create ?? defaultListHooksResolveInput, + update: hooks.resolveInput?.update ?? defaultListHooksResolveInput, + }, + validate: { + create: hooks.validate?.create ?? defaultOperationHook, + update: hooks.validate?.update ?? defaultOperationHook, + delete: hooks.validate?.delete ?? defaultOperationHook, + }, + beforeOperation: { + create: hooks.beforeOperation?.create ?? defaultOperationHook, + update: hooks.beforeOperation?.update ?? defaultOperationHook, + delete: hooks.beforeOperation?.delete ?? defaultOperationHook, + }, + afterOperation: { + create: hooks.afterOperation?.create ?? defaultOperationHook, + update: hooks.afterOperation?.update ?? defaultOperationHook, + delete: hooks.afterOperation?.delete ?? defaultOperationHook, + }, } } @@ -330,45 +279,27 @@ function defaultFieldHooksResolveInput ({ } function parseFieldHooks ( - fieldKey: string, hooks: FieldHooks, ): ResolvedFieldHooks { - /** @deprecated, TODO: remove in breaking change */ - if (hooks.validate !== undefined) { - if (hooks.validateInput !== undefined) throw new TypeError(`"hooks.validate" conflicts with "hooks.validateInput" for the "${fieldKey}" field`) - if (hooks.validateDelete !== undefined) throw new TypeError(`"hooks.validate" conflicts with "hooks.validateDelete" for the "${fieldKey}" field`) - - if (typeof hooks.validate === 'function') { - return parseFieldHooks(fieldKey, { - ...hooks, - validate: { - create: hooks.validate, - update: hooks.validate, - delete: hooks.validate, - } - }) - } - } - return { resolveInput: { - create: hooks.resolveInput ?? defaultFieldHooksResolveInput, - update: hooks.resolveInput ?? defaultFieldHooksResolveInput, + create: hooks.resolveInput?.create ?? defaultFieldHooksResolveInput, + update: hooks.resolveInput?.update ?? defaultFieldHooksResolveInput, }, validate: { - create: hooks.validate?.create ?? hooks.validateInput ?? defaultOperationHook, - update: hooks.validate?.update ?? hooks.validateInput ?? defaultOperationHook, - delete: hooks.validate?.delete ?? hooks.validateDelete ?? defaultOperationHook, + create: hooks.validate?.create ?? defaultOperationHook, + update: hooks.validate?.update ?? defaultOperationHook, + delete: hooks.validate?.delete ?? defaultOperationHook, }, beforeOperation: { - create: hooks.beforeOperation ?? defaultOperationHook, - update: hooks.beforeOperation ?? defaultOperationHook, - delete: hooks.beforeOperation ?? defaultOperationHook, + create: hooks.beforeOperation?.create ?? defaultOperationHook, + update: hooks.beforeOperation?.update ?? defaultOperationHook, + delete: hooks.beforeOperation?.delete ?? defaultOperationHook, }, afterOperation: { - create: hooks.afterOperation ?? defaultOperationHook, - update: hooks.afterOperation ?? defaultOperationHook, - delete: hooks.afterOperation ?? defaultOperationHook, + create: hooks.afterOperation?.create ?? defaultOperationHook, + update: hooks.afterOperation?.update ?? defaultOperationHook, + delete: hooks.afterOperation?.delete ?? defaultOperationHook, }, } } @@ -700,7 +631,7 @@ function getListsWithInitialisedFields ( dbField: f.dbField as ResolvedDBField, access: parseFieldAccessControl(f.access), - hooks: parseFieldHooks(fieldKey, f.hooks ?? {}), + hooks: parseFieldHooks(f.hooks ?? {}), graphql: { cacheHint: f.graphql?.cacheHint, isEnabled: isEnabledField, diff --git a/packages/core/src/lib/defaults.ts b/packages/core/src/lib/defaults.ts index 84c123b356b..5f261380aa8 100644 --- a/packages/core/src/lib/defaults.ts +++ b/packages/core/src/lib/defaults.ts @@ -48,25 +48,6 @@ function injectDefaults (config: KeystoneConfig, defaultIdField: IdFieldConfig) } } - /** @deprecated, TODO: remove in breaking change */ - for (const [listKey, list] of Object.entries(updated)) { - if (list.hooks === undefined) continue - if (list.hooks.validate !== undefined) { - if (list.hooks.validateInput !== undefined) throw new TypeError(`"hooks.validate" conflicts with "hooks.validateInput" for the "${listKey}" list`) - if (list.hooks.validateDelete !== undefined) throw new TypeError(`"hooks.validate" conflicts with "hooks.validateDelete" for the "${listKey}" list`) - continue - } - - list.hooks = { - ...list.hooks, - validate: { - create: list.hooks.validateInput, - update: list.hooks.validateInput, - delete: list.hooks.validateDelete - } - } - } - return updated } diff --git a/packages/core/src/types/config/hooks.ts b/packages/core/src/types/config/hooks.ts index 046476ebd3a..dd108fab1eb 100644 --- a/packages/core/src/types/config/hooks.ts +++ b/packages/core/src/types/config/hooks.ts @@ -51,55 +51,37 @@ export type ListHooks = { /** * Used to **modify the input** for create and update operations after default values and access control have been applied */ - resolveInput?: - | ResolveInputListHook - | { - create?: ResolveInputListHook - update?: ResolveInputListHook - } + resolveInput?: { + create?: ResolveInputListHook + update?: ResolveInputListHook + } /** * Used to **validate** if a create, update or delete operation is OK */ - validate?: - | ValidateHook - | { - create?: ValidateHook - update?: ValidateHook - delete?: ValidateHook - } - - /** - * @deprecated, replaced by validate^ - */ - validateInput?: ValidateHook - - /** - * @deprecated, replaced by validate^ - */ - validateDelete?: ValidateHook + validate?: { + create?: ValidateHook + update?: ValidateHook + delete?: ValidateHook + } /** * Used to **cause side effects** before a create, update, or delete operation once all validateInput hooks have resolved */ - beforeOperation?: - | BeforeOperationListHook - | { - create?: BeforeOperationListHook - update?: BeforeOperationListHook - delete?: BeforeOperationListHook - } + beforeOperation?: { + create?: BeforeOperationListHook + update?: BeforeOperationListHook + delete?: BeforeOperationListHook + } /** * Used to **cause side effects** after a create, update, or delete operation operation has occurred */ - afterOperation?: - | AfterOperationListHook - | { - create?: AfterOperationListHook - update?: AfterOperationListHook - delete?: AfterOperationListHook - } + afterOperation?: { + create?: AfterOperationListHook + update?: AfterOperationListHook + delete?: AfterOperationListHook + } } export type ResolvedListHooks = { @@ -131,46 +113,37 @@ export type FieldHooks< /** * Used to **modify the input** for create and update operations after default values and access control have been applied */ - resolveInput?: - | ResolveInputFieldHook -// TODO: add in breaking change -// | { -// create?: ResolveInputFieldHook -// update?: ResolveInputFieldHook -// } + resolveInput?: { + create?: ResolveInputFieldHook + update?: ResolveInputFieldHook + } /** * Used to **validate** if a create, update or delete operation is OK */ - validate?: - | ValidateFieldHook - | { - create?: ValidateFieldHook - update?: ValidateFieldHook - delete?: ValidateFieldHook - } - - /** - * @deprecated, replaced by validate^ - * Used to **validate the input** for create and update operations once all resolveInput hooks resolved - */ - validateInput?: ValidateFieldHook - - /** - * @deprecated, replaced by validate^ - * Used to **validate** that a delete operation can happen after access control has occurred - */ - validateDelete?: ValidateFieldHook + validate?: { + create?: ValidateFieldHook + update?: ValidateFieldHook + delete?: ValidateFieldHook + } /** * Used to **cause side effects** before a create, update, or delete operation once all validateInput hooks have resolved */ - beforeOperation?: BeforeOperationFieldHook + beforeOperation?: { + create?: BeforeOperationFieldHook + update?: BeforeOperationFieldHook + delete?: BeforeOperationFieldHook + } /** * Used to **cause side effects** after a create, update, or delete operation operation has occurred */ - afterOperation?: AfterOperationFieldHook + afterOperation?: { + create?: AfterOperationFieldHook + update?: AfterOperationFieldHook + delete?: AfterOperationFieldHook + } } export type ResolvedFieldHooks< diff --git a/packages/fields-document/src/DocumentEditor/tests/utils.tsx b/packages/fields-document/src/DocumentEditor/tests/utils.tsx index e3eb4026018..6d55ba1d5a0 100644 --- a/packages/fields-document/src/DocumentEditor/tests/utils.tsx +++ b/packages/fields-document/src/DocumentEditor/tests/utils.tsx @@ -373,7 +373,7 @@ function nodeToReactElement ( if (type !== undefined) { return createElement(type, { ...restNode, ...computedData, children }) } - // @ts-ignore TODO: can `type` actually be undefined? + // @ts-expect-error TODO: can `type` actually be undefined? return createElement('element', { ...node, ...computedData, children }) } diff --git a/tests/api-tests/hooks.test.ts b/tests/api-tests/hooks.test.ts index 6355758ac2c..f1a067d9158 100644 --- a/tests/api-tests/hooks.test.ts +++ b/tests/api-tests/hooks.test.ts @@ -32,8 +32,8 @@ function makeList ({ : hooks.validate === 'throws' ? makeThrower(`${__name}_${context}`) : ({ operation, resolvedData, addValidationError }: any) => { - addValidationError(`Validate_${__name}_${context}_${operation}`) - // TODO: mixed results + addValidationError(`Validate_${__name}_${context}_${operation}`) + // TODO: mixed results } } @@ -85,14 +85,25 @@ function makeList ({ basis: text(hooks.field ? { db: { isNullable: true }, // drops the implicit validation hook hooks: { - resolveInput: hooks.resolveInput ? replaceF : undefined, + resolveInput: hooks.resolveInput ? { + create: replaceF, + update: replaceF, + } : undefined, validate: { create: makeValidate('FVI'), update: makeValidate('FVI'), delete: makeValidate('FVI'), }, - beforeOperation: hooks.beforeOperation ? makeThrower(`${__name}_FBO`) : undefined, - afterOperation: hooks.afterOperation ? makeThrower(`${__name}_FAO`) : undefined + beforeOperation: hooks.beforeOperation ? { + create: makeThrower(`${__name}_FBO`), + update: makeThrower(`${__name}_FBO`), + delete: makeThrower(`${__name}_FBO`), + } : undefined, + afterOperation: hooks.afterOperation ? { + create: makeThrower(`${__name}_FAO`), + update: makeThrower(`${__name}_FAO`), + delete: makeThrower(`${__name}_FAO`), + } : undefined } } : {}), }, diff --git a/tests/api-tests/relationships/nested-mutations/create-many.test.ts b/tests/api-tests/relationships/nested-mutations/create-many.test.ts index 56c8f31a29a..328b55c796b 100644 --- a/tests/api-tests/relationships/nested-mutations/create-many.test.ts +++ b/tests/api-tests/relationships/nested-mutations/create-many.test.ts @@ -69,9 +69,14 @@ const runner2 = setupTestRunner({ content: text(), }, hooks: { - afterOperation () { - afterOperationWasCalled = true - }, + afterOperation: { + create () { + afterOperationWasCalled = true + }, + update () { + afterOperationWasCalled = true + }, + } }, access: allowAll, }), diff --git a/tests/sandbox/configs/7590-add-item-to-relationship-in-hook-cards-ui.ts b/tests/sandbox/configs/7590-add-item-to-relationship-in-hook-cards-ui.ts index 5bd8853e16b..57ff79ec92e 100644 --- a/tests/sandbox/configs/7590-add-item-to-relationship-in-hook-cards-ui.ts +++ b/tests/sandbox/configs/7590-add-item-to-relationship-in-hook-cards-ui.ts @@ -17,12 +17,22 @@ export const lists = { }, hooks: { // every time you save, add a random number - async resolveInput (args) { - return { - ...args.resolvedData[args.fieldKey], - create: { - value: Math.floor(Math.random() * 100000).toString(), - }, + resolveInput: { + create (args) { + return { + ...args.resolvedData[args.fieldKey], + create: { + value: Math.floor(Math.random() * 100000).toString(), + }, + } + }, + update (args) { + return { + ...args.resolvedData[args.fieldKey], + create: { + value: Math.floor(Math.random() * 100000).toString(), + }, + } } }, },