Skip to content

Commit 1b55b41

Browse files
authored
Adds {field}.hooks.validate.[create|update|delete] hooks (#9057)
1 parent 30413da commit 1b55b41

27 files changed

+875
-2993
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
----
2+
'@keystone-6/core': minor
3+
----
4+
5+
Adds `{field}.hooks.validate.[create|update|delete]` hooks, deprecates `validateInput` and `validateDelete` (throws if incompatible)

.changeset/text-null-default.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
----
2+
'@keystone-6/core': patch
3+
----
4+
5+
Fixes the `text` field type to accept a `defaultValue` of `null`

packages/core/src/fields/non-null-graphql.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ export function getResolvedIsNullable (
1313
return true
1414
}
1515

16+
export function resolveHasValidation ({
17+
db,
18+
validation
19+
}: {
20+
db?: { isNullable?: boolean },
21+
validation?: unknown,
22+
}) {
23+
if (db?.isNullable === false) return true
24+
if (validation !== undefined) return true
25+
return false
26+
}
27+
1628
export function assertReadIsNonNullAllowed<ListTypeInfo extends BaseListTypeInfo> (
1729
meta: FieldData,
1830
config: CommonFieldConfig<ListTypeInfo>,

packages/core/src/fields/types/bigInt/index.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import { humanize } from '../../../lib/utils'
22
import {
33
type BaseListTypeInfo,
4-
fieldType,
5-
type FieldTypeFunc,
64
type CommonFieldConfig,
5+
type FieldTypeFunc,
6+
fieldType,
77
orderDirectionEnum,
88
} from '../../../types'
99
import { graphql } from '../../..'
10-
import { assertReadIsNonNullAllowed, getResolvedIsNullable } from '../../non-null-graphql'
10+
import {
11+
assertReadIsNonNullAllowed,
12+
getResolvedIsNullable,
13+
resolveHasValidation,
14+
} from '../../non-null-graphql'
1115
import { filters } from '../../filters'
1216

1317
export type BigIntFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
@@ -30,14 +34,16 @@ export type BigIntFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
3034
const MAX_INT = 9223372036854775807n
3135
const MIN_INT = -9223372036854775808n
3236

33-
export const bigInt =
34-
<ListTypeInfo extends BaseListTypeInfo>({
37+
export function bigInt <ListTypeInfo extends BaseListTypeInfo>(
38+
config: BigIntFieldConfig<ListTypeInfo> = {}
39+
): FieldTypeFunc<ListTypeInfo> {
40+
const {
3541
isIndexed,
3642
defaultValue: _defaultValue,
3743
validation: _validation,
38-
...config
39-
}: BigIntFieldConfig<ListTypeInfo> = {}): FieldTypeFunc<ListTypeInfo> =>
40-
meta => {
44+
} = config
45+
46+
return (meta) => {
4147
const defaultValue = _defaultValue ?? null
4248
const hasAutoIncDefault =
4349
typeof defaultValue == 'object' &&
@@ -48,9 +54,7 @@ export const bigInt =
4854

4955
if (hasAutoIncDefault) {
5056
if (meta.provider === 'sqlite' || meta.provider === 'mysql') {
51-
throw new Error(
52-
`The bigInt field at ${meta.listKey}.${meta.fieldKey} specifies defaultValue: { kind: 'autoincrement' }, this is not supported on ${meta.provider}`
53-
)
57+
throw new Error(`The bigInt field at ${meta.listKey}.${meta.fieldKey} specifies defaultValue: { kind: 'autoincrement' }, this is not supported on ${meta.provider}`)
5458
}
5559
if (isNullable !== false) {
5660
throw new Error(
@@ -69,21 +73,18 @@ export const bigInt =
6973

7074
for (const type of ['min', 'max'] as const) {
7175
if (validation[type] > MAX_INT || validation[type] < MIN_INT) {
72-
throw new Error(
73-
`The bigInt field at ${meta.listKey}.${meta.fieldKey} specifies validation.${type}: ${validation[type]} which is outside of the range of a 64bit signed integer(${MIN_INT}n - ${MAX_INT}n) which is not allowed`
74-
)
76+
throw new Error(`The bigInt field at ${meta.listKey}.${meta.fieldKey} specifies validation.${type}: ${validation[type]} which is outside of the range of a 64bit signed integer(${MIN_INT}n - ${MAX_INT}n) which is not allowed`)
7577
}
7678
}
7779
if (validation.min > validation.max) {
78-
throw new Error(
79-
`The bigInt field at ${meta.listKey}.${meta.fieldKey} specifies a validation.max that is less than the validation.min, and therefore has no valid options`
80-
)
80+
throw new Error(`The bigInt field at ${meta.listKey}.${meta.fieldKey} specifies a validation.max that is less than the validation.min, and therefore has no valid options`)
8181
}
8282

8383
assertReadIsNonNullAllowed(meta, config, isNullable)
8484

8585
const mode = isNullable === false ? 'required' : 'optional'
8686
const fieldLabel = config.label ?? humanize(meta.fieldKey)
87+
const hasValidation = resolveHasValidation(config)
8788

8889
return fieldType({
8990
kind: 'scalar',
@@ -103,7 +104,7 @@ export const bigInt =
103104
...config,
104105
hooks: {
105106
...config.hooks,
106-
async validateInput (args) {
107+
validateInput: hasValidation ? async (args) => {
107108
const value = args.resolvedData[meta.fieldKey]
108109

109110
if (
@@ -128,7 +129,7 @@ export const bigInt =
128129
}
129130

130131
await config.hooks?.validateInput?.(args)
131-
},
132+
} : config.hooks?.validateInput
132133
},
133134
input: {
134135
uniqueWhere:
@@ -169,3 +170,4 @@ export const bigInt =
169170
},
170171
})
171172
}
173+
}

packages/core/src/fields/types/checkbox/index.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { userInputError } from '../../../lib/core/graphql-errors'
22
import {
33
type BaseListTypeInfo,
44
type CommonFieldConfig,
5-
fieldType,
65
type FieldTypeFunc,
6+
fieldType,
77
orderDirectionEnum,
88
} from '../../../types'
99
import { graphql } from '../../..'
@@ -19,13 +19,16 @@ export type CheckboxFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
1919
}
2020
}
2121

22-
export function checkbox <ListTypeInfo extends BaseListTypeInfo>({
23-
defaultValue = false,
24-
...config
25-
}: CheckboxFieldConfig<ListTypeInfo> = {}): FieldTypeFunc<ListTypeInfo> {
26-
return meta => {
22+
export function checkbox <ListTypeInfo extends BaseListTypeInfo>(
23+
config: CheckboxFieldConfig<ListTypeInfo> = {}
24+
): FieldTypeFunc<ListTypeInfo> {
25+
const {
26+
defaultValue = false,
27+
} = config
28+
29+
return (meta) => {
2730
if ((config as any).isIndexed === 'unique') {
28-
throw Error("isIndexed: 'unique' is not a supported option for field type checkbox")
31+
throw TypeError("isIndexed: 'unique' is not a supported option for field type checkbox")
2932
}
3033

3134
assertReadIsNonNullAllowed(meta, config, false)

packages/core/src/fields/types/multiselect/index.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,16 @@ export type MultiselectFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
4242
const MAX_INT = 2147483647
4343
const MIN_INT = -2147483648
4444

45-
export const multiselect =
46-
<ListTypeInfo extends BaseListTypeInfo>({
45+
export function multiselect <ListTypeInfo extends BaseListTypeInfo>(
46+
config: MultiselectFieldConfig<ListTypeInfo>
47+
): FieldTypeFunc<ListTypeInfo> {
48+
const {
4749
defaultValue = [],
48-
...config
49-
}: MultiselectFieldConfig<ListTypeInfo>): FieldTypeFunc<ListTypeInfo> =>
50-
meta => {
50+
} = config
51+
52+
return (meta) => {
5153
if ((config as any).isIndexed === 'unique') {
52-
throw Error("isIndexed: 'unique' is not a supported option for field type multiselect")
54+
throw TypeError("isIndexed: 'unique' is not a supported option for field type multiselect")
5355
}
5456
const fieldLabel = config.label ?? humanize(meta.fieldKey)
5557
assertReadIsNonNullAllowed(meta, config, false)
@@ -92,8 +94,7 @@ export const multiselect =
9294
hooks: {
9395
...config.hooks,
9496
async validateInput (args) {
95-
const selectedValues: readonly (string | number)[] | undefined =
96-
args.inputData[meta.fieldKey]
97+
const selectedValues: readonly (string | number)[] | undefined = args.inputData[meta.fieldKey]
9798
if (selectedValues !== undefined) {
9899
for (const value of selectedValues) {
99100
if (!possibleValues.has(value)) {
@@ -137,6 +138,7 @@ export const multiselect =
137138
}
138139
)
139140
}
141+
}
140142

141143
function configToOptionsAndGraphQLType (
142144
config: MultiselectFieldConfig<BaseListTypeInfo>,

packages/core/src/fields/types/text/index.ts

Lines changed: 38 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import { humanize } from '../../../lib/utils'
22
import {
33
type BaseListTypeInfo,
44
type CommonFieldConfig,
5+
type FieldTypeFunc,
56
fieldType,
67
orderDirectionEnum,
7-
type FieldTypeFunc,
88
} from '../../../types'
99
import { graphql } from '../../..'
10-
import { assertReadIsNonNullAllowed } from '../../non-null-graphql'
10+
import {
11+
assertReadIsNonNullAllowed,
12+
resolveHasValidation,
13+
} from '../../non-null-graphql'
1114
import { filters } from '../../filters'
1215

1316
export type TextFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
@@ -24,7 +27,7 @@ export type TextFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
2427
match?: { regex: RegExp, explanation?: string }
2528
length?: { min?: number, max?: number }
2629
}
27-
defaultValue?: string
30+
defaultValue?: string | null
2831
db?: {
2932
isNullable?: boolean
3033
map?: string
@@ -53,62 +56,56 @@ export type TextFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
5356
}
5457
}
5558

56-
export const text =
57-
<ListTypeInfo extends BaseListTypeInfo>({
59+
export function text <ListTypeInfo extends BaseListTypeInfo>(
60+
config: TextFieldConfig<ListTypeInfo> = {}
61+
): FieldTypeFunc<ListTypeInfo> {
62+
const {
5863
isIndexed,
59-
defaultValue: _defaultValue,
60-
validation: _validation,
61-
...config
62-
}: TextFieldConfig<ListTypeInfo> = {}): FieldTypeFunc<ListTypeInfo> =>
63-
meta => {
64+
defaultValue: defaultValue_,
65+
validation: validation_
66+
} = config
67+
68+
return (meta) => {
6469
for (const type of ['min', 'max'] as const) {
65-
const val = _validation?.length?.[type]
70+
const val = validation_?.length?.[type]
6671
if (val !== undefined && (!Number.isInteger(val) || val < 0)) {
67-
throw new Error(
68-
`The text field at ${meta.listKey}.${meta.fieldKey} specifies validation.length.${type}: ${val} but it must be a positive integer`
69-
)
72+
throw new Error(`The text field at ${meta.listKey}.${meta.fieldKey} specifies validation.length.${type}: ${val} but it must be a positive integer`)
7073
}
71-
if (_validation?.isRequired && val !== undefined && val === 0) {
72-
throw new Error(
73-
`The text field at ${meta.listKey}.${meta.fieldKey} specifies validation.isRequired: true and validation.length.${type}: 0, this is not allowed because validation.isRequired implies at least a min length of 1`
74-
)
74+
if (validation_?.isRequired && val !== undefined && val === 0) {
75+
throw new Error(`The text field at ${meta.listKey}.${meta.fieldKey} specifies validation.isRequired: true and validation.length.${type}: 0, this is not allowed because validation.isRequired implies at least a min length of 1`)
7576
}
7677
}
7778

7879
if (
79-
_validation?.length?.min !== undefined &&
80-
_validation?.length?.max !== undefined &&
81-
_validation?.length?.min > _validation?.length?.max
80+
validation_?.length?.min !== undefined &&
81+
validation_?.length?.max !== undefined &&
82+
validation_?.length?.min > validation_?.length?.max
8283
) {
83-
throw new Error(
84-
`The text field at ${meta.listKey}.${meta.fieldKey} specifies a validation.length.max that is less than the validation.length.min, and therefore has no valid options`
85-
)
84+
throw new Error(`The text field at ${meta.listKey}.${meta.fieldKey} specifies a validation.length.max that is less than the validation.length.min, and therefore has no valid options`)
8685
}
8786

88-
const validation = {
89-
..._validation,
87+
const validation = validation_ ? {
88+
...validation_,
9089
length: {
91-
min: _validation?.isRequired ? _validation?.length?.min ?? 1 : _validation?.length?.min,
92-
max: _validation?.length?.max,
90+
min: validation_?.isRequired ? validation_?.length?.min ?? 1 : validation_?.length?.min,
91+
max: validation_?.length?.max,
9392
},
94-
}
93+
} : undefined
9594

9695
// defaulted to false as a zero length string is preferred to null
9796
const isNullable = config.db?.isNullable ?? false
98-
99-
const fieldLabel = config.label ?? humanize(meta.fieldKey)
100-
10197
assertReadIsNonNullAllowed(meta, config, isNullable)
10298

99+
const defaultValue = isNullable ? (defaultValue_ ?? null) : (defaultValue_ ?? '')
100+
const fieldLabel = config.label ?? humanize(meta.fieldKey)
103101
const mode = isNullable ? 'optional' : 'required'
102+
const hasValidation = resolveHasValidation(config) || !isNullable // we make an exception for Text
104103

105-
const defaultValue =
106-
isNullable === false || _defaultValue !== undefined ? _defaultValue || '' : undefined
107104
return fieldType({
108105
kind: 'scalar',
109106
mode,
110107
scalar: 'String',
111-
default: defaultValue === undefined ? undefined : { kind: 'literal', value: defaultValue },
108+
default: (defaultValue === null) ? undefined : { kind: 'literal', value: defaultValue },
112109
index: isIndexed === true ? 'index' : isIndexed || undefined,
113110
map: config.db?.map,
114111
nativeType: config.db?.nativeType,
@@ -117,7 +114,7 @@ export const text =
117114
...config,
118115
hooks: {
119116
...config.hooks,
120-
async validateInput (args) {
117+
validateInput: hasValidation ? async (args) => {
121118
const val = args.resolvedData[meta.fieldKey]
122119
if (val === null && (validation?.isRequired || isNullable === false)) {
123120
args.addValidationError(`${fieldLabel} is required`)
@@ -127,25 +124,19 @@ export const text =
127124
if (validation.length.min === 1) {
128125
args.addValidationError(`${fieldLabel} must not be empty`)
129126
} else {
130-
args.addValidationError(
131-
`${fieldLabel} must be at least ${validation.length.min} characters long`
132-
)
127+
args.addValidationError(`${fieldLabel} must be at least ${validation.length.min} characters long`)
133128
}
134129
}
135130
if (validation?.length?.max !== undefined && val.length > validation.length.max) {
136-
args.addValidationError(
137-
`${fieldLabel} must be no longer than ${validation.length.max} characters`
138-
)
131+
args.addValidationError(`${fieldLabel} must be no longer than ${validation.length.max} characters`)
139132
}
140133
if (validation?.match && !validation.match.regex.test(val)) {
141-
args.addValidationError(
142-
validation.match.explanation || `${fieldLabel} must match ${validation.match.regex}`
143-
)
134+
args.addValidationError(validation.match.explanation || `${fieldLabel} must match ${validation.match.regex}`)
144135
}
145136
}
146137

147138
await config.hooks?.validateInput?.(args)
148-
},
139+
} : config.hooks?.validateInput
149140
},
150141
input: {
151142
uniqueWhere:
@@ -199,6 +190,7 @@ export const text =
199190
},
200191
})
201192
}
193+
}
202194

203195
export type TextFieldMeta = {
204196
displayMode: 'input' | 'textarea'

0 commit comments

Comments
 (0)