Skip to content

Commit

Permalink
unify validation and fix tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dcousens committed Jul 9, 2024
1 parent eda29b7 commit 2cf6c8c
Show file tree
Hide file tree
Showing 15 changed files with 268 additions and 261 deletions.
27 changes: 12 additions & 15 deletions packages/core/src/fields/non-null-graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ListTypeInfo extends BaseListTypeInfo> (
meta: FieldData,
config: {
Expand All @@ -40,22 +31,28 @@ export function makeValidateHook <ListTypeInfo extends BaseListTypeInfo> (
},
validation?: {
isRequired?: boolean
[key: string]: unknown
},
},
f?: ValidateFieldHook<ListTypeInfo, 'create' | 'update' | 'delete', ListTypeInfo['fields']>
) {
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`)
}
}

Expand All @@ -70,7 +67,7 @@ export function makeValidateHook <ListTypeInfo extends BaseListTypeInfo> (

return {
mode,
validate: undefined
validate: f
}
}

Expand Down
75 changes: 42 additions & 33 deletions packages/core/src/fields/types/bigInt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {
orderDirectionEnum,
} from '../../../types'
import { graphql } from '../../..'
import { filters } from '../../filters'
import {
resolveDbNullable,
makeValidateHook
} from '../../non-null-graphql'
import { filters } from '../../filters'
import { mergeFieldHooks } from '../../resolve-hooks'

export type BigIntFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
Expand All @@ -33,28 +33,31 @@ export type BigIntFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
const MAX_INT = 9223372036854775807n
const MIN_INT = -9223372036854775808n

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

const {
isRequired = false,
min,
max
} = validation

return (meta) => {
const defaultValue = _defaultValue ?? null
const hasAutoIncDefault =
typeof defaultValue == 'object' &&
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(validation, config.db)
if (isNullable !== false) {
throw new Error(
`${meta.listKey}.${meta.fieldKey} specifies defaultValue: { kind: 'autoincrement' } but doesn't specify db.isNullable: false.\n` +
Expand All @@ -63,45 +66,50 @@ export function bigInt <ListTypeInfo extends BaseListTypeInfo> (
)
}
}

const validation = {
isRequired: validation_?.isRequired ?? false,
min: validation_?.min ?? MIN_INT,
max: validation_?.max ?? MAX_INT,
if (min !== undefined && !Number.isInteger(min)) {
throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.min: ${min} but it must be an integer`)
}

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 (max !== undefined && !Number.isInteger(max)) {
throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.max: ${max} but it must be an integer`)
}
if (validation.min > validation.max) {
if (min !== undefined && (min > MAX_INT || min < MIN_INT)) {
throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.min: ${min} which is outside of the range of a 64-bit signed integer`)
}
if (max !== undefined && (max > MAX_INT || max < MIN_INT)) {
throw new Error(`${meta.listKey}.${meta.fieldKey} specifies validation.max: ${max} which is outside of the range of a 64-bit signed integer`)
}
if (
min !== undefined &&
max !== undefined &&
min > 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 = min !== undefined || 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 (min !== undefined && value < min) {
addValidationError(`value must be greater than or equal to ${min}`)
}

if (validation?.max !== undefined && value > validation.max) {
addValidationError(`value must be less than or equal to ${validation.max}`)
if (max !== undefined && value > max) {
addValidationError(`value must be less than or equal to ${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'
Expand Down Expand Up @@ -135,19 +143,20 @@ export function bigInt <ListTypeInfo extends BaseListTypeInfo> (
update: { arg: graphql.arg({ type: graphql.BigInt }) },
orderBy: { arg: graphql.arg({ type: orderDirectionEnum }) },
},
output: graphql.field({
type: graphql.BigInt,
}),
output: graphql.field({ type: graphql.BigInt, }),
__ksTelemetryFieldTypeName: '@keystone-6/bigInt',
views: '@keystone-6/core/fields/types/bigInt/views',
getAdminMeta () {
return {
validation: {
min: validation.min.toString(),
max: validation.max.toString(),
isRequired: validation.isRequired,
min: min?.toString() ?? `${MIN_INT}`,
max: max?.toString() ?? `${MAX_INT}`,
isRequired,
},
defaultValue: typeof defaultValue === 'bigint' ? defaultValue.toString() : defaultValue,
defaultValue:
typeof defaultValue === 'bigint'
? defaultValue.toString()
: defaultValue,
}
},
})
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/fields/types/calendarDay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,13 @@ export type CalendarDayFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
}
}

