From f5e64af37df2eb460c89d89fa3c8924fb34970ed Mon Sep 17 00:00:00 2001 From: Mitchell Hamilton Date: Wed, 28 Jul 2021 11:49:20 +1000 Subject: [PATCH 1/2] Unique where inputs in deletes (#6206) --- .changeset/wise-pianos-flash.md | 8 ++++++ examples-staging/assets-cloud/schema.graphql | 8 +++--- examples-staging/assets-local/schema.graphql | 8 +++--- examples-staging/auth/schema.graphql | 4 +-- examples-staging/basic/schema.graphql | 12 ++++---- .../ecommerce/mutations/checkout.ts | 2 +- examples-staging/ecommerce/schema.graphql | 28 +++++++++---------- .../embedded-nextjs/schema.graphql | 4 +-- .../graphql-api-endpoint/schema.graphql | 12 ++++---- examples-staging/playground/schema.graphql | 4 +-- examples-staging/roles/schema.graphql | 12 ++++---- examples-staging/sandbox/schema.graphql | 8 +++--- examples/blog/schema.graphql | 8 +++--- examples/custom-admin-ui-logo/schema.graphql | 8 +++--- examples/custom-field-view/schema.graphql | 8 +++--- examples/custom-field/schema.graphql | 8 +++--- examples/default-values/schema.graphql | 8 +++--- examples/document-field/schema.graphql | 8 +++--- examples/extend-graphql-schema/schema.graphql | 8 +++--- examples/json/schema.graphql | 8 +++--- examples/task-manager/schema.graphql | 8 +++--- examples/testing/schema.graphql | 8 +++--- examples/virtual-field/schema.graphql | 8 +++--- examples/with-auth/schema.graphql | 8 +++--- .../admin-ui/pages/ItemPage/index.tsx | 2 +- .../admin-ui/pages/ListPage/index.tsx | 6 ++-- .../keystone/src/lib/core/mutations/index.ts | 21 +++++++------- .../fixtures/basic-project/schema.graphql | 4 +-- packages/types/src/context.ts | 16 ++++++++--- tests/api-tests/access-control/authed.test.ts | 22 ++++++++------- .../mutations-list-declarative.test.ts | 13 +++++---- .../mutations-list-static.test.ts | 10 ++++--- .../access-control/not-authed.test.ts | 4 +-- tests/api-tests/fields/crud.test.ts | 10 +++++-- tests/api-tests/fields/types/document.test.ts | 2 +- tests/api-tests/hooks/validation.test.ts | 10 ++++--- tests/api-tests/queries/cache-hints.test.ts | 2 +- .../many-to-many-one-sided.test.ts | 2 +- .../crud-self-ref/many-to-many.test.ts | 2 +- .../one-to-many-one-sided.test.ts | 6 ++-- .../crud-self-ref/one-to-many.test.ts | 2 +- .../crud-self-ref/one-to-one.test.ts | 2 +- .../crud/many-to-many-one-sided.test.ts | 2 +- .../relationships/crud/many-to-many.test.ts | 2 +- .../crud/one-to-many-one-sided.test.ts | 6 ++-- .../relationships/crud/one-to-many.test.ts | 2 +- .../relationships/crud/one-to-one.test.ts | 4 +-- .../two-way-backreference/to-many.test.ts | 2 +- tests/test-projects/basic/schema.graphql | 8 +++--- 49 files changed, 199 insertions(+), 169 deletions(-) create mode 100644 .changeset/wise-pianos-flash.md diff --git a/.changeset/wise-pianos-flash.md b/.changeset/wise-pianos-flash.md new file mode 100644 index 00000000000..9a6d732caf1 --- /dev/null +++ b/.changeset/wise-pianos-flash.md @@ -0,0 +1,8 @@ +--- +'@keystone-next/keystone': major +'@keystone-next/types': major +--- + +The delete mutations now accept `where` unique inputs instead of only an `id`. + +If you have a list called `Item`, `deleteItem` now looks like `deleteItem(where: ItemWhereUniqueInput!): Item` and `deleteItems` now looks like `deleteItems(where: [ItemWhereUniqueInput!]!): [Item]` diff --git a/examples-staging/assets-cloud/schema.graphql b/examples-staging/assets-cloud/schema.graphql index db4ef456b16..7ef44fff5d7 100644 --- a/examples-staging/assets-cloud/schema.graphql +++ b/examples-staging/assets-cloud/schema.graphql @@ -255,14 +255,14 @@ type Mutation { createPosts(data: [PostsCreateInput]): [Post] updatePost(id: ID!, data: PostUpdateInput): Post updatePosts(data: [PostUpdateArgs]): [Post] - deletePost(id: ID!): Post - deletePosts(ids: [ID!]): [Post] + deletePost(where: PostWhereUniqueInput!): Post + deletePosts(where: [PostWhereUniqueInput!]!): [Post] createAuthor(data: AuthorCreateInput): Author createAuthors(data: [AuthorsCreateInput]): [Author] updateAuthor(id: ID!, data: AuthorUpdateInput): Author updateAuthors(data: [AuthorUpdateArgs]): [Author] - deleteAuthor(id: ID!): Author - deleteAuthors(ids: [ID!]): [Author] + deleteAuthor(where: AuthorWhereUniqueInput!): Author + deleteAuthors(where: [AuthorWhereUniqueInput!]!): [Author] } type Query { diff --git a/examples-staging/assets-local/schema.graphql b/examples-staging/assets-local/schema.graphql index 1f17857bb60..0aca326a079 100644 --- a/examples-staging/assets-local/schema.graphql +++ b/examples-staging/assets-local/schema.graphql @@ -233,14 +233,14 @@ type Mutation { createPosts(data: [PostsCreateInput]): [Post] updatePost(id: ID!, data: PostUpdateInput): Post updatePosts(data: [PostUpdateArgs]): [Post] - deletePost(id: ID!): Post - deletePosts(ids: [ID!]): [Post] + deletePost(where: PostWhereUniqueInput!): Post + deletePosts(where: [PostWhereUniqueInput!]!): [Post] createAuthor(data: AuthorCreateInput): Author createAuthors(data: [AuthorsCreateInput]): [Author] updateAuthor(id: ID!, data: AuthorUpdateInput): Author updateAuthors(data: [AuthorUpdateArgs]): [Author] - deleteAuthor(id: ID!): Author - deleteAuthors(ids: [ID!]): [Author] + deleteAuthor(where: AuthorWhereUniqueInput!): Author + deleteAuthors(where: [AuthorWhereUniqueInput!]!): [Author] } type Query { diff --git a/examples-staging/auth/schema.graphql b/examples-staging/auth/schema.graphql index 27a91b43f07..8737df31952 100644 --- a/examples-staging/auth/schema.graphql +++ b/examples-staging/auth/schema.graphql @@ -91,8 +91,8 @@ type Mutation { createUsers(data: [UsersCreateInput]): [User] updateUser(id: ID!, data: UserUpdateInput): User updateUsers(data: [UserUpdateArgs]): [User] - deleteUser(id: ID!): User - deleteUsers(ids: [ID!]): [User] + deleteUser(where: UserWhereUniqueInput!): User + deleteUsers(where: [UserWhereUniqueInput!]!): [User] authenticateUserWithPassword( email: String! password: String! diff --git a/examples-staging/basic/schema.graphql b/examples-staging/basic/schema.graphql index fd95da5072b..f10a7eaf32c 100644 --- a/examples-staging/basic/schema.graphql +++ b/examples-staging/basic/schema.graphql @@ -355,20 +355,20 @@ type Mutation { createUsers(data: [UsersCreateInput]): [User] updateUser(id: ID!, data: UserUpdateInput): User updateUsers(data: [UserUpdateArgs]): [User] - deleteUser(id: ID!): User - deleteUsers(ids: [ID!]): [User] + deleteUser(where: UserWhereUniqueInput!): User + deleteUsers(where: [UserWhereUniqueInput!]!): [User] createPhoneNumber(data: PhoneNumberCreateInput): PhoneNumber createPhoneNumbers(data: [PhoneNumbersCreateInput]): [PhoneNumber] updatePhoneNumber(id: ID!, data: PhoneNumberUpdateInput): PhoneNumber updatePhoneNumbers(data: [PhoneNumberUpdateArgs]): [PhoneNumber] - deletePhoneNumber(id: ID!): PhoneNumber - deletePhoneNumbers(ids: [ID!]): [PhoneNumber] + deletePhoneNumber(where: PhoneNumberWhereUniqueInput!): PhoneNumber + deletePhoneNumbers(where: [PhoneNumberWhereUniqueInput!]!): [PhoneNumber] createPost(data: PostCreateInput): Post createPosts(data: [PostsCreateInput]): [Post] updatePost(id: ID!, data: PostUpdateInput): Post updatePosts(data: [PostUpdateArgs]): [Post] - deletePost(id: ID!): Post - deletePosts(ids: [ID!]): [Post] + deletePost(where: PostWhereUniqueInput!): Post + deletePosts(where: [PostWhereUniqueInput!]!): [Post] authenticateUserWithPassword( email: String! password: String! diff --git a/examples-staging/ecommerce/mutations/checkout.ts b/examples-staging/ecommerce/mutations/checkout.ts index 09cd142d597..78645966d40 100644 --- a/examples-staging/ecommerce/mutations/checkout.ts +++ b/examples-staging/ecommerce/mutations/checkout.ts @@ -88,7 +88,7 @@ async function checkout(root: any, { token }: Arguments, context: KeystoneContex const cartItemIds = user.cart.map((cartItem: any) => cartItem.id); console.log('gonna create delete cartItems'); await context.lists.CartItem.deleteMany({ - ids: cartItemIds, + where: cartItemIds.map((id: string) => ({ id })), }); return order; } diff --git a/examples-staging/ecommerce/schema.graphql b/examples-staging/ecommerce/schema.graphql index f599403b371..384e512eb27 100644 --- a/examples-staging/ecommerce/schema.graphql +++ b/examples-staging/ecommerce/schema.graphql @@ -764,44 +764,44 @@ type Mutation { createUsers(data: [UsersCreateInput]): [User] updateUser(id: ID!, data: UserUpdateInput): User updateUsers(data: [UserUpdateArgs]): [User] - deleteUser(id: ID!): User - deleteUsers(ids: [ID!]): [User] + deleteUser(where: UserWhereUniqueInput!): User + deleteUsers(where: [UserWhereUniqueInput!]!): [User] createProduct(data: ProductCreateInput): Product createProducts(data: [ProductsCreateInput]): [Product] updateProduct(id: ID!, data: ProductUpdateInput): Product updateProducts(data: [ProductUpdateArgs]): [Product] - deleteProduct(id: ID!): Product - deleteProducts(ids: [ID!]): [Product] + deleteProduct(where: ProductWhereUniqueInput!): Product + deleteProducts(where: [ProductWhereUniqueInput!]!): [Product] createProductImage(data: ProductImageCreateInput): ProductImage createProductImages(data: [ProductImagesCreateInput]): [ProductImage] updateProductImage(id: ID!, data: ProductImageUpdateInput): ProductImage updateProductImages(data: [ProductImageUpdateArgs]): [ProductImage] - deleteProductImage(id: ID!): ProductImage - deleteProductImages(ids: [ID!]): [ProductImage] + deleteProductImage(where: ProductImageWhereUniqueInput!): ProductImage + deleteProductImages(where: [ProductImageWhereUniqueInput!]!): [ProductImage] createCartItem(data: CartItemCreateInput): CartItem createCartItems(data: [CartItemsCreateInput]): [CartItem] updateCartItem(id: ID!, data: CartItemUpdateInput): CartItem updateCartItems(data: [CartItemUpdateArgs]): [CartItem] - deleteCartItem(id: ID!): CartItem - deleteCartItems(ids: [ID!]): [CartItem] + deleteCartItem(where: CartItemWhereUniqueInput!): CartItem + deleteCartItems(where: [CartItemWhereUniqueInput!]!): [CartItem] createOrderItem(data: OrderItemCreateInput): OrderItem createOrderItems(data: [OrderItemsCreateInput]): [OrderItem] updateOrderItem(id: ID!, data: OrderItemUpdateInput): OrderItem updateOrderItems(data: [OrderItemUpdateArgs]): [OrderItem] - deleteOrderItem(id: ID!): OrderItem - deleteOrderItems(ids: [ID!]): [OrderItem] + deleteOrderItem(where: OrderItemWhereUniqueInput!): OrderItem + deleteOrderItems(where: [OrderItemWhereUniqueInput!]!): [OrderItem] createOrder(data: OrderCreateInput): Order createOrders(data: [OrdersCreateInput]): [Order] updateOrder(id: ID!, data: OrderUpdateInput): Order updateOrders(data: [OrderUpdateArgs]): [Order] - deleteOrder(id: ID!): Order - deleteOrders(ids: [ID!]): [Order] + deleteOrder(where: OrderWhereUniqueInput!): Order + deleteOrders(where: [OrderWhereUniqueInput!]!): [Order] createRole(data: RoleCreateInput): Role createRoles(data: [RolesCreateInput]): [Role] updateRole(id: ID!, data: RoleUpdateInput): Role updateRoles(data: [RoleUpdateArgs]): [Role] - deleteRole(id: ID!): Role - deleteRoles(ids: [ID!]): [Role] + deleteRole(where: RoleWhereUniqueInput!): Role + deleteRoles(where: [RoleWhereUniqueInput!]!): [Role] authenticateUserWithPassword( email: String! password: String! diff --git a/examples-staging/embedded-nextjs/schema.graphql b/examples-staging/embedded-nextjs/schema.graphql index 20a2063b387..3c4655fed16 100644 --- a/examples-staging/embedded-nextjs/schema.graphql +++ b/examples-staging/embedded-nextjs/schema.graphql @@ -86,8 +86,8 @@ type Mutation { createPosts(data: [PostsCreateInput]): [Post] updatePost(id: ID!, data: PostUpdateInput): Post updatePosts(data: [PostUpdateArgs]): [Post] - deletePost(id: ID!): Post - deletePosts(ids: [ID!]): [Post] + deletePost(where: PostWhereUniqueInput!): Post + deletePosts(where: [PostWhereUniqueInput!]!): [Post] } type Query { diff --git a/examples-staging/graphql-api-endpoint/schema.graphql b/examples-staging/graphql-api-endpoint/schema.graphql index 095423692b2..13bc9dd812d 100644 --- a/examples-staging/graphql-api-endpoint/schema.graphql +++ b/examples-staging/graphql-api-endpoint/schema.graphql @@ -322,20 +322,20 @@ type Mutation { createUsers(data: [UsersCreateInput]): [User] updateUser(id: ID!, data: UserUpdateInput): User updateUsers(data: [UserUpdateArgs]): [User] - deleteUser(id: ID!): User - deleteUsers(ids: [ID!]): [User] + deleteUser(where: UserWhereUniqueInput!): User + deleteUsers(where: [UserWhereUniqueInput!]!): [User] createPost(data: PostCreateInput): Post createPosts(data: [PostsCreateInput]): [Post] updatePost(id: ID!, data: PostUpdateInput): Post updatePosts(data: [PostUpdateArgs]): [Post] - deletePost(id: ID!): Post - deletePosts(ids: [ID!]): [Post] + deletePost(where: PostWhereUniqueInput!): Post + deletePosts(where: [PostWhereUniqueInput!]!): [Post] createTag(data: TagCreateInput): Tag createTags(data: [TagsCreateInput]): [Tag] updateTag(id: ID!, data: TagUpdateInput): Tag updateTags(data: [TagUpdateArgs]): [Tag] - deleteTag(id: ID!): Tag - deleteTags(ids: [ID!]): [Tag] + deleteTag(where: TagWhereUniqueInput!): Tag + deleteTags(where: [TagWhereUniqueInput!]!): [Tag] authenticateUserWithPassword( email: String! password: String! diff --git a/examples-staging/playground/schema.graphql b/examples-staging/playground/schema.graphql index 987ce514026..2a19209f604 100644 --- a/examples-staging/playground/schema.graphql +++ b/examples-staging/playground/schema.graphql @@ -66,8 +66,8 @@ type Mutation { createNotes(data: [NotesCreateInput]): [Note] updateNote(id: ID!, data: NoteUpdateInput): Note updateNotes(data: [NoteUpdateArgs]): [Note] - deleteNote(id: ID!): Note - deleteNotes(ids: [ID!]): [Note] + deleteNote(where: NoteWhereUniqueInput!): Note + deleteNotes(where: [NoteWhereUniqueInput!]!): [Note] } type Query { diff --git a/examples-staging/roles/schema.graphql b/examples-staging/roles/schema.graphql index b6fab947220..1ffe8df5daa 100644 --- a/examples-staging/roles/schema.graphql +++ b/examples-staging/roles/schema.graphql @@ -295,20 +295,20 @@ type Mutation { createTodos(data: [TodosCreateInput]): [Todo] updateTodo(id: ID!, data: TodoUpdateInput): Todo updateTodos(data: [TodoUpdateArgs]): [Todo] - deleteTodo(id: ID!): Todo - deleteTodos(ids: [ID!]): [Todo] + deleteTodo(where: TodoWhereUniqueInput!): Todo + deleteTodos(where: [TodoWhereUniqueInput!]!): [Todo] createPerson(data: PersonCreateInput): Person createPeople(data: [PeopleCreateInput]): [Person] updatePerson(id: ID!, data: PersonUpdateInput): Person updatePeople(data: [PersonUpdateArgs]): [Person] - deletePerson(id: ID!): Person - deletePeople(ids: [ID!]): [Person] + deletePerson(where: PersonWhereUniqueInput!): Person + deletePeople(where: [PersonWhereUniqueInput!]!): [Person] createRole(data: RoleCreateInput): Role createRoles(data: [RolesCreateInput]): [Role] updateRole(id: ID!, data: RoleUpdateInput): Role updateRoles(data: [RoleUpdateArgs]): [Role] - deleteRole(id: ID!): Role - deleteRoles(ids: [ID!]): [Role] + deleteRole(where: RoleWhereUniqueInput!): Role + deleteRoles(where: [RoleWhereUniqueInput!]!): [Role] authenticatePersonWithPassword( email: String! password: String! diff --git a/examples-staging/sandbox/schema.graphql b/examples-staging/sandbox/schema.graphql index 2cfd8a8081c..f7f6030e250 100644 --- a/examples-staging/sandbox/schema.graphql +++ b/examples-staging/sandbox/schema.graphql @@ -223,14 +223,14 @@ type Mutation { createTodos(data: [TodosCreateInput]): [Todo] updateTodo(id: ID!, data: TodoUpdateInput): Todo updateTodos(data: [TodoUpdateArgs]): [Todo] - deleteTodo(id: ID!): Todo - deleteTodos(ids: [ID!]): [Todo] + deleteTodo(where: TodoWhereUniqueInput!): Todo + deleteTodos(where: [TodoWhereUniqueInput!]!): [Todo] createUser(data: UserCreateInput): User createUsers(data: [UsersCreateInput]): [User] updateUser(id: ID!, data: UserUpdateInput): User updateUsers(data: [UserUpdateArgs]): [User] - deleteUser(id: ID!): User - deleteUsers(ids: [ID!]): [User] + deleteUser(where: UserWhereUniqueInput!): User + deleteUsers(where: [UserWhereUniqueInput!]!): [User] } type Query { diff --git a/examples/blog/schema.graphql b/examples/blog/schema.graphql index e89ebea716b..5ddc9da6ad0 100644 --- a/examples/blog/schema.graphql +++ b/examples/blog/schema.graphql @@ -193,14 +193,14 @@ type Mutation { createPosts(data: [PostsCreateInput]): [Post] updatePost(id: ID!, data: PostUpdateInput): Post updatePosts(data: [PostUpdateArgs]): [Post] - deletePost(id: ID!): Post - deletePosts(ids: [ID!]): [Post] + deletePost(where: PostWhereUniqueInput!): Post + deletePosts(where: [PostWhereUniqueInput!]!): [Post] createAuthor(data: AuthorCreateInput): Author createAuthors(data: [AuthorsCreateInput]): [Author] updateAuthor(id: ID!, data: AuthorUpdateInput): Author updateAuthors(data: [AuthorUpdateArgs]): [Author] - deleteAuthor(id: ID!): Author - deleteAuthors(ids: [ID!]): [Author] + deleteAuthor(where: AuthorWhereUniqueInput!): Author + deleteAuthors(where: [AuthorWhereUniqueInput!]!): [Author] } type Query { diff --git a/examples/custom-admin-ui-logo/schema.graphql b/examples/custom-admin-ui-logo/schema.graphql index 55380b7c1d2..30beff9bc69 100644 --- a/examples/custom-admin-ui-logo/schema.graphql +++ b/examples/custom-admin-ui-logo/schema.graphql @@ -179,14 +179,14 @@ type Mutation { createTasks(data: [TasksCreateInput]): [Task] updateTask(id: ID!, data: TaskUpdateInput): Task updateTasks(data: [TaskUpdateArgs]): [Task] - deleteTask(id: ID!): Task - deleteTasks(ids: [ID!]): [Task] + deleteTask(where: TaskWhereUniqueInput!): Task + deleteTasks(where: [TaskWhereUniqueInput!]!): [Task] createPerson(data: PersonCreateInput): Person createPeople(data: [PeopleCreateInput]): [Person] updatePerson(id: ID!, data: PersonUpdateInput): Person updatePeople(data: [PersonUpdateArgs]): [Person] - deletePerson(id: ID!): Person - deletePeople(ids: [ID!]): [Person] + deletePerson(where: PersonWhereUniqueInput!): Person + deletePeople(where: [PersonWhereUniqueInput!]!): [Person] } type Query { diff --git a/examples/custom-field-view/schema.graphql b/examples/custom-field-view/schema.graphql index 633316ca9e5..64ed5a1fcfd 100644 --- a/examples/custom-field-view/schema.graphql +++ b/examples/custom-field-view/schema.graphql @@ -182,14 +182,14 @@ type Mutation { createTasks(data: [TasksCreateInput]): [Task] updateTask(id: ID!, data: TaskUpdateInput): Task updateTasks(data: [TaskUpdateArgs]): [Task] - deleteTask(id: ID!): Task - deleteTasks(ids: [ID!]): [Task] + deleteTask(where: TaskWhereUniqueInput!): Task + deleteTasks(where: [TaskWhereUniqueInput!]!): [Task] createPerson(data: PersonCreateInput): Person createPeople(data: [PeopleCreateInput]): [Person] updatePerson(id: ID!, data: PersonUpdateInput): Person updatePeople(data: [PersonUpdateArgs]): [Person] - deletePerson(id: ID!): Person - deletePeople(ids: [ID!]): [Person] + deletePerson(where: PersonWhereUniqueInput!): Person + deletePeople(where: [PersonWhereUniqueInput!]!): [Person] } type Query { diff --git a/examples/custom-field/schema.graphql b/examples/custom-field/schema.graphql index 491ee9da0e4..0cdc565f96f 100644 --- a/examples/custom-field/schema.graphql +++ b/examples/custom-field/schema.graphql @@ -205,14 +205,14 @@ type Mutation { createPosts(data: [PostsCreateInput]): [Post] updatePost(id: ID!, data: PostUpdateInput): Post updatePosts(data: [PostUpdateArgs]): [Post] - deletePost(id: ID!): Post - deletePosts(ids: [ID!]): [Post] + deletePost(where: PostWhereUniqueInput!): Post + deletePosts(where: [PostWhereUniqueInput!]!): [Post] createAuthor(data: AuthorCreateInput): Author createAuthors(data: [AuthorsCreateInput]): [Author] updateAuthor(id: ID!, data: AuthorUpdateInput): Author updateAuthors(data: [AuthorUpdateArgs]): [Author] - deleteAuthor(id: ID!): Author - deleteAuthors(ids: [ID!]): [Author] + deleteAuthor(where: AuthorWhereUniqueInput!): Author + deleteAuthors(where: [AuthorWhereUniqueInput!]!): [Author] } type Query { diff --git a/examples/default-values/schema.graphql b/examples/default-values/schema.graphql index 55380b7c1d2..30beff9bc69 100644 --- a/examples/default-values/schema.graphql +++ b/examples/default-values/schema.graphql @@ -179,14 +179,14 @@ type Mutation { createTasks(data: [TasksCreateInput]): [Task] updateTask(id: ID!, data: TaskUpdateInput): Task updateTasks(data: [TaskUpdateArgs]): [Task] - deleteTask(id: ID!): Task - deleteTasks(ids: [ID!]): [Task] + deleteTask(where: TaskWhereUniqueInput!): Task + deleteTasks(where: [TaskWhereUniqueInput!]!): [Task] createPerson(data: PersonCreateInput): Person createPeople(data: [PeopleCreateInput]): [Person] updatePerson(id: ID!, data: PersonUpdateInput): Person updatePeople(data: [PersonUpdateArgs]): [Person] - deletePerson(id: ID!): Person - deletePeople(ids: [ID!]): [Person] + deletePerson(where: PersonWhereUniqueInput!): Person + deletePeople(where: [PersonWhereUniqueInput!]!): [Person] } type Query { diff --git a/examples/document-field/schema.graphql b/examples/document-field/schema.graphql index f59e850e4c3..0ad49bbde1a 100644 --- a/examples/document-field/schema.graphql +++ b/examples/document-field/schema.graphql @@ -208,14 +208,14 @@ type Mutation { createPosts(data: [PostsCreateInput]): [Post] updatePost(id: ID!, data: PostUpdateInput): Post updatePosts(data: [PostUpdateArgs]): [Post] - deletePost(id: ID!): Post - deletePosts(ids: [ID!]): [Post] + deletePost(where: PostWhereUniqueInput!): Post + deletePosts(where: [PostWhereUniqueInput!]!): [Post] createAuthor(data: AuthorCreateInput): Author createAuthors(data: [AuthorsCreateInput]): [Author] updateAuthor(id: ID!, data: AuthorUpdateInput): Author updateAuthors(data: [AuthorUpdateArgs]): [Author] - deleteAuthor(id: ID!): Author - deleteAuthors(ids: [ID!]): [Author] + deleteAuthor(where: AuthorWhereUniqueInput!): Author + deleteAuthors(where: [AuthorWhereUniqueInput!]!): [Author] } type Query { diff --git a/examples/extend-graphql-schema/schema.graphql b/examples/extend-graphql-schema/schema.graphql index ac8ce899eb7..bfba3284e41 100644 --- a/examples/extend-graphql-schema/schema.graphql +++ b/examples/extend-graphql-schema/schema.graphql @@ -193,14 +193,14 @@ type Mutation { createPosts(data: [PostsCreateInput]): [Post] updatePost(id: ID!, data: PostUpdateInput): Post updatePosts(data: [PostUpdateArgs]): [Post] - deletePost(id: ID!): Post - deletePosts(ids: [ID!]): [Post] + deletePost(where: PostWhereUniqueInput!): Post + deletePosts(where: [PostWhereUniqueInput!]!): [Post] createAuthor(data: AuthorCreateInput): Author createAuthors(data: [AuthorsCreateInput]): [Author] updateAuthor(id: ID!, data: AuthorUpdateInput): Author updateAuthors(data: [AuthorUpdateArgs]): [Author] - deleteAuthor(id: ID!): Author - deleteAuthors(ids: [ID!]): [Author] + deleteAuthor(where: AuthorWhereUniqueInput!): Author + deleteAuthors(where: [AuthorWhereUniqueInput!]!): [Author] """ Publish a post diff --git a/examples/json/schema.graphql b/examples/json/schema.graphql index 414c056db57..9aa15bc7ad4 100644 --- a/examples/json/schema.graphql +++ b/examples/json/schema.graphql @@ -156,14 +156,14 @@ type Mutation { createPackages(data: [PackagesCreateInput]): [Package] updatePackage(id: ID!, data: PackageUpdateInput): Package updatePackages(data: [PackageUpdateArgs]): [Package] - deletePackage(id: ID!): Package - deletePackages(ids: [ID!]): [Package] + deletePackage(where: PackageWhereUniqueInput!): Package + deletePackages(where: [PackageWhereUniqueInput!]!): [Package] createPerson(data: PersonCreateInput): Person createPeople(data: [PeopleCreateInput]): [Person] updatePerson(id: ID!, data: PersonUpdateInput): Person updatePeople(data: [PersonUpdateArgs]): [Person] - deletePerson(id: ID!): Person - deletePeople(ids: [ID!]): [Person] + deletePerson(where: PersonWhereUniqueInput!): Person + deletePeople(where: [PersonWhereUniqueInput!]!): [Person] } type Query { diff --git a/examples/task-manager/schema.graphql b/examples/task-manager/schema.graphql index 55380b7c1d2..30beff9bc69 100644 --- a/examples/task-manager/schema.graphql +++ b/examples/task-manager/schema.graphql @@ -179,14 +179,14 @@ type Mutation { createTasks(data: [TasksCreateInput]): [Task] updateTask(id: ID!, data: TaskUpdateInput): Task updateTasks(data: [TaskUpdateArgs]): [Task] - deleteTask(id: ID!): Task - deleteTasks(ids: [ID!]): [Task] + deleteTask(where: TaskWhereUniqueInput!): Task + deleteTasks(where: [TaskWhereUniqueInput!]!): [Task] createPerson(data: PersonCreateInput): Person createPeople(data: [PeopleCreateInput]): [Person] updatePerson(id: ID!, data: PersonUpdateInput): Person updatePeople(data: [PersonUpdateArgs]): [Person] - deletePerson(id: ID!): Person - deletePeople(ids: [ID!]): [Person] + deletePerson(where: PersonWhereUniqueInput!): Person + deletePeople(where: [PersonWhereUniqueInput!]!): [Person] } type Query { diff --git a/examples/testing/schema.graphql b/examples/testing/schema.graphql index 488720aca4f..02d9950b51c 100644 --- a/examples/testing/schema.graphql +++ b/examples/testing/schema.graphql @@ -198,14 +198,14 @@ type Mutation { createTasks(data: [TasksCreateInput]): [Task] updateTask(id: ID!, data: TaskUpdateInput): Task updateTasks(data: [TaskUpdateArgs]): [Task] - deleteTask(id: ID!): Task - deleteTasks(ids: [ID!]): [Task] + deleteTask(where: TaskWhereUniqueInput!): Task + deleteTasks(where: [TaskWhereUniqueInput!]!): [Task] createPerson(data: PersonCreateInput): Person createPeople(data: [PeopleCreateInput]): [Person] updatePerson(id: ID!, data: PersonUpdateInput): Person updatePeople(data: [PersonUpdateArgs]): [Person] - deletePerson(id: ID!): Person - deletePeople(ids: [ID!]): [Person] + deletePerson(where: PersonWhereUniqueInput!): Person + deletePeople(where: [PersonWhereUniqueInput!]!): [Person] authenticatePersonWithPassword( email: String! password: String! diff --git a/examples/virtual-field/schema.graphql b/examples/virtual-field/schema.graphql index 265e49a5433..e3a8d3b511c 100644 --- a/examples/virtual-field/schema.graphql +++ b/examples/virtual-field/schema.graphql @@ -204,14 +204,14 @@ type Mutation { createPosts(data: [PostsCreateInput]): [Post] updatePost(id: ID!, data: PostUpdateInput): Post updatePosts(data: [PostUpdateArgs]): [Post] - deletePost(id: ID!): Post - deletePosts(ids: [ID!]): [Post] + deletePost(where: PostWhereUniqueInput!): Post + deletePosts(where: [PostWhereUniqueInput!]!): [Post] createAuthor(data: AuthorCreateInput): Author createAuthors(data: [AuthorsCreateInput]): [Author] updateAuthor(id: ID!, data: AuthorUpdateInput): Author updateAuthors(data: [AuthorUpdateArgs]): [Author] - deleteAuthor(id: ID!): Author - deleteAuthors(ids: [ID!]): [Author] + deleteAuthor(where: AuthorWhereUniqueInput!): Author + deleteAuthors(where: [AuthorWhereUniqueInput!]!): [Author] } type Query { diff --git a/examples/with-auth/schema.graphql b/examples/with-auth/schema.graphql index 488720aca4f..02d9950b51c 100644 --- a/examples/with-auth/schema.graphql +++ b/examples/with-auth/schema.graphql @@ -198,14 +198,14 @@ type Mutation { createTasks(data: [TasksCreateInput]): [Task] updateTask(id: ID!, data: TaskUpdateInput): Task updateTasks(data: [TaskUpdateArgs]): [Task] - deleteTask(id: ID!): Task - deleteTasks(ids: [ID!]): [Task] + deleteTask(where: TaskWhereUniqueInput!): Task + deleteTasks(where: [TaskWhereUniqueInput!]!): [Task] createPerson(data: PersonCreateInput): Person createPeople(data: [PeopleCreateInput]): [Person] updatePerson(id: ID!, data: PersonUpdateInput): Person updatePeople(data: [PersonUpdateArgs]): [Person] - deletePerson(id: ID!): Person - deletePeople(ids: [ID!]): [Person] + deletePerson(where: PersonWhereUniqueInput!): Person + deletePeople(where: [PersonWhereUniqueInput!]!): [Person] authenticatePersonWithPassword( email: String! password: String! diff --git a/packages/keystone/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.tsx b/packages/keystone/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.tsx index 3e8ad8caef1..ad78a20b81c 100644 --- a/packages/keystone/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.tsx +++ b/packages/keystone/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.tsx @@ -225,7 +225,7 @@ function DeleteButton({ const toasts = useToasts(); const [deleteItem, { loading }] = useMutation( gql`mutation ($id: ID!) { - ${list.gqlNames.deleteMutationName}(id: $id) { + ${list.gqlNames.deleteMutationName}(where: { id: $id }) { id } }`, diff --git a/packages/keystone/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/index.tsx b/packages/keystone/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/index.tsx index 177dff1c384..e3b977c8737 100644 --- a/packages/keystone/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/index.tsx +++ b/packages/keystone/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/index.tsx @@ -436,8 +436,8 @@ function DeleteManyButton({ useMemo( () => gql` - mutation($ids: [ID!]!) { - ${list.gqlNames.deleteManyMutationName}(ids: $ids) { + mutation($where: [${list.gqlNames.whereUniqueInputName}!]!) { + ${list.gqlNames.deleteManyMutationName}(where: $where) { id } } @@ -468,7 +468,7 @@ function DeleteManyButton({ label: 'Delete', action: async () => { await deleteItems({ - variables: { ids: [...selectedItems] }, + variables: { where: [...selectedItems].map(id => ({ id })) }, }).catch(err => { toasts.addToast({ title: 'Failed to delete items', diff --git a/packages/keystone/src/lib/core/mutations/index.ts b/packages/keystone/src/lib/core/mutations/index.ts index 4c9229393de..c88d92d22f4 100644 --- a/packages/keystone/src/lib/core/mutations/index.ts +++ b/packages/keystone/src/lib/core/mutations/index.ts @@ -88,23 +88,22 @@ export function getMutationsForList(list: InitialisedList, provider: DatabasePro const deleteOne = schema.field({ type: list.types.output, - args: { id: schema.arg({ type: schema.nonNull(schema.ID) }) }, - resolve(rootVal, { id }, context) { - return deletes.deleteOne({ id }, list, context); + args: { where: schema.arg({ type: schema.nonNull(list.types.uniqueWhere) }) }, + resolve(rootVal, { where }, context) { + return deletes.deleteOne(where, list, context); }, }); const deleteMany = schema.field({ type: schema.list(list.types.output), - args: { ids: schema.arg({ type: schema.list(schema.nonNull(schema.ID)) }) }, - resolve(rootVal, { ids }, context) { + args: { + where: schema.arg({ + type: schema.nonNull(schema.list(schema.nonNull(list.types.uniqueWhere))), + }), + }, + resolve(rootVal, { where }, context) { return promisesButSettledWhenAllSettledAndInOrder( - deletes.deleteMany( - (ids || []).map(id => ({ id })), - list, - context, - provider - ) + deletes.deleteMany(where, list, context, provider) ); }, }); diff --git a/packages/keystone/src/scripts/tests/fixtures/basic-project/schema.graphql b/packages/keystone/src/scripts/tests/fixtures/basic-project/schema.graphql index 52fcac3e65e..9b0505c4968 100644 --- a/packages/keystone/src/scripts/tests/fixtures/basic-project/schema.graphql +++ b/packages/keystone/src/scripts/tests/fixtures/basic-project/schema.graphql @@ -66,8 +66,8 @@ type Mutation { createTodos(data: [TodosCreateInput]): [Todo] updateTodo(id: ID!, data: TodoUpdateInput): Todo updateTodos(data: [TodoUpdateArgs]): [Todo] - deleteTodo(id: ID!): Todo - deleteTodos(ids: [ID!]): [Todo] + deleteTodo(where: TodoWhereUniqueInput!): Todo + deleteTodos(where: [TodoWhereUniqueInput!]!): [Todo] } type Query { diff --git a/packages/types/src/context.ts b/packages/types/src/context.ts index 89c77a518d3..b19cd9cdf37 100644 --- a/packages/types/src/context.ts +++ b/packages/types/src/context.ts @@ -65,9 +65,15 @@ export type KeystoneListsAPI[]>; - deleteOne(args: { readonly id: string } & ResolveFields): Promise | null>; + deleteOne( + args: { + readonly where: KeystoneListsTypeInfo[Key]['inputs']['uniqueWhere']; + } & ResolveFields + ): Promise | null>; deleteMany( - args: { readonly ids: readonly string[] } & ResolveFields + args: { + readonly where: readonly KeystoneListsTypeInfo[Key]['inputs']['uniqueWhere'][]; + } & ResolveFields ): Promise[]>; }; }; @@ -113,9 +119,11 @@ export type KeystoneDbAPI; - deleteOne(args: { readonly id: string }): Promise; + deleteOne(args: { + readonly where: KeystoneListsTypeInfo[Key]['inputs']['uniqueWhere']; + }): Promise; deleteMany(args: { - readonly ids: readonly string[]; + readonly where: readonly KeystoneListsTypeInfo[Key]['inputs']['uniqueWhere'][]; }): Promise; }; }; diff --git a/tests/api-tests/access-control/authed.test.ts b/tests/api-tests/access-control/authed.test.ts index 8ef4eb30b57..d8ebf3f782d 100644 --- a/tests/api-tests/access-control/authed.test.ts +++ b/tests/api-tests/access-control/authed.test.ts @@ -99,7 +99,7 @@ describe('Authed', () => { const item = await context.lists[listKey].createOne({ data }); expect(item).not.toBe(null); expect(item.id).not.toBe(null); - await context.sudo().lists[listKey].deleteOne({ id: item.id }); + await context.sudo().lists[listKey].deleteOne({ where: { id: item.id } }); }); }); }); @@ -124,7 +124,7 @@ describe('Authed', () => { } else { expect(item[fieldName]).toBe(undefined); } - await context.sudo().lists[listKey].deleteOne({ id: item.id }); + await context.sudo().lists[listKey].deleteOne({ where: { id: item.id } }); }); }); }); @@ -143,7 +143,7 @@ describe('Authed', () => { }); expect(item).not.toBe(null); expect(item.id).not.toBe(null); - await context.sudo().lists[listKey].deleteOne({ id: item.id }); + await context.sudo().lists[listKey].deleteOne({ where: { id: item.id } }); }); }); }); @@ -395,7 +395,9 @@ describe('Authed', () => { test(`single allowed: ${JSON.stringify(access)}`, async () => { const { id } = await create({ name: 'Hello' }); - const deleted = await context.lists[nameFn[mode](access)].deleteOne({ id }); + const deleted = await context.lists[nameFn[mode](access)].deleteOne({ + where: { id }, + }); expect(deleted).not.toBe(null); expect(deleted!.id).toEqual(id); }); @@ -403,7 +405,7 @@ describe('Authed', () => { test(`single denies: ${JSON.stringify(access)}`, async () => { const { id: invalidId } = await create({ name: 'hi' }); const deleteMutationName = `delete${nameFn[mode](access)}`; - const query = `mutation { ${deleteMutationName}(id: "${invalidId}") { id } }`; + const query = `mutation { ${deleteMutationName}(where: { id: "${invalidId}" }) { id } }`; const { data, errors } = await context.graphql.raw({ query }); if (mode === 'imperative') { expect(errors).toBe(undefined); @@ -416,7 +418,7 @@ describe('Authed', () => { test(`single denies missing: ${JSON.stringify(access)}`, async () => { const deleteMutationName = `delete${nameFn[mode](access)}`; - const query = `mutation { ${deleteMutationName}(id: "${FAKE_ID[provider]}") { id } }`; + const query = `mutation { ${deleteMutationName}(where: { id: "${FAKE_ID[provider]}" }) { id } }`; const { data, errors } = await context.graphql.raw({ query }); expectNoAccess(data, errors, deleteMutationName); }); @@ -425,7 +427,7 @@ describe('Authed', () => { const { id: validId1 } = await create({ name: 'Hello' }); const { id: validId2 } = await create({ name: 'Hello' }); const multiDeleteMutationName = `delete${nameFn[mode](access)}s`; - const query = `mutation { ${multiDeleteMutationName}(ids: ["${validId1}", "${validId2}"]) { id } }`; + const query = `mutation { ${multiDeleteMutationName}(where: [{ id: "${validId1}" }, { id: "${validId2}" }]) { id } }`; const { data, errors } = await context.graphql.raw({ query }); expectNamedArray(data, errors, multiDeleteMutationName, [validId1, validId2]); }); @@ -434,7 +436,7 @@ describe('Authed', () => { const { id: validId1 } = await create({ name: 'hi' }); const { id: validId2 } = await create({ name: 'hi' }); const multiDeleteMutationName = `delete${nameFn[mode](access)}s`; - const query = `mutation { ${multiDeleteMutationName}(ids: ["${validId1}", "${validId2}"]) { id } }`; + const query = `mutation { ${multiDeleteMutationName}(where: [{ id: "${validId1}" }, { id: "${validId2}" }]) { id } }`; const { data, errors } = await context.graphql.raw({ query }); if (mode === 'imperative') { expectNamedArray(data, errors, multiDeleteMutationName, [validId1, validId2]); @@ -451,7 +453,7 @@ describe('Authed', () => { const { id: validId1 } = await create({ name: 'Hello' }); const { id: invalidId } = await create({ name: 'hi' }); const multiDeleteMutationName = `delete${nameFn[mode](access)}s`; - const query = `mutation { ${multiDeleteMutationName}(ids: ["${validId1}", "${invalidId}"]) { id } }`; + const query = `mutation { ${multiDeleteMutationName}(where: [{ id: "${validId1}" }, { id: "${invalidId}" }]) { id } }`; const { data, errors } = await context.graphql.raw({ query }); if (mode === 'imperative') { expectNamedArray(data, errors, multiDeleteMutationName, [validId1, invalidId]); @@ -463,7 +465,7 @@ describe('Authed', () => { test(`multi denies missing: ${JSON.stringify(access)}`, async () => { const multiDeleteMutationName = `delete${nameFn[mode](access)}s`; - const query = `mutation { ${multiDeleteMutationName}(ids: ["${FAKE_ID[provider]}", "${FAKE_ID_2[provider]}"]) { id } }`; + const query = `mutation { ${multiDeleteMutationName}(where: [{ id: "${FAKE_ID[provider]}" }, { id: "${FAKE_ID_2[provider]}" }]) { id } }`; const { data, errors } = await context.graphql.raw({ query }); expectAccessDenied(errors, [ { path: [multiDeleteMutationName, 0] }, diff --git a/tests/api-tests/access-control/mutations-list-declarative.test.ts b/tests/api-tests/access-control/mutations-list-declarative.test.ts index d191a1a54fd..7f7c43ed52e 100644 --- a/tests/api-tests/access-control/mutations-list-declarative.test.ts +++ b/tests/api-tests/access-control/mutations-list-declarative.test.ts @@ -55,11 +55,11 @@ describe('Access control - Imperative => declarative', () => { // Valid names should pass const user1 = await context.lists.User.createOne({ data: { name: 'good' } }); const user2 = await context.lists.User.createOne({ data: { name: 'no delete' } }); - await context.lists.User.deleteOne({ id: user1.id }); + await context.lists.User.deleteOne({ where: { id: user1.id } }); // Invalid name const { data, errors } = await context.graphql.raw({ - query: `mutation ($id: ID!) { deleteUser(id: $id) { id } }`, + query: `mutation ($id: ID!) { deleteUser(where: { id: $id }) { id } }`, variables: { id: user2.id }, }); @@ -155,10 +155,14 @@ describe('Access control - Imperative => declarative', () => { // Mix of good and bad names const { data, errors } = await context.graphql.raw({ - query: `mutation ($ids: [ID!]) { deleteUsers(ids: $ids) { id name } }`, - variables: { ids: [users[0].id, users[1].id, users[2].id, users[3].id] }, + query: `mutation ($where: [UserWhereUniqueInput!]!) { deleteUsers(where: $where) { id name } }`, + variables: { + where: [users[0].id, users[1].id, users[2].id, users[3].id].map(id => ({ id })), + }, }); + expectAccessDenied(errors, [{ path: ['deleteUsers', 1] }, { path: ['deleteUsers', 3] }]); + // Valid users are returned, invalid come back as null // The invalid deletes should have errors which point to the nulls in their path expect(data).toEqual({ @@ -169,7 +173,6 @@ describe('Access control - Imperative => declarative', () => { null, ], }); - expectAccessDenied(errors, [{ path: ['deleteUsers', 1] }, { path: ['deleteUsers', 3] }]); // Three users should still exist in the database const _users = await context.lists.User.findMany({ diff --git a/tests/api-tests/access-control/mutations-list-static.test.ts b/tests/api-tests/access-control/mutations-list-static.test.ts index c6dd438bfbe..3ce9600cacc 100644 --- a/tests/api-tests/access-control/mutations-list-static.test.ts +++ b/tests/api-tests/access-control/mutations-list-static.test.ts @@ -85,11 +85,11 @@ describe('Access control - Imperative => static', () => { // Valid names should pass const user1 = await context.lists.User.createOne({ data: { name: 'good' } }); const user2 = await context.lists.User.createOne({ data: { name: 'no delete' } }); - await context.lists.User.deleteOne({ id: user1.id }); + await context.lists.User.deleteOne({ where: { id: user1.id } }); // Invalid name const { data, errors } = await context.graphql.raw({ - query: `mutation ($id: ID!) { deleteUser(id: $id) { id } }`, + query: `mutation ($id: ID!) { deleteUser(where: { id: $id }) { id } }`, variables: { id: user2.id }, }); @@ -217,8 +217,10 @@ describe('Access control - Imperative => static', () => { // Mix of good and bad names const { data, errors } = await context.graphql.raw({ - query: `mutation ($ids: [ID!]) { deleteUsers(ids: $ids) { id name } }`, - variables: { ids: [users[0].id, users[1].id, users[2].id, users[3].id] }, + query: `mutation ($where: [UserWhereUniqueInput!]!) { deleteUsers(where: $where) { id name } }`, + variables: { + where: [users[0].id, users[1].id, users[2].id, users[3].id].map(id => ({ id })), + }, }); // Valid users are returned, invalid come back as null diff --git a/tests/api-tests/access-control/not-authed.test.ts b/tests/api-tests/access-control/not-authed.test.ts index b8dbe1c8898..752b0a93cf6 100644 --- a/tests/api-tests/access-control/not-authed.test.ts +++ b/tests/api-tests/access-control/not-authed.test.ts @@ -362,14 +362,14 @@ describe(`Not authed`, () => { .forEach(access => { test(`single denied: ${JSON.stringify(access)}`, async () => { const deleteMutationName = `delete${nameFn[mode](access)}`; - const query = `mutation { ${deleteMutationName}(id: "${FAKE_ID[provider]}") { id } }`; + const query = `mutation { ${deleteMutationName}(where: {id: "${FAKE_ID[provider]}" }) { id } }`; const { data, errors } = await context.graphql.raw({ query }); expectNoAccess(data, errors, deleteMutationName); }); test(`multi denied: ${JSON.stringify(access)}`, async () => { const multiDeleteMutationName = `delete${nameFn[mode](access)}s`; - const query = `mutation { ${multiDeleteMutationName}(ids: ["${FAKE_ID[provider]}"]) { id } }`; + const query = `mutation { ${multiDeleteMutationName}(where: [{ id: "${FAKE_ID[provider]}" }]) { id } }`; const { data, errors } = await context.graphql.raw({ query }); expect(data).toEqual({ [multiDeleteMutationName]: [null] }); diff --git a/tests/api-tests/fields/crud.test.ts b/tests/api-tests/fields/crud.test.ts index 3805287ba43..2d492c1cc89 100644 --- a/tests/api-tests/fields/crud.test.ts +++ b/tests/api-tests/fields/crud.test.ts @@ -101,7 +101,13 @@ testModules ? `id name ${readFieldName || fieldName} { ${subfieldName} }` : `id name ${readFieldName || fieldName}`; - const withHelpers = (wrappedFn: (args: any) => void | Promise) => { + const withHelpers = ( + wrappedFn: (args: { + context: KeystoneContext; + listKey: string; + items: readonly Record[]; + }) => void | Promise + ) => { return async ({ context, listKey }: { context: KeystoneContext; listKey: string }) => { const items = await context.lists[listKey].findMany({ orderBy: { name: 'asc' }, @@ -213,7 +219,7 @@ testModules keystoneTestWrapper( withHelpers(async ({ context, items, listKey }) => { const data = await context.lists[listKey].deleteOne({ - id: items[0].id, + where: { id: items[0].id }, query, }); expect(data).not.toBe(null); diff --git a/tests/api-tests/fields/types/document.test.ts b/tests/api-tests/fields/types/document.test.ts index 82f68d69e53..c3df3699ba4 100644 --- a/tests/api-tests/fields/types/document.test.ts +++ b/tests/api-tests/fields/types/document.test.ts @@ -215,7 +215,7 @@ describe('Document field type', () => { 'hydrateRelationships: true - dangling reference', runner(async ({ context }) => { const { alice, bob, charlie, post, content } = await initData({ context }); - await context.lists.Author.deleteOne({ id: bob.id }); + await context.lists.Author.deleteOne({ where: { id: bob.id } }); const _post = await context.lists.Post.findOne({ where: { id: post.id }, query: 'content { document(hydrateRelationships: true) }', diff --git a/tests/api-tests/hooks/validation.test.ts b/tests/api-tests/hooks/validation.test.ts index 730b94cca99..23fa581706f 100644 --- a/tests/api-tests/hooks/validation.test.ts +++ b/tests/api-tests/hooks/validation.test.ts @@ -77,11 +77,11 @@ describe('List Hooks: #validateInput()', () => { // Valid names should pass const user1 = await context.lists.User.createOne({ data: { name: 'good' } }); const user2 = await context.lists.User.createOne({ data: { name: 'no delete' } }); - await context.lists.User.deleteOne({ id: user1.id }); + await context.lists.User.deleteOne({ where: { id: user1.id } }); // Invalid name const { data, errors } = await context.graphql.raw({ - query: `mutation ($id: ID!) { deleteUser(id: $id) { id } }`, + query: `mutation ($id: ID!) { deleteUser(where: { id: $id }) { id } }`, variables: { id: user2.id }, }); @@ -206,8 +206,10 @@ describe('List Hooks: #validateInput()', () => { // Mix of good and bad names const { data, errors } = await context.graphql.raw({ - query: `mutation ($ids: [ID!]) { deleteUsers(ids: $ids) { id name } }`, - variables: { ids: [users[0].id, users[1].id, users[2].id, users[3].id] }, + query: `mutation ($where: [UserWhereUniqueInput!]!) { deleteUsers(where: $where) { id name } }`, + variables: { + where: [users[0].id, users[1].id, users[2].id, users[3].id].map(id => ({ id })), + }, }); // Valid users are returned, invalid come back as null diff --git a/tests/api-tests/queries/cache-hints.test.ts b/tests/api-tests/queries/cache-hints.test.ts index fe0e12d54bb..c9fe7518e6c 100644 --- a/tests/api-tests/queries/cache-hints.test.ts +++ b/tests/api-tests/queries/cache-hints.test.ts @@ -303,7 +303,7 @@ describe('cache hints', () => { const { body } = await graphQLRequest({ query: ` mutation { - deletePost(id: "${posts[0].id}") { + deletePost(where: { id: "${posts[0].id}" }) { id } } diff --git a/tests/api-tests/relationships/crud-self-ref/many-to-many-one-sided.test.ts b/tests/api-tests/relationships/crud-self-ref/many-to-many-one-sided.test.ts index eb01e96b2d6..62956e1d6ba 100644 --- a/tests/api-tests/relationships/crud-self-ref/many-to-many-one-sided.test.ts +++ b/tests/api-tests/relationships/crud-self-ref/many-to-many-one-sided.test.ts @@ -324,7 +324,7 @@ describe(`Many-to-many relationships`, () => { const { user, friend } = await createUserAndFriend(context); // Run the query to disconnect the location from company - const _user = await context.lists.User.deleteOne({ id: user.id }); + const _user = await context.lists.User.deleteOne({ where: { id: user.id } }); expect(_user?.id).toBe(user.id); // Check the link has been broken diff --git a/tests/api-tests/relationships/crud-self-ref/many-to-many.test.ts b/tests/api-tests/relationships/crud-self-ref/many-to-many.test.ts index a8d854a71a7..8a77b6401b3 100644 --- a/tests/api-tests/relationships/crud-self-ref/many-to-many.test.ts +++ b/tests/api-tests/relationships/crud-self-ref/many-to-many.test.ts @@ -410,7 +410,7 @@ describe(`Many-to-many relationships`, () => { const { user, friend } = await createUserAndFriend(context); // Run the query to disconnect the location from company - const _user = await context.lists.User.deleteOne({ id: user.id }); + const _user = await context.lists.User.deleteOne({ where: { id: user.id } }); expect(_user?.id).toBe(user.id); // Check the link has been broken diff --git a/tests/api-tests/relationships/crud-self-ref/one-to-many-one-sided.test.ts b/tests/api-tests/relationships/crud-self-ref/one-to-many-one-sided.test.ts index 22378b6610e..9b18346a8f7 100644 --- a/tests/api-tests/relationships/crud-self-ref/one-to-many-one-sided.test.ts +++ b/tests/api-tests/relationships/crud-self-ref/one-to-many-one-sided.test.ts @@ -305,7 +305,7 @@ describe(`One-to-many relationships`, () => { const { friend, user } = await createUserAndFriend(context); // Run the query to disconnect the location from company - const _user = await context.lists.User.deleteOne({ id: user.id }); + const _user = await context.lists.User.deleteOne({ where: { id: user.id } }); expect(_user?.id).toBe(user.id); // Check the link has been broken @@ -323,7 +323,7 @@ describe(`One-to-many relationships`, () => { // Delete company {name} const id = users.find(company => company.name === name)?.id; - const _user = await context.lists.User.deleteOne({ id }); + const _user = await context.lists.User.deleteOne({ where: { id } }); expect(_user?.id).toBe(id); // Check all the companies look how we expect @@ -380,7 +380,7 @@ describe(`One-to-many relationships`, () => { // Delete friend {name} const id = users.find(user => user.name === name)?.id; - const _user = await context.lists.User.deleteOne({ id }); + const _user = await context.lists.User.deleteOne({ where: { id } }); expect(_user?.id).toBe(id); // Check all the companies look how we expect diff --git a/tests/api-tests/relationships/crud-self-ref/one-to-many.test.ts b/tests/api-tests/relationships/crud-self-ref/one-to-many.test.ts index 3ae5b55b0b6..57239b8b6ed 100644 --- a/tests/api-tests/relationships/crud-self-ref/one-to-many.test.ts +++ b/tests/api-tests/relationships/crud-self-ref/one-to-many.test.ts @@ -443,7 +443,7 @@ describe(`One-to-many relationships`, () => { const { user, friend } = await createUserAndFriend(context); // Run the query to disconnect the location from company - const _user = await context.lists.User.deleteOne({ id: user.id }); + const _user = await context.lists.User.deleteOne({ where: { id: user.id } }); expect(_user?.id).toBe(user.id); // Check the link has been broken diff --git a/tests/api-tests/relationships/crud-self-ref/one-to-one.test.ts b/tests/api-tests/relationships/crud-self-ref/one-to-one.test.ts index c5b189effb3..ac2b9d4da59 100644 --- a/tests/api-tests/relationships/crud-self-ref/one-to-one.test.ts +++ b/tests/api-tests/relationships/crud-self-ref/one-to-one.test.ts @@ -427,7 +427,7 @@ describe(`One-to-one relationships`, () => { const { user, friend } = await createUserAndFriend(context); // Run the query to disconnect the location from company - const _user = await context.lists.User.deleteOne({ id: user.id }); + const _user = await context.lists.User.deleteOne({ where: { id: user.id } }); expect(_user?.id).toBe(user.id); // Check the link has been broken diff --git a/tests/api-tests/relationships/crud/many-to-many-one-sided.test.ts b/tests/api-tests/relationships/crud/many-to-many-one-sided.test.ts index 0e745c77018..1439f2ba07f 100644 --- a/tests/api-tests/relationships/crud/many-to-many-one-sided.test.ts +++ b/tests/api-tests/relationships/crud/many-to-many-one-sided.test.ts @@ -399,7 +399,7 @@ describe(`Many-to-many relationships`, () => { const { location, company } = await createCompanyAndLocation(context); // Run the query to disconnect the location from company - const _company = await context.lists.Company.deleteOne({ id: company.id }); + const _company = await context.lists.Company.deleteOne({ where: { id: company.id } }); expect(_company?.id).toBe(company.id); // Check the link has been broken diff --git a/tests/api-tests/relationships/crud/many-to-many.test.ts b/tests/api-tests/relationships/crud/many-to-many.test.ts index dfeca71811e..23762204047 100644 --- a/tests/api-tests/relationships/crud/many-to-many.test.ts +++ b/tests/api-tests/relationships/crud/many-to-many.test.ts @@ -507,7 +507,7 @@ describe(`Many-to-many relationships`, () => { const { location, company } = await createCompanyAndLocation(context); // Run the query to disconnect the location from company - const _company = await context.lists.Company.deleteOne({ id: company.id }); + const _company = await context.lists.Company.deleteOne({ where: { id: company.id } }); expect(_company?.id).toBe(company.id); // Check the link has been broken diff --git a/tests/api-tests/relationships/crud/one-to-many-one-sided.test.ts b/tests/api-tests/relationships/crud/one-to-many-one-sided.test.ts index 1a46a7a70ba..d4485f8b213 100644 --- a/tests/api-tests/relationships/crud/one-to-many-one-sided.test.ts +++ b/tests/api-tests/relationships/crud/one-to-many-one-sided.test.ts @@ -340,7 +340,7 @@ describe(`One-to-many relationships`, () => { const { location, company } = await createCompanyAndLocation(context); // Run the query to disconnect the location from company - const _company = await context.lists.Company.deleteOne({ id: company.id }); + const _company = await context.lists.Company.deleteOne({ where: { id: company.id } }); expect(_company?.id).toBe(company.id); // Check the link has been broken @@ -358,7 +358,7 @@ describe(`One-to-many relationships`, () => { // Delete company {name} const id = companies.find(company => company.name === name)?.id; - const _company = await context.lists.Company.deleteOne({ id }); + const _company = await context.lists.Company.deleteOne({ where: { id } }); expect(_company?.id).toBe(id); // Check all the companies look how we expect @@ -413,7 +413,7 @@ describe(`One-to-many relationships`, () => { // Delete location {name} const id = locations.find(location => location.name === name)?.id; - const deleted = await context.lists.Location.deleteOne({ id }); + const deleted = await context.lists.Location.deleteOne({ where: { id } }); expect(deleted).not.toBe(null); expect(deleted!.id).toBe(id); diff --git a/tests/api-tests/relationships/crud/one-to-many.test.ts b/tests/api-tests/relationships/crud/one-to-many.test.ts index 434aa544188..2cd15ee43be 100644 --- a/tests/api-tests/relationships/crud/one-to-many.test.ts +++ b/tests/api-tests/relationships/crud/one-to-many.test.ts @@ -477,7 +477,7 @@ describe(`One-to-many relationships`, () => { const { location, company } = await createCompanyAndLocation(context); // Run the query to disconnect the location from company - const _company = await context.lists.Company.deleteOne({ id: company.id }); + const _company = await context.lists.Company.deleteOne({ where: { id: company.id } }); expect(_company?.id).toBe(company.id); // Check the link has been broken diff --git a/tests/api-tests/relationships/crud/one-to-one.test.ts b/tests/api-tests/relationships/crud/one-to-one.test.ts index b3b42239d03..b5ca174b3b5 100644 --- a/tests/api-tests/relationships/crud/one-to-one.test.ts +++ b/tests/api-tests/relationships/crud/one-to-one.test.ts @@ -922,7 +922,7 @@ describe(`One-to-one relationships`, () => { const { location, company } = await createCompanyAndLocation(context); // Run the query to disconnect the location from company - const _company = await context.lists.Company.deleteOne({ id: company.id }); + const _company = await context.lists.Company.deleteOne({ where: { id: company.id } }); expect(_company?.id).toBe(company.id); // Check the link has been broken @@ -939,7 +939,7 @@ describe(`One-to-one relationships`, () => { const { location, company } = await createLocationAndCompany(context); // Run the query to disconnect the location from company - const _location = await context.lists.Location.deleteOne({ id: location.id }); + const _location = await context.lists.Location.deleteOne({ where: { id: location.id } }); expect(_location?.id).toBe(location.id); // Check the link has been broken diff --git a/tests/api-tests/relationships/nested-mutations/two-way-backreference/to-many.test.ts b/tests/api-tests/relationships/nested-mutations/two-way-backreference/to-many.test.ts index aa4a0b9b164..5dd19d2855e 100644 --- a/tests/api-tests/relationships/nested-mutations/two-way-backreference/to-many.test.ts +++ b/tests/api-tests/relationships/nested-mutations/two-way-backreference/to-many.test.ts @@ -351,7 +351,7 @@ test( compareIds(teacher2.students, [student1, student2]); // Run the query to delete the student - await context.lists.Student.deleteOne({ id: student1.id }); + await context.lists.Student.deleteOne({ where: { id: student1.id } }); teacher1 = await getTeacher(context, teacher1.id); teacher2 = await getTeacher(context, teacher2.id); student1 = await getStudent(context, student1.id); diff --git a/tests/test-projects/basic/schema.graphql b/tests/test-projects/basic/schema.graphql index 55380b7c1d2..30beff9bc69 100644 --- a/tests/test-projects/basic/schema.graphql +++ b/tests/test-projects/basic/schema.graphql @@ -179,14 +179,14 @@ type Mutation { createTasks(data: [TasksCreateInput]): [Task] updateTask(id: ID!, data: TaskUpdateInput): Task updateTasks(data: [TaskUpdateArgs]): [Task] - deleteTask(id: ID!): Task - deleteTasks(ids: [ID!]): [Task] + deleteTask(where: TaskWhereUniqueInput!): Task + deleteTasks(where: [TaskWhereUniqueInput!]!): [Task] createPerson(data: PersonCreateInput): Person createPeople(data: [PeopleCreateInput]): [Person] updatePerson(id: ID!, data: PersonUpdateInput): Person updatePeople(data: [PersonUpdateArgs]): [Person] - deletePerson(id: ID!): Person - deletePeople(ids: [ID!]): [Person] + deletePerson(where: PersonWhereUniqueInput!): Person + deletePeople(where: [PersonWhereUniqueInput!]!): [Person] } type Query { From e0b9e8d38b0d531bcc921f05435bd1985b47781a Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 28 Jul 2021 12:12:28 +1000 Subject: [PATCH 2/2] Admin UI customisation guides and examples (#6082) * init * guides for custom page and custom components * more examples commits * yarn.lock and example changes * add more markup * Update docs/pages/docs/guides/custom-admin-ui-pages.mdx Co-authored-by: Mitchell Hamilton * fix readme * more updates to the docs * add smoke tests * add to smoke tests in workflow * fill in links * add new guides to docs nav * fix docs * fix readME.md * changelog reset * add examples link to docs and README.md * update explanation of Old JSX transforms * change custom-admin-ui-components example to custom-admin-ui-logo * Update docs/pages/docs/guides/custom-admin-ui-pages.mdx Co-authored-by: Tim Leslie * Update docs/pages/docs/guides/custom-admin-ui-pages.mdx Co-authored-by: Tim Leslie * Update docs/pages/docs/guides/custom-admin-ui-pages.mdx Co-authored-by: Tim Leslie * Update docs/pages/docs/guides/custom-admin-ui-pages.mdx Co-authored-by: Tim Leslie * Update examples/custom-admin-ui-pages/README.md Co-authored-by: Tim Leslie * Update examples/custom-admin-ui-pages/README.md Co-authored-by: Tim Leslie * Update examples/custom-admin-ui-components/README.md Co-authored-by: Tim Leslie * Update examples/custom-admin-ui-pages/admin/pages/custom-page.tsx Co-authored-by: Tim Leslie * Update examples/custom-admin-ui-components/README.md Co-authored-by: Tim Leslie * fix up odds and ends in formatting for docs pages * Update docs/pages/docs/guides/custom-admin-ui-logo.mdx Co-authored-by: Tim Leslie * Update docs/pages/docs/guides/custom-admin-ui-logo.mdx Co-authored-by: Tim Leslie * Update docs/pages/docs/guides/custom-admin-ui-pages.mdx Co-authored-by: Tim Leslie * Update docs/pages/docs/guides/custom-admin-ui-pages.mdx Co-authored-by: Tim Leslie * Update docs/pages/docs/guides/custom-admin-ui-pages.mdx Co-authored-by: Tim Leslie * Update docs/pages/docs/guides/custom-admin-ui-logo.mdx Co-authored-by: Tim Leslie * Feedback from Tim * remove unnecessary files * yarn.lock * add changeset * Update examples/README.md Co-authored-by: Tim Leslie * update schema.graphql * http --> https Co-authored-by: Mitchell Hamilton Co-authored-by: Tim Leslie --- .changeset/good-cycles-ring.md | 5 + .github/workflows/tests.yml | 1 + docs/components/docs/ExamplesList.tsx | 8 + docs/components/docs/Navigation.tsx | 3 + .../docs/guides/custom-admin-ui-pages.mdx | 45 +++ examples/README.md | 3 +- examples/custom-admin-ui-pages/CHANGELOG.md | 1 + examples/custom-admin-ui-pages/README.md | 28 ++ .../admin/pages/custom-page.tsx | 34 ++ examples/custom-admin-ui-pages/keystone.ts | 10 + examples/custom-admin-ui-pages/package.json | 26 ++ examples/custom-admin-ui-pages/schema.graphql | 296 ++++++++++++++++++ examples/custom-admin-ui-pages/schema.prisma | 27 ++ examples/custom-admin-ui-pages/schema.ts | 28 ++ .../custom-admin-ui-pages.test.ts | 20 ++ yarn.lock | 2 +- 16 files changed, 535 insertions(+), 2 deletions(-) create mode 100644 .changeset/good-cycles-ring.md create mode 100644 docs/pages/docs/guides/custom-admin-ui-pages.mdx create mode 100644 examples/custom-admin-ui-pages/CHANGELOG.md create mode 100644 examples/custom-admin-ui-pages/README.md create mode 100644 examples/custom-admin-ui-pages/admin/pages/custom-page.tsx create mode 100644 examples/custom-admin-ui-pages/keystone.ts create mode 100644 examples/custom-admin-ui-pages/package.json create mode 100644 examples/custom-admin-ui-pages/schema.graphql create mode 100644 examples/custom-admin-ui-pages/schema.prisma create mode 100644 examples/custom-admin-ui-pages/schema.ts create mode 100644 tests/examples-smoke-tests/custom-admin-ui-pages.test.ts diff --git a/.changeset/good-cycles-ring.md b/.changeset/good-cycles-ring.md new file mode 100644 index 00000000000..9aefdfff97e --- /dev/null +++ b/.changeset/good-cycles-ring.md @@ -0,0 +1,5 @@ +--- +'@keystone-next/example-custom-admin-ui-pages': major +--- + +Initial version of the custom-admin-ui-pages example. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a005679387d..36a644b100f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -261,6 +261,7 @@ jobs: 'with-auth.test.ts', 'custom-field-view.test.ts', 'custom-field.test.ts', + 'custom-admin-ui-pages.test.ts', 'custom-admin-ui-logo.test.ts', ] fail-fast: false diff --git a/docs/components/docs/ExamplesList.tsx b/docs/components/docs/ExamplesList.tsx index b277c20a326..2e8bff620b6 100644 --- a/docs/components/docs/ExamplesList.tsx +++ b/docs/components/docs/ExamplesList.tsx @@ -123,6 +123,14 @@ export function Examples() { Adds a custom field type based on the integer field type which lets users rate items on a 5-star scale. Builds on the Blog starter project. + + Adds a custom page in the Admin UI. Builds on the Task Manager starter project. + Custom Admin UI Logo New + + Custom Admin UI Pages New + Access Control diff --git a/docs/pages/docs/guides/custom-admin-ui-pages.mdx b/docs/pages/docs/guides/custom-admin-ui-pages.mdx new file mode 100644 index 00000000000..8357a5656d6 --- /dev/null +++ b/docs/pages/docs/guides/custom-admin-ui-pages.mdx @@ -0,0 +1,45 @@ +import { ComingSoon } from '../../../components/docs/ComingSoon'; +import { Markdown } from '../../../components/Markdown'; +import { Alert } from '../../../components/primitives/Alert'; + +# Custom Admin UI Pages + +In this guide we'll show you how to add custom pages to the Keystone Admin UI. +As the Admin UI is built on top of Next.js, it exposes the same pages directory for adding custom pages. + +To create a custom page, ensure that the `admin/pages` directory exists in the root of your Keystone Project. +Much like with Next.js, all files in this directory will be added as routes to the Admin UI. +The default export of every file in this directory is expected to be a valid React Component rendered out as the contents of the route. + +```tsx +// admin/pages/MyCustomPage.tsx +export default function () { + return ( +

This is a custom Admin UI Page

+

It can be accessed via the route '/MyCustomPage'

+ ) +} +``` + +If you have styling constraints, we recommend using the jsx export from the `@keystone-ui/core` package, as this will ensure that the version of emotion you're using conforms with the version of emotion used internally within Keystone. + +```tsx +// admin/pages/MyCustomPage.tsx +/** @jsxRuntime classic */ +/** @jsx jsx */ +import { jsx } from '@keystone-ui/core'; +export default function () { + return ( +

