Skip to content

Commit

Permalink
Fix bigInt deserialisation (#8005)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Cousens <dcousens@users.noreply.github.com>
  • Loading branch information
dcousens and dcousens authored Oct 17, 2022
1 parent 709d1a8 commit c2b5704
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/lazy-strings-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-6/core': patch
---

Fixes BigInt values throwing on deserialisation in the item view
18 changes: 18 additions & 0 deletions examples/default-values/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type Task {
isComplete: Boolean
assignedTo: Person
finishBy: DateTime
viewCount: BigInt
}

enum TaskPriorityType {
Expand All @@ -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
}
Expand All @@ -32,6 +35,7 @@ input TaskWhereInput {
isComplete: BooleanFilter
assignedTo: PersonWhereInput
finishBy: DateTimeNullableFilter
viewCount: BigIntNullableFilter
}

input IDFilter {
Expand Down Expand Up @@ -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 {
Expand All @@ -115,6 +131,7 @@ input TaskUpdateInput {
isComplete: Boolean
assignedTo: PersonRelateToOneForUpdateInput
finishBy: DateTime
viewCount: BigInt
}

input PersonRelateToOneForUpdateInput {
Expand All @@ -134,6 +151,7 @@ input TaskCreateInput {
isComplete: Boolean
assignedTo: PersonRelateToOneForCreateInput
finishBy: DateTime
viewCount: BigInt
}

input PersonRelateToOneForCreateInput {
Expand Down
1 change: 1 addition & 0 deletions examples/default-values/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
Expand Down
7 changes: 6 additions & 1 deletion examples/default-values/schema.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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({
Expand Down
51 changes: 29 additions & 22 deletions packages/core/src/fields/types/bigInt/views/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -131,53 +141,43 @@ 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}`;
}
}

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: {
Expand Down Expand Up @@ -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 =>
Expand Down
6 changes: 1 addition & 5 deletions packages/core/src/fields/types/integer/views/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,7 @@ export function useFormattedInput<ParsedValue extends ParsedValueBase>(
// 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)
Expand Down

1 comment on commit c2b5704

@vercel
Copy link

@vercel vercel bot commented on c2b5704 Oct 17, 2022

Choose a reason for hiding this comment

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

Please sign in to comment.