Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/deprecate-validate-hooks-f.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
----
'@keystone-6/core': minor
----

Adds `{field}.hooks.validate.[create|update|delete]` hooks, deprecates `validateInput` and `validateDelete` (throws if incompatible)
5 changes: 5 additions & 0 deletions .changeset/text-null-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
----
'@keystone-6/core': patch
----

Fixes the `text` field type to accept a `defaultValue` of `null`
12 changes: 12 additions & 0 deletions packages/core/src/fields/non-null-graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ export function getResolvedIsNullable (
return true
}

export function resolveHasValidation ({
db,
validation
}: {
db?: { isNullable?: boolean },
validation?: unknown,
}) {
if (db?.isNullable === false) return true
if (validation !== undefined) return true
return false
}

export function assertReadIsNonNullAllowed<ListTypeInfo extends BaseListTypeInfo> (
meta: FieldData,
config: CommonFieldConfig<ListTypeInfo>,
Expand Down
40 changes: 21 additions & 19 deletions packages/core/src/fields/types/bigInt/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { humanize } from '../../../lib/utils'
import {
type BaseListTypeInfo,
fieldType,
type FieldTypeFunc,
type CommonFieldConfig,
type FieldTypeFunc,
fieldType,
orderDirectionEnum,
} from '../../../types'
import { graphql } from '../../..'
import { assertReadIsNonNullAllowed, getResolvedIsNullable } from '../../non-null-graphql'
import {
assertReadIsNonNullAllowed,
getResolvedIsNullable,
resolveHasValidation,
} from '../../non-null-graphql'
import { filters } from '../../filters'

export type BigIntFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
Expand All @@ -30,14 +34,16 @@ export type BigIntFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
const MAX_INT = 9223372036854775807n
const MIN_INT = -9223372036854775808n

export const bigInt =
<ListTypeInfo extends BaseListTypeInfo>({
export function bigInt <ListTypeInfo extends BaseListTypeInfo>(
config: BigIntFieldConfig<ListTypeInfo> = {}
): FieldTypeFunc<ListTypeInfo> {
const {
isIndexed,
defaultValue: _defaultValue,
validation: _validation,
...config
}: BigIntFieldConfig<ListTypeInfo> = {}): FieldTypeFunc<ListTypeInfo> =>
meta => {
} = config

return (meta) => {
const defaultValue = _defaultValue ?? null
const hasAutoIncDefault =
typeof defaultValue == 'object' &&
Expand All @@ -48,9 +54,7 @@ export const bigInt =

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

for (const type of ['min', 'max'] as const) {
if (validation[type] > MAX_INT || validation[type] < MIN_INT) {
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`
)
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`)
}
}
if (validation.min > validation.max) {
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`
)
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`)
}

assertReadIsNonNullAllowed(meta, config, isNullable)

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

return fieldType({
kind: 'scalar',
Expand All @@ -103,7 +104,7 @@ export const bigInt =
...config,
hooks: {
...config.hooks,
async validateInput (args) {
validateInput: hasValidation ? async (args) => {
const value = args.resolvedData[meta.fieldKey]

if (
Expand All @@ -128,7 +129,7 @@ export const bigInt =
}

await config.hooks?.validateInput?.(args)
},
} : config.hooks?.validateInput
},
input: {
uniqueWhere:
Expand Down Expand Up @@ -169,3 +170,4 @@ export const bigInt =
},
})
}
}
17 changes: 10 additions & 7 deletions packages/core/src/fields/types/checkbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { userInputError } from '../../../lib/core/graphql-errors'
import {
type BaseListTypeInfo,
type CommonFieldConfig,
fieldType,
type FieldTypeFunc,
fieldType,
orderDirectionEnum,
} from '../../../types'
import { graphql } from '../../..'
Expand All @@ -19,13 +19,16 @@ export type CheckboxFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
}
}