This is a custom Admin UI Page

+

It can be accessed via the route '/MyCustomPage'

+ ) +} +``` + +Of course this is purely a recommendation, if you would prefer to roll your own css-in-js solution in with your custom component please feel free to! Although this may require additional configuration outside of the scope of this guide. + +x> **Not all Next.js exports are available:** Keystone **only** supports the page component as a default export in the pages directory. This means that unlike with Next, auxillary exports such as `getStaticProps` and `getServerProps` are not supported. + +export default ({ children }) => {children}; diff --git a/examples/README.md b/examples/README.md index f705970cadf..e83361784cb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -26,7 +26,8 @@ Each project below demonstrates a Keystone feature you can learn about and exper - [Testing](./testing): Adds tests with `@keystone-next/testing` to the `withAuth()` example. - [Custom field](./custom-field): Adds a custom `stars` field to the Blog base. - [Custom field view](./custom-field-view): Adds a custom Admin UI view to a `json` field to the Task Manager base. -- [Custom Admin UI components](./custom-admin-ui-logo): Adds a custom logo in the Admin UI to the Task Manager base. +- [Custom Admin UI logo](./custom-admin-ui-logo): Adds a custom logo in the Admin UI to the Task Manager base. +- [Custom Admin UI pages](./custom-admin-ui-pages): Adds a custom page in the Admin UI to the Task Manager base. ## Running examples diff --git a/examples/custom-admin-ui-pages/CHANGELOG.md b/examples/custom-admin-ui-pages/CHANGELOG.md new file mode 100644 index 00000000000..88b4c31f841 --- /dev/null +++ b/examples/custom-admin-ui-pages/CHANGELOG.md @@ -0,0 +1 @@ +# @keystone-next/example-custom-admin-ui-pages diff --git a/examples/custom-admin-ui-pages/README.md b/examples/custom-admin-ui-pages/README.md new file mode 100644 index 00000000000..37622aac6b1 --- /dev/null +++ b/examples/custom-admin-ui-pages/README.md @@ -0,0 +1,28 @@ +## Feature Example - Custom Components for the Admin UI + +This project demonstrates how to create a custom page in the Admin UI. +It builds on the [Task Manager](../task-manager) starter project. + +## Instructions + +To run this project, clone the Keystone repository locally then navigate to this directory and run: + +```shell +yarn dev +``` + +This will start the Admin UI at [localhost:3000](http://localhost:3000). + +You can use the Admin UI to create items in your database. + +You can also access a GraphQL Playground at [localhost:3000/api/graphql](http://localhost:3000/api/graphql), which allows you to directly run GraphQL queries and mutations. + +🚀 Congratulations, you're now up and running with Keystone! + +## admin/pages + +This project leverages the `/admin/pages` directory. As elaborated on in the [Custom Pages](https://keystonejs.com/docs/guides/custom-admin-ui-pages) guide, this directory is used to generate additional routes in the Admin UI, a behaviour inherited from `Next.js`. The default export of files in this directory are expected to be **React Components**. +**All other exports are ignored** + +**NOTE** The Keystone monorepo leverages a babel config that means we use the old jsx transform (this doesn't have an impact on the code we ship to npm). +This is why there are `import React from 'react'` statements in our examples, this is NOT necessary outside of the Keystone repo (unless you have a babel config with the old jsx transform which is currently the default with @babel/preset-react) as you'll be using Next's babel config which uses the new jsx transform. diff --git a/examples/custom-admin-ui-pages/admin/pages/custom-page.tsx b/examples/custom-admin-ui-pages/admin/pages/custom-page.tsx new file mode 100644 index 00000000000..f109f136375 --- /dev/null +++ b/examples/custom-admin-ui-pages/admin/pages/custom-page.tsx @@ -0,0 +1,34 @@ +/** @jsx jsx */ +import { Fragment } from 'react'; +import { jsx } from '@keystone-ui/core'; +// Please note that while this capability is driven by Next.js's pages directory +// We do not currently support any of the auxillary methods that Next.js provides i.e. `getStaticProps` +// Presently the only export from the directory that is supported is the page component itself. +export default function CustomPage() { + return ( + +
+

+ Hello this is a custom page +

+

+ This is a custom page added to the Admin UI, leveraging @keystone-ui/core +

+
+
+ ); +} diff --git a/examples/custom-admin-ui-pages/keystone.ts b/examples/custom-admin-ui-pages/keystone.ts new file mode 100644 index 00000000000..e961168f85e --- /dev/null +++ b/examples/custom-admin-ui-pages/keystone.ts @@ -0,0 +1,10 @@ +import { config } from '@keystone-next/keystone/schema'; +import { lists } from './schema'; + +export default config({ + db: { + provider: 'sqlite', + url: process.env.DATABASE_URL || 'file:./keystone-example.db', + }, + lists, +}); diff --git a/examples/custom-admin-ui-pages/package.json b/examples/custom-admin-ui-pages/package.json new file mode 100644 index 00000000000..574a7a771e8 --- /dev/null +++ b/examples/custom-admin-ui-pages/package.json @@ -0,0 +1,26 @@ +{ + "name": "@keystone-next/example-custom-admin-ui-pages", + "version": "0.0.1", + "private": true, + "license": "MIT", + "scripts": { + "dev": "keystone-next dev", + "start": "keystone-next start", + "build": "keystone-next build" + }, + "dependencies": { + "@keystone-next/fields": "^13.0.0", + "@keystone-next/keystone": "^23.0.0", + "@keystone-next/types": "^23.0.0", + "@keystone-ui/core": "^3.1.0", + "next": "^10.2.3", + "react": "^17.0.2" + }, + "devDependencies": { + "typescript": "^4.3.5" + }, + "engines": { + "node": "^12.20 || >= 14.13" + }, + "repository": "https://github.com/keystonejs/keystone/tree/master/examples/custom-admin-ui-pages" +} diff --git a/examples/custom-admin-ui-pages/schema.graphql b/examples/custom-admin-ui-pages/schema.graphql new file mode 100644 index 00000000000..30beff9bc69 --- /dev/null +++ b/examples/custom-admin-ui-pages/schema.graphql @@ -0,0 +1,296 @@ +type Task { + id: ID! + label: String + priority: TaskPriorityType + isComplete: Boolean + assignedTo: Person + finishBy: String +} + +enum TaskPriorityType { + low + medium + high +} + +input TaskWhereInput { + AND: [TaskWhereInput!] + OR: [TaskWhereInput!] + id: ID + id_not: ID + id_lt: ID + id_lte: ID + id_gt: ID + id_gte: ID + id_in: [ID!] + id_not_in: [ID!] + label: String + label_not: String + label_contains: String + label_not_contains: String + label_in: [String] + label_not_in: [String] + priority: TaskPriorityType + priority_not: TaskPriorityType + priority_in: [TaskPriorityType] + priority_not_in: [TaskPriorityType] + isComplete: Boolean + isComplete_not: Boolean + assignedTo: PersonWhereInput + assignedTo_is_null: Boolean + finishBy: String + finishBy_not: String + finishBy_lt: String + finishBy_lte: String + finishBy_gt: String + finishBy_gte: String + finishBy_in: [String] + finishBy_not_in: [String] +} + +input TaskWhereUniqueInput { + id: ID +} + +input TaskOrderByInput { + id: OrderDirection + label: OrderDirection + priority: OrderDirection + isComplete: OrderDirection + finishBy: OrderDirection +} + +enum OrderDirection { + asc + desc +} + +input TaskUpdateInput { + label: String + priority: TaskPriorityType + isComplete: Boolean + assignedTo: PersonRelateToOneInput + finishBy: String +} + +input PersonRelateToOneInput { + create: PersonCreateInput + connect: PersonWhereUniqueInput + disconnect: PersonWhereUniqueInput + disconnectAll: Boolean +} + +input TaskUpdateArgs { + id: ID! + data: TaskUpdateInput +} + +input TaskCreateInput { + label: String + priority: TaskPriorityType + isComplete: Boolean + assignedTo: PersonRelateToOneInput + finishBy: String +} + +input TasksCreateInput { + data: TaskCreateInput +} + +type Person { + id: ID! + name: String + tasks( + where: TaskWhereInput! = {} + orderBy: [TaskOrderByInput!]! = [] + first: Int + skip: Int! = 0 + ): [Task!] + tasksCount(where: TaskWhereInput! = {}): Int +} + +input PersonWhereInput { + AND: [PersonWhereInput!] + OR: [PersonWhereInput!] + id: ID + id_not: ID + id_lt: ID + id_lte: ID + id_gt: ID + id_gte: ID + id_in: [ID!] + id_not_in: [ID!] + name: String + name_not: String + name_contains: String + name_not_contains: String + name_in: [String] + name_not_in: [String] + tasks_every: TaskWhereInput + tasks_some: TaskWhereInput + tasks_none: TaskWhereInput +} + +input PersonWhereUniqueInput { + id: ID +} + +input PersonOrderByInput { + id: OrderDirection + name: OrderDirection +} + +input PersonUpdateInput { + name: String + tasks: TaskRelateToManyInput +} + +input TaskRelateToManyInput { + create: [TaskCreateInput] + connect: [TaskWhereUniqueInput] + disconnect: [TaskWhereUniqueInput] + disconnectAll: Boolean +} + +input PersonUpdateArgs { + id: ID! + data: PersonUpdateInput +} + +input PersonCreateInput { + name: String + tasks: TaskRelateToManyInput +} + +input PeopleCreateInput { + data: PersonCreateInput +} + +""" +The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSON + @specifiedBy( + url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf" + ) + +type Mutation { + createTask(data: TaskCreateInput): Task + createTasks(data: [TasksCreateInput]): [Task] + updateTask(id: ID!, data: TaskUpdateInput): Task + updateTasks(data: [TaskUpdateArgs]): [Task] + deleteTask(where: TaskWhereUniqueInput!): Task + deleteTasks(where: [TaskWhereUniqueInput!]!): [Task] + createPerson(data: PersonCreateInput): Person + createPeople(data: [PeopleCreateInput]): [Person] + updatePerson(id: ID!, data: PersonUpdateInput): Person + updatePeople(data: [PersonUpdateArgs]): [Person] + deletePerson(where: PersonWhereUniqueInput!): Person + deletePeople(where: [PersonWhereUniqueInput!]!): [Person] +} + +type Query { + tasks( + where: TaskWhereInput! = {} + orderBy: [TaskOrderByInput!]! = [] + first: Int + skip: Int! = 0 + ): [Task!] + task(where: TaskWhereUniqueInput!): Task + tasksCount(where: TaskWhereInput! = {}): Int + people( + where: PersonWhereInput! = {} + orderBy: [PersonOrderByInput!]! = [] + first: Int + skip: Int! = 0 + ): [Person!] + person(where: PersonWhereUniqueInput!): Person + peopleCount(where: PersonWhereInput! = {}): Int + keystone: KeystoneMeta! +} + +type KeystoneMeta { + adminMeta: KeystoneAdminMeta! +} + +type KeystoneAdminMeta { + enableSignout: Boolean! + enableSessionItem: Boolean! + lists: [KeystoneAdminUIListMeta!]! + list(key: String!): KeystoneAdminUIListMeta +} + +type KeystoneAdminUIListMeta { + key: String! + itemQueryName: String! + listQueryName: String! + hideCreate: Boolean! + hideDelete: Boolean! + path: String! + label: String! + singular: String! + plural: String! + description: String + initialColumns: [String!]! + pageSize: Int! + labelField: String! + fields: [KeystoneAdminUIFieldMeta!]! + initialSort: KeystoneAdminUISort + isHidden: Boolean! +} + +type KeystoneAdminUIFieldMeta { + path: String! + label: String! + isOrderable: Boolean! + fieldMeta: JSON + viewsIndex: Int! + customViewsIndex: Int + createView: KeystoneAdminUIFieldMetaCreateView! + listView: KeystoneAdminUIFieldMetaListView! + itemView(id: ID!): KeystoneAdminUIFieldMetaItemView + search: QueryMode +} + +type KeystoneAdminUIFieldMetaCreateView { + fieldMode: KeystoneAdminUIFieldMetaCreateViewFieldMode! +} + +enum KeystoneAdminUIFieldMetaCreateViewFieldMode { + edit + hidden +} + +type KeystoneAdminUIFieldMetaListView { + fieldMode: KeystoneAdminUIFieldMetaListViewFieldMode! +} + +enum KeystoneAdminUIFieldMetaListViewFieldMode { + read + hidden +} + +type KeystoneAdminUIFieldMetaItemView { + fieldMode: KeystoneAdminUIFieldMetaItemViewFieldMode! +} + +enum KeystoneAdminUIFieldMetaItemViewFieldMode { + edit + read + hidden +} + +enum QueryMode { + default + insensitive +} + +type KeystoneAdminUISort { + field: String! + direction: KeystoneAdminUISortDirection! +} + +enum KeystoneAdminUISortDirection { + ASC + DESC +} diff --git a/examples/custom-admin-ui-pages/schema.prisma b/examples/custom-admin-ui-pages/schema.prisma new file mode 100644 index 00000000000..a1efaced2f7 --- /dev/null +++ b/examples/custom-admin-ui-pages/schema.prisma @@ -0,0 +1,27 @@ +datasource sqlite { + url = env("DATABASE_URL") + provider = "sqlite" +} + +generator client { + provider = "prisma-client-js" + output = "node_modules/.prisma/client" +} + +model Task { + id String @id @default(cuid()) + label String? + priority String? + isComplete Boolean? + assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) + assignedToId String? @map("assignedTo") + finishBy DateTime? + + @@index([assignedToId]) +} + +model Person { + id String @id @default(cuid()) + name String? + 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 new file mode 100644 index 00000000000..9b539803be8 --- /dev/null +++ b/examples/custom-admin-ui-pages/schema.ts @@ -0,0 +1,28 @@ +import { createSchema, list } from '@keystone-next/keystone/schema'; +import { checkbox, relationship, text, timestamp } from '@keystone-next/fields'; +import { select } from '@keystone-next/fields'; + +export const lists = createSchema({ + Task: list({ + fields: { + label: text({ isRequired: true }), + priority: select({ + dataType: 'enum', + options: [ + { label: 'Low', value: 'low' }, + { label: 'Medium', value: 'medium' }, + { label: 'High', value: 'high' }, + ], + }), + isComplete: checkbox(), + assignedTo: relationship({ ref: 'Person.tasks', many: false }), + finishBy: timestamp(), + }, + }), + Person: list({ + fields: { + name: text({ isRequired: true }), + tasks: relationship({ ref: 'Task.assignedTo', many: true }), + }, + }), +}); diff --git a/tests/examples-smoke-tests/custom-admin-ui-pages.test.ts b/tests/examples-smoke-tests/custom-admin-ui-pages.test.ts new file mode 100644 index 00000000000..408baf40399 --- /dev/null +++ b/tests/examples-smoke-tests/custom-admin-ui-pages.test.ts @@ -0,0 +1,20 @@ +import { Browser, Page } from 'playwright'; +import { exampleProjectTests } from './utils'; + +exampleProjectTests('custom-admin-ui-pages', browserType => { + let browser: Browser = undefined as any; + let page: Page = undefined as any; + beforeAll(async () => { + browser = await browserType.launch(); + page = await browser.newPage(); + await page.goto('http://localhost:3000'); + }); + test('Load list', async () => { + await page.goto('http://localhost:3000/custom-page'); + const content = await page.textContent('body h1'); + expect(content).toBe('Hello this is a custom page'); + }); + afterAll(async () => { + await browser.close(); + }); +}); diff --git a/yarn.lock b/yarn.lock index 00bacc3ebf6..79bf3cd837e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3881,7 +3881,7 @@ assert@2.0.0: object-is "^1.0.1" util "^0.12.0" -assert@^1.1.1, assert@^1.4.1: +assert@^1.1.1: version "1.5.0" resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==