diff --git a/packages/core/src/fields/non-null-graphql.ts b/packages/core/src/fields/non-null-graphql.ts index b099bf82736..f2438b7311b 100644 --- a/packages/core/src/fields/non-null-graphql.ts +++ b/packages/core/src/fields/non-null-graphql.ts @@ -17,15 +17,6 @@ export function resolveDbNullable ( return true } -function shouldAddValidation ( - db?: { isNullable?: boolean }, - validation?: unknown -) { - if (db?.isNullable === false) return true - if (validation !== undefined) return true - return false -} - export function makeValidateHook ( meta: FieldData, config: { @@ -40,22 +31,28 @@ export function makeValidateHook ( }, validation?: { isRequired?: boolean + [key: string]: unknown }, }, f?: ValidateFieldHook ) { const dbNullable = resolveDbNullable(config.validation, config.db) const mode = dbNullable ? ('optional' as const) : ('required' as const) + const valueRequired = config.validation?.isRequired || !dbNullable assertReadIsNonNullAllowed(meta, config, dbNullable) - const addValidation = shouldAddValidation(config.db, config.validation) + const addValidation = config.db?.isNullable === false || config.validation?.isRequired if (addValidation) { const validate = async function (args) { const { operation, addValidationError, resolvedData } = args - if (operation !== 'delete') { - const value = resolvedData[meta.fieldKey] - if ((config.validation?.isRequired || dbNullable === false) && value === null) { - addValidationError(`Missing value`) + + if (valueRequired) { + const value = resolvedData?.[meta.fieldKey] + if ( + (operation === 'create' && value === undefined) + || ((operation === 'create' || operation === 'update') && (value === null)) + ) { + addValidationError(`missing value`) } } @@ -70,7 +67,7 @@ export function makeValidateHook ( return { mode, - validate: undefined + validate: f } } diff --git a/packages/core/src/fields/types/bigInt/index.ts b/packages/core/src/fields/types/bigInt/index.ts index 3f117ebec36..8f20f5a755a 100644 --- a/packages/core/src/fields/types/bigInt/index.ts +++ b/packages/core/src/fields/types/bigInt/index.ts @@ -39,9 +39,15 @@ export function bigInt ( const { isIndexed, defaultValue: _defaultValue, - validation: validation_, + validation, } = config + const v = { + isRequired: validation?.isRequired ?? false, + min: validation?.min ?? MIN_INT, + max: validation?.max ?? MAX_INT, + } + return (meta) => { const defaultValue = _defaultValue ?? null const hasAutoIncDefault = @@ -49,12 +55,11 @@ export function bigInt ( defaultValue !== null && defaultValue.kind === 'autoincrement' - const isNullable = resolveDbNullable(validation_, config.db) - if (hasAutoIncDefault) { if (meta.provider === 'sqlite' || meta.provider === 'mysql') { throw new Error(`${meta.listKey}.${meta.fieldKey} specifies defaultValue: { kind: 'autoincrement' }, this is not supported on ${meta.provider}`) } + const isNullable = resolveDbNullable(v, config.db) if (isNullable !== false) { throw new Error( `${meta.listKey}.${meta.fieldKey} specifies defaultValue: { kind: 'autoincrement' } but doesn't specify db.isNullable: false.\n` + @@ -64,44 +69,39 @@ export function bigInt ( } } - const validation = { - isRequired: validation_?.isRequired ?? false, - min: validation_?.min ?? MIN_INT, - max: validation_?.max ?? MAX_INT, - } - for (const type of ['min', 'max'] as const) { - if (validation[type] > MAX_INT || validation[type] < MIN_INT) { - throw new Error(`${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 (v[type] > MAX_INT || v[type] < MIN_INT) { + throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.${type}: ${v[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) { + if (v.min > v.max) { throw new Error(`${meta.listKey}.${meta.fieldKey} specifies a validation.max that is less than the validation.min, and therefore has no valid options`) } + const hasAdditionalValidation = v.min !== undefined || v.max !== undefined const { mode, validate, - } = makeValidateHook(meta, config, ({ resolvedData, operation, addValidationError }) => { + } = makeValidateHook(meta, config, hasAdditionalValidation ? ({ resolvedData, operation, addValidationError }) => { if (operation === 'delete') return const value = resolvedData[meta.fieldKey] if (typeof value === 'number') { - if (validation?.min !== undefined && value < validation.min) { - addValidationError(`value must be greater than or equal to ${validation.min}`) + if (v?.min !== undefined && value < v.min) { + addValidationError(`value must be greater than or equal to ${v.min}`) } - if (validation?.max !== undefined && value > validation.max) { - addValidationError(`value must be less than or equal to ${validation.max}`) + if (v?.max !== undefined && value > v.max) { + addValidationError(`value must be less than or equal to ${v.max}`) } } - }) + } : undefined) return fieldType({ kind: 'scalar', mode, scalar: 'BigInt', - // This will resolve to 'index' if the boolean is true, otherwise other values - false will be converted to undefined + // this will resolve to 'index' if the boolean is true, otherwise other values - false will be converted to undefined index: isIndexed === true ? 'index' : isIndexed || undefined, default: typeof defaultValue === 'bigint' @@ -143,9 +143,9 @@ export function bigInt ( getAdminMeta () { return { validation: { - min: validation.min.toString(), - max: validation.max.toString(), - isRequired: validation.isRequired, + min: v.min.toString(), + max: v.max.toString(), + isRequired: v.isRequired, }, defaultValue: typeof defaultValue === 'bigint' ? defaultValue.toString() : defaultValue, } diff --git a/packages/core/src/fields/types/calendarDay/index.ts b/packages/core/src/fields/types/calendarDay/index.ts index ac6184b89f8..89e37f182c9 100644 --- a/packages/core/src/fields/types/calendarDay/index.ts +++ b/packages/core/src/fields/types/calendarDay/index.ts @@ -25,14 +25,13 @@ export type CalendarDayFieldConfig = } } -export const calendarDay = - ({ +export function calendarDay (config: CalendarDayFieldConfig = {}): FieldTypeFunc { + const { isIndexed, validation, defaultValue, - ...config - }: CalendarDayFieldConfig = {}): FieldTypeFunc => - meta => { + } = config + return (meta) => { if (typeof defaultValue === 'string') { try { graphql.CalendarDay.graphQLType.parseValue(defaultValue) @@ -126,6 +125,7 @@ export const calendarDay = }, }) } +} function dateStringToDateObjectInUTC (value: string) { return new Date(`${value}T00:00Z`) diff --git a/packages/core/src/fields/types/float/index.ts b/packages/core/src/fields/types/float/index.ts index 1f944e9f00f..7e3dd86b106 100644 --- a/packages/core/src/fields/types/float/index.ts +++ b/packages/core/src/fields/types/float/index.ts @@ -27,14 +27,13 @@ export type FloatFieldConfig = } } -export const float = - ({ - isIndexed, - validation, +export function float (config: FloatFieldConfig = {}): FieldTypeFunc { + const { defaultValue, - ...config - }: FloatFieldConfig = {}): FieldTypeFunc => - meta => { + isIndexed, + validation: v = {}, + } = config + return (meta) => { if ( defaultValue !== undefined && (typeof defaultValue !== 'number' || !Number.isFinite(defaultValue)) @@ -43,45 +42,46 @@ export const float = } if ( - validation?.min !== undefined && - (typeof validation.min !== 'number' || !Number.isFinite(validation.min)) + v.min !== undefined && + (typeof v.min !== 'number' || !Number.isFinite(v.min)) ) { - throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.min: ${validation.min} but it must be a valid finite number`) + throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.min: ${v.min} but it must be a valid finite number`) } if ( - validation?.max !== undefined && - (typeof validation.max !== 'number' || !Number.isFinite(validation.max)) + v.max !== undefined && + (typeof v.max !== 'number' || !Number.isFinite(v.max)) ) { - throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.max: ${validation.max} but it must be a valid finite number`) + throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.max: ${v.max} but it must be a valid finite number`) } if ( - validation?.min !== undefined && - validation?.max !== undefined && - validation.min > validation.max + v.min !== undefined && + v.max !== undefined && + v.min > v.max ) { throw new Error(`${meta.listKey}.${meta.fieldKey} specifies a validation.max that is less than the validation.min, and therefore has no valid options`) } + const hasAdditionalValidation = v.min !== undefined || v.max !== undefined const { mode, validate, - } = makeValidateHook(meta, config, ({ resolvedData, operation, addValidationError }) => { + } = makeValidateHook(meta, config, hasAdditionalValidation ? ({ resolvedData, operation, addValidationError }) => { if (operation === 'delete') return const value = resolvedData[meta.fieldKey] if (typeof value === 'number') { - if (validation?.max !== undefined && value > validation.max) { - addValidationError(`value must be less than or equal to ${validation.max}` + if (v.max !== undefined && value > v.max) { + addValidationError(`value must be less than or equal to ${v.max}` ) } - if (validation?.min !== undefined && value < validation.min) { - addValidationError(`value must be greater than or equal to ${validation.min}`) + if (v.min !== undefined && value < v.min) { + addValidationError(`value must be greater than or equal to ${v.min}`) } } - }) + } : undefined) return fieldType({ kind: 'scalar', @@ -95,8 +95,7 @@ export const float = ...config, hooks: mergeFieldHooks({ validate }, config.hooks), input: { - uniqueWhere: - isIndexed === 'unique' ? { arg: graphql.arg({ type: graphql.Float }) } : undefined, + uniqueWhere: isIndexed === 'unique' ? { arg: graphql.arg({ type: graphql.Float }) } : undefined, where: { arg: graphql.arg({ type: filters[meta.provider].Float[mode] }), resolve: mode === 'optional' ? filters.resolveCommon : undefined, @@ -124,12 +123,13 @@ export const float = getAdminMeta () { return { validation: { - min: validation?.min || null, - max: validation?.max || null, - isRequired: validation?.isRequired ?? false, + isRequired: v.isRequired ?? false, + min: v.min ?? null, + max: v.max ?? null, }, defaultValue: defaultValue ?? null, } }, }) } +} diff --git a/packages/core/src/fields/types/integer/index.ts b/packages/core/src/fields/types/integer/index.ts index 6a38f63a29f..f56e1252ceb 100644 --- a/packages/core/src/fields/types/integer/index.ts +++ b/packages/core/src/fields/types/integer/index.ts @@ -29,30 +29,29 @@ export type IntegerFieldConfig = } } -// These are the max and min values available to a 32 bit signed integer +// these are the lowest and highest values for a signed 32-bit integer const MAX_INT = 2147483647 const MIN_INT = -2147483648 -export function integer ({ - isIndexed, - defaultValue: _defaultValue, - validation, - ...config -}: IntegerFieldConfig = {}): FieldTypeFunc { - return meta => { +export function integer (config: IntegerFieldConfig = {}): FieldTypeFunc { + const { + defaultValue: _defaultValue, + isIndexed, + validation: v = {}, + } = config + + return (meta) => { const defaultValue = _defaultValue ?? null const hasAutoIncDefault = typeof defaultValue == 'object' && defaultValue !== null && defaultValue.kind === 'autoincrement' - const isNullable = resolveDbNullable(validation, config.db) + const isNullable = resolveDbNullable(v, config.db) if (hasAutoIncDefault) { if (meta.provider === 'sqlite' || meta.provider === 'mysql') { - throw new Error( - `${meta.listKey}.${meta.fieldKey} specifies defaultValue: { kind: 'autoincrement' }, this is not supported on ${meta.provider}` - ) + throw new Error(`${meta.listKey}.${meta.fieldKey} specifies defaultValue: { kind: 'autoincrement' }, this is not supported on ${meta.provider}`) } if (isNullable !== false) { throw new Error( @@ -63,45 +62,46 @@ export function integer ({ } } - if (validation?.min !== undefined && !Number.isInteger(validation.min)) { - throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.min: ${validation.min} but it must be an integer`) + if (v.min !== undefined && !Number.isInteger(v.min)) { + throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.min: ${v.min} but it must be an integer`) } - if (validation?.max !== undefined && !Number.isInteger(validation.max)) { - throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.max: ${validation.max} but it must be an integer`) + if (v.max !== undefined && !Number.isInteger(v.max)) { + throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.max: ${v.max} but it must be an integer`) } - if (validation?.min !== undefined && (validation?.min > MAX_INT || validation?.min < MIN_INT)) { - throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.min: ${validation.min} which is outside of the range of a 32bit signed integer (${MIN_INT} - ${MAX_INT}) which is not allowed`) + if (v.min !== undefined && (v.min > MAX_INT || v.min < MIN_INT)) { + throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.min: ${v.min} which is outside of the range of a 32bit signed integer (${MIN_INT} - ${MAX_INT}) which is not allowed`) } - if (validation?.max !== undefined && (validation?.max > MAX_INT || validation?.max < MIN_INT)) { - throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.max: ${validation.max} which is outside of the range of a 32bit signed integer (${MIN_INT} - ${MAX_INT}) which is not allowed`) + if (v.max !== undefined && (v.max > MAX_INT || v.max < MIN_INT)) { + throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.max: ${v.max} which is outside of the range of a 32bit signed integer (${MIN_INT} - ${MAX_INT}) which is not allowed`) } if ( - validation?.min !== undefined && - validation?.max !== undefined && - validation.min > validation.max + v.min !== undefined && + v.max !== undefined && + v.min > v.max ) { throw new Error(`${meta.listKey}.${meta.fieldKey} specifies a validation.max that is less than the validation.min, and therefore has no valid options`) } + const hasAdditionalValidation = v.min !== undefined || v.max !== undefined const { mode, validate, - } = makeValidateHook(meta, config, ({ resolvedData, operation, addValidationError }) => { + } = makeValidateHook(meta, config, hasAdditionalValidation ? ({ resolvedData, operation, addValidationError }) => { if (operation === 'delete') return const value = resolvedData[meta.fieldKey] if (typeof value === 'number') { - if (validation?.min !== undefined && value < validation.min) { - addValidationError(`value must be greater than or equal to ${validation.min}`) + if (v.min !== undefined && value < v.min) { + addValidationError(`value must be greater than or equal to ${v.min}`) } - if (validation?.max !== undefined && value > validation.max) { - addValidationError(`value must be less than or equal to ${validation.max}`) + if (v.max !== undefined && value > v.max) { + addValidationError(`value must be less than or equal to ${v.max}`) } } - }) + } : undefined) return fieldType({ kind: 'scalar', diff --git a/packages/core/src/fields/types/multiselect/index.ts b/packages/core/src/fields/types/multiselect/index.ts index 079d6fd191d..80e8ded55fb 100644 --- a/packages/core/src/fields/types/multiselect/index.ts +++ b/packages/core/src/fields/types/multiselect/index.ts @@ -40,7 +40,7 @@ export type MultiselectFieldConfig = } } -// These are the max and min values available to a 32 bit signed integer +// these are the lowest and highest values for a signed 32-bit integer const MAX_INT = 2147483647 const MIN_INT = -2147483648 diff --git a/packages/core/src/fields/types/text/index.ts b/packages/core/src/fields/types/text/index.ts index 5dadfffd27f..84504017610 100644 --- a/packages/core/src/fields/types/text/index.ts +++ b/packages/core/src/fields/types/text/index.ts @@ -57,68 +57,67 @@ export function text ( config: TextFieldConfig = {} ): FieldTypeFunc { const { - isIndexed, defaultValue: defaultValue_, - validation: validation_ + isIndexed, + validation = {} } = config config.db ??= {} config.db.isNullable ??= false // TODO: sigh, remove in breaking change? + const v = { + isRequired: validation.isRequired ?? false, + match: validation.match, + min: validation.isRequired ? validation.length?.min ?? 1 : validation.length?.min, + max: validation.length?.max, + } + return (meta) => { for (const type of ['min', 'max'] as const) { - const val = validation_?.length?.[type] + const val = v[type] if (val !== undefined && (!Number.isInteger(val) || val < 0)) { throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.length.${type}: ${val} but it must be a positive integer`) } - if (validation_?.isRequired && val !== undefined && val === 0) { + if (v.isRequired && val !== undefined && val === 0) { throw new Error(`${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 + v.min !== undefined && + v.max !== undefined && + v.min > v.max ) { throw new Error(`${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_ ? { - ...validation_, - length: { - 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 defaultValue = isNullable ? (defaultValue_ ?? null) : (defaultValue_ ?? '') - + const hasAdditionalValidation = v.match || v.min !== undefined || v.max !== undefined const { mode, validate, - } = makeValidateHook(meta, config, ({ resolvedData, operation, addValidationError }) => { + } = makeValidateHook(meta, config, hasAdditionalValidation ? ({ resolvedData, operation, addValidationError }) => { if (operation === 'delete') return const value = resolvedData[meta.fieldKey] if (value != null) { - if (validation?.length?.min !== undefined && value.length < validation.length.min) { - if (validation.length.min === 1) { + if (v.min !== undefined && value.length < v.min) { + if (v.min === 1) { addValidationError(`value must not be empty`) } else { - addValidationError(`value must be at least ${validation.length.min} characters long`) + addValidationError(`value must be at least ${v.min} characters long`) } } - if (validation?.length?.max !== undefined && value.length > validation.length.max) { - addValidationError(`value must be no longer than ${validation.length.max} characters`) + if (v.max !== undefined && value.length > v.max) { + addValidationError(`value must be no longer than ${v.max} characters`) } - if (validation?.match && !validation.match.regex.test(value)) { - addValidationError(validation.match.explanation || `value must match ${validation.match.regex}`) + if (v.match && !v.match.regex.test(value)) { + addValidationError(v.match.explanation || `value must match ${v.match.regex}`) } } - }) + } : undefined) return fieldType({ kind: 'scalar', @@ -133,8 +132,7 @@ export function text ( ...config, hooks: mergeFieldHooks({ validate }, config.hooks), input: { - uniqueWhere: - isIndexed === 'unique' ? { arg: graphql.arg({ type: graphql.String }) } : undefined, + uniqueWhere: isIndexed === 'unique' ? { arg: graphql.arg({ type: graphql.String }) } : undefined, where: { arg: graphql.arg({ type: filters[meta.provider].String[mode], @@ -166,17 +164,18 @@ export function text ( displayMode: config.ui?.displayMode ?? 'input', shouldUseModeInsensitive: meta.provider === 'postgresql', validation: { - isRequired: validation?.isRequired ?? false, - match: validation?.match - ? { - regex: { - source: validation.match.regex.source, - flags: validation.match.regex.flags, - }, - explanation: validation.match.explanation ?? null, - } - : null, - length: { max: validation?.length?.max ?? null, min: validation?.length?.min ?? null }, + isRequired: v.isRequired ?? false, + match: v.match ? { + regex: { + source: v.match.regex.source, + flags: v.match.regex.flags, + }, + explanation: v.match.explanation ?? null, + } : null, + length: { + max: v.max ?? null, + min: v.min ?? null + }, }, defaultValue: defaultValue ?? (isNullable ? null : ''), isNullable, diff --git a/tests/api-tests/fields/crud.test.ts b/tests/api-tests/fields/crud.test.ts index 70996d5cb76..bcfbe5e6b5d 100644 --- a/tests/api-tests/fields/crud.test.ts +++ b/tests/api-tests/fields/crud.test.ts @@ -4,7 +4,6 @@ import { text } from '@keystone-6/core/fields' import { type KeystoneContext } from '@keystone-6/core/types' import { setupTestRunner } from '@keystone-6/api-tests/test-runner' import { allowAll } from '@keystone-6/core/access' -import { humanize } from '../../../packages/core/src/lib/utils' import { dbProvider, expectSingleResolverError, @@ -221,7 +220,7 @@ for (const modulePath of testModules) { expectValidationError(errors, [ { path: [updateMutationName], - messages: [`Test.${fieldName}: ${humanize(fieldName)} is required`], + messages: [`Test.${fieldName}: missing value`], }, ]) } diff --git a/tests/api-tests/fields/required.test.ts b/tests/api-tests/fields/required.test.ts index 48f07ab2c91..5f14b2151b0 100644 --- a/tests/api-tests/fields/required.test.ts +++ b/tests/api-tests/fields/required.test.ts @@ -6,7 +6,6 @@ import { list } from '@keystone-6/core' import { text } from '@keystone-6/core/fields' import { setupTestRunner } from '@keystone-6/api-tests/test-runner' import { allowAll } from '@keystone-6/core/access' -import { humanize } from '../../../packages/core/src/lib/utils' import { dbProvider, expectValidationError @@ -85,23 +84,23 @@ for (const modulePath of testModules) { }, }) - const messages = [`Test.testField: ${humanize('testField')} is required`] + const messages = [`Test.testField: missing value`] test( 'Create an object without the required field', runner(async ({ context }) => { const { data, errors } = await context.graphql.raw({ query: ` - mutation { - createTest(data: { name: "test entry" } ) { id } - }`, + mutation { + createTest(data: { name: "test entry" } ) { id } + }`, }) expect(data).toEqual({ createTest: null }) expectValidationError(errors, [ { path: ['createTest'], messages: - mod.name === 'Text' ? ['Test.testField: Test Field must not be empty'] : messages, + mod.name === 'Text' ? ['Test.testField: value must not be empty'] : messages, }, ]) }) @@ -112,9 +111,9 @@ for (const modulePath of testModules) { runner(async ({ context }) => { const { data, errors } = await context.graphql.raw({ query: ` - mutation { - createTest(data: { name: "test entry", testField: null } ) { id } - }`, + mutation { + createTest(data: { name: "test entry", testField: null } ) { id } + }`, }) expect(data).toEqual({ createTest: null }) expectValidationError(errors, [