Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove deprecated hooks #9204

Merged
merged 11 commits into from
Feb 12, 2025
Merged
5 changes: 5 additions & 0 deletions .changeset/chilled-moons-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@keystone-6/core": major
---

Removed deprecated `validateInput` and `validateDelete` hooks and add object hook syntax to fields.
37 changes: 24 additions & 13 deletions examples/custom-field/2-stars-field/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ export function stars <ListTypeInfo extends BaseListTypeInfo> ({
maxStars = 5,
...config
}: StarsFieldConfig<ListTypeInfo> = {}): FieldTypeFunc<ListTypeInfo> {
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 (typeof v === 'number' && v >= 0 && v <= 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
Expand All @@ -34,15 +43,21 @@ export function stars <ListTypeInfo extends BaseListTypeInfo> ({
...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.
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: {
...config.hooks?.validate,
async create (args) {
const err = validate(args.resolvedData[meta.fieldKey])
if (err) args.addValidationError(err)
await validateCreate?.(args)
},
async update (args) {
const err = validate(args.resolvedData[meta.fieldKey])
if (err) args.addValidationError(err)
await validateUpdate?.(args)
}
await config.hooks?.validateInput?.(args)
},
}
},
// all of these inputs are optional if they don't make sense for a particular field type
input: {
Expand All @@ -53,16 +68,12 @@ export function stars <ListTypeInfo extends BaseListTypeInfo> ({
// 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
},
Expand Down
8 changes: 4 additions & 4 deletions examples/custom-field/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ export const lists = {
return resolvedData[fieldKey]
},

validateInput: async ({
validate: async ({
resolvedData,
inputData,
item,
addValidationError,
fieldKey,
}) => {
console.log('Post.content.hooks.validateInput', {
console.log('Post.content.hooks.validate', {
resolvedData,
inputData,
item,
Expand Down Expand Up @@ -100,8 +100,8 @@ export const lists = {
},
},

validateInput: async ({ resolvedData, operation, inputData, item, addValidationError }) => {
console.log('Post.hooks.validateInput', { 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')
Expand Down
15 changes: 6 additions & 9 deletions examples/field-groups/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
},
}
},
}),
},
Expand Down
75 changes: 33 additions & 42 deletions examples/hooks/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand All @@ -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()
}
}
}),
},
}),
Expand All @@ -131,29 +115,37 @@ export const lists = {
return resolvedData
},
},
validateInput: ({ context, operation, inputData, addValidationError }) => {
const { title, content } = inputData

if (operation === 'update' && 'feedback' in inputData) {
const { feedback } = inputData
if (/profanity/i.test(feedback ?? '')) return addValidationError('Unacceptable feedback')
}
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

// 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
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 (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)
},

afterOperation: {
create: ({ inputData, item }) => {
console.log(`Post afterOperation.create`, inputData, '->', item)
Expand All @@ -162,7 +154,6 @@ export const lists = {
update: ({ originalItem, item }) => {
console.log(`Post afterOperation.update`, originalItem, '->', item)
},

delete: ({ originalItem }) => {
console.log(`Post afterOperation.delete`, originalItem, '-> deleted')
},
Expand Down
40 changes: 25 additions & 15 deletions examples/reuse/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,23 @@ function trackingByHooks<
// FieldKey extends 'createdBy' | 'updatedBy' // TODO: refined types for the return types
> (immutable: boolean = false): FieldHooks<ListTypeInfo> {
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
},
}
}
}

Expand All @@ -76,18 +81,23 @@ function trackingAtHooks<
> (immutable: boolean = false): FieldHooks<ListTypeInfo> {
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
},
}
}
}

Expand Down
16 changes: 9 additions & 7 deletions examples/usecase-roles/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,17 @@ export const lists: Lists<Session> = {
},
},
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
},
},
}
}),
},
}),
Expand Down
11 changes: 6 additions & 5 deletions examples/usecase-versioning/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
},
},
}),
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/fields/non-null-graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -33,6 +35,9 @@ export function makeValidateHook <ListTypeInfo extends BaseListTypeInfo> (
isRequired?: boolean
[key: string]: unknown
}
hooks?: {
validate?: FieldHooks<ListTypeInfo>['validate']
}
},
f?: ValidateFieldHook<ListTypeInfo, 'create' | 'update' | 'delete', ListTypeInfo['fields']>
) {
Expand Down Expand Up @@ -61,13 +66,13 @@ export function makeValidateHook <ListTypeInfo extends BaseListTypeInfo> (

return {
mode,
validate,
validate: merge(validate, config.hooks?.validate)
}
}

return {
mode,
validate: f
validate: merge(f, config.hooks?.validate)
}
}

Expand Down
Loading