From a94ef558e22975cd675ea52402678866a2ce3648 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 8 Jun 2023 13:39:46 +1000 Subject: [PATCH 1/8] add (failing) field groups example --- examples/field-groups/keystone.ts | 15 ++ examples/field-groups/package.json | 20 ++ examples/field-groups/sandbox.config.json | 7 + examples/field-groups/schema.graphql | 233 ++++++++++++++++++++++ examples/field-groups/schema.prisma | 20 ++ examples/field-groups/schema.ts | 49 +++++ examples/hooks/schema.ts | 53 ++++- pnpm-lock.yaml | 16 ++ 8 files changed, 406 insertions(+), 7 deletions(-) create mode 100644 examples/field-groups/keystone.ts create mode 100644 examples/field-groups/package.json create mode 100644 examples/field-groups/sandbox.config.json create mode 100644 examples/field-groups/schema.graphql create mode 100644 examples/field-groups/schema.prisma create mode 100644 examples/field-groups/schema.ts diff --git a/examples/field-groups/keystone.ts b/examples/field-groups/keystone.ts new file mode 100644 index 00000000000..2cbda129b21 --- /dev/null +++ b/examples/field-groups/keystone.ts @@ -0,0 +1,15 @@ +import { config } from '@keystone-6/core'; +import { fixPrismaPath } from '../example-utils'; +import { lists } from './schema'; +import type { TypeInfo } from '.keystone/types'; + +export default config({ + db: { + provider: 'sqlite', + url: process.env.DATABASE_URL || 'file:./keystone-example.db', + + // WARNING: this is only needed for our monorepo examples, dont do this + ...fixPrismaPath, + }, + lists, +}); diff --git a/examples/field-groups/package.json b/examples/field-groups/package.json new file mode 100644 index 00000000000..f400477c925 --- /dev/null +++ b/examples/field-groups/package.json @@ -0,0 +1,20 @@ +{ + "name": "@keystone-6/field-groups", + "version": "0.0.1", + "private": true, + "license": "MIT", + "scripts": { + "dev": "keystone dev", + "start": "keystone start", + "build": "keystone build", + "postinstall": "keystone postinstall" + }, + "dependencies": { + "@keystone-6/core": "^5.0.0", + "@prisma/client": "^4.15.0" + }, + "devDependencies": { + "prisma": "^4.15.0", + "typescript": "~5.0.0" + } +} diff --git a/examples/field-groups/sandbox.config.json b/examples/field-groups/sandbox.config.json new file mode 100644 index 00000000000..7a34682ee45 --- /dev/null +++ b/examples/field-groups/sandbox.config.json @@ -0,0 +1,7 @@ +{ + "template": "node", + "container": { + "startScript": "keystone dev", + "node": "16" + } +} diff --git a/examples/field-groups/schema.graphql b/examples/field-groups/schema.graphql new file mode 100644 index 00000000000..b071d3c2619 --- /dev/null +++ b/examples/field-groups/schema.graphql @@ -0,0 +1,233 @@ +# This file is automatically generated by Keystone, do not modify it manually. +# Modify your Keystone config when you want to change this. + +type Post { + id: ID! + title: String + slug: String + content: String +} + +input PostWhereUniqueInput { + id: ID +} + +input PostWhereInput { + AND: [PostWhereInput!] + OR: [PostWhereInput!] + NOT: [PostWhereInput!] + id: IDFilter + title: StringFilter + slug: StringFilter + content: StringNullableFilter +} + +input IDFilter { + equals: ID + in: [ID!] + notIn: [ID!] + lt: ID + lte: ID + gt: ID + gte: ID + not: IDFilter +} + +input StringFilter { + equals: String + in: [String!] + notIn: [String!] + lt: String + lte: String + gt: String + gte: String + contains: String + startsWith: String + endsWith: String + not: NestedStringFilter +} + +input NestedStringFilter { + equals: String + in: [String!] + notIn: [String!] + lt: String + lte: String + gt: String + gte: String + contains: String + startsWith: String + endsWith: String + not: NestedStringFilter +} + +input StringNullableFilter { + equals: String + in: [String!] + notIn: [String!] + lt: String + lte: String + gt: String + gte: String + contains: String + startsWith: String + endsWith: String + not: StringNullableFilter +} + +input PostOrderByInput { + id: OrderDirection + title: OrderDirection + slug: OrderDirection + content: OrderDirection +} + +enum OrderDirection { + asc + desc +} + +input PostUpdateInput { + title: String! + slug: String + content: String +} + +input PostUpdateArgs { + where: PostWhereUniqueInput! + data: PostUpdateInput! +} + +input PostCreateInput { + title: String! = "" + slug: String + content: String +} + +""" +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 { + createPost(data: PostCreateInput!): Post + createPosts(data: [PostCreateInput!]!): [Post] + updatePost(where: PostWhereUniqueInput!, data: PostUpdateInput!): Post + updatePosts(data: [PostUpdateArgs!]!): [Post] + deletePost(where: PostWhereUniqueInput!): Post + deletePosts(where: [PostWhereUniqueInput!]!): [Post] +} + +type Query { + posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] + post(where: PostWhereUniqueInput!): Post + postsCount(where: PostWhereInput! = {}): Int + keystone: KeystoneMeta! +} + +type KeystoneMeta { + adminMeta: KeystoneAdminMeta! +} + +type KeystoneAdminMeta { + 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!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! + initialSort: KeystoneAdminUISort + isHidden: Boolean! + isSingleton: Boolean! +} + +type KeystoneAdminUIFieldMeta { + path: String! + label: String! + description: String + isOrderable: Boolean! + isFilterable: Boolean! + isNonNull: [KeystoneAdminUIFieldMetaIsNonNull!] + fieldMeta: JSON + viewsIndex: Int! + customViewsIndex: Int + createView: KeystoneAdminUIFieldMetaCreateView! + listView: KeystoneAdminUIFieldMetaListView! + itemView(id: ID): KeystoneAdminUIFieldMetaItemView + search: QueryMode +} + +enum KeystoneAdminUIFieldMetaIsNonNull { + read + create + update +} + +type KeystoneAdminUIFieldMetaCreateView { + fieldMode: KeystoneAdminUIFieldMetaCreateViewFieldMode! +} + +enum KeystoneAdminUIFieldMetaCreateViewFieldMode { + edit + hidden +} + +type KeystoneAdminUIFieldMetaListView { + fieldMode: KeystoneAdminUIFieldMetaListViewFieldMode! +} + +enum KeystoneAdminUIFieldMetaListViewFieldMode { + read + hidden +} + +type KeystoneAdminUIFieldMetaItemView { + fieldMode: KeystoneAdminUIFieldMetaItemViewFieldMode + fieldPosition: KeystoneAdminUIFieldMetaItemViewFieldPosition +} + +enum KeystoneAdminUIFieldMetaItemViewFieldMode { + edit + read + hidden +} + +enum KeystoneAdminUIFieldMetaItemViewFieldPosition { + form + sidebar +} + +enum QueryMode { + default + insensitive +} + +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + +type KeystoneAdminUISort { + field: String! + direction: KeystoneAdminUISortDirection! +} + +enum KeystoneAdminUISortDirection { + ASC + DESC +} diff --git a/examples/field-groups/schema.prisma b/examples/field-groups/schema.prisma new file mode 100644 index 00000000000..c4b7bddcb1c --- /dev/null +++ b/examples/field-groups/schema.prisma @@ -0,0 +1,20 @@ +// This file is automatically generated by Keystone, do not modify it manually. +// Modify your Keystone config when you want to change this. + +datasource sqlite { + url = env("DATABASE_URL") + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") + provider = "sqlite" +} + +generator client { + provider = "prisma-client-js" + output = "node_modules/.myprisma/client" +} + +model Post { + id String @id @default(cuid()) + title String @default("") + slug String @default("") + content String? +} diff --git a/examples/field-groups/schema.ts b/examples/field-groups/schema.ts new file mode 100644 index 00000000000..16355e48fd4 --- /dev/null +++ b/examples/field-groups/schema.ts @@ -0,0 +1,49 @@ +import { list, group } from '@keystone-6/core'; +import { allowAll, denyAll } from '@keystone-6/core/access'; +import { text } from '@keystone-6/core/fields'; +import type { Lists } from '.keystone/types'; + +export const lists: Lists = { + Post: list({ + access: allowAll, + fields: { + title: text({ + validation: { isRequired: true }, + db: { isNullable: false }, + graphql: { isNonNull: { create: true, update: true } }, + }), + ...group({ + label: 'Meta', + description: 'Some automatically updated meta fields', + fields: { + slug: text({ + access: { + read: allowAll, + create: denyAll, + update: denyAll, + }, + // for this example, we are going to use a hook for fun + // defaultValue: { kind: 'now' } + hooks: { + resolveInput: ({ context, operation, resolvedData, fieldKey }) => { + // TODO: text should allow you to prevent a defaultValue, then Prisma create could be non-null + // if (operation === 'create') return resolvedData.title.replace(/ /g, '-').toLowerCase() + if (operation === 'create') { + return resolvedData.title?.replace(/ /g, '-').toLowerCase(); + } + + return resolvedData[fieldKey]; + }, + }, + }), + }, + }), + + content: text({ + db: { + isNullable: true, + }, + }), + }, + }), +}; diff --git a/examples/hooks/schema.ts b/examples/hooks/schema.ts index 3d40f30bf7b..82f133d3780 100644 --- a/examples/hooks/schema.ts +++ b/examples/hooks/schema.ts @@ -1,4 +1,4 @@ -import { list } from '@keystone-6/core'; +import { list, group } from '@keystone-6/core'; import { allowAll, denyAll } from '@keystone-6/core/access'; import { checkbox, text, timestamp } from '@keystone-6/core/fields'; import type { Lists } from '.keystone/types'; @@ -41,20 +41,59 @@ export const lists: Lists = { }), preventDelete: checkbox(), - createdBy: text({ ...readOnly }), - createdAt: timestamp({ ...readOnly }), - updatedBy: text({ ...readOnly }), - updatedAt: timestamp({ ...readOnly }), + ...group({ + label: 'Authorship', + description: 'Fields that show who created and updated this Post', + fields: { + createdBy: text({ ...readOnly }), + createdAt: timestamp({ + ...readOnly, + + // TODO: explain + // defaultValue: { kind: 'now' } + + hooks: { + resolveInput: ({ context, operation, resolvedData, fieldKey }) => { + if (operation === 'create') return new Date(); + return resolvedData[fieldKey]; + }, + }, + + // TODO: this would be nice + // hooks: { + // resolveInput: { + // create: () => new Date() + // } + // } + }), + + updatedBy: text({ ...readOnly }), + updatedAt: timestamp({ + ...readOnly, + // TODO: explain + // db: { + // updatedAt: true + // }, + + // TODO: this would be nice + // hooks: { + // resolveInput: { + // update: () => new Date() + // } + // } + }), + }, + }), }, hooks: { resolveInput: { create: ({ context, resolvedData }) => { - resolvedData.createdAt = new Date(); + //resolvedData.createdAt = new Date(); // see createdAt field hook resolvedData.createdBy = `${context.req?.socket.remoteAddress} (${context.req?.headers['user-agent']})`; return resolvedData; }, update: ({ context, resolvedData }) => { - resolvedData.updatedAt = new Date(); + //resolvedData.updatedAt = new Date(); // see updatedAt field hook resolvedData.updatedBy = `${context.req?.socket.remoteAddress} (${context.req?.headers['user-agent']})`; return resolvedData; }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d3e2ef8df2..79f0b7c3322 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1189,6 +1189,22 @@ importers: specifier: ~5.0.0 version: 5.0.2 + examples/field-groups: + dependencies: + '@keystone-6/core': + specifier: ^5.0.0 + version: link:../../packages/core + '@prisma/client': + specifier: ^4.15.0 + version: 4.15.0(prisma@4.15.0) + devDependencies: + prisma: + specifier: ^4.15.0 + version: 4.15.0 + typescript: + specifier: ~5.0.0 + version: 5.0.2 + examples/framework-astro: dependencies: '@keystone-6/core': From c5fed7c6fc9fbd20d89d7c22521a7105884a0fe0 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 8 Jun 2023 13:42:12 +1000 Subject: [PATCH 2/8] fixes broken types for field groups --- .changeset/fix-group-types.md | 5 ++++ packages/core/src/lib/core/types-for-lists.ts | 14 +++++------ packages/core/src/schema.ts | 24 ++++++++++--------- 3 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 .changeset/fix-group-types.md diff --git a/.changeset/fix-group-types.md b/.changeset/fix-group-types.md new file mode 100644 index 00000000000..6f962f07469 --- /dev/null +++ b/.changeset/fix-group-types.md @@ -0,0 +1,5 @@ +--- +'@keystone-6/core': patch +--- + +Fixes broken types for field groups, regression from https://github.com/keystonejs/keystone/pull/8570 diff --git a/packages/core/src/lib/core/types-for-lists.ts b/packages/core/src/lib/core/types-for-lists.ts index c1584e2dff1..242dfedacbb 100644 --- a/packages/core/src/lib/core/types-for-lists.ts +++ b/packages/core/src/lib/core/types-for-lists.ts @@ -48,17 +48,15 @@ export type InitialisedField = Omit; - groups: FieldGroupConfig[]; + groups: { + fields: BaseListTypeInfo['fields'][]; + label: string; + description: string | null; + }[]; hooks: ResolvedListHooks; @@ -223,7 +221,7 @@ function getListsWithInitialisedFields( for (const [listKey, list] of Object.entries(listsConfig)) { const intermediateList = intermediateLists[listKey]; const resultFields: Record = {}; - const groups: FieldGroupConfig[] = []; + const groups = []; const fieldKeys = Object.keys(list.fields); for (const [idx, [fieldKey, fieldFunc]] of Object.entries(list.fields).entries()) { diff --git a/packages/core/src/schema.ts b/packages/core/src/schema.ts index ba9a3d06cf1..b600676a04c 100644 --- a/packages/core/src/schema.ts +++ b/packages/core/src/schema.ts @@ -1,4 +1,3 @@ -import type { FieldGroupConfig } from './lib/core/types-for-lists'; import type { BaseFields, BaseListTypeInfo, @@ -13,26 +12,29 @@ export function config(config: KeystoneCo let i = 0; export function group< - Fields extends BaseFields, + __Fields extends BaseFields, // TODO: remove in breaking change ListTypeInfo extends BaseListTypeInfo ->(config: { label: string; description?: string; fields: Fields }): Fields { +>(config: { + label: string; + description?: string; + fields: BaseFields; +}): BaseFields { const keys = Object.keys(config.fields); if (keys.some(key => key.startsWith('__group'))) { throw new Error('groups cannot be nested'); } - const groupConfig: FieldGroupConfig = { - fields: keys, - label: config.label, - description: config.description ?? null, - }; + return { - [`__group${i++}`]: groupConfig, + [`__group${i++}`]: { + fields: keys, + label: config.label, + description: config.description ?? null, + }, ...config.fields, - }; + } as any; // TODO: FIXME, see types-for-lists.ts:getListsWithInitialisedFields } export function list< - Fields extends BaseFields, ListTypeInfo extends BaseListTypeInfo // TODO: remove in breaking change >(config: ListConfig): ListConfig { return { ...config }; From 3a4a62e8ae768d0f42bc490ecc6266207d2acb40 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 8 Jun 2023 13:50:32 +1000 Subject: [PATCH 3/8] inline printInterim*Type functions --- packages/core/src/lib/schema-type-printer.tsx | 79 ++++--------------- 1 file changed, 17 insertions(+), 62 deletions(-) diff --git a/packages/core/src/lib/schema-type-printer.tsx b/packages/core/src/lib/schema-type-printer.tsx index 18a610c4c88..b3316f24bf8 100644 --- a/packages/core/src/lib/schema-type-printer.tsx +++ b/packages/core/src/lib/schema-type-printer.tsx @@ -90,54 +90,6 @@ function printInputTypesFromSchema(schema: GraphQLSchema, scalars: Record { - const prismaKey = `${fieldKey}_${subFieldKey}`; - return ( - ' ' + - printInterimFieldType({ - prismaClientPath, - listKey, - fieldKey: subFieldKey, - prismaKey, - operation, - }) - ); - }), - ` };`, - ].join('\n'); -} - function printInterimType( prismaClientPath: string, list: L, @@ -145,27 +97,30 @@ function printInterimType( typename: string, operation: 'Create' | 'Update' ) { + const prismaType = `import('${prismaClientPath}').Prisma.${listKey}${operation}Input`; + return [ `type Resolved${typename} = {`, ...Object.entries(list.fields).map(([fieldKey, { dbField }]) => { if (dbField.kind === 'none' || fieldKey === 'id') return ` ${fieldKey}?: undefined;`; + if (dbField.kind === 'multi') { - return printInterimMultiFieldType({ - prismaClientPath, - listKey, - fieldKey, - operation, - fields: dbField.fields, - }); + return [ + ` ${fieldKey}: {`, + ...Object.entries(dbField.fields).map(([subFieldKey, subDbField]) => { + // TODO: untrue if a db defaultValue is set +// const required = operation === 'Create' && subDbField.mode === 'required' ? '' : '?'; + const required = '?'; + return ` ${subFieldKey}${required}: ${prismaType}['${fieldKey}_${subFieldKey}'];`; + }), + ` };`, + ].join('\n'); } - return printInterimFieldType({ - prismaClientPath, - listKey, - fieldKey, - prismaKey: fieldKey, - operation, - }); + // TODO: untrue if a db defaultValue is set +// const required = operation === 'Create' && dbField.mode === 'required' ? '' : '?'; + const required = '?'; + return ` ${fieldKey}${required}: ${prismaType}['${fieldKey}'];`; }), `};`, ].join('\n'); From 0201b5be4ff3968c8d6e366eb2337c7d77c99af0 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 8 Jun 2023 14:13:54 +1000 Subject: [PATCH 4/8] use BaseFields internally --- packages/core/src/lib/schema-type-printer.tsx | 4 ++-- packages/core/src/schema.ts | 3 ++- packages/core/src/types/config/lists.ts | 14 +++++++------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/core/src/lib/schema-type-printer.tsx b/packages/core/src/lib/schema-type-printer.tsx index b3316f24bf8..6e4469760f9 100644 --- a/packages/core/src/lib/schema-type-printer.tsx +++ b/packages/core/src/lib/schema-type-printer.tsx @@ -109,7 +109,7 @@ function printInterimType( ` ${fieldKey}: {`, ...Object.entries(dbField.fields).map(([subFieldKey, subDbField]) => { // TODO: untrue if a db defaultValue is set -// const required = operation === 'Create' && subDbField.mode === 'required' ? '' : '?'; + // const required = operation === 'Create' && subDbField.mode === 'required' ? '' : '?'; const required = '?'; return ` ${subFieldKey}${required}: ${prismaType}['${fieldKey}_${subFieldKey}'];`; }), @@ -118,7 +118,7 @@ function printInterimType( } // TODO: untrue if a db defaultValue is set -// const required = operation === 'Create' && dbField.mode === 'required' ? '' : '?'; + // const required = operation === 'Create' && dbField.mode === 'required' ? '' : '?'; const required = '?'; return ` ${fieldKey}${required}: ${prismaType}['${fieldKey}'];`; }), diff --git a/packages/core/src/schema.ts b/packages/core/src/schema.ts index b600676a04c..446cf00881b 100644 --- a/packages/core/src/schema.ts +++ b/packages/core/src/schema.ts @@ -35,7 +35,8 @@ export function group< } export function list< - ListTypeInfo extends BaseListTypeInfo // TODO: remove in breaking change + __Fields extends BaseFields, // TODO: remove in breaking change + ListTypeInfo extends BaseListTypeInfo >(config: ListConfig): ListConfig { return { ...config }; } diff --git a/packages/core/src/types/config/lists.ts b/packages/core/src/types/config/lists.ts index a90cac2b4a8..39131a40875 100644 --- a/packages/core/src/types/config/lists.ts +++ b/packages/core/src/types/config/lists.ts @@ -25,7 +25,7 @@ export type IdFieldConfig = export type ListConfig< ListTypeInfo extends BaseListTypeInfo, - Fields extends BaseFields = BaseFields // TODO: remove in breaking change + __Fields extends BaseFields = BaseFields // TODO: remove in breaking change > = { isSingleton?: boolean; fields: BaseFields; @@ -37,7 +37,7 @@ export type ListConfig< access: ListAccessControl; /** Config for how this list should act in the Admin UI */ - ui?: ListAdminUIConfig>; + ui?: ListAdminUIConfig; /** * Hooks to modify the behaviour of GraphQL operations at certain points @@ -61,19 +61,19 @@ export type ListConfig< export type ListAdminUIConfig< ListTypeInfo extends BaseListTypeInfo, - Fields extends BaseFields = BaseFields // TODO: remove in breaking change + __Fields extends BaseFields = BaseFields // TODO: remove in breaking change > = { /** * The field to use as a label in the Admin UI. If you want to base the label off more than a single field, use a virtual field and reference that field here. * @default 'label', if it exists, falling back to 'name', then 'title', and finally 'id', which is guaranteed to exist. */ - labelField?: 'id' | Exclude; + labelField?: 'id' | Exclude, number>; /** * The fields used by the Admin UI when searching this list. * It is always possible to search by id and `id` should not be specified in this option. * @default The `labelField` if it has a string `contains` filter, otherwise none. */ - searchFields?: readonly Extract[]; + searchFields?: readonly Extract, string>[]; /** The path that the list should be at in the Admin UI */ // Not currently used. Should be passed into `keystone.createList()`. @@ -141,9 +141,9 @@ export type ListAdminUIConfig< * Users of the Admin UI can select different columns to show in the UI. * @default the first three fields in the list */ - initialColumns?: readonly ('id' | keyof Fields)[]; + initialColumns?: readonly ('id' | keyof BaseFields)[]; // was previously top-level defaultSort - initialSort?: { field: 'id' | keyof Fields; direction: 'ASC' | 'DESC' }; + initialSort?: { field: 'id' | keyof BaseFields; direction: 'ASC' | 'DESC' }; // was previously defaultPageSize pageSize?: number; // default number of items to display per page on the list screen }; From 85cfac6e26508baf1ba6ac90df5d8ea8a77f5f8f Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 8 Jun 2023 13:52:48 +1000 Subject: [PATCH 5/8] fix types for allOperations --- .changeset/fix-all-ops.md | 5 + examples/usecase-roles/access.ts | 95 ++++++++----------- examples/usecase-roles/schema.ts | 19 ++-- packages/core/src/access.ts | 13 +-- .../core/src/types/config/access-control.ts | 2 +- 5 files changed, 62 insertions(+), 72 deletions(-) create mode 100644 .changeset/fix-all-ops.md diff --git a/.changeset/fix-all-ops.md b/.changeset/fix-all-ops.md new file mode 100644 index 00000000000..40002ed0e7f --- /dev/null +++ b/.changeset/fix-all-ops.md @@ -0,0 +1,5 @@ +--- +'@keystone-6/core': patch +--- + +Fixes types for `allOperations`, regression from https://github.com/keystonejs/keystone/pull/8570 diff --git a/examples/usecase-roles/access.ts b/examples/usecase-roles/access.ts index 93a9dc0ac22..fa0cecf8a9a 100644 --- a/examples/usecase-roles/access.ts +++ b/examples/usecase-roles/access.ts @@ -17,27 +17,24 @@ export type Session = { }; }; -type ListAccessArgs = { +type AccessArgs = { session?: Session; }; -/* - The basic level of access to the system is being signed in as a valid user. This gives you access - to the Admin UI, access to your own User and Todo items, and read access to roles. -*/ -export const isSignedIn = ({ session }: ListAccessArgs) => { - return !!session; -}; +// this function checks only that a session actually exists, nothing else +export function isSignedIn({ session }: AccessArgs) { + return Boolean(session); +} /* Permissions are shorthand functions for checking that the current user's role has the specified permission boolean set to true */ export const permissions = { - canCreateTodos: ({ session }: ListAccessArgs) => !!session?.data.role?.canCreateTodos, - canManageAllTodos: ({ session }: ListAccessArgs) => !!session?.data.role?.canManageAllTodos, - canManagePeople: ({ session }: ListAccessArgs) => !!session?.data.role?.canManagePeople, - canManageRoles: ({ session }: ListAccessArgs) => !!session?.data.role?.canManageRoles, + canCreateTodos: ({ session }: AccessArgs) => session?.data.role?.canCreateTodos ?? false, + canManageAllTodos: ({ session }: AccessArgs) => session?.data.role?.canManageAllTodos ?? false, + canManagePeople: ({ session }: AccessArgs) => session?.data.role?.canManagePeople ?? false, + canManageRoles: ({ session }: AccessArgs) => session?.data.role?.canManageRoles ?? false, // TODO: add canViewAdminUI }; @@ -46,12 +43,11 @@ export const permissions = { all or no items are available) or a set of filters that limit the available items */ export const rules = { - canReadTodos: ({ session }: ListAccessArgs) => { - if (!session) { - // No session? No todos. - return false; - } else if (session.data.role?.canManageAllTodos) { - // Can see all todos that are: assigned to them, or not private + canReadTodos: ({ session }: AccessArgs) => { + if (!session) return false; + + if (session.data.role?.canManageAllTodos) { + // can see all todos that are: assigned to them, or not private return { OR: [ { assignedTo: { id: { equals: session.itemId } } }, @@ -59,45 +55,36 @@ export const rules = { { NOT: { isPrivate: { equals: true } } }, ], }; - } else { - // Can only see their own todos - return { assignedTo: { id: { equals: session.itemId } } }; } + + // default to only seeing your own todos + return { assignedTo: { id: { equals: session.itemId } } }; }, - canManageTodos: ({ session }: ListAccessArgs) => { - if (!session) { - // No session? No todos. - return false; - } else if (session.data.role?.canManageAllTodos) { - // Can manage todos? go for it - return true; - } else { - // Can only manage their own todos - return { assignedTo: { id: { equals: session.itemId } } }; - } + canManageTodos: ({ session }: AccessArgs) => { + if (!session) return false; + + // can manage every todo? + if (session.data.role?.canManageAllTodos) return true; + + // default to only managing your own todos + return { assignedTo: { id: { equals: session.itemId } } }; }, - canReadPeople: ({ session }: ListAccessArgs) => { - if (!session) { - // No session? No people. - return false; - } else if (session.data.role?.canSeeOtherPeople) { - // Can see everyone - return true; - } else { - // Can only see yourself - return { id: { equals: session.itemId } }; - } + canReadPeople: ({ session }: AccessArgs) => { + if (!session) return false; + + // can see everyone? + if (session.data.role?.canSeeOtherPeople) return true; + + // default to only seeing yourself + return { id: { equals: session.itemId } }; }, - canUpdatePeople: ({ session }: ListAccessArgs) => { - if (!session) { - // No session? No people. - return false; - } else if (session.data.role?.canEditOtherPeople) { - // Can update everyone - return true; - } else { - // Can update yourself - return { id: { equals: session.itemId } }; - } + canUpdatePeople: ({ session }: AccessArgs) => { + if (!session) return false; + + // can update everyone? + if (session.data.role?.canEditOtherPeople) return true; + + // default to only updating yourself + return { id: { equals: session.itemId } }; }, }; diff --git a/examples/usecase-roles/schema.ts b/examples/usecase-roles/schema.ts index 27aaf1ee933..6a823004d0b 100644 --- a/examples/usecase-roles/schema.ts +++ b/examples/usecase-roles/schema.ts @@ -3,6 +3,12 @@ import { allOperations, denyAll } from '@keystone-6/core/access'; import { checkbox, password, relationship, text } from '@keystone-6/core/fields'; import { isSignedIn, permissions, rules } from './access'; +import type { Session } from './access'; +import type { Lists } from '.keystone/types'; + +// WARNING: this example is for demonstration purposes only +// as with each of our examples, it has not been vetted +// or tested for any particular usage /* The set of permissions a role could have would change based on application requirements, so the @@ -13,7 +19,7 @@ import { isSignedIn, permissions, rules } from './access'; - All users can see and manage todo items assigned to themselves */ -export const lists = { +export const lists: Lists = { Todo: list({ /* SPEC @@ -49,13 +55,9 @@ export const lists = { }, }, fields: { - /* The label of the todo item */ label: text({ validation: { isRequired: true } }), - /* Whether the todo item is complete */ isComplete: checkbox({ defaultValue: false }), - /* Private todo items are only visible to the user they are assigned to */ isPrivate: checkbox({ defaultValue: false }), - /* The person the todo item is assigned to */ assignedTo: relationship({ ref: 'User.tasks', ui: { @@ -136,13 +138,12 @@ export const lists = { // the user's password, used as the secret field for authentication // should not be publicly visible password: password({ - validation: { isRequired: true }, access: { read: denyAll, // TODO: is this required? update: ({ session, item }) => permissions.canManagePeople({ session }) || session.itemId === item.id, }, - // TODO: is anything else required + validation: { isRequired: true }, }), /* The role assigned to the user */ role: relationship({ @@ -209,8 +210,8 @@ export const lists = { }, }, fields: { - /* The name of the role */ name: text({ validation: { isRequired: true } }), + /* Create Todos means: - create todos (can only assign them to others with canManageAllTodos) */ canCreateTodos: checkbox({ defaultValue: false }), @@ -234,7 +235,7 @@ export const lists = { /* Use AdminUI means: - can access the Admin UI next app */ canUseAdminUI: checkbox({ defaultValue: false }), - /* This list of People assigned to this role */ + assignedTo: relationship({ ref: 'User.role', many: true, diff --git a/packages/core/src/access.ts b/packages/core/src/access.ts index 8a748ebd6a2..37e291394c7 100644 --- a/packages/core/src/access.ts +++ b/packages/core/src/access.ts @@ -1,6 +1,5 @@ import type { MaybePromise } from './types/utils'; import type { BaseListTypeInfo } from './types'; -import type { AccessOperation, BaseAccessArgs } from './types/config/access-control'; export function allowAll() { return true; @@ -16,13 +15,11 @@ export function unfiltered(): MaybePromis return true; } -export function allOperations( - func: (args: BaseAccessArgs & { operation: AccessOperation }) => boolean -) { +export function allOperations(f: F) { return { - query: func, - create: func, - update: func, - delete: func, + query: f, + create: f, + update: f, + delete: f, }; } diff --git a/packages/core/src/types/config/access-control.ts b/packages/core/src/types/config/access-control.ts index 1b65977d3f8..0636d8ad2ca 100644 --- a/packages/core/src/types/config/access-control.ts +++ b/packages/core/src/types/config/access-control.ts @@ -67,7 +67,7 @@ type ListAccessControlFunction = ( ) => MaybePromise; type ListAccessControlObject = { - // These functions should return `true` if access is allowed or `false` if access is denied. + // these functions should return `true` if access is allowed or `false` if access is denied. operation: | ListOperationAccessControl | { From c66fb164f37bb0b446ac10caebb5e0ef5964f5b9 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 8 Jun 2023 14:57:10 +1000 Subject: [PATCH 6/8] dont use generic fieldKey for inline hooks, add missing updatedAt hook --- examples/field-groups/schema.ts | 4 ++-- examples/hooks/schema.ts | 40 ++++++++++++++++++++------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/examples/field-groups/schema.ts b/examples/field-groups/schema.ts index 16355e48fd4..2e972b55897 100644 --- a/examples/field-groups/schema.ts +++ b/examples/field-groups/schema.ts @@ -25,14 +25,14 @@ export const lists: Lists = { // for this example, we are going to use a hook for fun // defaultValue: { kind: 'now' } hooks: { - resolveInput: ({ context, operation, resolvedData, fieldKey }) => { + resolveInput: ({ context, operation, resolvedData }) => { // TODO: text should allow you to prevent a defaultValue, then Prisma create could be non-null // if (operation === 'create') return resolvedData.title.replace(/ /g, '-').toLowerCase() if (operation === 'create') { return resolvedData.title?.replace(/ /g, '-').toLowerCase(); } - return resolvedData[fieldKey]; + return resolvedData.slug; }, }, }), diff --git a/examples/hooks/schema.ts b/examples/hooks/schema.ts index 82f133d3780..4a25866bae7 100644 --- a/examples/hooks/schema.ts +++ b/examples/hooks/schema.ts @@ -50,37 +50,45 @@ export const lists: Lists = { ...readOnly, // TODO: explain - // defaultValue: { kind: 'now' } + // defaultValue: { kind: 'now' } hooks: { - resolveInput: ({ context, operation, resolvedData, fieldKey }) => { + resolveInput: ({ context, operation, resolvedData }) => { if (operation === 'create') return new Date(); - return resolvedData[fieldKey]; + return resolvedData.createdAt; }, }, // TODO: this would be nice - // hooks: { - // resolveInput: { - // create: () => new Date() - // } - // } + // hooks: { + // resolveInput: { + // create: () => new Date() + // } + // } }), updatedBy: text({ ...readOnly }), updatedAt: timestamp({ ...readOnly, + // TODO: explain - // db: { - // updatedAt: true - // }, + // db: { + // updatedAt: true + // }, + + hooks: { + resolveInput: ({ context, operation, resolvedData }) => { + if (operation === 'update') return new Date(); + return resolvedData.updatedAt; + }, + }, // TODO: this would be nice - // hooks: { - // resolveInput: { - // update: () => new Date() - // } - // } + // hooks: { + // resolveInput: { + // update: () => new Date() + // } + // } }), }, }), From 8c794f2c7aa35f3c164f045434d7d8653f48ffcc Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 8 Jun 2023 15:05:05 +1000 Subject: [PATCH 7/8] rename required to optional --- packages/auth/src/index.ts | 2 +- packages/core/src/lib/schema-type-printer.tsx | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 4a8a86338f8..6087b8d6ef6 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -1,4 +1,4 @@ -import { +import type { AdminFileToWrite, BaseListTypeInfo, KeystoneConfig, diff --git a/packages/core/src/lib/schema-type-printer.tsx b/packages/core/src/lib/schema-type-printer.tsx index 6e4469760f9..7e443adc259 100644 --- a/packages/core/src/lib/schema-type-printer.tsx +++ b/packages/core/src/lib/schema-type-printer.tsx @@ -109,18 +109,18 @@ function printInterimType( ` ${fieldKey}: {`, ...Object.entries(dbField.fields).map(([subFieldKey, subDbField]) => { // TODO: untrue if a db defaultValue is set - // const required = operation === 'Create' && subDbField.mode === 'required' ? '' : '?'; - const required = '?'; - return ` ${subFieldKey}${required}: ${prismaType}['${fieldKey}_${subFieldKey}'];`; + // const optional = operation === 'Create' && subDbField.mode === 'required' ? '' : '?'; + const optional = '?'; + return ` ${subFieldKey}${optional}: ${prismaType}['${fieldKey}_${subFieldKey}'];`; }), ` };`, ].join('\n'); } // TODO: untrue if a db defaultValue is set - // const required = operation === 'Create' && dbField.mode === 'required' ? '' : '?'; - const required = '?'; - return ` ${fieldKey}${required}: ${prismaType}['${fieldKey}'];`; + // const optional = operation === 'Create' && dbField.mode === 'required' ? '' : '?'; + const optional = '?'; + return ` ${fieldKey}${optional}: ${prismaType}['${fieldKey}'];`; }), `};`, ].join('\n'); From fd847614d21e015e00179b87e73121b8008b6e45 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 8 Jun 2023 15:05:45 +1000 Subject: [PATCH 8/8] prefer import type --- packages/auth/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 6087b8d6ef6..caa8886c9b9 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -8,7 +8,7 @@ import type { } from '@keystone-6/core/types'; import { password, timestamp } from '@keystone-6/core/fields'; -import { AuthConfig, AuthGqlNames } from './types'; +import type { AuthConfig, AuthGqlNames } from './types'; import { getSchemaExtension } from './schema'; import { signinTemplate } from './templates/signin'; import { initTemplate } from './templates/init';