export function checkbox <ListTypeInfo extends BaseListTypeInfo>({
defaultValue = false,
...config
}: CheckboxFieldConfig<ListTypeInfo> = {}): FieldTypeFunc<ListTypeInfo> {
return meta => {
export function checkbox <ListTypeInfo extends BaseListTypeInfo>(
config: CheckboxFieldConfig<ListTypeInfo> = {}
): FieldTypeFunc<ListTypeInfo> {
const {
defaultValue = false,
} = config

return (meta) => {
if ((config as any).isIndexed === 'unique') {
throw Error("isIndexed: 'unique' is not a supported option for field type checkbox")
throw TypeError("isIndexed: 'unique' is not a supported option for field type checkbox")
Copy link
Member Author

@dcousens dcousens Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a type error, and I think we should remove this in the future

}

assertReadIsNonNullAllowed(meta, config, false)
Expand Down
18 changes: 10 additions & 8 deletions packages/core/src/fields/types/multiselect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,16 @@ export type MultiselectFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
const MAX_INT = 2147483647
const MIN_INT = -2147483648

export const multiselect =
<ListTypeInfo extends BaseListTypeInfo>({
export function multiselect <ListTypeInfo extends BaseListTypeInfo>(
config: MultiselectFieldConfig<ListTypeInfo>
): FieldTypeFunc<ListTypeInfo> {
const {
defaultValue = [],
...config
}: MultiselectFieldConfig<ListTypeInfo>): FieldTypeFunc<ListTypeInfo> =>
meta => {
} = config

return (meta) => {
if ((config as any).isIndexed === 'unique') {
throw Error("isIndexed: 'unique' is not a supported option for field type multiselect")
throw TypeError("isIndexed: 'unique' is not a supported option for field type multiselect")
}
const fieldLabel = config.label ?? humanize(meta.fieldKey)
assertReadIsNonNullAllowed(meta, config, false)
Expand Down Expand Up @@ -92,8 +94,7 @@ export const multiselect =
hooks: {
...config.hooks,
async validateInput (args) {
const selectedValues: readonly (string | number)[] | undefined =
args.inputData[meta.fieldKey]
const selectedValues: readonly (string | number)[] | undefined = args.inputData[meta.fieldKey]
if (selectedValues !== undefined) {
for (const value of selectedValues) {
if (!possibleValues.has(value)) {
Expand Down Expand Up @@ -137,6 +138,7 @@ export const multiselect =
}
)
}
}

function configToOptionsAndGraphQLType (
config: MultiselectFieldConfig<BaseListTypeInfo>,
Expand Down
84 changes: 38 additions & 46 deletions packages/core/src/fields/types/text/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { humanize } from '../../../lib/utils'
import {
type BaseListTypeInfo,
type CommonFieldConfig,
type FieldTypeFunc,
fieldType,
orderDirectionEnum,
type FieldTypeFunc,
} from '../../../types'
import { graphql } from '../../..'
import { assertReadIsNonNullAllowed } from '../../non-null-graphql'
import {
assertReadIsNonNullAllowed,
resolveHasValidation,
} from '../../non-null-graphql'
import { filters } from '../../filters'

export type TextFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
Expand All @@ -24,7 +27,7 @@ export type TextFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
match?: { regex: RegExp, explanation?: string }
length?: { min?: number, max?: number }
}
defaultValue?: string
defaultValue?: string | null
Copy link
Member Author

@dcousens dcousens Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike other fields, the ambiguity of ?: representing '' or null is why this needs to exist.
We should allow null as a defaultValue typically, and maybe check it at build time too.

db?: {
isNullable?: boolean
map?: string
Expand Down Expand Up @@ -53,62 +56,56 @@ export type TextFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
}
}

export const text =
<ListTypeInfo extends BaseListTypeInfo>({
export function text <ListTypeInfo extends BaseListTypeInfo>(
config: TextFieldConfig<ListTypeInfo> = {}
): FieldTypeFunc<ListTypeInfo> {
const {
isIndexed,
defaultValue: _defaultValue,
validation: _validation,
...config
}: TextFieldConfig<ListTypeInfo> = {}): FieldTypeFunc<ListTypeInfo> =>
meta => {
defaultValue: defaultValue_,
validation: validation_
} = config

return (meta) => {
for (const type of ['min', 'max'] as const) {
const val = _validation?.length?.[type]
const val = validation_?.length?.[type]
if (val !== undefined && (!Number.isInteger(val) || val < 0)) {
throw new Error(
`The text field at ${meta.listKey}.${meta.fieldKey} specifies validation.length.${type}: ${val} but it must be a positive integer`
)
throw new Error(`The text field at ${meta.listKey}.${meta.fieldKey} specifies validation.length.${type}: ${val} but it must be a positive integer`)
}
if (_validation?.isRequired && val !== undefined && val === 0) {
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`
)
if (validation_?.isRequired && val !== undefined && val === 0) {
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`)
}
}

if (
_validation?.length?.min !== undefined &&
_validation?.length?.max !== undefined &&
_validation?.length?.min > _validation?.length?.max
validation_?.length?.min !== undefined &&
validation_?.length?.max !== undefined &&
validation_?.length?.min > validation_?.length?.max
) {
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`
)
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`)
}

const validation = {
..._validation,
const validation = validation_ ? {
...validation_,
length: {
min: _validation?.isRequired ? _validation?.length?.min ?? 1 : _validation?.length?.min,
max: _validation?.length?.max,
min: validation_?.isRequired ? validation_?.length?.min ?? 1 : validation_?.length?.min,
max: validation_?.length?.max,
},
}
} : undefined

// defaulted to false as a zero length string is preferred to null
const isNullable = config.db?.isNullable ?? false

const fieldLabel = config.label ?? humanize(meta.fieldKey)

assertReadIsNonNullAllowed(meta, config, isNullable)

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

const defaultValue =
isNullable === false || _defaultValue !== undefined ? _defaultValue || '' : undefined
return fieldType({
kind: 'scalar',
mode,
scalar: 'String',
default: defaultValue === undefined ? undefined : { kind: 'literal', value: defaultValue },
default: (defaultValue === null) ? undefined : { kind: 'literal', value: defaultValue },
index: isIndexed === true ? 'index' : isIndexed || undefined,
map: config.db?.map,
nativeType: config.db?.nativeType,
Expand All @@ -117,7 +114,7 @@ export const text =
...config,
hooks: {
...config.hooks,
async validateInput (args) {
validateInput: hasValidation ? async (args) => {
const val = args.resolvedData[meta.fieldKey]
if (val === null && (validation?.isRequired || isNullable === false)) {
args.addValidationError(`${fieldLabel} is required`)
Expand All @@ -127,25 +124,19 @@ export const text =
if (validation.length.min === 1) {
args.addValidationError(`${fieldLabel} must not be empty`)
} else {
args.addValidationError(
`${fieldLabel} must be at least ${validation.length.min} characters long`
)
args.addValidationError(`${fieldLabel} must be at least ${validation.length.min} characters long`)
}
}
if (validation?.length?.max !== undefined && val.length > validation.length.max) {
args.addValidationError(
`${fieldLabel} must be no longer than ${validation.length.max} characters`
)
args.addValidationError(`${fieldLabel} must be no longer than ${validation.length.max} characters`)
}
if (validation?.match && !validation.match.regex.test(val)) {
args.addValidationError(
validation.match.explanation || `${fieldLabel} must match ${validation.match.regex}`
)
args.addValidationError(validation.match.explanation || `${fieldLabel} must match ${validation.match.regex}`)
}
}

await config.hooks?.validateInput?.(args)
},
} : config.hooks?.validateInput
},
input: {
uniqueWhere:
Expand Down Expand Up @@ -199,6 +190,7 @@ export const text =
},
})
}
}

export type TextFieldMeta = {
displayMode: 'input' | 'textarea'
Expand Down
Loading