diff --git a/.changeset/lazy-strings-guess.md b/.changeset/lazy-strings-guess.md new file mode 100644 index 00000000000..09f3d8e0741 --- /dev/null +++ b/.changeset/lazy-strings-guess.md @@ -0,0 +1,5 @@ +--- +'@keystone-6/core': patch +--- + +Fixes BigInt values throwing on deserialisation in the item view diff --git a/examples/default-values/schema.graphql b/examples/default-values/schema.graphql index c3bc4b10f58..aa67d35a1a7 100644 --- a/examples/default-values/schema.graphql +++ b/examples/default-values/schema.graphql @@ -8,6 +8,7 @@ type Task { isComplete: Boolean assignedTo: Person finishBy: DateTime + viewCount: BigInt } enum TaskPriorityType { @@ -18,6 +19,8 @@ enum TaskPriorityType { scalar DateTime @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc3339#section-5.6") +scalar BigInt + input TaskWhereUniqueInput { id: ID } @@ -32,6 +35,7 @@ input TaskWhereInput { isComplete: BooleanFilter assignedTo: PersonWhereInput finishBy: DateTimeNullableFilter + viewCount: BigIntNullableFilter } input IDFilter { @@ -96,12 +100,24 @@ input DateTimeNullableFilter { not: DateTimeNullableFilter } +input BigIntNullableFilter { + equals: BigInt + in: [BigInt!] + notIn: [BigInt!] + lt: BigInt + lte: BigInt + gt: BigInt + gte: BigInt + not: BigIntNullableFilter +} + input TaskOrderByInput { id: OrderDirection label: OrderDirection priority: OrderDirection isComplete: OrderDirection finishBy: OrderDirection + viewCount: OrderDirection } enum OrderDirection { @@ -115,6 +131,7 @@ input TaskUpdateInput { isComplete: Boolean assignedTo: PersonRelateToOneForUpdateInput finishBy: DateTime + viewCount: BigInt } input PersonRelateToOneForUpdateInput { @@ -134,6 +151,7 @@ input TaskCreateInput { isComplete: Boolean assignedTo: PersonRelateToOneForCreateInput finishBy: DateTime + viewCount: BigInt } input PersonRelateToOneForCreateInput { diff --git a/examples/default-values/schema.prisma b/examples/default-values/schema.prisma index 974677659d4..27c848fdb8f 100644 --- a/examples/default-values/schema.prisma +++ b/examples/default-values/schema.prisma @@ -20,6 +20,7 @@ model Task { assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) assignedToId String? @map("assignedTo") finishBy DateTime? + viewCount BigInt? @default(0) @@index([assignedToId]) } diff --git a/examples/default-values/schema.ts b/examples/default-values/schema.ts index 93ba2ceee66..0cd3b3dd84d 100644 --- a/examples/default-values/schema.ts +++ b/examples/default-values/schema.ts @@ -1,5 +1,5 @@ import { list } from '@keystone-6/core'; -import { checkbox, relationship, text, timestamp } from '@keystone-6/core/fields'; +import { bigInt, checkbox, relationship, text, timestamp } from '@keystone-6/core/fields'; import { select } from '@keystone-6/core/fields'; import { allowAll } from '@keystone-6/core/access'; import { Lists } from '.keystone/types'; @@ -19,6 +19,7 @@ export const lists: Lists = { hooks: { resolveInput({ resolvedData, inputData }) { if (inputData.priority === undefined) { + // default to high if "urgent" is in the label if (inputData.label && inputData.label.toLowerCase().includes('urgent')) { return 'high'; } else { @@ -64,6 +65,10 @@ export const lists: Lists = { }, }, }), + // Static default: When a task is first created, it has been viewed zero times + viewCount: bigInt({ + defaultValue: 0n, + }), }, }), Person: list({ diff --git a/packages/core/src/fields/types/bigInt/views/index.tsx b/packages/core/src/fields/types/bigInt/views/index.tsx index aca2ee9e396..76dbae0db50 100644 --- a/packages/core/src/fields/types/bigInt/views/index.tsx +++ b/packages/core/src/fields/types/bigInt/views/index.tsx @@ -14,6 +14,16 @@ import { import { CellLink, CellContainer } from '../../../../admin-ui/components'; import { useFormattedInput } from '../../integer/views/utils'; +type Validation = { + isRequired: boolean; + min: bigint; + max: bigint; +}; + +type Value = + | { kind: 'create'; value: string | bigint | null } + | { kind: 'update'; value: string | bigint | null; initial: unknown | null }; + function BigIntInput({ value, onChange, @@ -131,36 +141,36 @@ export const CardValue: CardValueComponent = ({ item, field }) => { }; function validate( - value: Value, + state: Value, validation: Validation, label: string, hasAutoIncrementDefault: boolean ): string | undefined { - const val = value.value; - if (typeof val === 'string') { - return `${label} must be a whole number`; + const { kind, value } = state; + if (typeof value === 'string') { + return `${label} must be a BigInt`; } - // if we recieve null initially on the item view and the current value is null, + // if we receive null initially on the item view and the current value is null, // we should always allow saving it because: // - the value might be null in the database and we don't want to prevent saving the whole item because of that // - we might have null because of an access control error - if (value.kind === 'update' && value.initial === null && val === null) { + if (kind === 'update' && state.initial === null && value === null) { return undefined; } - if (value.kind === 'create' && value.value === null && hasAutoIncrementDefault) { + if (kind === 'create' && value === null && hasAutoIncrementDefault) { return undefined; } - if (validation.isRequired && val === null) { + if (validation.isRequired && value === null) { return `${label} is required`; } - if (typeof val === 'bigint') { - if (val < validation.min) { + if (typeof value === 'bigint') { + if (value < validation.min) { return `${label} must be greater than or equal to ${validation.min}`; } - if (val > validation.max) { + if (value > validation.max) { return `${label} must be less than or equal to ${validation.max}`; } } @@ -168,16 +178,6 @@ function validate( return undefined; } -type Validation = { - isRequired: boolean; - min: bigint; - max: bigint; -}; - -type Value = - | { kind: 'update'; initial: bigint | null; value: string | bigint | null } - | { kind: 'create'; value: string | bigint | null }; - export const controller = ( config: FieldControllerConfig<{ validation: { @@ -214,7 +214,14 @@ export const controller = ( ? BigInt(config.fieldMeta.defaultValue) : null, }, - deserialize: data => ({ kind: 'update', value: data[config.path], initial: data[config.path] }), + deserialize: data => { + const raw = data[config.path]; + return { + kind: 'update', + value: raw === null ? null : BigInt(raw), + initial: raw, + }; + }, serialize: value => ({ [config.path]: value.value === null ? null : value.value.toString() }), hasAutoIncrementDefault, validate: value => diff --git a/packages/core/src/fields/types/integer/views/utils.tsx b/packages/core/src/fields/types/integer/views/utils.tsx index de28a5ddeac..388b8903b44 100644 --- a/packages/core/src/fields/types/integer/views/utils.tsx +++ b/packages/core/src/fields/types/integer/views/utils.tsx @@ -24,11 +24,7 @@ export function useFormattedInput( // typeof value === 'string' implies the unparsed form // typeof value !== 'string' implies the parsed form if (typeof value === 'string' && typeof config.parse(value) !== 'string') { - throw new Error( - `Valid values must be passed in as a parsed value, not a raw value. The value you passed was \`${JSON.stringify( - value - )}\`, you should pass \`${JSON.stringify(config.parse(value))}\` instead` - ); + throw new Error(`Expected ${typeof config.parse(value)}, got ${typeof value}`); } let [internalValueState, setInternalValueState] = useState(() => typeof value === 'string' ? value : config.format(value)