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

Update built-in fields to new validate hook syntax #9166

Merged
merged 16 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
dedupe makeValidationHook for mode and validate hook
  • Loading branch information
dcousens committed Jul 9, 2024
commit d9cfc5d57d8fdd0709c88c87b03dceba6b155a62
70 changes: 64 additions & 6 deletions packages/core/src/fields/non-null-graphql.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { type BaseListTypeInfo, type CommonFieldConfig, type FieldData } from '../types'
import {
type BaseListTypeInfo,
type FieldData,
} from '../types'
import {
type ValidateFieldHook
} from '../types/config/hooks'

export function getResolvedIsNullable (
validation: undefined | { isRequired?: boolean },
db: undefined | { isNullable?: boolean }
): boolean {
if (db?.isNullable === false) {
return false
}
if (db?.isNullable === false) return false
if (db?.isNullable === undefined && validation?.isRequired) {
return false
}
return true
}

export function resolveHasValidation (
function resolveHasValidation (
db?: { isNullable?: boolean },
validation?: unknown
) {
Expand All @@ -22,9 +26,63 @@ export function resolveHasValidation (
return false
}

export function makeValidateHook <ListTypeInfo extends BaseListTypeInfo> (
meta: FieldData,
config: {
label?: string,
db?: {
isNullable?: boolean
},
graphql?: {
isNonNull?: {
read?: boolean
}
},
validation?: {
isRequired?: boolean
},
},
f?: ValidateFieldHook<ListTypeInfo, 'create' | 'update' | 'delete', ListTypeInfo['fields']>
) {
const resolvedIsNullable = getResolvedIsNullable(config.validation, config.db)
const mode = resolvedIsNullable === false ? ('required' as const) : ('optional' as const)

assertReadIsNonNullAllowed(meta, config, resolvedIsNullable)
const hasValidation = resolveHasValidation(config.db, config.validation)
if (hasValidation) {
const validate = async function (args) {
const { operation, addValidationError, resolvedData } = args
if (operation !== 'delete') {
const value = resolvedData[meta.fieldKey]
if ((config.validation?.isRequired || resolvedIsNullable === false) && value === null) {
addValidationError(`Missing value`)
}
}

await f?.(args)
} satisfies ValidateFieldHook<ListTypeInfo, 'create' | 'update' | 'delete', ListTypeInfo['fields']>

return {
mode,
validate,
}
}

return {
mode,
validate: undefined
}
}

export function assertReadIsNonNullAllowed<ListTypeInfo extends BaseListTypeInfo> (
meta: FieldData,
config: CommonFieldConfig<ListTypeInfo>,
config: {
graphql?: {
isNonNull?: {
read?: boolean
}
}
},
resolvedIsNullable: boolean
) {
if (!resolvedIsNullable) return
Expand Down
56 changes: 16 additions & 40 deletions packages/core/src/fields/types/bigInt/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { humanize } from '../../../lib/utils'
import {
type BaseListTypeInfo,
type CommonFieldConfig,
Expand All @@ -8,15 +7,11 @@ import {
} from '../../../types'
import { graphql } from '../../..'
import {
assertReadIsNonNullAllowed,
getResolvedIsNullable,
resolveHasValidation,
makeValidateHook
} from '../../non-null-graphql'
import { filters } from '../../filters'
import {
type InternalFieldHooks,
mergeFieldHooks,
} from '../../resolve-hooks'
import { mergeFieldHooks } from '../../resolve-hooks'

export type BigIntFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
CommonFieldConfig<ListTypeInfo> & {
Expand Down Expand Up @@ -84,41 +79,23 @@ export function bigInt <ListTypeInfo extends BaseListTypeInfo> (
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`)
}
Copy link
Member

@dcousens dcousens Jul 9, 2024

Choose a reason for hiding this comment

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

I used this opportunity to unify each of the numerical fields to share nearly the same code for min and max validation.


assertReadIsNonNullAllowed(meta, config, isNullable)

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

const hooks: InternalFieldHooks<ListTypeInfo> = {}
if (hasValidation) {
hooks.validate = ({ resolvedData, operation, addValidationError }) => {
if (operation === 'delete') return

const value = resolvedData[meta.fieldKey]
const {
mode,
validate,
} = makeValidateHook(meta, config, ({ resolvedData, operation, addValidationError }) => {
if (operation === 'delete') return

if (
(validation?.isRequired || isNullable === false) &&
(value === null ||
(operation === 'create' && value === undefined && !hasAutoIncDefault))
) {
addValidationError(`${fieldLabel} is required`)
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 (typeof value === 'number') {
if (validation?.min !== undefined && value < validation.min) {
addValidationError(
`${fieldLabel} must be greater than or equal to ${validation.min}`
)
}

if (validation?.max !== undefined && value > validation.max) {
addValidationError(
`${fieldLabel} must be less than or equal to ${validation.max}`
)
}
if (validation?.max !== undefined && value > validation.max) {
addValidationError(`value must be less than or equal to ${validation.max}`)
}
}
}
})

return fieldType({
kind: 'scalar',
Expand All @@ -136,10 +113,9 @@ export function bigInt <ListTypeInfo extends BaseListTypeInfo> (
extendPrismaSchema: config.db?.extendPrismaSchema,
})({
...config,
hooks: mergeFieldHooks(hooks, config.hooks),
hooks: mergeFieldHooks({ validate }, config.hooks),
input: {
uniqueWhere:
isIndexed === 'unique' ? { arg: graphql.arg({ type: graphql.BigInt }) } : undefined,
uniqueWhere: isIndexed === 'unique' ? { arg: graphql.arg({ type: graphql.BigInt }) } : undefined,
where: {
arg: graphql.arg({ type: filters[meta.provider].BigInt[mode] }),
resolve: mode === 'optional' ? filters.resolveCommon : undefined,
Expand Down
39 changes: 10 additions & 29 deletions packages/core/src/fields/types/calendarDay/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import { humanize } from '../../../lib/utils'
import {
type BaseListTypeInfo,
fieldType,
type FieldTypeFunc,
type CommonFieldConfig,
type FieldTypeFunc,
fieldType,
orderDirectionEnum,
} from '../../../types'
import { type CalendarDayFieldMeta } from './views'
import { graphql } from '../../..'
import {
assertReadIsNonNullAllowed,
getResolvedIsNullable,
resolveHasValidation,
} from '../../non-null-graphql'
import { filters } from '../../filters'
import { type CalendarDayFieldMeta } from './views'
import { mergeFieldHooks, type InternalFieldHooks } from '../../resolve-hooks'
import { makeValidateHook } from '../../non-null-graphql'
import { mergeFieldHooks } from '../../resolve-hooks'

export type CalendarDayFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
CommonFieldConfig<ListTypeInfo> & {
Expand Down Expand Up @@ -48,13 +43,7 @@ export const calendarDay =
}
}

const resolvedIsNullable = getResolvedIsNullable(validation, config.db)
assertReadIsNonNullAllowed(meta, config, resolvedIsNullable)

const mode = resolvedIsNullable === false ? 'required' : 'optional'
const fieldLabel = config.label ?? humanize(meta.fieldKey)
const usesNativeDateType = meta.provider === 'postgresql' || meta.provider === 'mysql'
const hasValidation = resolveHasValidation(config.db, validation)

function resolveInput (value: string | null | undefined) {
if (meta.provider === 'sqlite' || value == null) {
Expand All @@ -63,20 +52,12 @@ export const calendarDay =
return dateStringToDateObjectInUTC(value)
}

const {
mode,
validate,
} = makeValidateHook(meta, config)
const commonResolveFilter = mode === 'optional' ? filters.resolveCommon : <T>(x: T) => x

const hooks: InternalFieldHooks<ListTypeInfo> = {}
if (hasValidation) {
hooks.validate = ({ resolvedData, addValidationError, operation }) => {
if (operation === 'delete') return

const value = resolvedData[meta.fieldKey]
if ((validation?.isRequired || resolvedIsNullable === false) && value === null) {
addValidationError(`${fieldLabel} is required`)
}
}
}

return fieldType({
kind: 'scalar',
mode,
Expand All @@ -94,7 +75,7 @@ export const calendarDay =
nativeType: usesNativeDateType ? 'Date' : undefined,
})({
...config,
hooks: mergeFieldHooks(hooks, config.hooks),
hooks: mergeFieldHooks({ validate }, config.hooks),
input: {
uniqueWhere:
isIndexed === 'unique'
Expand Down
61 changes: 22 additions & 39 deletions packages/core/src/fields/types/decimal/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import { humanize } from '../../../lib/utils'
import {
fieldType,
type FieldTypeFunc,
type BaseListTypeInfo,
type CommonFieldConfig,
type FieldData,
type FieldTypeFunc,
fieldType,
orderDirectionEnum,
Decimal,
type FieldData,
} from '../../../types'
import { graphql } from '../../..'
import {
assertReadIsNonNullAllowed,
getResolvedIsNullable,
resolveHasValidation,
} from '../../non-null-graphql'
import { filters } from '../../filters'
import { type DecimalFieldMeta } from './views'
import { mergeFieldHooks, type InternalFieldHooks } from '../../resolve-hooks'
import { makeValidateHook } from '../../non-null-graphql'
import { mergeFieldHooks } from '../../resolve-hooks'

export type DecimalFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
CommonFieldConfig<ListTypeInfo> & {
Expand Down Expand Up @@ -86,8 +81,6 @@ export const decimal =
)
}

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

const max =
validation?.max === undefined
? undefined
Expand All @@ -108,12 +101,24 @@ export const decimal =
? undefined
: parseDecimalValueOption(meta, defaultValue, 'defaultValue')

const isNullable = getResolvedIsNullable(validation, config.db)
const hasValidation = resolveHasValidation(config.db, validation)
const {
mode,
validate,
} = 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)) {
addValidationError(`value must be greater than or equal to ${min}`)
}

assertReadIsNonNullAllowed(meta, config, isNullable)
if (max !== undefined && val.greaterThan(max)) {
addValidationError(`value must be less than or equal to ${max}`)
}
}
})

const mode = isNullable === false ? 'required' : 'optional'
const index = isIndexed === true ? 'index' : isIndexed || undefined
const dbField = {
kind: 'scalar',
Expand All @@ -127,31 +132,9 @@ export const decimal =
extendPrismaSchema: config.db?.extendPrismaSchema,
} as const

const hooks: InternalFieldHooks<ListTypeInfo> = {}
if (hasValidation) {
hooks.validate = ({ resolvedData, addValidationError, operation }) => {
if (operation === 'delete') return

const val: Decimal | null | undefined = resolvedData[meta.fieldKey]

if (val === null && (validation?.isRequired || isNullable === false)) {
addValidationError(`${fieldLabel} is required`)
}
if (val != null) {
if (min !== undefined && val.lessThan(min)) {
addValidationError(`${fieldLabel} must be greater than or equal to ${min}`)
}

if (max !== undefined && val.greaterThan(max)) {
addValidationError(`${fieldLabel} must be less than or equal to ${max}`)
}
}
}
}

return fieldType(dbField)({
...config,
hooks: mergeFieldHooks(hooks, config.hooks),
hooks: mergeFieldHooks({ validate }, config.hooks),
input: {
uniqueWhere:
isIndexed === 'unique' ? { arg: graphql.arg({ type: graphql.Decimal }) } : undefined,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/fields/types/file/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export function file <ListTypeInfo extends BaseListTypeInfo> (config: FileFieldC
const filenameKey = `${fieldKey}_filename`
const filename = args.item[filenameKey]

// This will occur on an update where a file already existed but has been
// this will occur on an update where a file already existed but has been
// changed, or on a delete, where there is no longer an item
if (
(args.operation === 'delete' ||
Expand Down
Loading