From 78981bdb77c4de57360d74bc1a50212f58523282 Mon Sep 17 00:00:00 2001 From: Gautam Singh <5769869+gautamsi@users.noreply.github.com> Date: Tue, 11 Feb 2025 07:58:20 -0600 Subject: [PATCH 01/11] remove types for deprecated hooks --- .changeset/chilled-moons-walk.md | 6 + examples/custom-field/2-stars-field/index.ts | 21 ++- examples/custom-field/schema.ts | 82 ++++++++--- examples/custom-output-paths/schema.ts | 59 +++++--- .../custom-session-invalidation/README.md | 18 ++- examples/default-values/schema.ts | 98 +++++++++---- .../extend-graphql-subscriptions/schema.ts | 35 +++-- examples/field-groups/schema.ts | 15 +- examples/hooks/schema.ts | 87 ++++++------ examples/reuse/schema.ts | 40 ++++-- examples/usecase-roles/schema.ts | 16 ++- examples/usecase-versioning/schema.ts | 11 +- packages/core/src/fields/resolve-hooks.ts | 54 +------- .../core/src/fields/types/bigInt/index.ts | 11 +- .../src/fields/types/calendarDay/index.ts | 11 +- .../core/src/fields/types/decimal/index.ts | 11 +- packages/core/src/fields/types/file/index.ts | 45 +++--- packages/core/src/fields/types/float/index.ts | 11 +- packages/core/src/fields/types/image/index.ts | 55 ++++---- .../core/src/fields/types/integer/index.ts | 11 +- .../src/fields/types/multiselect/index.ts | 11 +- .../core/src/fields/types/password/index.ts | 11 +- .../core/src/fields/types/select/index.ts | 11 +- packages/core/src/fields/types/text/index.ts | 11 +- .../core/src/fields/types/timestamp/index.ts | 11 +- .../core/src/lib/core/initialise-lists.ts | 131 +++++------------- packages/core/src/schema.ts | 19 --- packages/core/src/types/config/hooks.ts | 103 +++++--------- tests/api-tests/hooks.test.ts | 21 ++- .../nested-mutations/create-many.test.ts | 11 +- 30 files changed, 552 insertions(+), 485 deletions(-) create mode 100644 .changeset/chilled-moons-walk.md 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 3f2bcb6bbef..8e2531af911 100644 --- a/examples/custom-field/2-stars-field/index.ts +++ b/examples/custom-field/2-stars-field/index.ts @@ -36,13 +36,22 @@ export function 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 1d4642e242c..60290917fee 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-roles/schema.ts b/examples/usecase-roles/schema.ts index e6dffff922f..1622a07e6bd 100644 --- a/examples/usecase-roles/schema.ts +++ b/examples/usecase-roles/schema.ts @@ -69,15 +69,17 @@ export const lists: 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 8d243078fa4..546b4fedadc 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 & { @@ -122,7 +122,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: g.arg({ type: g.BigInt }) } : undefined, where: { diff --git a/packages/core/src/fields/types/calendarDay/index.ts b/packages/core/src/fields/types/calendarDay/index.ts index b9705c505fa..1d768611d50 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 { g } 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 669b484d2ee..1890585913a 100644 --- a/packages/core/src/fields/types/decimal/index.ts +++ b/packages/core/src/fields/types/decimal/index.ts @@ -9,7 +9,7 @@ import { import { g } from '../../..' import { filters } from '../../filters' import { makeValidateHook } from '../../non-null-graphql' -import { mergeFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type DecimalFieldConfig = CommonFieldConfig & { @@ -124,7 +124,14 @@ export function decimal (config: Decimal 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: g.arg({ type: g.Decimal }) } : undefined, where: { diff --git a/packages/core/src/fields/types/file/index.ts b/packages/core/src/fields/types/file/index.ts index a8471cabfd7..b8ac9c50810 100644 --- a/packages/core/src/fields/types/file/index.ts +++ b/packages/core/src/fields/types/file/index.ts @@ -7,10 +7,7 @@ import { fieldType, } from '../../../types' import { g } from '../../..' -import { - type InternalFieldHooks, - mergeFieldHooks, -} from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type FileFieldConfig = CommonFieldConfig & { @@ -66,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) } } } @@ -96,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 29ceb0048a8..ae19c541057 100644 --- a/packages/core/src/fields/types/float/index.ts +++ b/packages/core/src/fields/types/float/index.ts @@ -8,7 +8,7 @@ import { import { g } 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 & { @@ -90,7 +90,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: g.arg({ type: g.Float }) } : undefined, where: { diff --git a/packages/core/src/fields/types/image/index.ts b/packages/core/src/fields/types/image/index.ts index 2d1f087207d..aa6daf5b849 100644 --- a/packages/core/src/fields/types/image/index.ts +++ b/packages/core/src/fields/types/image/index.ts @@ -9,10 +9,7 @@ import type { import { fieldType, } from '../../../types' import { g } from '../../..' import { SUPPORTED_IMAGE_EXTENSIONS } from './utils' -import { - type InternalFieldHooks, - mergeFieldHooks, -} from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type ImageFieldConfig = CommonFieldConfig & { @@ -92,27 +89,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) { // TODO: types + 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) } } } @@ -129,7 +123,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 9d531cd6741..c270443add9 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 & { @@ -118,7 +118,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: g.arg({ type: g.Int }) } : undefined, where: { diff --git a/packages/core/src/fields/types/multiselect/index.ts b/packages/core/src/fields/types/multiselect/index.ts index 53025a03917..b7adc55986a 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 { g } from '../../..' import { makeValidateHook } from '../../non-null-graphql' -import { mergeFieldHooks } from '../../resolve-hooks' +import { merge } from '../../resolve-hooks' export type MultiselectFieldConfig = CommonFieldConfig & @@ -113,7 +113,14 @@ export function multiselect ( ...config, ui, __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 dfe2128cd27..b33cdb7d43b 100644 --- a/packages/core/src/fields/types/password/index.ts +++ b/packages/core/src/fields/types/password/index.ts @@ -11,8 +11,8 @@ import { import { g } from '../../..' import { type PasswordFieldMeta } from './views' import { makeValidateHook } from '../../non-null-graphql' -import { mergeFieldHooks } from '../../resolve-hooks' import { isObjectType, type GraphQLSchema } from 'graphql' +import { merge } from '../../resolve-hooks' export type PasswordFieldConfig = CommonFieldConfig & { @@ -136,7 +136,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/select/index.ts b/packages/core/src/fields/types/select/index.ts index eaf7f4e61c5..386b4173558 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 { g } 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 a02ef3d9636..116f01dcdb9 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 { g } 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: g.arg({ type: g.String }) } : undefined, where: { diff --git a/packages/core/src/fields/types/timestamp/index.ts b/packages/core/src/fields/types/timestamp/index.ts index 9aed981f77f..7258e4b5ddf 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 { g } 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: g.arg({ type: g.DateTime }) } : undefined, where: { diff --git a/packages/core/src/lib/core/initialise-lists.ts b/packages/core/src/lib/core/initialise-lists.ts index 4552790c287..f65aeec0f79 100644 --- a/packages/core/src/lib/core/initialise-lists.ts +++ b/packages/core/src/lib/core/initialise-lists.ts @@ -239,78 +239,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, + }, } } @@ -325,45 +274,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, }, } } @@ -695,7 +626,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/schema.ts b/packages/core/src/schema.ts index 6263985935d..c441c8d5569 100644 --- a/packages/core/src/schema.ts +++ b/packages/core/src/schema.ts @@ -52,25 +52,6 @@ function injectDefaults (config: KeystoneConfigPre, defaultIdField: IdFieldConfi } } - /** @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/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, }), From 8dfc885681cdc7fc434be552b67bb02f560b1ae9 Mon Sep 17 00:00:00 2001 From: Gautam Singh <5769869+gautamsi@users.noreply.github.com> Date: Tue, 11 Feb 2025 08:30:53 -0600 Subject: [PATCH 02/11] fix structure field code --- packages/fields-document/src/structure.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/fields-document/src/structure.ts b/packages/fields-document/src/structure.ts index 45cdec8e635..743b66dd6d5 100644 --- a/packages/fields-document/src/structure.ts +++ b/packages/fields-document/src/structure.ts @@ -48,9 +48,10 @@ export function structure ({ ...config, hooks: { ...config.hooks, - async resolveInput (args) { - let val = args.resolvedData[meta.fieldKey] - if (args.operation === 'update') { + resolveInput:{ + ...config.hooks?.resolveInput, + update: async args => { + let val = args.resolvedData[meta.fieldKey] let prevVal = args.item[meta.fieldKey] if (meta.provider === 'sqlite') { prevVal = JSON.parse(prevVal as any) @@ -60,14 +61,13 @@ export function structure ({ if (meta.provider === 'sqlite') { val = JSON.stringify(val) } - } - - return config.hooks?.resolveInput - ? config.hooks.resolveInput({ + return config.hooks?.resolveInput?.update + ? config.hooks.resolveInput.update({ ...args, resolvedData: { ...args.resolvedData, [meta.fieldKey]: val }, }) : val + }, }, }, input: { From a6285b53360f15dd6215178234f160e6bfaf9cd2 Mon Sep 17 00:00:00 2001 From: Emma Hamilton Date: Wed, 12 Feb 2025 11:10:12 +1000 Subject: [PATCH 03/11] Keep top level hook functions (type changes) --- packages/core/src/types/config/hooks.ts | 93 +++++++++++++++---------- 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/packages/core/src/types/config/hooks.ts b/packages/core/src/types/config/hooks.ts index dd108fab1eb..a8cb1c985d1 100644 --- a/packages/core/src/types/config/hooks.ts +++ b/packages/core/src/types/config/hooks.ts @@ -51,37 +51,45 @@ export type ListHooks = { /** * Used to **modify the input** for create and update operations after default values and access control have been applied */ - resolveInput?: { - create?: ResolveInputListHook - update?: ResolveInputListHook - } + resolveInput?: + | ResolveInputListHook + | { + create?: ResolveInputListHook + update?: ResolveInputListHook + } /** * Used to **validate** if a create, update or delete operation is OK */ - validate?: { - create?: ValidateHook - update?: ValidateHook - delete?: ValidateHook - } + validate?: + | ValidateHook + | { + 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?: { - create?: BeforeOperationListHook - update?: BeforeOperationListHook - delete?: BeforeOperationListHook - } + beforeOperation?: + | BeforeOperationListHook + | { + create?: BeforeOperationListHook + update?: BeforeOperationListHook + delete?: BeforeOperationListHook + } /** * Used to **cause side effects** after a create, update, or delete operation operation has occurred */ - afterOperation?: { - create?: AfterOperationListHook - update?: AfterOperationListHook - delete?: AfterOperationListHook - } + afterOperation?: + | AfterOperationListHook + | { + create?: AfterOperationListHook + update?: AfterOperationListHook + delete?: AfterOperationListHook + } } export type ResolvedListHooks = { @@ -113,37 +121,46 @@ export type FieldHooks< /** * Used to **modify the input** for create and update operations after default values and access control have been applied */ - resolveInput?: { - create?: ResolveInputFieldHook - update?: ResolveInputFieldHook - } + resolveInput?: + | ResolveInputFieldHook + | { + create?: ResolveInputFieldHook + update?: ResolveInputFieldHook + } + /** * Used to **validate** if a create, update or delete operation is OK */ - validate?: { - create?: ValidateFieldHook - update?: ValidateFieldHook - delete?: ValidateFieldHook - } + validate?: + | ValidateFieldHook + | { + 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?: { - create?: BeforeOperationFieldHook - update?: BeforeOperationFieldHook - delete?: BeforeOperationFieldHook - } + beforeOperation?: + | BeforeOperationFieldHook + | { + create?: BeforeOperationFieldHook + update?: BeforeOperationFieldHook + delete?: BeforeOperationFieldHook + } /** * Used to **cause side effects** after a create, update, or delete operation operation has occurred */ - afterOperation?: { - create?: AfterOperationFieldHook - update?: AfterOperationFieldHook - delete?: AfterOperationFieldHook - } + afterOperation?: + | AfterOperationFieldHook + | { + create?: AfterOperationFieldHook + update?: AfterOperationFieldHook + delete?: AfterOperationFieldHook + } } export type ResolvedFieldHooks< From 4270ef1ed7da4288a3d0541edc0ad1a9b73058a3 Mon Sep 17 00:00:00 2001 From: Emma Hamilton Date: Wed, 12 Feb 2025 11:21:32 +1000 Subject: [PATCH 04/11] Revert examples changes where it makes sense to use the top level hook functions --- examples/custom-field/schema.ts | 82 +++++----------- examples/custom-output-paths/schema.ts | 59 ++++------- .../custom-session-invalidation/README.md | 18 +--- examples/default-values/schema.ts | 98 ++++++------------- .../extend-graphql-subscriptions/schema.ts | 35 +++---- examples/hooks/schema.ts | 12 +-- 6 files changed, 90 insertions(+), 214 deletions(-) diff --git a/examples/custom-field/schema.ts b/examples/custom-field/schema.ts index 2bdaf10ce40..33eb0189b85 100644 --- a/examples/custom-field/schema.ts +++ b/examples/custom-field/schema.ts @@ -20,58 +20,31 @@ export const lists = { }, hooks: { - 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 ({ + resolveInput: async ({ resolvedData, operation, inputData, item, fieldKey }) => { + console.log('Post.content.hooks.resolveInput', { resolvedData, + operation, inputData, item, - addValidationError, fieldKey, - }) => { - console.log('Post.content.hooks.validateInput.create', { - resolvedData, - inputData, - item, - fieldKey, - }) - }, - update: async ({ + }) + return resolvedData[fieldKey] + }, + + validate: async ({ + resolvedData, + inputData, + item, + addValidationError, + fieldKey, + }) => { + console.log('Post.content.hooks.validate', { resolvedData, inputData, item, - addValidationError, fieldKey, - }) => { - console.log('Post.content.hooks.validateInput.update', { - resolvedData, - inputData, - item, - fieldKey, - }) - }, - } + }) + }, }, }), rating: stars({ @@ -127,22 +100,13 @@ export const lists = { }, }, - validate: { - create: async ({ resolvedData, operation, inputData, item, addValidationError }) => { - console.log('Post.hooks.validateInput.create', { resolvedData, operation, inputData, item }) + validate: async ({ resolvedData, operation, inputData, item, addValidationError }) => { + console.log('Post.hooks.validate', { resolvedData, operation, inputData, item }) - 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') - } - }, - } + 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 b3d5b8835a4..e81eb845ad0 100644 --- a/examples/custom-output-paths/schema.ts +++ b/examples/custom-output-paths/schema.ts @@ -13,46 +13,25 @@ export const lists = { }, hooks: { - 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) - }, - } + 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) + }, }, }), } satisfies Lists diff --git a/examples/custom-session-invalidation/README.md b/examples/custom-session-invalidation/README.md index e07d2839d8c..3c2a0101f0b 100644 --- a/examples/custom-session-invalidation/README.md +++ b/examples/custom-session-invalidation/README.md @@ -37,19 +37,11 @@ We add one new field, `passwordChangedAt`, to the `Person` list. Setting the `pa passwordChangedAt: timestamp({ access: () => false, hooks: { - resolveInput: { - create: ({ resolvedData }) => { - if (resolvedData.password) { - return new Date(); - } - return; - }, - update: ({ resolvedData }) => { - if (resolvedData.password) { - return new Date(); - } - return; - }, + resolveInput: ({ resolvedData }) => { + if (resolvedData.password) { + return new Date(); + } + return; }, }, ui: { diff --git a/examples/default-values/schema.ts b/examples/default-values/schema.ts index 9d6fd54dc14..839dee0e55b 100644 --- a/examples/default-values/schema.ts +++ b/examples/default-values/schema.ts @@ -17,30 +17,17 @@ export const lists = { { label: 'High', value: 'high' }, ], hooks: { - 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' - } + 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' } - 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 - }, - } + } + return resolvedData.priority + }, }, }), @@ -52,58 +39,33 @@ export const lists = { many: false, hooks: { // dynamic default: if unassigned, find an anonymous user and assign the task to them - 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 - }, - 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 } } - } + 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 } } } - - return resolvedData.assignedTo - }, - } + } + + return resolvedData.assignedTo + }, }, }), // dynamic default: we set the due date to be 7 days in the future finishBy: timestamp({ hooks: { - 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 - }, - } + resolveInput ({ 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 db417b85ee2..ce54e31ff53 100644 --- a/examples/extend-graphql-subscriptions/schema.ts +++ b/examples/extend-graphql-subscriptions/schema.ts @@ -12,30 +12,17 @@ export const lists = { access: allowAll, hooks: { // this hook publishes posts to the 'POST_UPDATED' channel when a post mutated - 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, - }) - }, - } + 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, + }) + }, }, fields: { title: text({ validation: { isRequired: true } }), diff --git a/examples/hooks/schema.ts b/examples/hooks/schema.ts index a03f5668488..4967fcca59e 100644 --- a/examples/hooks/schema.ts +++ b/examples/hooks/schema.ts @@ -143,16 +143,8 @@ export const lists = { if (preventDelete) return addValidationError('Cannot delete Post, preventDelete is true') }, }, - 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) - }, + beforeOperation: ({ item, resolvedData, operation }) => { + console.log(`Post beforeOperation.${operation}`, resolvedData) }, afterOperation: { create: ({ inputData, item }) => { From 60fddb4ff263833c0504f4e2ee45803ec2ddf3f4 Mon Sep 17 00:00:00 2001 From: Emma Hamilton Date: Wed, 12 Feb 2025 12:19:48 +1000 Subject: [PATCH 05/11] Implement behaviour --- examples/custom-field/2-stars-field/index.ts | 9 ++- packages/core/src/fields/non-null-graphql.ts | 9 ++- packages/core/src/fields/resolve-hooks.ts | 64 +++++++++++++++++-- .../core/src/fields/types/bigInt/index.ts | 7 +- .../src/fields/types/calendarDay/index.ts | 7 +- .../core/src/fields/types/decimal/index.ts | 7 +- packages/core/src/fields/types/file/index.ts | 9 ++- packages/core/src/fields/types/float/index.ts | 7 +- packages/core/src/fields/types/image/index.ts | 9 ++- .../core/src/fields/types/integer/index.ts | 7 +- .../src/fields/types/multiselect/index.ts | 7 +- .../core/src/fields/types/password/index.ts | 7 +- .../core/src/fields/types/select/index.ts | 7 +- packages/core/src/fields/types/text/index.ts | 7 +- .../core/src/fields/types/timestamp/index.ts | 7 +- .../core/src/lib/core/initialise-lists.ts | 46 ++++--------- packages/fields-document/src/structure.ts | 9 +-- 17 files changed, 104 insertions(+), 121 deletions(-) diff --git a/examples/custom-field/2-stars-field/index.ts b/examples/custom-field/2-stars-field/index.ts index 8e2531af911..ad367e7cad2 100644 --- a/examples/custom-field/2-stars-field/index.ts +++ b/examples/custom-field/2-stars-field/index.ts @@ -22,6 +22,8 @@ export function stars ({ maxStars = 5, ...config }: StarsFieldConfig = {}): FieldTypeFunc { + const validateCreate = typeof config.hooks?.validate === 'function' ? config.hooks.validate : config.hooks?.validate?.create + const validateUpdate = typeof config.hooks?.validate === 'function' ? config.hooks.validate : config.hooks?.validate?.update return meta => fieldType({ // this configures what data is stored in the database @@ -34,22 +36,23 @@ export function stars ({ ...config, hooks: { ...config.hooks, - // We use the `validateInput` hook to ensure that the user doesn't set an out of range value. + // We use the `validate` hooks 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. validate: { + ...config.hooks?.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) + await validateCreate?.(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 validateUpdate?.(args) } } }, diff --git a/packages/core/src/fields/non-null-graphql.ts b/packages/core/src/fields/non-null-graphql.ts index 8e89d6aa3eb..bde5beb1b1c 100644 --- a/packages/core/src/fields/non-null-graphql.ts +++ b/packages/core/src/fields/non-null-graphql.ts @@ -2,9 +2,11 @@ import { type BaseListTypeInfo, type FieldData, } from '../types' +import type { FieldHooks } from '../types/config/hooks' import { type ValidateFieldHook } from '../types/config/hooks' +import { merge } from './resolve-hooks' export function resolveDbNullable ( validation: undefined | { isRequired?: boolean }, @@ -33,6 +35,9 @@ export function makeValidateHook ( isRequired?: boolean [key: string]: unknown } + hooks?: { + validate?: FieldHooks['validate'] + } }, f?: ValidateFieldHook ) { @@ -61,13 +66,13 @@ export function makeValidateHook ( return { mode, - validate, + validate: merge(validate, config.hooks?.validate) } } return { mode, - validate: f + validate: merge(f, config.hooks?.validate) } } diff --git a/packages/core/src/fields/resolve-hooks.ts b/packages/core/src/fields/resolve-hooks.ts index 44e47573210..013ec0a17a3 100644 --- a/packages/core/src/fields/resolve-hooks.ts +++ b/packages/core/src/fields/resolve-hooks.ts @@ -1,13 +1,63 @@ import { type MaybePromise } from '../types' -export function merge < - R, - A extends (r: R) => MaybePromise, - B extends (r: R) => MaybePromise -> (a?: A, b?: B) { - if (!a && !b) return undefined - return async (args: R) => { +function mergeVoidFn < + Args, + A extends ((args: Args) => MaybePromise) | undefined, + B extends ((args: Args) => MaybePromise) | undefined +> (a: A, b: B) { + if (!a) return b + if (!b) return a + return async (args: Args) => { await a?.(args) await b?.(args) } } + +type ExpandedHooks = { + create?: (args: CreateArgs) => MaybePromise + update?: (args: UpdateArgs) => MaybePromise + delete?: (args: DeleteArgs) => MaybePromise +} + +type Hooks = + | ((args: CreateArgs | UpdateArgs | DeleteArgs) => MaybePromise) + | ExpandedHooks + +export function merge ( + a: Hooks | undefined, + b: Hooks | undefined, +): Hooks | undefined { + if (!a) return b + if (!b) return a + if (typeof a === 'function' && typeof b === 'function') { + return mergeVoidFn(a, b) + } + const expandedA = expandHooks(a) + const expandedB = expandHooks(b) + return { + create: mergeVoidFn(expandedA.create, expandedB.create), + update: mergeVoidFn(expandedA.update, expandedB.update), + delete: mergeVoidFn(expandedA.delete, expandedB.delete), + } +} + +function expandHooks ( + fn: Hooks, +): ExpandedHooks { + return typeof fn === 'function' + ? { create: fn, update: fn, delete: fn } + : (fn) +} + +const emptyFn = () => {} + +export function expandVoidHooks ( + hooks: Hooks | undefined, +): Required> { + const expanded = hooks ? expandHooks(hooks) : {} + return { + create: expanded.create ?? emptyFn, + update: expanded.update ?? emptyFn, + delete: expanded.delete ?? emptyFn + } +} \ No newline at end of file diff --git a/packages/core/src/fields/types/bigInt/index.ts b/packages/core/src/fields/types/bigInt/index.ts index 546b4fedadc..c5f2007df37 100644 --- a/packages/core/src/fields/types/bigInt/index.ts +++ b/packages/core/src/fields/types/bigInt/index.ts @@ -11,7 +11,6 @@ import { resolveDbNullable, makeValidateHook } from '../../non-null-graphql' -import { merge } from '../../resolve-hooks' export type BigIntFieldConfig = CommonFieldConfig & { @@ -124,11 +123,7 @@ export function bigInt (config: BigIntFi ...config, hooks: { ...config.hooks, - validate: { - ...config.hooks?.validate, - create: merge(validate, config.hooks?.validate?.create), - update: merge(validate, config.hooks?.validate?.update), - } + validate }, input: { uniqueWhere: isIndexed === 'unique' ? { arg: g.arg({ type: g.BigInt }) } : undefined, diff --git a/packages/core/src/fields/types/calendarDay/index.ts b/packages/core/src/fields/types/calendarDay/index.ts index 1d768611d50..bb3f59789b0 100644 --- a/packages/core/src/fields/types/calendarDay/index.ts +++ b/packages/core/src/fields/types/calendarDay/index.ts @@ -9,7 +9,6 @@ import { type CalendarDayFieldMeta } from './views' import { g } from '../../..' import { filters } from '../../filters' import { makeValidateHook } from '../../non-null-graphql' -import { merge } from '../../resolve-hooks' export type CalendarDayFieldConfig = CommonFieldConfig & { @@ -76,11 +75,7 @@ export function calendarDay (config: Cal ...config, hooks: { ...config.hooks, - validate: { - ...config.hooks?.validate, - create: merge(validate, config.hooks?.validate?.create), - update: merge(validate, config.hooks?.validate?.update), - }, + validate }, input: { uniqueWhere: diff --git a/packages/core/src/fields/types/decimal/index.ts b/packages/core/src/fields/types/decimal/index.ts index 1890585913a..a8afb4d7fe9 100644 --- a/packages/core/src/fields/types/decimal/index.ts +++ b/packages/core/src/fields/types/decimal/index.ts @@ -9,7 +9,6 @@ import { import { g } from '../../..' import { filters } from '../../filters' import { makeValidateHook } from '../../non-null-graphql' -import { merge } from '../../resolve-hooks' export type DecimalFieldConfig = CommonFieldConfig & { @@ -126,11 +125,7 @@ export function decimal (config: Decimal ...config, hooks: { ...config.hooks, - validate: { - ...config.hooks?.validate, - create: merge(validate, config.hooks?.validate?.create), - update: merge(validate, config.hooks?.validate?.update), - }, + validate }, input: { uniqueWhere: isIndexed === 'unique' ? { arg: g.arg({ type: g.Decimal }) } : undefined, diff --git a/packages/core/src/fields/types/file/index.ts b/packages/core/src/fields/types/file/index.ts index b8ac9c50810..738bb2e9025 100644 --- a/packages/core/src/fields/types/file/index.ts +++ b/packages/core/src/fields/types/file/index.ts @@ -94,11 +94,10 @@ export function file (config: FileFieldC ? config.hooks : { ...config.hooks, - beforeOperation: { - ...config.hooks?.beforeOperation, - update: merge(config.hooks?.beforeOperation?.update, beforeOperationResolver), - delete: merge(config.hooks?.beforeOperation?.delete, beforeOperationResolver), - }, + beforeOperation: merge(config.hooks?.beforeOperation, { + update: beforeOperationResolver, + delete: beforeOperationResolver, + }) }, input: { create: { diff --git a/packages/core/src/fields/types/float/index.ts b/packages/core/src/fields/types/float/index.ts index ae19c541057..27e8084d4af 100644 --- a/packages/core/src/fields/types/float/index.ts +++ b/packages/core/src/fields/types/float/index.ts @@ -8,7 +8,6 @@ import { import { g } from '../../..' import { filters } from '../../filters' import { makeValidateHook } from '../../non-null-graphql' -import { merge } from '../../resolve-hooks' export type FloatFieldConfig = CommonFieldConfig & { @@ -92,11 +91,7 @@ export function float (config: FloatFiel ...config, hooks: { ...config.hooks, - validate: { - ...config.hooks?.validate, - create: merge(validate, config.hooks?.validate?.create), - update: merge(validate, config.hooks?.validate?.update), - }, + validate }, input: { uniqueWhere: isIndexed === 'unique' ? { arg: g.arg({ type: g.Float }) } : undefined, diff --git a/packages/core/src/fields/types/image/index.ts b/packages/core/src/fields/types/image/index.ts index aa6daf5b849..d3d5ce305c3 100644 --- a/packages/core/src/fields/types/image/index.ts +++ b/packages/core/src/fields/types/image/index.ts @@ -127,11 +127,10 @@ export function image (config: ImageFiel ? config.hooks : { ...config.hooks, - beforeOperation: { - ...config.hooks?.beforeOperation, - update: merge(config.hooks?.beforeOperation?.update, beforeOperationResolver), - delete: merge(config.hooks?.beforeOperation?.delete, beforeOperationResolver), - }, + beforeOperation: merge(config.hooks?.beforeOperation, { + update: beforeOperationResolver, + delete: beforeOperationResolver, + }), }, input: { create: { diff --git a/packages/core/src/fields/types/integer/index.ts b/packages/core/src/fields/types/integer/index.ts index c270443add9..becb2f96d9c 100644 --- a/packages/core/src/fields/types/integer/index.ts +++ b/packages/core/src/fields/types/integer/index.ts @@ -11,7 +11,6 @@ import { resolveDbNullable, makeValidateHook } from '../../non-null-graphql' -import { merge } from '../../resolve-hooks' export type IntegerFieldConfig = CommonFieldConfig & { @@ -120,11 +119,7 @@ export function integer (config: Integer ...config, hooks: { ...config.hooks, - validate: { - ...config.hooks?.validate, - create: merge(validate, config.hooks?.validate?.create), - update: merge(validate, config.hooks?.validate?.update), - }, + validate }, input: { uniqueWhere: isIndexed === 'unique' ? { arg: g.arg({ type: g.Int }) } : undefined, diff --git a/packages/core/src/fields/types/multiselect/index.ts b/packages/core/src/fields/types/multiselect/index.ts index b7adc55986a..2a5f43617c4 100644 --- a/packages/core/src/fields/types/multiselect/index.ts +++ b/packages/core/src/fields/types/multiselect/index.ts @@ -9,7 +9,6 @@ import { } from '../../../types' import { g } from '../../..' import { makeValidateHook } from '../../non-null-graphql' -import { merge } from '../../resolve-hooks' export type MultiselectFieldConfig = CommonFieldConfig & @@ -115,11 +114,7 @@ export function multiselect ( __ksTelemetryFieldTypeName: '@keystone-6/multiselect', hooks: { ...config.hooks, - validate: { - ...config.hooks?.validate, - create: merge(validate, config.hooks?.validate?.create), - update: merge(validate, config.hooks?.validate?.update), - }, + validate }, views: '@keystone-6/core/fields/types/multiselect/views', getAdminMeta: () => ({ diff --git a/packages/core/src/fields/types/password/index.ts b/packages/core/src/fields/types/password/index.ts index b33cdb7d43b..8b8f77fd2f5 100644 --- a/packages/core/src/fields/types/password/index.ts +++ b/packages/core/src/fields/types/password/index.ts @@ -12,7 +12,6 @@ import { g } from '../../..' import { type PasswordFieldMeta } from './views' import { makeValidateHook } from '../../non-null-graphql' import { isObjectType, type GraphQLSchema } from 'graphql' -import { merge } from '../../resolve-hooks' export type PasswordFieldConfig = CommonFieldConfig & { @@ -138,11 +137,7 @@ export function password (config: Passwo ...config, hooks: { ...config.hooks, - validate: { - ...config.hooks?.validate, - create: merge(validate, config.hooks?.validate?.create), - update: merge(validate, config.hooks?.validate?.update), - }, + validate }, input: { where: diff --git a/packages/core/src/fields/types/select/index.ts b/packages/core/src/fields/types/select/index.ts index 386b4173558..9b8a3e9b5ce 100644 --- a/packages/core/src/fields/types/select/index.ts +++ b/packages/core/src/fields/types/select/index.ts @@ -10,7 +10,6 @@ import { import { g } from '../../..' import { filters } from '../../filters' import { makeValidateHook } from '../../non-null-graphql' -import { merge } from '../../resolve-hooks' export type SelectFieldConfig = CommonFieldConfig & @@ -97,11 +96,7 @@ export function select (config: SelectFi ui, hooks: { ...config.hooks, - validate: { - ...config.hooks?.validate, - create: merge(validate, config.hooks?.validate?.create), - update: merge(validate, config.hooks?.validate?.update), - }, + validate }, __ksTelemetryFieldTypeName: '@keystone-6/select', views: '@keystone-6/core/fields/types/select/views', diff --git a/packages/core/src/fields/types/text/index.ts b/packages/core/src/fields/types/text/index.ts index 116f01dcdb9..90655d99575 100644 --- a/packages/core/src/fields/types/text/index.ts +++ b/packages/core/src/fields/types/text/index.ts @@ -8,7 +8,6 @@ import { import { g } from '../../..' import { makeValidateHook } from '../../non-null-graphql' import { filters } from '../../filters' -import { merge } from '../../resolve-hooks' export type TextFieldConfig = CommonFieldConfig & { @@ -144,11 +143,7 @@ export function text ( ...config, hooks: { ...config.hooks, - validate: { - ...config.hooks?.validate, - create: merge(validate, config.hooks?.validate?.create), - update: merge(validate, config.hooks?.validate?.update), - }, + validate }, input: { uniqueWhere: isIndexed === 'unique' ? { arg: g.arg({ type: g.String }) } : undefined, diff --git a/packages/core/src/fields/types/timestamp/index.ts b/packages/core/src/fields/types/timestamp/index.ts index 7258e4b5ddf..f548b7ed69f 100644 --- a/packages/core/src/fields/types/timestamp/index.ts +++ b/packages/core/src/fields/types/timestamp/index.ts @@ -8,7 +8,6 @@ import { import { g } from '../../..' import { filters } from '../../filters' import { makeValidateHook } from '../../non-null-graphql' -import { merge } from '../../resolve-hooks' import { type TimestampFieldMeta } from './views' export type TimestampFieldConfig = @@ -75,11 +74,7 @@ export function timestamp ( ...config, hooks: { ...config.hooks, - validate: { - ...config.hooks?.validate, - create: merge(validate, config.hooks?.validate?.create), - update: merge(validate, config.hooks?.validate?.update), - }, + validate }, input: { uniqueWhere: isIndexed === 'unique' ? { arg: g.arg({ type: g.DateTime }) } : undefined, diff --git a/packages/core/src/lib/core/initialise-lists.ts b/packages/core/src/lib/core/initialise-lists.ts index f65aeec0f79..e1e69fbb4c5 100644 --- a/packages/core/src/lib/core/initialise-lists.ts +++ b/packages/core/src/lib/core/initialise-lists.ts @@ -44,6 +44,7 @@ import { } from './resolve-relationships' import { outputTypeField } from './queries/output-field' import { assertFieldsValid } from './field-assertions' +import { expandVoidHooks } from '../../fields/resolve-hooks' export type InitialisedField = { fieldKey: string @@ -234,7 +235,6 @@ function getIsEnabledField (f: FieldConfigType, listKey: string, list: Partially } } -function defaultOperationHook () {} function defaultListHooksResolveInput ({ resolvedData }: { resolvedData: any }) { return resolvedData } @@ -242,24 +242,12 @@ function defaultListHooksResolveInput ({ resolvedData }: { resolvedData: any }) function parseListHooks (hooks: ListHooks): ResolvedListHooks { return { 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, + create: typeof hooks.resolveInput === 'function' ? hooks.resolveInput : hooks.resolveInput?.create ?? defaultListHooksResolveInput, + update: typeof hooks.resolveInput === 'function' ? hooks.resolveInput : hooks.resolveInput?.update ?? defaultListHooksResolveInput, }, + validate: expandVoidHooks(hooks.validate), + beforeOperation: expandVoidHooks(hooks.beforeOperation), + afterOperation: expandVoidHooks(hooks.afterOperation), } } @@ -278,24 +266,12 @@ function parseFieldHooks ( ): ResolvedFieldHooks { return { resolveInput: { - create: hooks.resolveInput?.create ?? defaultFieldHooksResolveInput, - update: hooks.resolveInput?.update ?? defaultFieldHooksResolveInput, - }, - 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, + create: typeof hooks.resolveInput === 'function' ? hooks.resolveInput : hooks.resolveInput?.create ?? defaultFieldHooksResolveInput, + update: typeof hooks.resolveInput === 'function' ? hooks.resolveInput : hooks.resolveInput?.update ?? defaultFieldHooksResolveInput, }, + validate: expandVoidHooks(hooks.validate), + beforeOperation: expandVoidHooks(hooks.beforeOperation), + afterOperation: expandVoidHooks(hooks.afterOperation), } } diff --git a/packages/fields-document/src/structure.ts b/packages/fields-document/src/structure.ts index 743b66dd6d5..f314b680171 100644 --- a/packages/fields-document/src/structure.ts +++ b/packages/fields-document/src/structure.ts @@ -42,14 +42,15 @@ export function structure ({ const unreferencedConcreteInterfaceImplementations: g.ObjectType[] = [] const name = meta.listKey + meta.fieldKey[0].toUpperCase() + meta.fieldKey.slice(1) + const innerUpdate = typeof config.hooks?.resolveInput === 'function' ? config.hooks.resolveInput : config.hooks?.resolveInput?.update return jsonFieldTypePolyfilledForSQLite( meta.provider, { ...config, hooks: { ...config.hooks, - resolveInput:{ - ...config.hooks?.resolveInput, + resolveInput: { + create: typeof config.hooks?.resolveInput === 'function' ? config.hooks.resolveInput : config.hooks?.resolveInput?.create, update: async args => { let val = args.resolvedData[meta.fieldKey] let prevVal = args.item[meta.fieldKey] @@ -61,8 +62,8 @@ export function structure ({ if (meta.provider === 'sqlite') { val = JSON.stringify(val) } - return config.hooks?.resolveInput?.update - ? config.hooks.resolveInput.update({ + return innerUpdate + ? innerUpdate({ ...args, resolvedData: { ...args.resolvedData, [meta.fieldKey]: val }, }) From 14dcf3935d0e4b9e891560b516e14d7039a328b9 Mon Sep 17 00:00:00 2001 From: Emma Hamilton Date: Wed, 12 Feb 2025 12:22:51 +1000 Subject: [PATCH 06/11] Newline --- packages/core/src/fields/resolve-hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/fields/resolve-hooks.ts b/packages/core/src/fields/resolve-hooks.ts index 013ec0a17a3..c8a122e988a 100644 --- a/packages/core/src/fields/resolve-hooks.ts +++ b/packages/core/src/fields/resolve-hooks.ts @@ -60,4 +60,4 @@ export function expandVoidHooks ( update: expanded.update ?? emptyFn, delete: expanded.delete ?? emptyFn } -} \ No newline at end of file +} From 07df142a7ca5dac39e60c29155dba3e295bd5996 Mon Sep 17 00:00:00 2001 From: Emma Hamilton Date: Wed, 12 Feb 2025 12:23:12 +1000 Subject: [PATCH 07/11] Revert changes to tests --- tests/api-tests/hooks.test.ts | 21 +++++-------------- .../nested-mutations/create-many.test.ts | 11 +++------- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/tests/api-tests/hooks.test.ts b/tests/api-tests/hooks.test.ts index f1a067d9158..6355758ac2c 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,25 +85,14 @@ function makeList ({ basis: text(hooks.field ? { db: { isNullable: true }, // drops the implicit validation hook hooks: { - resolveInput: hooks.resolveInput ? { - create: replaceF, - update: replaceF, - } : undefined, + resolveInput: hooks.resolveInput ? replaceF : undefined, validate: { create: makeValidate('FVI'), update: makeValidate('FVI'), delete: makeValidate('FVI'), }, - 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 + beforeOperation: hooks.beforeOperation ? makeThrower(`${__name}_FBO`) : undefined, + afterOperation: hooks.afterOperation ? 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 328b55c796b..56c8f31a29a 100644 --- a/tests/api-tests/relationships/nested-mutations/create-many.test.ts +++ b/tests/api-tests/relationships/nested-mutations/create-many.test.ts @@ -69,14 +69,9 @@ const runner2 = setupTestRunner({ content: text(), }, hooks: { - afterOperation: { - create () { - afterOperationWasCalled = true - }, - update () { - afterOperationWasCalled = true - }, - } + afterOperation () { + afterOperationWasCalled = true + }, }, access: allowAll, }), From 3eaa5b0f726a0f785cd2e2c4881394bfd67139e3 Mon Sep 17 00:00:00 2001 From: Emma Hamilton Date: Wed, 12 Feb 2025 12:27:26 +1000 Subject: [PATCH 08/11] Changeset --- .changeset/chilled-moons-walk.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.changeset/chilled-moons-walk.md b/.changeset/chilled-moons-walk.md index 6a5464f1977..b97e6377a37 100644 --- a/.changeset/chilled-moons-walk.md +++ b/.changeset/chilled-moons-walk.md @@ -2,5 +2,4 @@ "@keystone-6/core": major --- -Removed deprecated list and field hooks. -Fixed field hooks of all the in built types +Removed deprecated `validateInput` and `validateDelete` hooks and add object hook syntax to fields. From bc551f73a6d2c78b292f02a20f86ca313ba77377 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Wed, 12 Feb 2025 15:06:16 +1100 Subject: [PATCH 09/11] extract validate --- examples/custom-field/2-stars-field/index.ts | 27 ++++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/examples/custom-field/2-stars-field/index.ts b/examples/custom-field/2-stars-field/index.ts index ad367e7cad2..715cb2f4449 100644 --- a/examples/custom-field/2-stars-field/index.ts +++ b/examples/custom-field/2-stars-field/index.ts @@ -24,6 +24,13 @@ export function stars ({ }: StarsFieldConfig = {}): FieldTypeFunc { const validateCreate = typeof config.hooks?.validate === 'function' ? config.hooks.validate : config.hooks?.validate?.create const validateUpdate = typeof config.hooks?.validate === 'function' ? config.hooks.validate : config.hooks?.validate?.update + + function validate (v: unknown) { + if (v === null) return + if (v >= 0 && <= maxStars) return + return `The value must be within the range of 0-${maxStars}` + } + return meta => fieldType({ // this configures what data is stored in the database @@ -41,17 +48,13 @@ export function stars ({ validate: { ...config.hooks?.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}`) - } + const err = validate(args.resolvedData[meta.fieldKey]) + if (err) args.addValidationError(err) await validateCreate?.(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}`) - } + const err = validate(args.resolvedData[meta.fieldKey]) + if (err) args.addValidationError(err) await validateUpdate?.(args) } } @@ -65,16 +68,12 @@ export function stars ({ // this function can be omitted, it is here purely to show how you could change it resolve (val, context) { // if it's null, then the value will be set to null in the database - if (val === null) { - return null - } + if (val === null) return null // if it's undefined(which means that it was omitted in the request) // returning undefined will mean "don't change the existing value" // note that this means that this function is called on every update to an item // including when the field is not updated - if (val === undefined) { - return undefined - } + if (val === undefined) return undefined // if it's not null or undefined, it must be a number return val }, From 9ca1675573960fbbec7c9e9c6cbdfea55e028781 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Wed, 12 Feb 2025 15:11:53 +1100 Subject: [PATCH 10/11] fixup extract --- examples/custom-field/2-stars-field/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/custom-field/2-stars-field/index.ts b/examples/custom-field/2-stars-field/index.ts index 715cb2f4449..57fe7d8d830 100644 --- a/examples/custom-field/2-stars-field/index.ts +++ b/examples/custom-field/2-stars-field/index.ts @@ -27,7 +27,7 @@ export function stars ({ function validate (v: unknown) { if (v === null) return - if (v >= 0 && <= maxStars) return + if (v >= 0 && v <= maxStars) return return `The value must be within the range of 0-${maxStars}` } From c3c565d37a7a5dbbbac3fcc7b8a9c7c1eb54e493 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Wed, 12 Feb 2025 15:48:28 +1100 Subject: [PATCH 11/11] add missing typeof --- examples/custom-field/2-stars-field/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/custom-field/2-stars-field/index.ts b/examples/custom-field/2-stars-field/index.ts index 57fe7d8d830..ea6f09a4bb8 100644 --- a/examples/custom-field/2-stars-field/index.ts +++ b/examples/custom-field/2-stars-field/index.ts @@ -27,7 +27,7 @@ export function stars ({ function validate (v: unknown) { if (v === null) return - if (v >= 0 && v <= maxStars) return + if (typeof v === 'number' && v >= 0 && v <= maxStars) return return `The value must be within the range of 0-${maxStars}` }