From fb18f365fb27a18c8be49130f81c7e8be55a5d77 Mon Sep 17 00:00:00 2001 From: Tim Leslie Date: Thu, 22 Apr 2021 15:45:44 +1000 Subject: [PATCH] Add API docs for the list items API --- .changeset/fair-shrimps-love.md | 6 + docs/components/Navigation.tsx | 4 +- docs/pages/apis/list-items.mdx | 149 +++++++++++++++++- .../keystone/src/lib/context/itemAPI.ts | 8 +- packages-next/types/src/context.ts | 8 +- tests/api-tests/access-control/authed.test.ts | 13 +- tests/api-tests/fields/crud.test.ts | 6 +- .../crud-self-ref/one-to-one.test.ts | 10 +- .../relationships/crud/one-to-one.test.ts | 8 +- .../relationships/filtering/nested.test.ts | 6 +- .../relationships/many-to-one-to-one.test.ts | 10 +- .../two-way-backreference/to-many.test.ts | 6 +- 12 files changed, 182 insertions(+), 52 deletions(-) create mode 100644 .changeset/fair-shrimps-love.md diff --git a/.changeset/fair-shrimps-love.md b/.changeset/fair-shrimps-love.md new file mode 100644 index 00000000000..78bddf88a6c --- /dev/null +++ b/.changeset/fair-shrimps-love.md @@ -0,0 +1,6 @@ +--- +'@keystone-next/keystone': patch +'@keystone-next/types': patch +--- + +Updated the list item and db item APIs to include an empty default for `.findMany()` and `.count()`. diff --git a/docs/components/Navigation.tsx b/docs/components/Navigation.tsx index 23f7a2e89db..55dff002b68 100644 --- a/docs/components/Navigation.tsx +++ b/docs/components/Navigation.tsx @@ -98,9 +98,7 @@ export function Navigation() { Authentication API Context API GraphQL API - - List Item API - + List Item API ); diff --git a/docs/pages/apis/list-items.mdx b/docs/pages/apis/list-items.mdx index 78d60db63f1..579e2bc9537 100644 --- a/docs/pages/apis/list-items.mdx +++ b/docs/pages/apis/list-items.mdx @@ -1,8 +1,153 @@ import { Markdown } from '../../components/Page'; -import { ComingSoon } from '../../components/ComingSoon'; # List Items API - +The list items API provides a programmatic API for running CRUD operations against your GraphQL API. +For each list in your system the following API is available at `context.lists.`. + +``` +{ + findOne({ where: { id }, query }), + findMany({ where, first, skip, sortBy, query }), + count({ where, first, skip }), + createOne({ data, query }), + createMany({ data, query }), + updateOne({ id, data, query }), + updateMany({ data, query }), + deleteOne({ id, query }), + deleteMany({ ids, query }), +} +``` + +The arguments to these functions closely correspond to their equivalent GraphQL APIs, making it easy to switch between the programmatic API and the GraphQL API. + +The `query` argument, which defaults to `'id'` for all the functions, is a string which indicates which fields should be returned by the operation. +Unless otherwise specified, the other arguments to all functions are required. + +The functions in the API all work by directly executing queries and mutations against your GraphQL API. + +### findOne + +```typescript +const user = await context.lists.User.findOne({ + where: { id: '...' }, + query: 'id name posts { id title }', +}); +``` + +### findMany + +All arguments are optional. + +```typescript +const users = await context.lists.User.findMany({ + where: { name_starts_with: 'A' }, + first: 10, + skip: 20, + sortBy: ['name_ASC'], + query: 'id name posts { id title }', +}); +``` + +### count + +All arguments are optional. + +```typescript +const count = await context.lists.User.count({ + where: { name_starts_with: 'A' }, + first: 10, + skip: 20, +}); +``` + +### createOne + +```typescript +const user = await context.lists.User.createOne({ + data: { + name: 'Alice', + posts: { create: [{ title: 'My first post' }] }, + }, + query: 'id name posts { id title }', +}); +``` + +### createMany + +```typescript +const users = await context.lists.User.createOne({ + data: [ + { + data: { + name: 'Alice', + posts: [{ create: { title: 'Alices first post' } }], + }, + }, + { + data: { + name: 'Bob', + posts: [{ create: { title: 'Bobs first post' } }], + }, + }, + ], + query: 'id name posts { id title }', +}); +``` + +### updateOne + +```typescript +const user = await context.lists.User.updateOne({ + id: '...', + data: { + name: 'Alice', + posts: { create: [{ title: 'My first post' }] }, + }, + query: 'id name posts { id title }', +}); +``` + +### updateMany + +```typescript +const users = await context.lists.User.updateMany({ + data: [ + { + id: '...', + data: { + name: 'Alice', + posts: [{ create: { title: 'Alices first post' } }], + }, + }, + { + id: '...', + data: { + name: 'Bob', + posts: [{ create: { title: 'Bobs first post' } }], + }, + }, + ], + query: 'id name posts { id title }', +}); +``` + +### deleteOne + +```typescript +const user = await context.lists.User.deleteOne({ + id: '...', + query: 'id name posts { id title }', +}); +``` + +### deleteMany + +```typescript +const user = await context.lists.User.deleteMany({ + ids: ['...', '...'], + query: 'id name posts { id title }', +}); +``` export default ({ children }) => {children}; diff --git a/packages-next/keystone/src/lib/context/itemAPI.ts b/packages-next/keystone/src/lib/context/itemAPI.ts index d27c73fe4eb..2fd0d05de30 100644 --- a/packages-next/keystone/src/lib/context/itemAPI.ts +++ b/packages-next/keystone/src/lib/context/itemAPI.ts @@ -70,7 +70,7 @@ export function itemAPIForList( return list.itemQuery(args, context); } }, - findMany({ query, resolveFields, ...rawArgs }) { + findMany({ query, resolveFields, ...rawArgs } = {}) { if (!getArgs.findMany) throw new Error('You do not have access to this resource'); const returnFields = defaultQueryParam(query, resolveFields); if (returnFields) { @@ -81,7 +81,7 @@ export function itemAPIForList( return list.listQuery(args, context); } }, - async count(rawArgs) { + async count(rawArgs = {}) { if (!getArgs.count) throw new Error('You do not have access to this resource'); const args = getArgs.count(rawArgs!); return (await list.listQueryMeta(args, context)).getCount(); @@ -166,12 +166,12 @@ export function itemDbAPIForList( const args = getArgs.findOne(rawArgs) as { where: { id: string } }; return list.itemQuery(args, context); }, - findMany(rawArgs) { + findMany(rawArgs = {}) { if (!getArgs.findMany) throw new Error('You do not have access to this resource'); const args = getArgs.findMany(rawArgs); return list.listQuery(args, context); }, - async count(rawArgs) { + async count(rawArgs = {}) { if (!getArgs.count) throw new Error('You do not have access to this resource'); const args = getArgs.count(rawArgs!); return (await list.listQueryMeta(args, context)).getCount(); diff --git a/packages-next/types/src/context.ts b/packages-next/types/src/context.ts index 579f32b5da9..311a03ef00e 100644 --- a/packages-next/types/src/context.ts +++ b/packages-next/types/src/context.ts @@ -34,12 +34,12 @@ export type KeystoneListsAPI< > = { [Key in keyof KeystoneListsTypeInfo]: { findMany( - args: KeystoneListsTypeInfo[Key]['args']['listQuery'] & ResolveFields + args?: KeystoneListsTypeInfo[Key]['args']['listQuery'] & ResolveFields ): Promise[]>; findOne( args: { readonly where: { readonly id: string } } & ResolveFields ): Promise>; - count(args: KeystoneListsTypeInfo[Key]['args']['listQuery']): Promise; + count(args?: KeystoneListsTypeInfo[Key]['args']['listQuery']): Promise; updateOne( args: { readonly id: string; @@ -86,12 +86,12 @@ type ResolveFields = { export type KeystoneDbAPI> = { [Key in keyof KeystoneListsTypeInfo]: { findMany( - args: KeystoneListsTypeInfo[Key]['args']['listQuery'] + args?: KeystoneListsTypeInfo[Key]['args']['listQuery'] ): Promise; findOne(args: { readonly where: { readonly id: string }; }): Promise; - count(args: KeystoneListsTypeInfo[Key]['args']['listQuery']): Promise; + count(args?: KeystoneListsTypeInfo[Key]['args']['listQuery']): Promise; updateOne(args: { readonly id: string; readonly data: KeystoneListsTypeInfo[Key]['inputs']['update']; diff --git a/tests/api-tests/access-control/authed.test.ts b/tests/api-tests/access-control/authed.test.ts index bc311bcd1a2..6f5eda48793 100644 --- a/tests/api-tests/access-control/authed.test.ts +++ b/tests/api-tests/access-control/authed.test.ts @@ -103,10 +103,8 @@ multiAdapterRunners().map(({ before, after, provider }) => const data = await context.exitSudo().graphql.run({ query }); expect(data[createMutationName]).not.toBe(null); expect(data[createMutationName].id).not.toBe(null); - await deleteItem({ - context, - listKey: nameFn[mode](access), - itemId: data[createMutationName].id, + await context.lists[nameFn[mode](access)].deleteOne({ + id: data[createMutationName].id, }); }); }); @@ -278,10 +276,9 @@ multiAdapterRunners().map(({ before, after, provider }) => const item = items[listKey][0]; const fieldName = getFieldName(access); const singleQueryName = listKey; - await updateItem({ - context, - listKey, - item: { id: item.id, data: { [fieldName]: 'hello' } }, + await context.lists[listKey].updateOne({ + id: item.id, + data: { [fieldName]: 'hello' }, }); const query = `query { ${singleQueryName}(where: { id: "${item.id}" }) { id ${fieldName} } }`; const data = await context.exitSudo().graphql.run({ query }); diff --git a/tests/api-tests/fields/crud.test.ts b/tests/api-tests/fields/crud.test.ts index 22727571555..1a45d4fd499 100644 --- a/tests/api-tests/fields/crud.test.ts +++ b/tests/api-tests/fields/crud.test.ts @@ -100,11 +100,9 @@ multiAdapterRunners().map(({ runner, provider }) => context: KeystoneContext; listKey: string; }) => { - const items = await getItems({ - context, - listKey, - returnFields, + const items = await context.lists[listKey].findMany({ sortBy: ['name_ASC'], + query: returnFields, }); return wrappedFn({ context, listKey, items }); }; 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 cfef24eb44a..6da368a7361 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 @@ -175,14 +175,8 @@ multiAdapterRunners().map(({ runner, provider }) => 'Count', runner(setupKeystone, async ({ context }) => { await createInitialData(context); - const data = await context.graphql.run({ - query: ` - { - _allUsersMeta { count } - } - `, - }); - expect(data._allUsersMeta.count).toEqual(3); + const count = await context.lists.User.count(); + expect(count).toEqual(3); }) ); 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 9c4979283c4..db305580396 100644 --- a/tests/api-tests/relationships/crud/one-to-one.test.ts +++ b/tests/api-tests/relationships/crud/one-to-one.test.ts @@ -591,11 +591,9 @@ multiAdapterRunners().map(({ runner, provider }) => }); expect(result).toHaveLength(1); - const result1 = await getItem({ - context, - listKey: 'Company', - itemId: company1.id, - returnFields: 'id location { id }', + const result1 = await context.lists.Company.findOne({ + where: { id: company1.id }, + query: 'id location { id }', }); expect(result1?.location).toBe(null); diff --git a/tests/api-tests/relationships/filtering/nested.test.ts b/tests/api-tests/relationships/filtering/nested.test.ts index 761ba683c80..6b4600256ad 100644 --- a/tests/api-tests/relationships/filtering/nested.test.ts +++ b/tests/api-tests/relationships/filtering/nested.test.ts @@ -294,10 +294,8 @@ multiAdapterRunners().map(({ runner, provider }) => test( 'nested to-many relationship meta can be filtered', runner(setupKeystone, async ({ context }) => { - const ids = await createItems({ - context, - listKey: 'Post', - items: [ + const ids = await context.lists.Post.createMany({ + data: [ { data: { content: 'Hello world' } }, { data: { content: 'hi world' } }, { data: { content: 'Hello? Or hi?' } }, diff --git a/tests/api-tests/relationships/many-to-one-to-one.test.ts b/tests/api-tests/relationships/many-to-one-to-one.test.ts index e6d36ea5bba..1d733946417 100644 --- a/tests/api-tests/relationships/many-to-one-to-one.test.ts +++ b/tests/api-tests/relationships/many-to-one-to-one.test.ts @@ -4,7 +4,7 @@ import { gen, sampleOne } from 'testcheck'; import { text, relationship } from '@keystone-next/fields'; import { createSchema, list } from '@keystone-next/keystone/schema'; import { multiAdapterRunners, setupFromConfig } from '@keystone-next/test-utils-legacy'; -import { createItem, createItems } from '@keystone-next/server-side-graphql-client-legacy'; +import { createItems } from '@keystone-next/server-side-graphql-client-legacy'; const alphanumGenerator = gen.alphaNumString.notEmpty(); @@ -54,10 +54,8 @@ const createCompanyAndLocation = async (context: KeystoneContext) => { ], }); - return createItem({ - context, - listKey: 'Owner', - item: { + return context.lists.Owner.createOne({ + data: { name: sampleOne(alphanumGenerator), companies: { create: [ @@ -91,7 +89,7 @@ const createCompanyAndLocation = async (context: KeystoneContext) => { ], }, }, - returnFields: 'id name companies { id name location { id name custodians { id name } } }', + query: 'id name companies { id name location { id name custodians { id name } } }', }); }; 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 2d04a7c8a74..0eae55b5875 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 @@ -274,10 +274,8 @@ multiAdapterRunners().map(({ runner, provider }) => item: { teachers: { connect: [{ id: teacher1.id }, { id: teacher2.id }] } }, }); - await updateItems({ - context, - listKey: 'Teacher', - items: [ + await context.lists.Teacher.updateMany({ + data: [ { id: teacher1.id, data: { students: { connect: [{ id: student1.id }, { id: student2.id }] } },