diff --git a/.changeset/dirty-crabs-bathe.md b/.changeset/dirty-crabs-bathe.md new file mode 100644 index 00000000000..cae33132504 --- /dev/null +++ b/.changeset/dirty-crabs-bathe.md @@ -0,0 +1,5 @@ +--- +'@keystone-next/keystone': major +--- + +In the `text` field, `defaultValue` is now a static value, `isRequired` has moved to `validation.isRequired` and also requires that the value has a length of at least one, along with new `validation.lenght.min`, `validation.length.max` and `validation.match` options. The `text` field is also now non-nullable at the database-level by default and can be made nullable by setting the `isNullable` option to `true`. `graphql.read.isNonNull` can also be set if the field does not have `isNullable: true` and 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. `graphql.create.isNonNull` can also be set if you have no create access control and you don't intend to add any in the future, it will make the GraphQL create input field non-nullable. diff --git a/docs/pages/docs/apis/fields.mdx b/docs/pages/docs/apis/fields.mdx index 055afb67135..ccd3370aac5 100644 --- a/docs/pages/docs/apis/fields.mdx +++ b/docs/pages/docs/apis/fields.mdx @@ -385,7 +385,7 @@ Options: `value` is either a `string` (for `{ type: 'string' }` or `{ type: 'enum' }`), or a `number` (for `{ type: 'integer' }`). The `value` will be used in the GraphQL API and stored in the database. - `isNullable` (default: `true`): If `false` then this field will be made non-nullable in the database and it will never be possible to set as `null`. -- `defaultValue` (default: `false`): This value will be used for the field when creating items if no explicit value is set. +- `defaultValue` (default: `undefined`): This value will be used for the field when creating items if no explicit value is set. - `validation.isRequired` (default: `false`): If `true` then this field can never be set to `null`. Unlike `isNullable`, this will require that a value is provided in the Admin UI. It will also validate this when creating and updating an item through the GraphQL API but it will not enforce it at the database level. @@ -439,16 +439,30 @@ A `text` field represents a string value. Options: -- `defaultValue` (default: `undefined`): Can be either a string value or an async function which takes an argument `({ context, originalInput })` and returns a string 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`. +- `isNullable` (default: `false`): If `true` then this field will be made nullable in the database and it will be possible to set as `null`. +- `defaultValue` (default: `isNullable === true ? undefined : ''`): This value will be used for the field when creating items if no explicit value is set. +- `validation.isRequired` (default: `false`): If `true` then this field can never be set to `null` or an empty string. + Unlike `isNullable`, this will require that a value with at least 1 character is provided in the Admin UI. + It will also validate this when creating and updating an item through the GraphQL API but it will not enforce it at the database level. + If you would like to enforce it being non-null at the database-level and in the Admin UI, you can set both `isNullable: false` and `validation.isRequired: true`. +- `validation.length.min` (default: `0`): This describes the minimum number allowed. If you attempt to submit a string shorter than this, you will get a validation error. +- `validation.length.max` (default: `undefined`): This describes the maximum length allowed. If you attempt to submit a string longer than this, you will get a validation error. +- `validation.match` (default: `undefined`): This describes a pattern that values for this field must match + - `validation.match.regex`: The regular expression + - `validation.match.explanation` (default: `${fieldLabel} must match ${validation.match.regex}`): A message shown in the Admin when a value doesn't match the regex and returned as a validation error from the GraphQL API - `isIndexed` (default: `false`) - If `true` then this field will be indexed by the database. - If `'unique'` then all values of this field must be unique. - `ui` (default: `{ displayMode: 'input' }`): Configures the display mode of the field in the Admin UI. Can be one of `['input', 'textarea']`. +- `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, list } from '@keystone-next/keystone'; diff --git a/docs/pages/docs/guides/hooks.mdx b/docs/pages/docs/guides/hooks.mdx index 4e7a078d36c..66cb27bf7dc 100644 --- a/docs/pages/docs/guides/hooks.mdx +++ b/docs/pages/docs/guides/hooks.mdx @@ -61,8 +61,8 @@ export default config({ lists: { Post: list({ fields: { - title: text({ isRequired: true }), - content: text({ isRequired: true }), + title: text({ validation: { isRequired: true } }), + content: text({ validation: { isRequired: true } }), }, hooks: { resolveInput: ({ resolvedData }) => { @@ -108,8 +108,8 @@ export default config({ lists: { Post: list({ fields: { - title: text({ isRequired: true }), - content: text({ isRequired: true }), + title: text({ validation: { isRequired: true } }), + content: text({ validation: { isRequired: true } }), }, hooks: { validateInput: ({ resolvedData, addValidationError }) => { @@ -207,6 +207,7 @@ export default config({ fields: { name: text(), email: text({ + isNullable: true, hooks: { validateInput: ({ addValidationError, resolvedData, fieldKey }) => { const email = resolvedData[fieldKey]; diff --git a/docs/pages/docs/guides/testing.mdx b/docs/pages/docs/guides/testing.mdx index 7c747ab8602..311e0b1f326 100644 --- a/docs/pages/docs/guides/testing.mdx +++ b/docs/pages/docs/guides/testing.mdx @@ -98,7 +98,7 @@ runner(async ({ context }) => { expect(errors).toHaveLength(1); expect(errors[0].path).toEqual(['createPerson']); expect(errors[0].message).toEqual( - 'You provided invalid data for this operation.\n - Person.name: Required field "name" is null or undefined.' + 'You provided invalid data for this operation.\n - Person.name: Name must not be empty' ); }) ``` diff --git a/docs/pages/docs/guides/virtual-fields.mdx b/docs/pages/docs/guides/virtual-fields.mdx index bc08bc1a541..95332674003 100644 --- a/docs/pages/docs/guides/virtual-fields.mdx +++ b/docs/pages/docs/guides/virtual-fields.mdx @@ -108,7 +108,7 @@ export default config({ }), Author: list({ fields: { - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), }, }), }, @@ -273,8 +273,8 @@ export const lists = { }), Author: list({ fields: { - name: text({ isRequired: true }), - email: text({ isRequired: true, isIndexed: 'unique' }), + name: text({ validation: { isRequired: true } }), + email: text({ isIndexed: 'unique', validation: { isRequired: true } }), posts: relationship({ ref: 'Post.author', many: true }), latestPost: virtual({ field: lists => diff --git a/docs/pages/docs/walkthroughs/embedded-mode-with-sqlite-nextjs.mdx b/docs/pages/docs/walkthroughs/embedded-mode-with-sqlite-nextjs.mdx index 8ba41dbada8..b64b152ed78 100644 --- a/docs/pages/docs/walkthroughs/embedded-mode-with-sqlite-nextjs.mdx +++ b/docs/pages/docs/walkthroughs/embedded-mode-with-sqlite-nextjs.mdx @@ -108,7 +108,7 @@ import { text } from '@keystone-next/fields'; const Post = list({ fields: { - title: text({ isRequired: true }), + title: text({ validation: { isRequired: true } }), slug: text(), content: text(), }, diff --git a/docs/pages/index.tsx b/docs/pages/index.tsx index d0531c1ec4d..e7c7f71639f 100644 --- a/docs/pages/index.tsx +++ b/docs/pages/index.tsx @@ -420,7 +420,7 @@ import { document, text, timestamp, password, relationship } from '@keystone-nex export const lists = { Post: list({ fields: { - title: text({ isRequired: true }), + title: text({ validation: { isRequired: true } }), content: document(), publishDate: timestamp(), author: relationship({ ref: 'Author.posts', many: false }), @@ -428,8 +428,8 @@ export const lists = { }), Author: list({ fields: { - name: text({ isRequired: true }), - email: text({ isRequired: true, isIndexed: 'unique' }), + name: text({ validation: { isRequired: true } }), + email: text({ isIndexed: 'unique', validation: { isRequired: true } }), password: password(), posts: relationship({ ref: 'Post.author', many: true }), }, diff --git a/docs/pages/updates/new-graphql-api.mdx b/docs/pages/updates/new-graphql-api.mdx index 7617f74c4b2..773897c6e50 100644 --- a/docs/pages/updates/new-graphql-api.mdx +++ b/docs/pages/updates/new-graphql-api.mdx @@ -17,7 +17,7 @@ To illustrate the changes, we’ll refer to the `Task` list in the following sch export const lists = { Task: list({ fields: { - label: text({ isRequired: true }), + label: text({ validation: { isRequired: true } }), priority: select({ type: 'enum', options: [ @@ -34,7 +34,7 @@ export const lists = { }), Person: list({ fields: { - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), tasks: relationship({ ref: 'Task.assignedTo', many: true }), }, }), diff --git a/examples-staging/assets-cloud/schema.prisma b/examples-staging/assets-cloud/schema.prisma index ebffa8603da..fa2202dc8f8 100644 --- a/examples-staging/assets-cloud/schema.prisma +++ b/examples-staging/assets-cloud/schema.prisma @@ -14,9 +14,9 @@ generator client { model Post { id String @id @default(cuid()) - title String? + title String @default("") status String? - content String? + content String @default("") publishDate DateTime? author Author? @relation("Post_author", fields: [authorId], references: [id]) authorId String? @map("author") @@ -34,8 +34,8 @@ model Post { } model Author { - id String @id @default(cuid()) - name String? - email String? @unique - posts Post[] @relation("Post_author") + id String @id @default(cuid()) + name String @default("") + email String @unique @default("") + posts Post[] @relation("Post_author") } \ No newline at end of file diff --git a/examples-staging/assets-cloud/schema.ts b/examples-staging/assets-cloud/schema.ts index d6a054bf1e2..e36387bdd7e 100644 --- a/examples-staging/assets-cloud/schema.ts +++ b/examples-staging/assets-cloud/schema.ts @@ -4,7 +4,7 @@ import { select, relationship, text, timestamp, image, file } from '@keystone-ne export const lists = { Post: list({ fields: { - title: text({ isRequired: true }), + title: text({ validation: { isRequired: true } }), status: select({ type: 'enum', options: [ @@ -21,8 +21,8 @@ export const lists = { }), Author: list({ fields: { - name: text({ isRequired: true }), - email: text({ isRequired: true, isIndexed: 'unique' }), + name: text({ validation: { isRequired: true } }), + email: text({ isIndexed: 'unique', validation: { isRequired: true } }), posts: relationship({ ref: 'Post.author', many: true }), }, }), diff --git a/examples-staging/assets-local/schema.prisma b/examples-staging/assets-local/schema.prisma index a83bc0baf8a..19520cff0a8 100644 --- a/examples-staging/assets-local/schema.prisma +++ b/examples-staging/assets-local/schema.prisma @@ -14,9 +14,9 @@ generator client { model Post { id String @id @default(cuid()) - title String? + title String @default("") status String? - content String? + content String @default("") publishDate DateTime? author Author? @relation("Post_author", fields: [authorId], references: [id]) authorId String? @map("author") @@ -31,8 +31,8 @@ model Post { } model Author { - id String @id @default(cuid()) - name String? - email String? @unique - posts Post[] @relation("Post_author") + id String @id @default(cuid()) + name String @default("") + email String @unique @default("") + posts Post[] @relation("Post_author") } \ No newline at end of file diff --git a/examples-staging/assets-local/schema.ts b/examples-staging/assets-local/schema.ts index d61a171eaff..5b6ec1ed5d6 100644 --- a/examples-staging/assets-local/schema.ts +++ b/examples-staging/assets-local/schema.ts @@ -4,7 +4,7 @@ import { select, relationship, text, timestamp, image } from '@keystone-next/key export const lists = { Post: list({ fields: { - title: text({ isRequired: true }), + title: text({ validation: { isRequired: true } }), status: select({ type: 'enum', options: [ @@ -20,8 +20,8 @@ export const lists = { }), Author: list({ fields: { - name: text({ isRequired: true }), - email: text({ isRequired: true, isIndexed: 'unique' }), + name: text({ validation: { isRequired: true } }), + email: text({ isIndexed: 'unique', validation: { isRequired: true } }), posts: relationship({ ref: 'Post.author', many: true }), }, }), diff --git a/examples-staging/auth/schema.graphql b/examples-staging/auth/schema.graphql index 3d5b4a07377..480c23ff172 100644 --- a/examples-staging/auth/schema.graphql +++ b/examples-staging/auth/schema.graphql @@ -61,7 +61,7 @@ input UserWhereInput { OR: [UserWhereInput!] NOT: [UserWhereInput!] id: IDFilter - email: StringNullableFilter + email: StringFilter } input IDFilter { @@ -75,7 +75,7 @@ input IDFilter { not: IDFilter } -input StringNullableFilter { +input StringFilter { equals: String in: [String!] notIn: [String!] @@ -86,10 +86,10 @@ input StringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } -input NestedStringNullableFilter { +input NestedStringFilter { equals: String in: [String!] notIn: [String!] @@ -100,7 +100,7 @@ input NestedStringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } input UserOrderByInput { diff --git a/examples-staging/auth/schema.prisma b/examples-staging/auth/schema.prisma index dbc982a1f25..010f4b244b5 100644 --- a/examples-staging/auth/schema.prisma +++ b/examples-staging/auth/schema.prisma @@ -14,8 +14,8 @@ generator client { model User { id String @id @default(cuid()) - name String? - email String? @unique + name String @default("") + email String @unique @default("") password String? isAdmin Boolean @default(false) } \ No newline at end of file diff --git a/examples-staging/auth/schema.ts b/examples-staging/auth/schema.ts index 0e9c2321417..b06576a7c83 100644 --- a/examples-staging/auth/schema.ts +++ b/examples-staging/auth/schema.ts @@ -19,9 +19,9 @@ export const lists = { }, fields: { // The user's name - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), // The user's email address, used as the identity field for auth - email: text({ isRequired: true, isIndexed: 'unique', isFilterable: true }), + email: text({ isIndexed: 'unique', isFilterable: true, validation: { isRequired: true } }), // The user's password, used as the secret field for auth password: password({ access: { diff --git a/examples-staging/basic/admin/fieldViews/Test.tsx b/examples-staging/basic/admin/fieldViews/Test.tsx index 72ccc49bcf2..25dfcbe72c1 100644 --- a/examples-staging/basic/admin/fieldViews/Test.tsx +++ b/examples-staging/basic/admin/fieldViews/Test.tsx @@ -13,14 +13,18 @@ export const Field = ({ field, value, onChange, autoFocus }: FieldProps onChange(event.target.value)} - value={value} + onChange={event => { + onChange({ ...value, inner: { kind: 'value', value: event.target.value } }); + }} + value={value.inner.kind === 'null' ? '' : value.inner.value} /> ) : ( onChange(event.target.value)} - value={value} + onChange={event => { + onChange({ ...value, inner: { kind: 'value', value: event.target.value } }); + }} + value={value.inner.kind === 'null' ? '' : value.inner.value} /> ) ) : ( diff --git a/examples-staging/basic/schema.graphql b/examples-staging/basic/schema.graphql index b12d7dd167f..3ee1c953048 100644 --- a/examples-staging/basic/schema.graphql +++ b/examples-staging/basic/schema.graphql @@ -141,7 +141,7 @@ input UserWhereInput { OR: [UserWhereInput!] NOT: [UserWhereInput!] id: IDFilter - email: StringNullableFilter + email: StringFilter } input IDFilter { @@ -155,7 +155,7 @@ input IDFilter { not: IDFilter } -input StringNullableFilter { +input StringFilter { equals: String in: [String!] notIn: [String!] @@ -166,10 +166,10 @@ input StringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } -input NestedStringNullableFilter { +input NestedStringFilter { equals: String in: [String!] notIn: [String!] @@ -180,7 +180,7 @@ input NestedStringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } input UserOrderByInput { diff --git a/examples-staging/basic/schema.prisma b/examples-staging/basic/schema.prisma index 471cf0512fa..818f7049960 100644 --- a/examples-staging/basic/schema.prisma +++ b/examples-staging/basic/schema.prisma @@ -14,8 +14,8 @@ generator client { model User { id String @id @default(cuid()) - name String? - email String? @unique + name String @default("") + email String @unique @default("") avatar_filesize Int? avatar_extension String? avatar_width Int? @@ -27,7 +27,7 @@ model User { attachment_filename String? password String? isAdmin Boolean @default(false) - roles String? + roles String @default("") phoneNumbers PhoneNumber[] @relation("PhoneNumber_user") posts Post[] @relation("Post_author") } @@ -37,14 +37,14 @@ model PhoneNumber { user User? @relation("PhoneNumber_user", fields: [userId], references: [id]) userId String? @map("user") type String? - value String? + value String @default("") @@index([userId]) } model Post { id String @id @default(cuid()) - title String? + title String @default("") status String @default("draft") content String @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]") publishDate DateTime? diff --git a/examples-staging/basic/schema.ts b/examples-staging/basic/schema.ts index 16e9a8c1d5b..578883cb4a7 100644 --- a/examples-staging/basic/schema.ts +++ b/examples-staging/basic/schema.ts @@ -43,9 +43,9 @@ export const lists = { }, fields: { /** The user's first and last name. */ - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), /** Email is used to log into the system. */ - email: text({ isRequired: true, isIndexed: 'unique', isFilterable: true }), + email: text({ isIndexed: 'unique', isFilterable: true, validation: { isRequired: true } }), /** Avatar upload for the users profile, stored locally */ avatar: image(), attachment: file(), diff --git a/examples-staging/ecommerce/schema.graphql b/examples-staging/ecommerce/schema.graphql index 5e70f6ada39..f86c7d595df 100644 --- a/examples-staging/ecommerce/schema.graphql +++ b/examples-staging/ecommerce/schema.graphql @@ -157,7 +157,7 @@ input UserWhereInput { OR: [UserWhereInput!] NOT: [UserWhereInput!] id: IDFilter - email: StringNullableFilter + email: StringFilter } input IDFilter { @@ -171,7 +171,7 @@ input IDFilter { not: IDFilter } -input StringNullableFilter { +input StringFilter { equals: String in: [String!] notIn: [String!] @@ -182,10 +182,10 @@ input StringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } -input NestedStringNullableFilter { +input NestedStringFilter { equals: String in: [String!] notIn: [String!] @@ -196,7 +196,7 @@ input NestedStringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } input UserOrderByInput { diff --git a/examples-staging/ecommerce/schema.prisma b/examples-staging/ecommerce/schema.prisma index a0616c65df7..4ac377ec39d 100644 --- a/examples-staging/ecommerce/schema.prisma +++ b/examples-staging/ecommerce/schema.prisma @@ -14,8 +14,8 @@ generator client { model User { id String @id @default(cuid()) - name String? - email String? @unique + name String @default("") + email String @unique @default("") password String? cart CartItem[] @relation("CartItem_user") orders Order[] @relation("Order_user") @@ -31,8 +31,8 @@ model User { model Product { id String @id @default(cuid()) - name String? - description String? + name String @default("") + description String @default("") photo ProductImage? @relation("Product_photo", fields: [photoId], references: [id]) photoId String? @unique @map("photo") status String? @default("DRAFT") @@ -47,7 +47,7 @@ model Product { model ProductImage { id String @id @default(cuid()) image String? - altText String? + altText String @default("") product Product? @relation("Product_photo") from_OrderItem_photo OrderItem[] @relation("OrderItem_photo") } @@ -66,8 +66,8 @@ model CartItem { model OrderItem { id String @id @default(cuid()) - name String? - description String? + name String @default("") + description String @default("") photo ProductImage? @relation("OrderItem_photo", fields: [photoId], references: [id]) photoId String? @map("photo") price Int? @@ -85,14 +85,14 @@ model Order { items OrderItem[] @relation("OrderItem_order") user User? @relation("Order_user", fields: [userId], references: [id]) userId String? @map("user") - charge String? + charge String @default("") @@index([userId]) } model Role { id String @id @default(cuid()) - name String? + name String @default("") canManageProducts Boolean @default(false) canSeeOtherUsers Boolean @default(false) canManageUsers Boolean @default(false) diff --git a/examples-staging/ecommerce/schemas/OrderItem.ts b/examples-staging/ecommerce/schemas/OrderItem.ts index a65f9237e32..2b0697475aa 100644 --- a/examples-staging/ecommerce/schemas/OrderItem.ts +++ b/examples-staging/ecommerce/schemas/OrderItem.ts @@ -14,7 +14,7 @@ export const OrderItem = list({ }, }, fields: { - name: text({ isRequired: true, isOrderable: true }), + name: text({ validation: { isRequired: true }, isOrderable: true }), description: text({ ui: { displayMode: 'textarea', diff --git a/examples-staging/ecommerce/schemas/Product.ts b/examples-staging/ecommerce/schemas/Product.ts index 0ce607c0206..f8d7df4bbf3 100644 --- a/examples-staging/ecommerce/schemas/Product.ts +++ b/examples-staging/ecommerce/schemas/Product.ts @@ -14,7 +14,7 @@ export const Product = list({ }, }, fields: { - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), description: text({ ui: { displayMode: 'textarea', diff --git a/examples-staging/ecommerce/schemas/Role.ts b/examples-staging/ecommerce/schemas/Role.ts index 78ab5f2b57c..06f1308e24c 100644 --- a/examples-staging/ecommerce/schemas/Role.ts +++ b/examples-staging/ecommerce/schemas/Role.ts @@ -18,7 +18,7 @@ export const Role = list({ isHidden: args => !permissions.canManageRoles(args), }, fields: { - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), ...permissionFields, assignedTo: relationship({ ref: 'User.role', // TODO: Add this to the User diff --git a/examples-staging/ecommerce/schemas/User.ts b/examples-staging/ecommerce/schemas/User.ts index 6b205ba166a..75a62965ae6 100644 --- a/examples-staging/ecommerce/schemas/User.ts +++ b/examples-staging/ecommerce/schemas/User.ts @@ -21,8 +21,8 @@ export const User = list({ hideDelete: args => !permissions.canManageUsers(args), }, fields: { - name: text({ isRequired: true }), - email: text({ isRequired: true, isIndexed: 'unique', isFilterable: true }), + name: text({ validation: { isRequired: true } }), + email: text({ isIndexed: 'unique', isFilterable: true, validation: { isRequired: true } }), password: password(), cart: relationship({ ref: 'CartItem.user', diff --git a/examples-staging/embedded-nextjs/schema.prisma b/examples-staging/embedded-nextjs/schema.prisma index df85c87d985..7034342dccf 100644 --- a/examples-staging/embedded-nextjs/schema.prisma +++ b/examples-staging/embedded-nextjs/schema.prisma @@ -13,8 +13,8 @@ generator client { } model Post { - id String @id @default(cuid()) - title String? - slug String? - content String? + id String @id @default(cuid()) + title String @default("") + slug String @default("") + content String @default("") } \ No newline at end of file diff --git a/examples-staging/embedded-nextjs/schema.ts b/examples-staging/embedded-nextjs/schema.ts index 68b772c2059..a0802047699 100644 --- a/examples-staging/embedded-nextjs/schema.ts +++ b/examples-staging/embedded-nextjs/schema.ts @@ -3,7 +3,7 @@ import { text } from '@keystone-next/keystone/fields'; export const Post = list({ fields: { - title: text({ isRequired: true }), + title: text({ validation: { isRequired: true } }), slug: text(), content: text(), }, diff --git a/examples-staging/graphql-api-endpoint/schema.graphql b/examples-staging/graphql-api-endpoint/schema.graphql index 7c9ee4cd0e6..d3c969f4fa5 100644 --- a/examples-staging/graphql-api-endpoint/schema.graphql +++ b/examples-staging/graphql-api-endpoint/schema.graphql @@ -79,7 +79,7 @@ input UserWhereInput { OR: [UserWhereInput!] NOT: [UserWhereInput!] id: IDFilter - email: StringNullableFilter + email: StringFilter } input IDFilter { @@ -93,7 +93,7 @@ input IDFilter { not: IDFilter } -input StringNullableFilter { +input StringFilter { equals: String in: [String!] notIn: [String!] @@ -105,7 +105,7 @@ input StringNullableFilter { startsWith: String endsWith: String mode: QueryMode - not: NestedStringNullableFilter + not: NestedStringFilter } enum QueryMode { @@ -113,7 +113,7 @@ enum QueryMode { insensitive } -input NestedStringNullableFilter { +input NestedStringFilter { equals: String in: [String!] notIn: [String!] @@ -124,7 +124,7 @@ input NestedStringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } input UserOrderByInput { diff --git a/examples-staging/graphql-api-endpoint/schema.prisma b/examples-staging/graphql-api-endpoint/schema.prisma index 07c9890ef6a..93657eb7c2e 100644 --- a/examples-staging/graphql-api-endpoint/schema.prisma +++ b/examples-staging/graphql-api-endpoint/schema.prisma @@ -14,15 +14,15 @@ generator client { model User { id String @id @default(cuid()) - name String? - email String? @unique + name String @default("") + email String @unique @default("") password String? posts Post[] @relation("Post_author") } model Post { id String @id @default(cuid()) - title String? + title String @default("") status String? content Json @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]") publishDate DateTime? @@ -34,7 +34,7 @@ model Post { } model Tag { - id String @id @default(cuid()) - name String? - posts Post[] @relation("Post_tags_Tag_posts") + id String @id @default(cuid()) + name String @default("") + posts Post[] @relation("Post_tags_Tag_posts") } \ No newline at end of file diff --git a/examples-staging/graphql-api-endpoint/schema.ts b/examples-staging/graphql-api-endpoint/schema.ts index 448103f25de..ae86e032292 100644 --- a/examples-staging/graphql-api-endpoint/schema.ts +++ b/examples-staging/graphql-api-endpoint/schema.ts @@ -10,8 +10,8 @@ export const lists = { }, }, fields: { - name: text({ isRequired: true }), - email: text({ isRequired: true, isIndexed: 'unique', isFilterable: true }), + name: text({ validation: { isRequired: true } }), + email: text({ isIndexed: 'unique', isFilterable: true, validation: { isRequired: true } }), password: password(), posts: relationship({ ref: 'Post.author', many: true }), }, diff --git a/examples-staging/roles/schema.graphql b/examples-staging/roles/schema.graphql index 7a4a22ae36a..025546c22ef 100644 --- a/examples-staging/roles/schema.graphql +++ b/examples-staging/roles/schema.graphql @@ -149,10 +149,10 @@ input PersonWhereInput { OR: [PersonWhereInput!] NOT: [PersonWhereInput!] id: IDFilter - email: StringNullableFilter + email: StringFilter } -input StringNullableFilter { +input StringFilter { equals: String in: [String!] notIn: [String!] @@ -163,10 +163,10 @@ input StringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } -input NestedStringNullableFilter { +input NestedStringFilter { equals: String in: [String!] notIn: [String!] @@ -177,7 +177,7 @@ input NestedStringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } input PersonOrderByInput { diff --git a/examples-staging/roles/schema.prisma b/examples-staging/roles/schema.prisma index 826400248cc..1c678576858 100644 --- a/examples-staging/roles/schema.prisma +++ b/examples-staging/roles/schema.prisma @@ -14,7 +14,7 @@ generator client { model Todo { id String @id @default(cuid()) - label String? + label String @default("") isComplete Boolean @default(false) isPrivate Boolean @default(false) assignedTo Person? @relation("Todo_assignedTo", fields: [assignedToId], references: [id]) @@ -25,8 +25,8 @@ model Todo { model Person { id String @id @default(cuid()) - name String? - email String? @unique + name String @default("") + email String @unique @default("") password String? role Role? @relation("Person_role", fields: [roleId], references: [id]) roleId String? @map("role") @@ -37,7 +37,7 @@ model Person { model Role { id String @id @default(cuid()) - name String? + name String @default("") canCreateTodos Boolean @default(false) canManageAllTodos Boolean @default(false) canSeeOtherPeople Boolean @default(false) diff --git a/examples-staging/roles/schema.ts b/examples-staging/roles/schema.ts index cd8f557abff..83d4f6beb86 100644 --- a/examples-staging/roles/schema.ts +++ b/examples-staging/roles/schema.ts @@ -48,7 +48,7 @@ export const lists = { }, fields: { /* The label of the todo item */ - label: text({ isRequired: true }), + label: text({ validation: { isRequired: true } }), /* Whether the todo item is complete */ isComplete: checkbox({ defaultValue: false }), /* Private todo items are only visible to the user they are assigned to */ @@ -118,9 +118,9 @@ export const lists = { }, fields: { /* The name of the user */ - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), /* The email of the user, used to sign in */ - email: text({ isRequired: true, isIndexed: 'unique', isFilterable: true }), + email: text({ isIndexed: 'unique', isFilterable: true, validation: { isRequired: true } }), /* The password of the user */ password: password({ isRequired: true, @@ -197,7 +197,7 @@ export const lists = { }, fields: { /* The name of the role */ - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), /* Create Todos means: - create todos (can only assign them to others with canManageAllTodos) */ canCreateTodos: checkbox({ defaultValue: false }), diff --git a/examples-staging/sandbox/schema.prisma b/examples-staging/sandbox/schema.prisma index 9739b48acbe..8a01b6dc2f2 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? + label String @default("") isComplete Boolean @default(false) assignedTo User? @relation("Todo_assignedTo", fields: [assignedToId], references: [id]) assignedToId String? @map("assignedTo") @@ -27,8 +27,8 @@ model Todo { model User { id String @id @default(cuid()) - name String? - email String? + name String @default("") + email String @default("") password String? tasks Todo[] @relation("Todo_assignedTo") createdAt DateTime? diff --git a/examples-staging/sandbox/schema.ts b/examples-staging/sandbox/schema.ts index 18db3479f60..68b405dd9c6 100644 --- a/examples-staging/sandbox/schema.ts +++ b/examples-staging/sandbox/schema.ts @@ -52,7 +52,7 @@ export const lists = { }, }, fields: { - label: text({ isRequired: true }), + label: text({ validation: { isRequired: true } }), isComplete: checkbox(), assignedTo: relationship({ ref: 'User.tasks' }), finishBy: timestamp(), @@ -66,7 +66,7 @@ export const lists = { }, }, fields: { - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), email: text(), password: password(), tasks: relationship({ diff --git a/examples/blog/schema.graphql b/examples/blog/schema.graphql index 8cb5cbfc0a5..5dbc12c4c96 100644 --- a/examples/blog/schema.graphql +++ b/examples/blog/schema.graphql @@ -24,7 +24,7 @@ input PostWhereInput { OR: [PostWhereInput!] NOT: [PostWhereInput!] id: IDFilter - title: StringNullableFilter + title: StringFilter } input IDFilter { @@ -38,7 +38,7 @@ input IDFilter { not: IDFilter } -input StringNullableFilter { +input StringFilter { equals: String in: [String!] notIn: [String!] @@ -49,10 +49,10 @@ input StringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } -input NestedStringNullableFilter { +input NestedStringFilter { equals: String in: [String!] notIn: [String!] @@ -63,7 +63,7 @@ input NestedStringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } input PostOrderByInput { diff --git a/examples/blog/schema.prisma b/examples/blog/schema.prisma index 74c0e34a2e2..169b647328a 100644 --- a/examples/blog/schema.prisma +++ b/examples/blog/schema.prisma @@ -14,9 +14,9 @@ generator client { model Post { id String @id @default(cuid()) - title String? + title String @default("") status String? - content String? + content String @default("") publishDate DateTime? author Author? @relation("Post_author", fields: [authorId], references: [id]) authorId String? @map("author") @@ -25,8 +25,8 @@ model Post { } model Author { - id String @id @default(cuid()) - name String? - email String? @unique - posts Post[] @relation("Post_author") + id String @id @default(cuid()) + name String @default("") + email String @unique @default("") + posts Post[] @relation("Post_author") } \ No newline at end of file diff --git a/examples/blog/schema.ts b/examples/blog/schema.ts index 74b286e629e..26f1e34608d 100644 --- a/examples/blog/schema.ts +++ b/examples/blog/schema.ts @@ -4,7 +4,7 @@ import { select, relationship, text, timestamp } from '@keystone-next/keystone/f export const lists = { Post: list({ fields: { - title: text({ isRequired: true, isFilterable: true }), + title: text({ validation: { isRequired: true }, isFilterable: true }), status: select({ type: 'enum', options: [ @@ -19,8 +19,8 @@ export const lists = { }), Author: list({ fields: { - name: text({ isRequired: true }), - email: text({ isRequired: true, isIndexed: 'unique' }), + name: text({ validation: { isRequired: true } }), + email: text({ isIndexed: 'unique', validation: { isRequired: true } }), posts: relationship({ ref: 'Post.author', many: true }), }, }), diff --git a/examples/custom-admin-ui-logo/schema.graphql b/examples/custom-admin-ui-logo/schema.graphql index 546de8db159..7f1c5c7d6a6 100644 --- a/examples/custom-admin-ui-logo/schema.graphql +++ b/examples/custom-admin-ui-logo/schema.graphql @@ -25,7 +25,7 @@ input TaskWhereInput { OR: [TaskWhereInput!] NOT: [TaskWhereInput!] id: IDFilter - label: StringNullableFilter + label: StringFilter priority: TaskPriorityTypeNullableFilter isComplete: BooleanFilter assignedTo: PersonWhereInput @@ -43,7 +43,7 @@ input IDFilter { not: IDFilter } -input StringNullableFilter { +input StringFilter { equals: String in: [String!] notIn: [String!] @@ -54,10 +54,10 @@ input StringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } -input NestedStringNullableFilter { +input NestedStringFilter { equals: String in: [String!] notIn: [String!] @@ -68,7 +68,7 @@ input NestedStringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } input TaskPriorityTypeNullableFilter { @@ -160,7 +160,7 @@ input PersonWhereInput { OR: [PersonWhereInput!] NOT: [PersonWhereInput!] id: IDFilter - name: StringNullableFilter + name: StringFilter tasks: TaskManyRelationFilter } diff --git a/examples/custom-admin-ui-logo/schema.prisma b/examples/custom-admin-ui-logo/schema.prisma index 55c8804b04f..ca297ff42e9 100644 --- a/examples/custom-admin-ui-logo/schema.prisma +++ b/examples/custom-admin-ui-logo/schema.prisma @@ -14,7 +14,7 @@ generator client { model Task { id String @id @default(cuid()) - label String? + label String @default("") priority String? isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) @@ -25,7 +25,7 @@ model Task { } model Person { - id String @id @default(cuid()) - name String? - tasks Task[] @relation("Task_assignedTo") + id String @id @default(cuid()) + name String @default("") + tasks Task[] @relation("Task_assignedTo") } \ No newline at end of file diff --git a/examples/custom-admin-ui-logo/schema.ts b/examples/custom-admin-ui-logo/schema.ts index b10f82a13e2..cbf962e2341 100644 --- a/examples/custom-admin-ui-logo/schema.ts +++ b/examples/custom-admin-ui-logo/schema.ts @@ -5,7 +5,7 @@ import { select } from '@keystone-next/keystone/fields'; export const lists = { Task: list({ fields: { - label: text({ isRequired: true }), + label: text({ validation: { isRequired: true } }), priority: select({ type: 'enum', options: [ @@ -23,7 +23,7 @@ export const lists = { }), Person: list({ fields: { - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), tasks: relationship({ ref: 'Task.assignedTo', many: true }), }, defaultIsFilterable: true, diff --git a/examples/custom-admin-ui-navigation/schema.graphql b/examples/custom-admin-ui-navigation/schema.graphql index 546de8db159..7f1c5c7d6a6 100644 --- a/examples/custom-admin-ui-navigation/schema.graphql +++ b/examples/custom-admin-ui-navigation/schema.graphql @@ -25,7 +25,7 @@ input TaskWhereInput { OR: [TaskWhereInput!] NOT: [TaskWhereInput!] id: IDFilter - label: StringNullableFilter + label: StringFilter priority: TaskPriorityTypeNullableFilter isComplete: BooleanFilter assignedTo: PersonWhereInput @@ -43,7 +43,7 @@ input IDFilter { not: IDFilter } -input StringNullableFilter { +input StringFilter { equals: String in: [String!] notIn: [String!] @@ -54,10 +54,10 @@ input StringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } -input NestedStringNullableFilter { +input NestedStringFilter { equals: String in: [String!] notIn: [String!] @@ -68,7 +68,7 @@ input NestedStringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } input TaskPriorityTypeNullableFilter { @@ -160,7 +160,7 @@ input PersonWhereInput { OR: [PersonWhereInput!] NOT: [PersonWhereInput!] id: IDFilter - name: StringNullableFilter + name: StringFilter tasks: TaskManyRelationFilter } diff --git a/examples/custom-admin-ui-navigation/schema.prisma b/examples/custom-admin-ui-navigation/schema.prisma index 55c8804b04f..ca297ff42e9 100644 --- a/examples/custom-admin-ui-navigation/schema.prisma +++ b/examples/custom-admin-ui-navigation/schema.prisma @@ -14,7 +14,7 @@ generator client { model Task { id String @id @default(cuid()) - label String? + label String @default("") priority String? isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) @@ -25,7 +25,7 @@ model Task { } model Person { - id String @id @default(cuid()) - name String? - tasks Task[] @relation("Task_assignedTo") + id String @id @default(cuid()) + name String @default("") + tasks Task[] @relation("Task_assignedTo") } \ No newline at end of file diff --git a/examples/custom-admin-ui-navigation/schema.ts b/examples/custom-admin-ui-navigation/schema.ts index b10f82a13e2..cbf962e2341 100644 --- a/examples/custom-admin-ui-navigation/schema.ts +++ b/examples/custom-admin-ui-navigation/schema.ts @@ -5,7 +5,7 @@ import { select } from '@keystone-next/keystone/fields'; export const lists = { Task: list({ fields: { - label: text({ isRequired: true }), + label: text({ validation: { isRequired: true } }), priority: select({ type: 'enum', options: [ @@ -23,7 +23,7 @@ export const lists = { }), Person: list({ fields: { - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), tasks: relationship({ ref: 'Task.assignedTo', many: true }), }, defaultIsFilterable: true, diff --git a/examples/custom-admin-ui-pages/schema.graphql b/examples/custom-admin-ui-pages/schema.graphql index 546de8db159..7f1c5c7d6a6 100644 --- a/examples/custom-admin-ui-pages/schema.graphql +++ b/examples/custom-admin-ui-pages/schema.graphql @@ -25,7 +25,7 @@ input TaskWhereInput { OR: [TaskWhereInput!] NOT: [TaskWhereInput!] id: IDFilter - label: StringNullableFilter + label: StringFilter priority: TaskPriorityTypeNullableFilter isComplete: BooleanFilter assignedTo: PersonWhereInput @@ -43,7 +43,7 @@ input IDFilter { not: IDFilter } -input StringNullableFilter { +input StringFilter { equals: String in: [String!] notIn: [String!] @@ -54,10 +54,10 @@ input StringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } -input NestedStringNullableFilter { +input NestedStringFilter { equals: String in: [String!] notIn: [String!] @@ -68,7 +68,7 @@ input NestedStringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } input TaskPriorityTypeNullableFilter { @@ -160,7 +160,7 @@ input PersonWhereInput { OR: [PersonWhereInput!] NOT: [PersonWhereInput!] id: IDFilter - name: StringNullableFilter + name: StringFilter tasks: TaskManyRelationFilter } diff --git a/examples/custom-admin-ui-pages/schema.prisma b/examples/custom-admin-ui-pages/schema.prisma index 55c8804b04f..ca297ff42e9 100644 --- a/examples/custom-admin-ui-pages/schema.prisma +++ b/examples/custom-admin-ui-pages/schema.prisma @@ -14,7 +14,7 @@ generator client { model Task { id String @id @default(cuid()) - label String? + label String @default("") priority String? isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) @@ -25,7 +25,7 @@ model Task { } model Person { - id String @id @default(cuid()) - name String? - tasks Task[] @relation("Task_assignedTo") + id String @id @default(cuid()) + name String @default("") + tasks Task[] @relation("Task_assignedTo") } \ No newline at end of file diff --git a/examples/custom-admin-ui-pages/schema.ts b/examples/custom-admin-ui-pages/schema.ts index b10f82a13e2..cbf962e2341 100644 --- a/examples/custom-admin-ui-pages/schema.ts +++ b/examples/custom-admin-ui-pages/schema.ts @@ -5,7 +5,7 @@ import { select } from '@keystone-next/keystone/fields'; export const lists = { Task: list({ fields: { - label: text({ isRequired: true }), + label: text({ validation: { isRequired: true } }), priority: select({ type: 'enum', options: [ @@ -23,7 +23,7 @@ export const lists = { }), Person: list({ fields: { - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), tasks: relationship({ ref: 'Task.assignedTo', many: true }), }, defaultIsFilterable: true, diff --git a/examples/custom-field-view/schema.graphql b/examples/custom-field-view/schema.graphql index 32721af1668..82041e6a634 100644 --- a/examples/custom-field-view/schema.graphql +++ b/examples/custom-field-view/schema.graphql @@ -26,7 +26,7 @@ input TaskWhereInput { OR: [TaskWhereInput!] NOT: [TaskWhereInput!] id: IDFilter - label: StringNullableFilter + label: StringFilter priority: TaskPriorityTypeNullableFilter isComplete: BooleanFilter assignedTo: PersonWhereInput @@ -44,7 +44,7 @@ input IDFilter { not: IDFilter } -input StringNullableFilter { +input StringFilter { equals: String in: [String!] notIn: [String!] @@ -55,10 +55,10 @@ input StringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } -input NestedStringNullableFilter { +input NestedStringFilter { equals: String in: [String!] notIn: [String!] @@ -69,7 +69,7 @@ input NestedStringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } input TaskPriorityTypeNullableFilter { @@ -163,7 +163,7 @@ input PersonWhereInput { OR: [PersonWhereInput!] NOT: [PersonWhereInput!] id: IDFilter - name: StringNullableFilter + name: StringFilter tasks: TaskManyRelationFilter } diff --git a/examples/custom-field-view/schema.prisma b/examples/custom-field-view/schema.prisma index 571ab56047e..ef70a7838be 100644 --- a/examples/custom-field-view/schema.prisma +++ b/examples/custom-field-view/schema.prisma @@ -14,7 +14,7 @@ generator client { model Task { id String @id @default(cuid()) - label String? + label String @default("") priority String? isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) @@ -26,7 +26,7 @@ model Task { } model Person { - id String @id @default(cuid()) - name String? - tasks Task[] @relation("Task_assignedTo") + id String @id @default(cuid()) + name String @default("") + tasks Task[] @relation("Task_assignedTo") } \ No newline at end of file diff --git a/examples/custom-field-view/schema.ts b/examples/custom-field-view/schema.ts index 7240c3f2874..2d22f03014a 100644 --- a/examples/custom-field-view/schema.ts +++ b/examples/custom-field-view/schema.ts @@ -5,7 +5,7 @@ import { json, select } from '@keystone-next/keystone/fields'; export const lists = { Task: list({ fields: { - label: text({ isRequired: true }), + label: text({ validation: { isRequired: true } }), priority: select({ type: 'enum', options: [ @@ -32,7 +32,7 @@ export const lists = { }), Person: list({ fields: { - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), tasks: relationship({ ref: 'Task.assignedTo', many: true }), }, defaultIsFilterable: true, diff --git a/examples/custom-field/schema.prisma b/examples/custom-field/schema.prisma index 4305c37ba86..3bbdf1d51fa 100644 --- a/examples/custom-field/schema.prisma +++ b/examples/custom-field/schema.prisma @@ -14,9 +14,9 @@ generator client { model Post { id String @id @default(cuid()) - title String? + title String @default("") status String? - content String? + content String @default("") rating Int? publishDate DateTime? author Author? @relation("Post_author", fields: [authorId], references: [id]) @@ -26,8 +26,8 @@ model Post { } model Author { - id String @id @default(cuid()) - name String? - email String? @unique - posts Post[] @relation("Post_author") + id String @id @default(cuid()) + name String @default("") + email String @unique @default("") + posts Post[] @relation("Post_author") } \ No newline at end of file diff --git a/examples/custom-field/schema.ts b/examples/custom-field/schema.ts index 701a7d325ba..926b0388712 100644 --- a/examples/custom-field/schema.ts +++ b/examples/custom-field/schema.ts @@ -5,7 +5,7 @@ import { stars } from './stars-field'; export const lists = { Post: list({ fields: { - title: text({ isRequired: true }), + title: text({ validation: { isRequired: true } }), status: select({ type: 'enum', options: [ @@ -21,8 +21,8 @@ export const lists = { }), Author: list({ fields: { - name: text({ isRequired: true }), - email: text({ isRequired: true, isIndexed: 'unique' }), + name: text({ validation: { isRequired: true } }), + email: text({ isIndexed: 'unique', validation: { isRequired: true } }), posts: relationship({ ref: 'Post.author', many: true }), }, }), diff --git a/examples/default-values/schema.graphql b/examples/default-values/schema.graphql index 546de8db159..7f1c5c7d6a6 100644 --- a/examples/default-values/schema.graphql +++ b/examples/default-values/schema.graphql @@ -25,7 +25,7 @@ input TaskWhereInput { OR: [TaskWhereInput!] NOT: [TaskWhereInput!] id: IDFilter - label: StringNullableFilter + label: StringFilter priority: TaskPriorityTypeNullableFilter isComplete: BooleanFilter assignedTo: PersonWhereInput @@ -43,7 +43,7 @@ input IDFilter { not: IDFilter } -input StringNullableFilter { +input StringFilter { equals: String in: [String!] notIn: [String!] @@ -54,10 +54,10 @@ input StringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } -input NestedStringNullableFilter { +input NestedStringFilter { equals: String in: [String!] notIn: [String!] @@ -68,7 +68,7 @@ input NestedStringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } input TaskPriorityTypeNullableFilter { @@ -160,7 +160,7 @@ input PersonWhereInput { OR: [PersonWhereInput!] NOT: [PersonWhereInput!] id: IDFilter - name: StringNullableFilter + name: StringFilter tasks: TaskManyRelationFilter } diff --git a/examples/default-values/schema.prisma b/examples/default-values/schema.prisma index 55c8804b04f..ca297ff42e9 100644 --- a/examples/default-values/schema.prisma +++ b/examples/default-values/schema.prisma @@ -14,7 +14,7 @@ generator client { model Task { id String @id @default(cuid()) - label String? + label String @default("") priority String? isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) @@ -25,7 +25,7 @@ model Task { } model Person { - id String @id @default(cuid()) - name String? - tasks Task[] @relation("Task_assignedTo") + id String @id @default(cuid()) + name String @default("") + tasks Task[] @relation("Task_assignedTo") } \ No newline at end of file diff --git a/examples/default-values/schema.ts b/examples/default-values/schema.ts index 656b43b280b..b2c14c4deb3 100644 --- a/examples/default-values/schema.ts +++ b/examples/default-values/schema.ts @@ -5,7 +5,7 @@ import { select } from '@keystone-next/keystone/fields'; export const lists = { Task: list({ fields: { - label: text({ isRequired: true }), + label: text({ validation: { isRequired: true } }), priority: select({ type: 'enum', options: [ @@ -59,7 +59,7 @@ export const lists = { }), Person: list({ fields: { - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), tasks: relationship({ ref: 'Task.assignedTo', many: true }), }, defaultIsFilterable: true, diff --git a/examples/document-field/schema.prisma b/examples/document-field/schema.prisma index 069b66a708d..f4aeacaf829 100644 --- a/examples/document-field/schema.prisma +++ b/examples/document-field/schema.prisma @@ -14,8 +14,8 @@ generator client { model Post { id String @id @default(cuid()) - title String? - slug String? @unique + title String @default("") + slug String @unique @default("") status String? content String @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]") publishDate DateTime? @@ -26,9 +26,9 @@ model Post { } model Author { - id String @id @default(cuid()) - name String? - email String? @unique - posts Post[] @relation("Post_author") - bio String @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]") + id String @id @default(cuid()) + name String @default("") + email String @unique @default("") + posts Post[] @relation("Post_author") + bio String @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]") } \ No newline at end of file diff --git a/examples/document-field/schema.ts b/examples/document-field/schema.ts index 888646c5f23..ff870a85030 100644 --- a/examples/document-field/schema.ts +++ b/examples/document-field/schema.ts @@ -5,8 +5,8 @@ import { document } from '@keystone-next/fields-document'; export const lists = { Post: list({ fields: { - title: text({ isRequired: true }), - slug: text({ isRequired: true, isIndexed: 'unique' }), + title: text({ validation: { isRequired: true } }), + slug: text({ isIndexed: 'unique', validation: { isRequired: true } }), status: select({ type: 'enum', options: [ @@ -42,8 +42,8 @@ export const lists = { }), Author: list({ fields: { - name: text({ isRequired: true }), - email: text({ isRequired: true, isIndexed: 'unique' }), + name: text({ validation: { isRequired: true } }), + email: text({ isIndexed: 'unique', validation: { isRequired: true } }), posts: relationship({ ref: 'Post.author', many: true }), bio: document({ // We want to constrain the formatting in Author bios to a limited set of options. diff --git a/examples/extend-graphql-schema/schema.prisma b/examples/extend-graphql-schema/schema.prisma index 74c0e34a2e2..169b647328a 100644 --- a/examples/extend-graphql-schema/schema.prisma +++ b/examples/extend-graphql-schema/schema.prisma @@ -14,9 +14,9 @@ generator client { model Post { id String @id @default(cuid()) - title String? + title String @default("") status String? - content String? + content String @default("") publishDate DateTime? author Author? @relation("Post_author", fields: [authorId], references: [id]) authorId String? @map("author") @@ -25,8 +25,8 @@ model Post { } model Author { - id String @id @default(cuid()) - name String? - email String? @unique - posts Post[] @relation("Post_author") + id String @id @default(cuid()) + name String @default("") + email String @unique @default("") + posts Post[] @relation("Post_author") } \ No newline at end of file diff --git a/examples/extend-graphql-schema/schema.ts b/examples/extend-graphql-schema/schema.ts index 03c30443357..57fdfd53705 100644 --- a/examples/extend-graphql-schema/schema.ts +++ b/examples/extend-graphql-schema/schema.ts @@ -4,7 +4,7 @@ import { select, relationship, text, timestamp } from '@keystone-next/keystone/f export const lists = { Post: list({ fields: { - title: text({ isRequired: true }), + title: text({ validation: { isRequired: true } }), status: select({ type: 'enum', options: [ @@ -19,8 +19,8 @@ export const lists = { }), Author: list({ fields: { - name: text({ isRequired: true }), - email: text({ isRequired: true, isIndexed: 'unique' }), + name: text({ validation: { isRequired: true } }), + email: text({ isIndexed: 'unique', validation: { isRequired: true } }), posts: relationship({ ref: 'Post.author', many: true }), }, }), diff --git a/examples/json/schema.prisma b/examples/json/schema.prisma index 25a99500579..2b6d8883d8f 100644 --- a/examples/json/schema.prisma +++ b/examples/json/schema.prisma @@ -14,7 +14,7 @@ generator client { model Package { id String @id @default(cuid()) - label String? + label String @default("") pkgjson String? isPrivate Boolean @default(false) ownedBy Person? @relation("Package_ownedBy", fields: [ownedById], references: [id]) @@ -25,6 +25,6 @@ model Package { model Person { id String @id @default(cuid()) - name String? + name String @default("") packages Package[] @relation("Package_ownedBy") } \ No newline at end of file diff --git a/examples/json/schema.ts b/examples/json/schema.ts index 381c55483c4..9a0fb31b962 100644 --- a/examples/json/schema.ts +++ b/examples/json/schema.ts @@ -4,7 +4,7 @@ import { checkbox, json, relationship, text } from '@keystone-next/keystone/fiel export const lists = { Package: list({ fields: { - label: text({ isRequired: true }), + label: text({ validation: { isRequired: true } }), pkgjson: json(), isPrivate: checkbox(), ownedBy: relationship({ ref: 'Person.packages', many: false }), @@ -12,7 +12,7 @@ export const lists = { }), Person: list({ fields: { - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), packages: relationship({ ref: 'Package.ownedBy', many: true }), }, }), diff --git a/examples/rest-api/schema.graphql b/examples/rest-api/schema.graphql index 546de8db159..7f1c5c7d6a6 100644 --- a/examples/rest-api/schema.graphql +++ b/examples/rest-api/schema.graphql @@ -25,7 +25,7 @@ input TaskWhereInput { OR: [TaskWhereInput!] NOT: [TaskWhereInput!] id: IDFilter - label: StringNullableFilter + label: StringFilter priority: TaskPriorityTypeNullableFilter isComplete: BooleanFilter assignedTo: PersonWhereInput @@ -43,7 +43,7 @@ input IDFilter { not: IDFilter } -input StringNullableFilter { +input StringFilter { equals: String in: [String!] notIn: [String!] @@ -54,10 +54,10 @@ input StringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } -input NestedStringNullableFilter { +input NestedStringFilter { equals: String in: [String!] notIn: [String!] @@ -68,7 +68,7 @@ input NestedStringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } input TaskPriorityTypeNullableFilter { @@ -160,7 +160,7 @@ input PersonWhereInput { OR: [PersonWhereInput!] NOT: [PersonWhereInput!] id: IDFilter - name: StringNullableFilter + name: StringFilter tasks: TaskManyRelationFilter } diff --git a/examples/rest-api/schema.prisma b/examples/rest-api/schema.prisma index 55c8804b04f..ca297ff42e9 100644 --- a/examples/rest-api/schema.prisma +++ b/examples/rest-api/schema.prisma @@ -14,7 +14,7 @@ generator client { model Task { id String @id @default(cuid()) - label String? + label String @default("") priority String? isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) @@ -25,7 +25,7 @@ model Task { } model Person { - id String @id @default(cuid()) - name String? - tasks Task[] @relation("Task_assignedTo") + id String @id @default(cuid()) + name String @default("") + tasks Task[] @relation("Task_assignedTo") } \ No newline at end of file diff --git a/examples/rest-api/schema.ts b/examples/rest-api/schema.ts index b10f82a13e2..cbf962e2341 100644 --- a/examples/rest-api/schema.ts +++ b/examples/rest-api/schema.ts @@ -5,7 +5,7 @@ import { select } from '@keystone-next/keystone/fields'; export const lists = { Task: list({ fields: { - label: text({ isRequired: true }), + label: text({ validation: { isRequired: true } }), priority: select({ type: 'enum', options: [ @@ -23,7 +23,7 @@ export const lists = { }), Person: list({ fields: { - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), tasks: relationship({ ref: 'Task.assignedTo', many: true }), }, defaultIsFilterable: true, diff --git a/examples/task-manager/schema.graphql b/examples/task-manager/schema.graphql index 546de8db159..7f1c5c7d6a6 100644 --- a/examples/task-manager/schema.graphql +++ b/examples/task-manager/schema.graphql @@ -25,7 +25,7 @@ input TaskWhereInput { OR: [TaskWhereInput!] NOT: [TaskWhereInput!] id: IDFilter - label: StringNullableFilter + label: StringFilter priority: TaskPriorityTypeNullableFilter isComplete: BooleanFilter assignedTo: PersonWhereInput @@ -43,7 +43,7 @@ input IDFilter { not: IDFilter } -input StringNullableFilter { +input StringFilter { equals: String in: [String!] notIn: [String!] @@ -54,10 +54,10 @@ input StringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } -input NestedStringNullableFilter { +input NestedStringFilter { equals: String in: [String!] notIn: [String!] @@ -68,7 +68,7 @@ input NestedStringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } input TaskPriorityTypeNullableFilter { @@ -160,7 +160,7 @@ input PersonWhereInput { OR: [PersonWhereInput!] NOT: [PersonWhereInput!] id: IDFilter - name: StringNullableFilter + name: StringFilter tasks: TaskManyRelationFilter } diff --git a/examples/task-manager/schema.prisma b/examples/task-manager/schema.prisma index 55c8804b04f..ca297ff42e9 100644 --- a/examples/task-manager/schema.prisma +++ b/examples/task-manager/schema.prisma @@ -14,7 +14,7 @@ generator client { model Task { id String @id @default(cuid()) - label String? + label String @default("") priority String? isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) @@ -25,7 +25,7 @@ model Task { } model Person { - id String @id @default(cuid()) - name String? - tasks Task[] @relation("Task_assignedTo") + id String @id @default(cuid()) + name String @default("") + tasks Task[] @relation("Task_assignedTo") } \ No newline at end of file diff --git a/examples/task-manager/schema.ts b/examples/task-manager/schema.ts index b10f82a13e2..cbf962e2341 100644 --- a/examples/task-manager/schema.ts +++ b/examples/task-manager/schema.ts @@ -5,7 +5,7 @@ import { select } from '@keystone-next/keystone/fields'; export const lists = { Task: list({ fields: { - label: text({ isRequired: true }), + label: text({ validation: { isRequired: true } }), priority: select({ type: 'enum', options: [ @@ -23,7 +23,7 @@ export const lists = { }), Person: list({ fields: { - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), tasks: relationship({ ref: 'Task.assignedTo', many: true }), }, defaultIsFilterable: true, diff --git a/examples/testing/example.test.ts b/examples/testing/example.test.ts index 2214f1cf4f6..ad88c04a6e9 100644 --- a/examples/testing/example.test.ts +++ b/examples/testing/example.test.ts @@ -58,7 +58,7 @@ describe('Example tests using test runner', () => { expect(errors).toHaveLength(1); expect(errors![0].path).toEqual(['createPerson']); expect(errors![0].message).toEqual( - 'You provided invalid data for this operation.\n - Person.name: Required field "name" is null or undefined.' + 'You provided invalid data for this operation.\n - Person.name: Name must not be empty' ); }) ); diff --git a/examples/testing/schema.graphql b/examples/testing/schema.graphql index f6f5e80b968..c9a49da4bc7 100644 --- a/examples/testing/schema.graphql +++ b/examples/testing/schema.graphql @@ -69,7 +69,7 @@ input TaskWhereInput { OR: [TaskWhereInput!] NOT: [TaskWhereInput!] id: IDFilter - label: StringNullableFilter + label: StringFilter priority: TaskPriorityTypeNullableFilter isComplete: BooleanFilter assignedTo: PersonWhereInput @@ -87,7 +87,7 @@ input IDFilter { not: IDFilter } -input StringNullableFilter { +input StringFilter { equals: String in: [String!] notIn: [String!] @@ -98,10 +98,10 @@ input StringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } -input NestedStringNullableFilter { +input NestedStringFilter { equals: String in: [String!] notIn: [String!] @@ -112,7 +112,7 @@ input NestedStringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } input TaskPriorityTypeNullableFilter { @@ -211,8 +211,8 @@ input PersonWhereInput { OR: [PersonWhereInput!] NOT: [PersonWhereInput!] id: IDFilter - name: StringNullableFilter - email: StringNullableFilter + name: StringFilter + email: StringFilter password: PasswordFilter tasks: TaskManyRelationFilter } diff --git a/examples/testing/schema.prisma b/examples/testing/schema.prisma index 734683cf35b..06eb759f0a2 100644 --- a/examples/testing/schema.prisma +++ b/examples/testing/schema.prisma @@ -14,7 +14,7 @@ generator client { model Task { id String @id @default(cuid()) - label String? + label String @default("") priority String? isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) @@ -26,8 +26,8 @@ model Task { model Person { id String @id @default(cuid()) - name String? - email String? @unique + name String @default("") + email String @unique @default("") password String? tasks Task[] @relation("Task_assignedTo") } \ No newline at end of file diff --git a/examples/testing/schema.ts b/examples/testing/schema.ts index f546334760d..adcc8c80d1c 100644 --- a/examples/testing/schema.ts +++ b/examples/testing/schema.ts @@ -5,7 +5,7 @@ import { select } from '@keystone-next/keystone/fields'; export const lists = { Task: list({ fields: { - label: text({ isRequired: true }), + label: text({ validation: { isRequired: true } }), priority: select({ type: 'enum', options: [ @@ -36,8 +36,8 @@ export const lists = { }), Person: list({ fields: { - name: text({ isRequired: true }), - email: text({ isRequired: true, isIndexed: 'unique' }), + name: text({ validation: { isRequired: true } }), + email: text({ isIndexed: 'unique', validation: { isRequired: true } }), password: password({ isRequired: true }), tasks: relationship({ ref: 'Task.assignedTo', many: true }), }, diff --git a/examples/virtual-field/schema.prisma b/examples/virtual-field/schema.prisma index 74c0e34a2e2..169b647328a 100644 --- a/examples/virtual-field/schema.prisma +++ b/examples/virtual-field/schema.prisma @@ -14,9 +14,9 @@ generator client { model Post { id String @id @default(cuid()) - title String? + title String @default("") status String? - content String? + content String @default("") publishDate DateTime? author Author? @relation("Post_author", fields: [authorId], references: [id]) authorId String? @map("author") @@ -25,8 +25,8 @@ model Post { } model Author { - id String @id @default(cuid()) - name String? - email String? @unique - posts Post[] @relation("Post_author") + id String @id @default(cuid()) + name String @default("") + email String @unique @default("") + posts Post[] @relation("Post_author") } \ No newline at end of file diff --git a/examples/virtual-field/schema.ts b/examples/virtual-field/schema.ts index b752325948a..006edf17990 100644 --- a/examples/virtual-field/schema.ts +++ b/examples/virtual-field/schema.ts @@ -5,7 +5,7 @@ import { graphql } from '@keystone-next/keystone/types'; export const lists = { Post: list({ fields: { - title: text({ isRequired: true }), + title: text({ validation: { isRequired: true } }), status: select({ type: 'enum', options: [ @@ -92,8 +92,8 @@ export const lists = { }), Author: list({ fields: { - name: text({ isRequired: true }), - email: text({ isRequired: true, isIndexed: 'unique' }), + name: text({ validation: { isRequired: true } }), + email: text({ isIndexed: 'unique', validation: { isRequired: true } }), posts: relationship({ ref: 'Post.author', many: true }), // A virtual field which returns a type derived from a Keystone list. latestPost: virtual({ diff --git a/examples/with-auth/README.md b/examples/with-auth/README.md index b396f528bd1..4c8834cb9a3 100644 --- a/examples/with-auth/README.md +++ b/examples/with-auth/README.md @@ -34,7 +34,7 @@ We add two new fields, `email` and `password`, to the `Person` list. These are used as our _identity_ and _secret_ fields for login. ```typescript - email: text({ isRequired: true, isIndexed: 'unique', isFilterable: true }), + email: text({ isIndexed: 'unique', isFilterable: true, validation: { isRequired: true } }), password: password({ isRequired: true }), ``` diff --git a/examples/with-auth/schema.graphql b/examples/with-auth/schema.graphql index f6f5e80b968..c9a49da4bc7 100644 --- a/examples/with-auth/schema.graphql +++ b/examples/with-auth/schema.graphql @@ -69,7 +69,7 @@ input TaskWhereInput { OR: [TaskWhereInput!] NOT: [TaskWhereInput!] id: IDFilter - label: StringNullableFilter + label: StringFilter priority: TaskPriorityTypeNullableFilter isComplete: BooleanFilter assignedTo: PersonWhereInput @@ -87,7 +87,7 @@ input IDFilter { not: IDFilter } -input StringNullableFilter { +input StringFilter { equals: String in: [String!] notIn: [String!] @@ -98,10 +98,10 @@ input StringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } -input NestedStringNullableFilter { +input NestedStringFilter { equals: String in: [String!] notIn: [String!] @@ -112,7 +112,7 @@ input NestedStringNullableFilter { contains: String startsWith: String endsWith: String - not: NestedStringNullableFilter + not: NestedStringFilter } input TaskPriorityTypeNullableFilter { @@ -211,8 +211,8 @@ input PersonWhereInput { OR: [PersonWhereInput!] NOT: [PersonWhereInput!] id: IDFilter - name: StringNullableFilter - email: StringNullableFilter + name: StringFilter + email: StringFilter password: PasswordFilter tasks: TaskManyRelationFilter } diff --git a/examples/with-auth/schema.prisma b/examples/with-auth/schema.prisma index 734683cf35b..06eb759f0a2 100644 --- a/examples/with-auth/schema.prisma +++ b/examples/with-auth/schema.prisma @@ -14,7 +14,7 @@ generator client { model Task { id String @id @default(cuid()) - label String? + label String @default("") priority String? isComplete Boolean @default(false) assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) @@ -26,8 +26,8 @@ model Task { model Person { id String @id @default(cuid()) - name String? - email String? @unique + name String @default("") + email String @unique @default("") password String? tasks Task[] @relation("Task_assignedTo") } \ No newline at end of file diff --git a/examples/with-auth/schema.ts b/examples/with-auth/schema.ts index 3873696c91e..c649994cf1b 100644 --- a/examples/with-auth/schema.ts +++ b/examples/with-auth/schema.ts @@ -5,7 +5,7 @@ import { select } from '@keystone-next/keystone/fields'; export const lists = { Task: list({ fields: { - label: text({ isRequired: true }), + label: text({ validation: { isRequired: true } }), priority: select({ type: 'enum', options: [ @@ -23,11 +23,11 @@ export const lists = { }), Person: list({ fields: { - name: text({ isRequired: true }), + name: text({ validation: { isRequired: true } }), // Added an email and password pair to be used with authentication // The email address is going to be used as the identity field, so it's // important that we set isRequired, isIndexed: 'unique', and isFilterable. - email: text({ isRequired: true, isIndexed: 'unique', isFilterable: true }), + email: text({ isIndexed: 'unique', isFilterable: true, validation: { isRequired: true } }), // The password field stores a hash of the supplied password, and // we want to ensure that all people have a password set, so we use // the isRequired flag. diff --git a/packages/keystone/src/fields/types/text/index.ts b/packages/keystone/src/fields/types/text/index.ts index 5af4cbb2c9b..d42887c2f9b 100644 --- a/packages/keystone/src/fields/types/text/index.ts +++ b/packages/keystone/src/fields/types/text/index.ts @@ -1,6 +1,6 @@ +import { humanize } from '../../../lib/utils'; import { BaseGeneratedListTypes, - FieldDefaultValue, CommonFieldConfig, fieldType, graphql, @@ -8,51 +8,185 @@ import { FieldTypeFunc, filters, } from '../../../types'; +import { assertCreateIsNonNullAllowed, assertReadIsNonNullAllowed } from '../../non-null-graphql'; import { resolveView } from '../../resolve-view'; export type TextFieldConfig = CommonFieldConfig & { - defaultValue?: FieldDefaultValue; - isIndexed?: boolean | 'unique'; - isRequired?: boolean; + isIndexed?: true | 'unique'; ui?: { displayMode?: 'input' | 'textarea'; }; - }; + validation?: { + /** + * Makes the field disallow null values and require a string at least 1 character long + */ + isRequired?: boolean; + match?: { regex: RegExp; explanation?: string }; + length?: { min?: number; max?: number }; + }; + defaultValue?: string; + graphql?: { create?: { isNonNull?: boolean } }; + } & ({ isNullable?: false; graphql?: { read?: { isNonNull?: boolean } } } | { isNullable: true }); export const text = ({ isIndexed, - isRequired, - defaultValue, + defaultValue: _defaultValue, + validation: _validation, ...config }: TextFieldConfig = {}): FieldTypeFunc => - meta => - fieldType({ + meta => { + const { isNullable = false } = config; + + for (const type of ['min', 'max'] as const) { + const val = _validation?.length?.[type]; + if (val !== undefined && (!Number.isInteger(val) || val < 0)) { + throw new Error( + `The text field at ${meta.listKey}.${meta.fieldKey} specifies validation.length.${type}: ${val} but it must be a positive integer` + ); + } + if (_validation?.isRequired && val !== undefined && val === 0) { + throw new Error( + `The text field at ${meta.listKey}.${meta.fieldKey} specifies validation.isRequired: true and validation.length.${type}: 0, this is not allowed because validation.isRequired implies at least a min length of 1` + ); + } + } + + if ( + _validation?.length?.min !== undefined && + _validation?.length?.max !== undefined && + _validation?.length?.min > _validation?.length?.max + ) { + throw new Error( + `The text field at ${meta.listKey}.${meta.fieldKey} specifies a validation.length.max that is less than the validation.length.min, and therefore has no valid options` + ); + } + + const validation = { + ..._validation, + length: { + min: _validation?.isRequired ? _validation?.length?.min ?? 1 : _validation?.length?.min, + max: _validation?.length?.max, + }, + }; + + const fieldLabel = config.label ?? humanize(meta.fieldKey); + + if (!config.isNullable) { + assertReadIsNonNullAllowed(meta, config); + } + assertCreateIsNonNullAllowed(meta, config); + + const mode = isNullable ? 'optional' : 'required'; + + const defaultValue = + isNullable === false || _defaultValue !== undefined ? _defaultValue || '' : undefined; + return fieldType({ kind: 'scalar', - mode: 'optional', + mode, scalar: 'String', + default: defaultValue === undefined ? undefined : { kind: 'literal', value: defaultValue }, index: isIndexed === true ? 'index' : isIndexed || undefined, })({ ...config, + hooks: { + ...config.hooks, + async validateInput(args) { + const val = args.resolvedData[meta.fieldKey]; + if (val === null && validation?.isRequired) { + args.addValidationError(`${fieldLabel} is required`); + } + if (val != null) { + if (validation?.length?.min !== undefined && val.length < validation.length.min) { + if (validation.length.min === 1) { + args.addValidationError(`${fieldLabel} must not be empty`); + } else { + args.addValidationError( + `${fieldLabel} must be at least ${validation.length.min} characters long` + ); + } + } + if (validation?.length?.max !== undefined && val.length > validation.length.max) { + args.addValidationError( + `${fieldLabel} must be no longer than ${validation.length.min} characters` + ); + } + if (validation?.match && !validation.match.regex.test(val)) { + args.addValidationError( + validation.match.explanation || `${fieldLabel} must match ${validation.match.regex}` + ); + } + } + + await config.hooks?.validateInput?.(args); + }, + }, input: { uniqueWhere: isIndexed === 'unique' ? { arg: graphql.arg({ type: graphql.String }) } : undefined, where: { - arg: graphql.arg({ type: filters[meta.provider].String.optional }), - resolve: filters.resolveString, + arg: graphql.arg({ + type: filters[meta.provider].String[mode], + }), + resolve: mode === 'required' ? undefined : filters.resolveString, + }, + create: { + arg: graphql.arg({ + type: config.graphql?.create?.isNonNull + ? graphql.nonNull(graphql.String) + : graphql.String, + defaultValue: config.graphql?.create?.isNonNull ? defaultValue : undefined, + }), + resolve(val) { + if (val === undefined) { + return defaultValue ?? null; + } + return val; + }, }, - create: { arg: graphql.arg({ type: graphql.String }) }, update: { arg: graphql.arg({ type: graphql.String }) }, orderBy: { arg: graphql.arg({ type: orderDirectionEnum }) }, }, - output: graphql.field({ type: graphql.String }), + output: graphql.field({ + type: + config.isNullable !== true && config.graphql?.read?.isNonNull + ? graphql.nonNull(graphql.String) + : graphql.String, + }), views: resolveView('text/views'), - getAdminMeta() { + getAdminMeta(): TextFieldMeta { return { displayMode: config.ui?.displayMode ?? 'input', shouldUseModeInsensitive: meta.provider === 'postgresql', + validation: { + isRequired: validation?.isRequired ?? false, + match: validation?.match + ? { + regex: { + source: validation.match.regex.source, + flags: validation.match.regex.flags, + }, + explanation: validation.match.explanation ?? null, + } + : null, + length: { max: validation?.length?.max ?? null, min: validation?.length?.min ?? null }, + }, + defaultValue: defaultValue ?? (isNullable ? null : ''), + isNullable, }; }, - __legacy: { defaultValue, isRequired }, }); + }; + +export type TextFieldMeta = { + displayMode: 'input' | 'textarea'; + shouldUseModeInsensitive: boolean; + isNullable: boolean; + validation: { + isRequired: boolean; + match: { regex: { source: string; flags: string }; explanation: string | null } | null; + length: { min: number | null; max: number | null }; + }; + defaultValue: string | null; +}; diff --git a/packages/keystone/src/fields/types/text/tests/nullable/test-fixtures.ts b/packages/keystone/src/fields/types/text/tests/nullable/test-fixtures.ts new file mode 100644 index 00000000000..f7080f5e4bd --- /dev/null +++ b/packages/keystone/src/fields/types/text/tests/nullable/test-fixtures.ts @@ -0,0 +1,47 @@ +import { DatabaseProvider } from '../../../../../types'; +import { text } from '../..'; + +export const name = 'Text with isNullable: true'; +export const typeFunction = (config: any) => text({ ...config, isNullable: true }); +export const exampleValue = () => 'foo'; +export const exampleValue2 = () => 'bar'; +export const supportsUnique = true; +export const skipRequiredTest = true; +export const fieldName = 'testField'; + +export const getTestFields = () => ({ + testField: text({ isFilterable: true, isNullable: true }), +}); + +export const initItems = () => { + return [ + { name: 'a', testField: '' }, + { name: 'b', testField: 'other' }, + { name: 'c', testField: 'FOOBAR' }, + { name: 'd', testField: 'fooBAR' }, + { name: 'e', testField: 'foobar' }, + { name: 'f', testField: null }, + { name: 'g' }, + ]; +}; + +export const storedValues = () => [ + { name: 'a', testField: '' }, + { name: 'b', testField: 'other' }, + { name: 'c', testField: 'FOOBAR' }, + { name: 'd', testField: 'fooBAR' }, + { name: 'e', testField: 'foobar' }, + { name: 'f', testField: null }, + { name: 'g', testField: null }, +]; + +export const supportedFilters = (provider: DatabaseProvider) => [ + 'null_equality', + 'equality', + provider !== 'sqlite' && 'equality_case_insensitive', + 'in_empty_null', + 'in_value', + provider !== 'sqlite' && 'string', + provider !== 'sqlite' && 'string_case_insensitive', + 'unique_equality', +]; diff --git a/packages/keystone/src/fields/types/text/tests/test-fixtures.ts b/packages/keystone/src/fields/types/text/tests/test-fixtures.ts index 04376d9864e..d8e08956ec7 100644 --- a/packages/keystone/src/fields/types/text/tests/test-fixtures.ts +++ b/packages/keystone/src/fields/types/text/tests/test-fixtures.ts @@ -1,4 +1,3 @@ -import { DatabaseProvider } from '../../../../types'; import { text } from '..'; export const name = 'Text'; @@ -6,6 +5,8 @@ export const typeFunction = text; export const exampleValue = () => 'foo'; export const exampleValue2 = () => 'bar'; export const supportsUnique = true; +export const skipRequiredTest = true; +export const supportsGraphQLIsNonNull = true; export const fieldName = 'testField'; export const getTestFields = () => ({ @@ -19,7 +20,7 @@ export const initItems = () => { { name: 'c', testField: 'FOOBAR' }, { name: 'd', testField: 'fooBAR' }, { name: 'e', testField: 'foobar' }, - { name: 'f', testField: null }, + { name: 'f' }, { name: 'g' }, ]; }; @@ -30,17 +31,8 @@ export const storedValues = () => [ { name: 'c', testField: 'FOOBAR' }, { name: 'd', testField: 'fooBAR' }, { name: 'e', testField: 'foobar' }, - { name: 'f', testField: null }, - { name: 'g', testField: null }, + { name: 'f', testField: '' }, + { name: 'g', testField: '' }, ]; -export const supportedFilters = (provider: DatabaseProvider) => [ - 'null_equality', - 'equality', - provider !== 'sqlite' && 'equality_case_insensitive', - 'in_empty_null', - 'in_value', - provider !== 'sqlite' && 'string', - provider !== 'sqlite' && 'string_case_insensitive', - 'unique_equality', -]; +export const supportedFilters = () => []; diff --git a/packages/keystone/src/fields/types/text/views/index.tsx b/packages/keystone/src/fields/types/text/views/index.tsx index a74854a3d86..79f9fdeeab9 100644 --- a/packages/keystone/src/fields/types/text/views/index.tsx +++ b/packages/keystone/src/fields/types/text/views/index.tsx @@ -1,7 +1,8 @@ /** @jsxRuntime classic */ /** @jsx jsx */ -import { jsx } from '@keystone-ui/core'; -import { FieldContainer, FieldLabel, TextArea, TextInput } from '@keystone-ui/fields'; +import { jsx, Stack, useTheme } from '@keystone-ui/core'; +import { Checkbox, FieldContainer, FieldLabel, TextArea, TextInput } from '@keystone-ui/fields'; +import { useState } from 'react'; import { CardValueComponent, CellComponent, @@ -11,30 +12,92 @@ import { } from '../../../../types'; import { CellContainer, CellLink } from '../../../../admin-ui/components'; -export const Field = ({ field, value, onChange, autoFocus }: FieldProps) => ( - - {field.label} - {onChange ? ( - field.displayMode === 'textarea' ? ( -