diff --git a/.changeset/rare-baboons-repeat.md b/.changeset/rare-baboons-repeat.md new file mode 100644 index 00000000000..77221ac1697 --- /dev/null +++ b/.changeset/rare-baboons-repeat.md @@ -0,0 +1,46 @@ +--- +'@keystone-next/keystone': major +--- + +The `checkbox` field is now non-nullable in the database, if you need three states, you should use `select()`. The field no longer accepts dynamic default values and it will default to `false` unless a different `defaultValue` is specified. `graphql.isNonNull` can also be set if you have no read access control and you don't intend to add any in the future, it will make the GraphQL output field non-nullable. + +If you're using SQLite, Prisma will generate a migration that makes the column non-nullable and sets any rows that have + +If you're using PostgreSQL, Prisma will generate a migration but you'll need to modify it if you have nulls in a checkbox field. Keystone will say that the migration cannot be executed: + +``` +✨ Starting Keystone +⭐️ Dev Server Ready on http://localhost:3000 +✨ Generating GraphQL and Prisma schemas +✨ There has been a change to your Keystone schema that requires a migration + +⚠️ We found changes that cannot be executed: + + • Made the column `isAdmin` on table `User` required, but there are 1 existing NULL values. + +✔ Name of migration … make-is-admin-non-null +✨ A migration has been created at migrations/20210906053141_make_is_admin_non_null +Please edit the migration and run keystone-next dev again to apply the migration +``` + +The generated migration will look like this: + +```sql +/* + Warnings: + + - Made the column `isAdmin` on table `User` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "isAdmin" SET NOT NULL, +ALTER COLUMN "isAdmin" SET DEFAULT false; +``` + +To make it set any null values to false in your database, you need to modify it so that it looks like this but with the table and column names replaced. + +```sql +ALTER TABLE "User" ALTER COLUMN "isAdmin" SET DEFAULT false; +UPDATE "User" SET "isAdmin" = DEFAULT WHERE "isAdmin" IS NULL; +ALTER TABLE "User" ALTER COLUMN "isAdmin" SET NOT NULL; +``` \ No newline at end of file diff --git a/docs/pages/docs/apis/fields.mdx b/docs/pages/docs/apis/fields.mdx index 4ffbb2f6632..9a64029b5f7 100644 --- a/docs/pages/docs/apis/fields.mdx +++ b/docs/pages/docs/apis/fields.mdx @@ -137,11 +137,15 @@ A `checkbox` field represents a boolean (`true`/`false`) value. Options: -- `defaultValue` (default: `undefined`): Can be either a boolean value or an async function which takes an argument `({ context, originalInput })` and returns a boolean value. - This value will be used for the field when creating items if no explicit value is set. - `context` is a [`KeystoneContext`](./context) object. - `originalInput` is an object containing the data passed in to the `create` mutation. -- `isRequired` (default: `false`): If `true` then this field can never be set to `null`. +- `defaultValue` (default: `false`): This value will be used for the field when creating items if no explicit value is set. +- `graphql.read.isNonNull` (default: `false`): If you have no read access control and you don't intend to add any in the future, + you can set this to true and the output field will be non-nullable. This is only allowed when you have no read access control because otherwise, + when access is denied, `null` will be returned which will cause an error since the field is non-nullable and the error + will propagate up until a nullable field is found which means the entire item will be unreadable and when doing an `items` query, all the items will be unreadable. +- `graphql.create.isNonNull` (default: `false`): If you have no create access control and you want to explicitly show that this is field is non-nullable in the create input + you can set this to true and the create field will be non-nullable and have a default value at the GraphQL level. + This is only allowed when you have no create access control because otherwise, the item will always fail access control + if a user doesn't have access to create the particular field regardless of whether or not they specify the field in the create. ```typescript import { config, createSchema, list } from '@keystone-next/keystone'; @@ -153,7 +157,14 @@ export default config({ fields: { fieldName: checkbox({ defaultValue: true, - isRequired: true, + graphql: { + read: { + isNonNull: true + }, + create: { + isNonNull: true + }, + } }), /* ... */ }, diff --git a/examples-staging/auth/schema.prisma b/examples-staging/auth/schema.prisma index 7e8ccdd031e..9e95f502180 100644 --- a/examples-staging/auth/schema.prisma +++ b/examples-staging/auth/schema.prisma @@ -12,9 +12,9 @@ generator client { } model User { - id String @id @default(cuid()) + id String @id @default(cuid()) name String? - email String? @unique + email String? @unique password String? - isAdmin Boolean? + isAdmin Boolean @default(false) } \ No newline at end of file diff --git a/examples-staging/basic/schema.prisma b/examples-staging/basic/schema.prisma index d02d00119c1..7dec910681c 100644 --- a/examples-staging/basic/schema.prisma +++ b/examples-staging/basic/schema.prisma @@ -25,7 +25,7 @@ model User { attachment_mode String? attachment_filename String? password String? - isAdmin Boolean? + isAdmin Boolean @default(false) roles String? phoneNumbers PhoneNumber[] @relation("PhoneNumber_user") posts Post[] @relation("Post_author") diff --git a/examples-staging/ecommerce/schema.prisma b/examples-staging/ecommerce/schema.prisma index b2d19d2f554..64c9f1f7c91 100644 --- a/examples-staging/ecommerce/schema.prisma +++ b/examples-staging/ecommerce/schema.prisma @@ -90,13 +90,13 @@ model Order { } model Role { - id String @id @default(cuid()) + id String @id @default(cuid()) name String? - canManageProducts Boolean? - canSeeOtherUsers Boolean? - canManageUsers Boolean? - canManageRoles Boolean? - canManageCart Boolean? - canManageOrders Boolean? - assignedTo User[] @relation("User_role") + canManageProducts Boolean @default(false) + canSeeOtherUsers Boolean @default(false) + canManageUsers Boolean @default(false) + canManageRoles Boolean @default(false) + canManageCart Boolean @default(false) + canManageOrders Boolean @default(false) + assignedTo User[] @relation("User_role") } \ No newline at end of file diff --git a/examples-staging/roles/schema.prisma b/examples-staging/roles/schema.prisma index e23b4b60055..6dfad7f9120 100644 --- a/examples-staging/roles/schema.prisma +++ b/examples-staging/roles/schema.prisma @@ -12,12 +12,12 @@ generator client { } model Todo { - id String @id @default(cuid()) + id String @id @default(cuid()) label String? - isComplete Boolean? - isPrivate Boolean? - assignedTo Person? @relation("Todo_assignedTo", fields: [assignedToId], references: [id]) - assignedToId String? @map("assignedTo") + isComplete Boolean @default(false) + isPrivate Boolean @default(false) + assignedTo Person? @relation("Todo_assignedTo", fields: [assignedToId], references: [id]) + assignedToId String? @map("assignedTo") @@index([assignedToId]) } @@ -37,11 +37,11 @@ model Person { model Role { id String @id @default(cuid()) name String? - canCreateTodos Boolean? - canManageAllTodos Boolean? - canSeeOtherPeople Boolean? - canEditOtherPeople Boolean? - canManagePeople Boolean? - canManageRoles Boolean? + canCreateTodos Boolean @default(false) + canManageAllTodos Boolean @default(false) + canSeeOtherPeople Boolean @default(false) + canEditOtherPeople Boolean @default(false) + canManagePeople Boolean @default(false) + canManageRoles Boolean @default(false) assignedTo Person[] @relation("Person_role") } \ No newline at end of file diff --git a/examples-staging/sandbox/schema.prisma b/examples-staging/sandbox/schema.prisma index 8d49455e864..3905a7c87dd 100644 --- a/examples-staging/sandbox/schema.prisma +++ b/examples-staging/sandbox/schema.prisma @@ -14,7 +14,7 @@ generator client { model Todo { id String @id @default(cuid()) label String? - isComplete Boolean? + isComplete Boolean @default(false) assignedTo User? @relation("Todo_assignedTo", fields: [assignedToId], references: [id]) assignedToId String? @map("assignedTo") finishBy DateTime? diff --git a/examples/custom-admin-ui-logo/schema.graphql b/examples/custom-admin-ui-logo/schema.graphql index 986be342995..546de8db159 100644 --- a/examples/custom-admin-ui-logo/schema.graphql +++ b/examples/custom-admin-ui-logo/schema.graphql @@ -27,7 +27,7 @@ input TaskWhereInput { id: IDFilter label: StringNullableFilter priority: TaskPriorityTypeNullableFilter - isComplete: BooleanNullableFilter + isComplete: BooleanFilter assignedTo: PersonWhereInput finishBy: DateTimeNullableFilter } @@ -78,9 +78,9 @@ input TaskPriorityTypeNullableFilter { not: TaskPriorityTypeNullableFilter } -input BooleanNullableFilter { +input BooleanFilter { equals: Boolean - not: BooleanNullableFilter + not: BooleanFilter } input DateTimeNullableFilter { diff --git a/examples/custom-admin-ui-logo/schema.prisma b/examples/custom-admin-ui-logo/schema.prisma index 78461ee962d..872f311ea90 100644 --- a/examples/custom-admin-ui-logo/schema.prisma +++ b/examples/custom-admin-ui-logo/schema.prisma @@ -15,7 +15,7 @@ model Task { id String @id @default(cuid()) label String? priority String? - isComplete Boolean? + isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) assignedToId String? @map("assignedTo") finishBy DateTime? diff --git a/examples/custom-admin-ui-navigation/schema.graphql b/examples/custom-admin-ui-navigation/schema.graphql index 986be342995..546de8db159 100644 --- a/examples/custom-admin-ui-navigation/schema.graphql +++ b/examples/custom-admin-ui-navigation/schema.graphql @@ -27,7 +27,7 @@ input TaskWhereInput { id: IDFilter label: StringNullableFilter priority: TaskPriorityTypeNullableFilter - isComplete: BooleanNullableFilter + isComplete: BooleanFilter assignedTo: PersonWhereInput finishBy: DateTimeNullableFilter } @@ -78,9 +78,9 @@ input TaskPriorityTypeNullableFilter { not: TaskPriorityTypeNullableFilter } -input BooleanNullableFilter { +input BooleanFilter { equals: Boolean - not: BooleanNullableFilter + not: BooleanFilter } input DateTimeNullableFilter { diff --git a/examples/custom-admin-ui-navigation/schema.prisma b/examples/custom-admin-ui-navigation/schema.prisma index 78461ee962d..872f311ea90 100644 --- a/examples/custom-admin-ui-navigation/schema.prisma +++ b/examples/custom-admin-ui-navigation/schema.prisma @@ -15,7 +15,7 @@ model Task { id String @id @default(cuid()) label String? priority String? - isComplete Boolean? + isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) assignedToId String? @map("assignedTo") finishBy DateTime? diff --git a/examples/custom-admin-ui-pages/schema.graphql b/examples/custom-admin-ui-pages/schema.graphql index 986be342995..546de8db159 100644 --- a/examples/custom-admin-ui-pages/schema.graphql +++ b/examples/custom-admin-ui-pages/schema.graphql @@ -27,7 +27,7 @@ input TaskWhereInput { id: IDFilter label: StringNullableFilter priority: TaskPriorityTypeNullableFilter - isComplete: BooleanNullableFilter + isComplete: BooleanFilter assignedTo: PersonWhereInput finishBy: DateTimeNullableFilter } @@ -78,9 +78,9 @@ input TaskPriorityTypeNullableFilter { not: TaskPriorityTypeNullableFilter } -input BooleanNullableFilter { +input BooleanFilter { equals: Boolean - not: BooleanNullableFilter + not: BooleanFilter } input DateTimeNullableFilter { diff --git a/examples/custom-admin-ui-pages/schema.prisma b/examples/custom-admin-ui-pages/schema.prisma index 78461ee962d..872f311ea90 100644 --- a/examples/custom-admin-ui-pages/schema.prisma +++ b/examples/custom-admin-ui-pages/schema.prisma @@ -15,7 +15,7 @@ model Task { id String @id @default(cuid()) label String? priority String? - isComplete Boolean? + isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) assignedToId String? @map("assignedTo") finishBy DateTime? diff --git a/examples/custom-field-view/schema.graphql b/examples/custom-field-view/schema.graphql index 01a3d665af2..32721af1668 100644 --- a/examples/custom-field-view/schema.graphql +++ b/examples/custom-field-view/schema.graphql @@ -28,7 +28,7 @@ input TaskWhereInput { id: IDFilter label: StringNullableFilter priority: TaskPriorityTypeNullableFilter - isComplete: BooleanNullableFilter + isComplete: BooleanFilter assignedTo: PersonWhereInput finishBy: DateTimeNullableFilter } @@ -79,9 +79,9 @@ input TaskPriorityTypeNullableFilter { not: TaskPriorityTypeNullableFilter } -input BooleanNullableFilter { +input BooleanFilter { equals: Boolean - not: BooleanNullableFilter + not: BooleanFilter } input DateTimeNullableFilter { diff --git a/examples/custom-field-view/schema.prisma b/examples/custom-field-view/schema.prisma index f9d9dc0a7e6..cf008f69e07 100644 --- a/examples/custom-field-view/schema.prisma +++ b/examples/custom-field-view/schema.prisma @@ -15,7 +15,7 @@ model Task { id String @id @default(cuid()) label String? priority String? - isComplete Boolean? + isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) assignedToId String? @map("assignedTo") finishBy DateTime? diff --git a/examples/default-values/schema.graphql b/examples/default-values/schema.graphql index 986be342995..546de8db159 100644 --- a/examples/default-values/schema.graphql +++ b/examples/default-values/schema.graphql @@ -27,7 +27,7 @@ input TaskWhereInput { id: IDFilter label: StringNullableFilter priority: TaskPriorityTypeNullableFilter - isComplete: BooleanNullableFilter + isComplete: BooleanFilter assignedTo: PersonWhereInput finishBy: DateTimeNullableFilter } @@ -78,9 +78,9 @@ input TaskPriorityTypeNullableFilter { not: TaskPriorityTypeNullableFilter } -input BooleanNullableFilter { +input BooleanFilter { equals: Boolean - not: BooleanNullableFilter + not: BooleanFilter } input DateTimeNullableFilter { diff --git a/examples/default-values/schema.prisma b/examples/default-values/schema.prisma index 78461ee962d..872f311ea90 100644 --- a/examples/default-values/schema.prisma +++ b/examples/default-values/schema.prisma @@ -15,7 +15,7 @@ model Task { id String @id @default(cuid()) label String? priority String? - isComplete Boolean? + isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) assignedToId String? @map("assignedTo") finishBy DateTime? diff --git a/examples/json/schema.prisma b/examples/json/schema.prisma index 7e77ac3aea3..b4dd5312b55 100644 --- a/examples/json/schema.prisma +++ b/examples/json/schema.prisma @@ -12,12 +12,12 @@ generator client { } model Package { - id String @id @default(cuid()) + id String @id @default(cuid()) label String? pkgjson String? - isPrivate Boolean? - ownedBy Person? @relation("Package_ownedBy", fields: [ownedById], references: [id]) - ownedById String? @map("ownedBy") + isPrivate Boolean @default(false) + ownedBy Person? @relation("Package_ownedBy", fields: [ownedById], references: [id]) + ownedById String? @map("ownedBy") @@index([ownedById]) } diff --git a/examples/task-manager/schema.graphql b/examples/task-manager/schema.graphql index 986be342995..546de8db159 100644 --- a/examples/task-manager/schema.graphql +++ b/examples/task-manager/schema.graphql @@ -27,7 +27,7 @@ input TaskWhereInput { id: IDFilter label: StringNullableFilter priority: TaskPriorityTypeNullableFilter - isComplete: BooleanNullableFilter + isComplete: BooleanFilter assignedTo: PersonWhereInput finishBy: DateTimeNullableFilter } @@ -78,9 +78,9 @@ input TaskPriorityTypeNullableFilter { not: TaskPriorityTypeNullableFilter } -input BooleanNullableFilter { +input BooleanFilter { equals: Boolean - not: BooleanNullableFilter + not: BooleanFilter } input DateTimeNullableFilter { diff --git a/examples/task-manager/schema.prisma b/examples/task-manager/schema.prisma index 78461ee962d..872f311ea90 100644 --- a/examples/task-manager/schema.prisma +++ b/examples/task-manager/schema.prisma @@ -15,7 +15,7 @@ model Task { id String @id @default(cuid()) label String? priority String? - isComplete Boolean? + isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) assignedToId String? @map("assignedTo") finishBy DateTime? diff --git a/examples/testing/schema.graphql b/examples/testing/schema.graphql index b7b89b882cb..9fdc40a47ea 100644 --- a/examples/testing/schema.graphql +++ b/examples/testing/schema.graphql @@ -80,7 +80,7 @@ input TaskWhereInput { id: IDFilter label: StringNullableFilter priority: TaskPriorityTypeNullableFilter - isComplete: BooleanNullableFilter + isComplete: BooleanFilter assignedTo: PersonWhereInput finishBy: DateTimeNullableFilter } @@ -131,9 +131,9 @@ input TaskPriorityTypeNullableFilter { not: TaskPriorityTypeNullableFilter } -input BooleanNullableFilter { +input BooleanFilter { equals: Boolean - not: BooleanNullableFilter + not: BooleanFilter } input DateTimeNullableFilter { diff --git a/examples/testing/schema.prisma b/examples/testing/schema.prisma index 56ac89a216f..e2931cd5089 100644 --- a/examples/testing/schema.prisma +++ b/examples/testing/schema.prisma @@ -15,7 +15,7 @@ model Task { id String @id @default(cuid()) label String? priority String? - isComplete Boolean? + isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) assignedToId String? @map("assignedTo") finishBy DateTime? diff --git a/examples/with-auth/schema.graphql b/examples/with-auth/schema.graphql index b7b89b882cb..9fdc40a47ea 100644 --- a/examples/with-auth/schema.graphql +++ b/examples/with-auth/schema.graphql @@ -80,7 +80,7 @@ input TaskWhereInput { id: IDFilter label: StringNullableFilter priority: TaskPriorityTypeNullableFilter - isComplete: BooleanNullableFilter + isComplete: BooleanFilter assignedTo: PersonWhereInput finishBy: DateTimeNullableFilter } @@ -131,9 +131,9 @@ input TaskPriorityTypeNullableFilter { not: TaskPriorityTypeNullableFilter } -input BooleanNullableFilter { +input BooleanFilter { equals: Boolean - not: BooleanNullableFilter + not: BooleanFilter } input DateTimeNullableFilter { diff --git a/examples/with-auth/schema.prisma b/examples/with-auth/schema.prisma index 56ac89a216f..e2931cd5089 100644 --- a/examples/with-auth/schema.prisma +++ b/examples/with-auth/schema.prisma @@ -15,7 +15,7 @@ model Task { id String @id @default(cuid()) label String? priority String? - isComplete Boolean? + isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) assignedToId String? @map("assignedTo") finishBy DateTime? diff --git a/packages/keystone/src/fields/non-null-graphql.ts b/packages/keystone/src/fields/non-null-graphql.ts new file mode 100644 index 00000000000..0b54e7766db --- /dev/null +++ b/packages/keystone/src/fields/non-null-graphql.ts @@ -0,0 +1,49 @@ +import { BaseGeneratedListTypes, FieldAccessControl, FieldData } from '../types'; + +export function hasReadAccessControl( + access: FieldAccessControl | undefined +) { + if (access === undefined) { + return false; + } + return typeof access === 'function' || typeof access.read === 'function'; +} + +export function hasCreateAccessControl( + access: FieldAccessControl | undefined +) { + if (access === undefined) { + return false; + } + return typeof access === 'function' || typeof access.create === 'function'; +} + +export function assertReadIsNonNullAllowed( + meta: FieldData, + config: { + access?: FieldAccessControl | undefined; + graphql?: { read?: { isNonNull?: boolean } }; + } +) { + if (config.graphql?.read?.isNonNull && hasReadAccessControl(config.access)) { + throw new Error( + `The field at ${meta.listKey}.${meta.fieldKey} sets graphql.read.isNonNull: true and has read access control, this is not allowed.\n` + + 'Either disable graphql.read.isNonNull or read access control.' + ); + } +} + +export function assertCreateIsNonNullAllowed( + meta: FieldData, + config: { + access?: FieldAccessControl | undefined; + graphql?: { create?: { isNonNull?: boolean } }; + } +) { + if (config.graphql?.create?.isNonNull && hasCreateAccessControl(config.access)) { + throw new Error( + `The field at ${meta.listKey}.${meta.fieldKey} sets graphql.create.isNonNull: true and has create access control, this is not allowed.\n` + + 'Either disable graphql.create.isNonNull or create access control.' + ); + } +} diff --git a/packages/keystone/src/fields/types/checkbox/index.ts b/packages/keystone/src/fields/types/checkbox/index.ts index a163c8c632a..f835f258db1 100644 --- a/packages/keystone/src/fields/types/checkbox/index.ts +++ b/packages/keystone/src/fields/types/checkbox/index.ts @@ -1,6 +1,5 @@ import { BaseGeneratedListTypes, - FieldDefaultValue, CommonFieldConfig, fieldType, FieldTypeFunc, @@ -8,18 +7,21 @@ import { graphql, filters, } from '../../../types'; +import { assertCreateIsNonNullAllowed, assertReadIsNonNullAllowed } from '../../non-null-graphql'; import { resolveView } from '../../resolve-view'; export type CheckboxFieldConfig = CommonFieldConfig & { - defaultValue?: FieldDefaultValue; - isRequired?: boolean; + defaultValue?: boolean; + graphql?: { + read?: { isNonNull?: boolean }; + create?: { isNonNull?: boolean }; + }; }; export const checkbox = ({ - isRequired, - defaultValue, + defaultValue = false, ...config }: CheckboxFieldConfig = {}): FieldTypeFunc => meta => { @@ -27,19 +29,47 @@ export const checkbox = throw Error("isIndexed: 'unique' is not a supported option for field type checkbox"); } - return fieldType({ kind: 'scalar', mode: 'optional', scalar: 'Boolean' })({ + assertReadIsNonNullAllowed(meta, config); + assertCreateIsNonNullAllowed(meta, config); + + return fieldType({ + kind: 'scalar', + mode: 'required', + scalar: 'Boolean', + default: { kind: 'literal', value: defaultValue }, + })({ ...config, input: { - where: { - arg: graphql.arg({ type: filters[meta.provider].Boolean.optional }), - resolve: filters.resolveCommon, + where: { arg: graphql.arg({ type: filters[meta.provider].Boolean.required }) }, + create: { + arg: graphql.arg({ + type: config.graphql?.create?.isNonNull + ? graphql.nonNull(graphql.Boolean) + : graphql.Boolean, + defaultValue: config.graphql?.create?.isNonNull ? defaultValue : undefined, + }), + resolve(val) { + if (val === null) { + throw new Error('checkbox fields cannot be set to null'); + } + return val ?? defaultValue; + }, + }, + update: { + arg: graphql.arg({ type: graphql.Boolean }), + resolve(val) { + if (val === null) { + throw new Error('checkbox fields cannot be set to null'); + } + return val; + }, }, - create: { arg: graphql.arg({ type: graphql.Boolean }) }, - update: { arg: graphql.arg({ type: graphql.Boolean }) }, orderBy: { arg: graphql.arg({ type: orderDirectionEnum }) }, }, - output: graphql.field({ type: graphql.Boolean }), + output: graphql.field({ + type: config.graphql?.read?.isNonNull ? graphql.nonNull(graphql.Boolean) : graphql.Boolean, + }), views: resolveView('checkbox/views'), - __legacy: { isRequired, defaultValue }, + getAdminMeta: () => ({ defaultValue }), }); }; diff --git a/packages/keystone/src/fields/types/checkbox/tests/test-fixtures.ts b/packages/keystone/src/fields/types/checkbox/tests/test-fixtures.ts index d58aaeeb2cb..f94083b90b6 100644 --- a/packages/keystone/src/fields/types/checkbox/tests/test-fixtures.ts +++ b/packages/keystone/src/fields/types/checkbox/tests/test-fixtures.ts @@ -6,6 +6,8 @@ export const exampleValue = () => true; export const exampleValue2 = () => false; export const supportsUnique = false; export const fieldName = 'enabled'; +export const skipRequiredTest = true; +export const supportsGraphQLIsNonNull = true; export const getTestFields = () => ({ enabled: checkbox({ isFilterable: true }), @@ -18,8 +20,8 @@ export const initItems = () => { { name: 'person3', enabled: false }, { name: 'person4', enabled: true }, { name: 'person5', enabled: false }, - { name: 'person6', enabled: null }, - { name: 'person7', enabled: null }, + { name: 'person6', enabled: false }, + { name: 'person7', enabled: false }, ]; }; @@ -29,8 +31,8 @@ export const storedValues = () => [ { name: 'person3', enabled: false }, { name: 'person4', enabled: true }, { name: 'person5', enabled: false }, - { name: 'person6', enabled: null }, - { name: 'person7', enabled: null }, + { name: 'person6', enabled: false }, + { name: 'person7', enabled: false }, ]; -export const supportedFilters = () => ['null_equality', 'equality']; +export const supportedFilters = () => ['equality']; diff --git a/packages/keystone/src/fields/types/checkbox/views/index.tsx b/packages/keystone/src/fields/types/checkbox/views/index.tsx index 9caa3a8edd4..90ea2fb26d9 100644 --- a/packages/keystone/src/fields/types/checkbox/views/index.tsx +++ b/packages/keystone/src/fields/types/checkbox/views/index.tsx @@ -54,12 +54,14 @@ export const CardValue: CardValueComponent = ({ item, field }) => { type CheckboxController = FieldController; -export const controller = (config: FieldControllerConfig): CheckboxController => { +export const controller = ( + config: FieldControllerConfig<{ defaultValue: boolean }> +): CheckboxController => { return { path: config.path, label: config.label, graphqlSelection: config.path, - defaultValue: false, + defaultValue: config.fieldMeta.defaultValue, deserialize(item) { const value = item[config.path]; return typeof value === 'boolean' ? value : false; diff --git a/packages/keystone/src/scripts/tests/migrations.test.ts b/packages/keystone/src/scripts/tests/migrations.test.ts index 61e4d79cb21..07ae5861d3d 100644 --- a/packages/keystone/src/scripts/tests/migrations.test.ts +++ b/packages/keystone/src/scripts/tests/migrations.test.ts @@ -320,9 +320,9 @@ describe('useMigrations: true', () => { } model Todo { - id String @id + id String @id title String? - isComplete Boolean? + isComplete Boolean @default(false) } " `); @@ -330,8 +330,18 @@ describe('useMigrations: true', () => { const { migration, migrationName } = await getGeneratedMigration(tmp, 2, 'add_is_complete'); expect(migration).toMatchInlineSnapshot(` - "-- AlterTable - ALTER TABLE \\"Todo\\" ADD COLUMN \\"isComplete\\" BOOLEAN; + "-- RedefineTables + PRAGMA foreign_keys=OFF; + CREATE TABLE \\"new_Todo\\" ( + \\"id\\" TEXT NOT NULL PRIMARY KEY, + \\"title\\" TEXT, + \\"isComplete\\" BOOLEAN NOT NULL DEFAULT false + ); + INSERT INTO \\"new_Todo\\" (\\"id\\", \\"title\\") SELECT \\"id\\", \\"title\\" FROM \\"Todo\\"; + DROP TABLE \\"Todo\\"; + ALTER TABLE \\"new_Todo\\" RENAME TO \\"Todo\\"; + PRAGMA foreign_key_check; + PRAGMA foreign_keys=ON; " `); @@ -580,8 +590,18 @@ describe('useMigrations: true', () => { ); expect(await fs.readFile(`${tmp}/migrations/${migrationName}/migration.sql`, 'utf8')) .toMatchInlineSnapshot(` - "-- AlterTable - ALTER TABLE \\"Todo\\" ADD COLUMN \\"isComplete\\" BOOLEAN; + "-- RedefineTables + PRAGMA foreign_keys=OFF; + CREATE TABLE \\"new_Todo\\" ( + \\"id\\" TEXT NOT NULL PRIMARY KEY, + \\"title\\" TEXT, + \\"isComplete\\" BOOLEAN NOT NULL DEFAULT false + ); + INSERT INTO \\"new_Todo\\" (\\"id\\", \\"title\\") SELECT \\"id\\", \\"title\\" FROM \\"Todo\\"; + DROP TABLE \\"Todo\\"; + ALTER TABLE \\"new_Todo\\" RENAME TO \\"Todo\\"; + PRAGMA foreign_key_check; + PRAGMA foreign_keys=ON; " `); diff --git a/packages/keystone/src/types/next-fields.ts b/packages/keystone/src/types/next-fields.ts index 74f224de6d5..1891296c93c 100644 --- a/packages/keystone/src/types/next-fields.ts +++ b/packages/keystone/src/types/next-fields.ts @@ -292,7 +292,7 @@ export type CreateFieldInputArg< > = { arg: TArg; } & (TArg extends graphql.Arg - ? DBFieldToInputValue extends graphql.InferValueFromArg + ? graphql.InferValueFromArg extends DBFieldToInputValue ? { resolve?: CreateFieldInputResolver, TDBField>; } diff --git a/tests/api-tests/fields/non-null.test.ts b/tests/api-tests/fields/non-null.test.ts new file mode 100644 index 00000000000..01abe2fc236 --- /dev/null +++ b/tests/api-tests/fields/non-null.test.ts @@ -0,0 +1,109 @@ +import globby from 'globby'; +import { createSchema, list } from '@keystone-next/keystone'; +import { text } from '@keystone-next/keystone/fields'; +import { setupTestEnv } from '@keystone-next/keystone/testing'; +import { assertInputObjectType, assertObjectType, GraphQLNonNull } from 'graphql'; +import { apiTestConfig } from '../utils'; + +const testModules = globby.sync(`packages/**/src/**/test-fixtures.{js,ts}`, { + absolute: true, +}); +testModules + .map(require) + .filter( + ({ unSupportedAdapterList = [], name }) => + name !== 'ID' && !unSupportedAdapterList.includes(process.env.TEST_ADAPTER) + ) + .forEach(mod => { + (mod.testMatrix || ['default']).forEach((matrixValue: string) => { + describe(`${mod.name} - ${matrixValue} - graphql.isNonNull`, () => { + beforeEach(() => { + if (mod.beforeEach) { + mod.beforeEach(); + } + }); + afterEach(async () => { + if (mod.afterEach) { + await mod.afterEach(); + } + }); + beforeAll(() => { + if (mod.beforeAll) { + mod.beforeAll(); + } + }); + afterAll(async () => { + if (mod.afterAll) { + await mod.afterAll(); + } + }); + + const getSchema = async (fieldConfig: any) => { + const { testArgs } = await setupTestEnv({ + config: apiTestConfig({ + lists: createSchema({ + Test: list({ + fields: { + name: text(), + testField: mod.typeFunction({ + ...(mod.fieldConfig ? mod.fieldConfig(matrixValue) : {}), + ...fieldConfig, + }), + }, + }), + }), + images: { upload: 'local', local: { storagePath: 'tmp_test_images' } }, + files: { upload: 'local', local: { storagePath: 'tmp_test_files' } }, + }), + }); + return testArgs.context.graphql.schema; + }; + + if (mod.supportsGraphQLIsNonNull) { + test('Sets the output field as non-null when graphql.read.isNonNull is set', async () => { + const schema = await getSchema({ graphql: { read: { isNonNull: true } } }); + const outputType = assertObjectType(schema.getType('Test')); + expect(outputType.getFields().testField.type).toBeInstanceOf(GraphQLNonNull); + }); + test('Throws when graphql.read.isNonNull and read access control is set', async () => { + const error = await getSchema({ + graphql: { read: { isNonNull: true } }, + access: { read: () => false }, + }).catch(x => x); + expect(error).toBeInstanceOf(Error); + expect(error.message).toEqual( + `The field at Test.testField sets graphql.read.isNonNull: true and has read access control, this is not allowed.\n` + + `Either disable graphql.read.isNonNull or read access control.` + ); + }); + test('Sets the create field as non-null when graphql.create.isNonNull is set', async () => { + const schema = await getSchema({ graphql: { create: { isNonNull: true } } }); + const createType = assertInputObjectType(schema.getType('TestCreateInput')); + expect(createType.getFields().testField.type).toBeInstanceOf(GraphQLNonNull); + }); + test('Throws when graphql.create.isNonNull and create access control is set', async () => { + const error = await getSchema({ + graphql: { create: { isNonNull: true } }, + access: { create: () => false }, + }).catch(x => x); + expect(error).toBeInstanceOf(Error); + expect(error.message).toEqual( + `The field at Test.testField sets graphql.create.isNonNull: true and has create access control, this is not allowed.\n` + + `Either disable graphql.create.isNonNull or create access control.` + ); + }); + } + + test("Output field is nullable when graphql.read.isNonNull isn't set", async () => { + const schema = await getSchema({}); + const outputType = assertObjectType(schema.getType('Test')); + expect(outputType.getFields().testField.type).not.toBeInstanceOf(GraphQLNonNull); + }); + test("Create field is nullable when graphql.create.isNonNull isn't set", async () => { + const schema = await getSchema({}); + const createType = assertInputObjectType(schema.getType('TestCreateInput')); + expect(createType.getFields().testField.type).not.toBeInstanceOf(GraphQLNonNull); + }); + }); + }); + }); diff --git a/tests/test-projects/basic/schema.graphql b/tests/test-projects/basic/schema.graphql index 7cb29237157..0ebede15c7f 100644 --- a/tests/test-projects/basic/schema.graphql +++ b/tests/test-projects/basic/schema.graphql @@ -27,7 +27,7 @@ input TaskWhereInput { id: IDFilter label: StringNullableFilter priority: TaskPriorityTypeNullableFilter - isComplete: BooleanNullableFilter + isComplete: BooleanFilter assignedTo: PersonWhereInput finishBy: DateTimeNullableFilter } @@ -78,9 +78,9 @@ input TaskPriorityTypeNullableFilter { not: TaskPriorityTypeNullableFilter } -input BooleanNullableFilter { +input BooleanFilter { equals: Boolean - not: BooleanNullableFilter + not: BooleanFilter } input DateTimeNullableFilter { diff --git a/tests/test-projects/basic/schema.prisma b/tests/test-projects/basic/schema.prisma index 981c3d6d1fb..9d04fa76402 100644 --- a/tests/test-projects/basic/schema.prisma +++ b/tests/test-projects/basic/schema.prisma @@ -15,7 +15,7 @@ model Task { id String @id @default(cuid()) label String? priority String? - isComplete Boolean? + isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) assignedToId String? @map("assignedTo") finishBy DateTime? diff --git a/tests/test-projects/crud-notifications/schema.graphql b/tests/test-projects/crud-notifications/schema.graphql index 986be342995..546de8db159 100644 --- a/tests/test-projects/crud-notifications/schema.graphql +++ b/tests/test-projects/crud-notifications/schema.graphql @@ -27,7 +27,7 @@ input TaskWhereInput { id: IDFilter label: StringNullableFilter priority: TaskPriorityTypeNullableFilter - isComplete: BooleanNullableFilter + isComplete: BooleanFilter assignedTo: PersonWhereInput finishBy: DateTimeNullableFilter } @@ -78,9 +78,9 @@ input TaskPriorityTypeNullableFilter { not: TaskPriorityTypeNullableFilter } -input BooleanNullableFilter { +input BooleanFilter { equals: Boolean - not: BooleanNullableFilter + not: BooleanFilter } input DateTimeNullableFilter { diff --git a/tests/test-projects/crud-notifications/schema.prisma b/tests/test-projects/crud-notifications/schema.prisma index 78461ee962d..872f311ea90 100644 --- a/tests/test-projects/crud-notifications/schema.prisma +++ b/tests/test-projects/crud-notifications/schema.prisma @@ -15,7 +15,7 @@ model Task { id String @id @default(cuid()) label String? priority String? - isComplete Boolean? + isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) assignedToId String? @map("assignedTo") finishBy DateTime?