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

Fix bigInt deserialisation #8005

Merged
merged 6 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
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