export const calendarDay =
<ListTypeInfo extends BaseListTypeInfo>({
export function calendarDay <ListTypeInfo extends BaseListTypeInfo>(config: CalendarDayFieldConfig<ListTypeInfo> = {}): FieldTypeFunc<ListTypeInfo> {
const {
isIndexed,
validation,
defaultValue,
...config
}: CalendarDayFieldConfig<ListTypeInfo> = {}): FieldTypeFunc<ListTypeInfo> =>
meta => {
} = config
return (meta) => {
if (typeof defaultValue === 'string') {
try {
graphql.CalendarDay.graphQLType.parseValue(defaultValue)
Expand Down Expand Up @@ -126,6 +125,7 @@ export const calendarDay =
},
})
}
}

function dateStringToDateObjectInUTC (value: string) {
return new Date(`${value}T00:00Z`)
Expand Down
19 changes: 10 additions & 9 deletions packages/core/src/fields/types/decimal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,16 @@ function parseDecimalValueOption (meta: FieldData, value: string, name: string)
return decimal
}

export const decimal =
<ListTypeInfo extends BaseListTypeInfo>({
export function decimal <ListTypeInfo extends BaseListTypeInfo>(config: DecimalFieldConfig<ListTypeInfo> = {}): FieldTypeFunc<ListTypeInfo> {
const {
isIndexed,
precision = 18,
scale = 4,
validation,
defaultValue,
...config
}: DecimalFieldConfig<ListTypeInfo> = {}): FieldTypeFunc<ListTypeInfo> =>
meta => {
} = config

return (meta) => {
if (meta.provider === 'sqlite') {
throw new Error('The decimal field does not support sqlite')
}
Expand Down Expand Up @@ -107,13 +107,13 @@ export const decimal =
} = makeValidateHook(meta, config, ({ resolvedData, operation, addValidationError }) => {
if (operation === 'delete') return

const val: Decimal | null | undefined = resolvedData[meta.fieldKey]
if (val != null) {
if (min !== undefined && val.lessThan(min)) {
const value: Decimal | null | undefined = resolvedData[meta.fieldKey]
if (value != null) {
if (min !== undefined && value.lessThan(min)) {
addValidationError(`value must be greater than or equal to ${min}`)
}

if (max !== undefined && val.greaterThan(max)) {
if (max !== undefined && value.greaterThan(max)) {
addValidationError(`value must be less than or equal to ${max}`)
}
}
Expand Down Expand Up @@ -184,3 +184,4 @@ export const decimal =
}),
})
}
}
54 changes: 27 additions & 27 deletions packages/core/src/fields/types/float/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,13 @@ export type FloatFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
}
}

export const float =
<ListTypeInfo extends BaseListTypeInfo>({
isIndexed,
validation,
export function float <ListTypeInfo extends BaseListTypeInfo>(config: FloatFieldConfig<ListTypeInfo> = {}): FieldTypeFunc<ListTypeInfo> {
const {
defaultValue,
...config
}: FloatFieldConfig<ListTypeInfo> = {}): FieldTypeFunc<ListTypeInfo> =>
meta => {
isIndexed,
validation: v = {},
} = config
return (meta) => {
if (
defaultValue !== undefined &&
(typeof defaultValue !== 'number' || !Number.isFinite(defaultValue))
Expand All @@ -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',
Expand All @@ -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,
Expand Down Expand Up @@ -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,
}
},
})
}
}
Loading

0 comments on commit 2cf6c8c

Please sign in to comment.