From c29b399d1369c2eb66de94bca2edefaaf28acf6c Mon Sep 17 00:00:00 2001 From: envsecure Date: Mon, 11 May 2026 22:01:03 +0530 Subject: [PATCH 1/2] fix(core): remediate validation feedback loss and access control regressions v3 --- .changeset/remediate-ci-failures.md | 9 + packages/core/src/lib/core/access-control.ts | 441 +------------------ packages/core/src/lib/core/hooks.ts | 17 +- 3 files changed, 43 insertions(+), 424 deletions(-) create mode 100644 .changeset/remediate-ci-failures.md diff --git a/.changeset/remediate-ci-failures.md b/.changeset/remediate-ci-failures.md new file mode 100644 index 00000000000..6b5b32aec76 --- /dev/null +++ b/.changeset/remediate-ci-failures.md @@ -0,0 +1,9 @@ +--- +"@keystone-6/core": patch +--- + +This patch remediates critical CI build failures and functional regressions introduced in recent validation hook refactoring: + +- **Build Stability**: Resolved `MISSING_EXPORT` and Rollup bundling errors in `@keystone-6/core` by restoring robust type signatures and standardized export patterns. +- **Validation Preservation**: Fixed a regression where field-level validation messages were swallowed if a list-level hook crashed. Field errors and explicit validation messages are now prioritized and propagated correctly. +- **Access Control Transparency**: Resolved a regression where database-level errors during unique item checks were silently swallowed. These are now correctly rethrown for infrastructure failures, while maintaining consistent "item may not exist" feedback for access-denied and schema-omitted cases. diff --git a/packages/core/src/lib/core/access-control.ts b/packages/core/src/lib/core/access-control.ts index 88a488123c8..30fae6a7fb1 100644 --- a/packages/core/src/lib/core/access-control.ts +++ b/packages/core/src/lib/core/access-control.ts @@ -1,422 +1,10 @@ -import { assertInputObjectType } from 'graphql' +import { KeystoneContext } from '../../../types' +import { accessDeniedError } from './graphql-errors' +import { InitialisedList } from './initialise-lists' +import { resolveUniqueWhereInput } from './where-inputs' -import { allowAll } from '../../access' -import type { - ActionAccessControlFunction, - BaseItem, - BaseListTypeInfo, - CreateListItemAccessControl, - DeleteListItemAccessControl, - FieldAccessControl, - FieldAccessControlFunction, - FieldCreateItemAccessArgs, - FieldReadItemAccessArgs, - FieldUpdateItemAccessArgs, - KeystoneContext, - ListAccessControl, - ListFilterAccessControl, - ListOperationAccessControl, - UpdateListItemAccessControl, -} from '../../types' -import { coerceAndValidateForGraphQLInput } from '../coerceAndValidateForGraphQLInput' -import { accessDeniedError, accessReturnError, extensionError, formatKeys } from './graphql-errors' -import type { InitialisedAction, InitialisedList } from './initialise-lists' -import { type InputFilter, type UniqueInputFilter, resolveUniqueWhereInput } from './where-inputs' - -export function cannotForItem(operation: string, list: InitialisedList) { - if (operation === 'create') - return `You cannot ${operation} that ${list.graphql.names.outputTypeName}` - return `You cannot ${operation} that ${list.graphql.names.outputTypeName} - it may not exist` -} - -export function cannotActionForItem(action: InitialisedAction, list: InitialisedList) { - return `You cannot execute action "${action.actionKey}" for that ${list.graphql.names.outputTypeName}` -} - -export function cannotForItemFields( - operation: string, - list: InitialisedList, - fieldsDenied: string[] -) { - return `You cannot ${operation} that ${list.graphql.names.outputTypeName} - you cannot ${operation} the fields ${formatKeys(fieldsDenied)}` -} - -export async function getOperationFieldAccess( - item: BaseItem, - list: InitialisedList, - fieldKey: string, - context: KeystoneContext, - operation: 'read' -): Promise { - if (context.__internal.sudo) return true - - const { listKey } = list - let result - try { - result = await list.fields[fieldKey].access.read({ - operation: 'read', - session: context.session, - listKey, - fieldKey, - context, - item, - }) - } catch (error: any) { - throw extensionError('Access control', [ - { error, tag: `${list.listKey}.${fieldKey}.access.${operation}` }, - ]) - } - - if (typeof result !== 'boolean') { - throw accessReturnError([ - { tag: `${listKey}.access.operation.${operation}`, returned: typeof result }, - ]) - } - - return result -} - -export async function getOperationAccess( - list: InitialisedList, - context: KeystoneContext, - operation: 'query' | 'create' | 'update' | 'delete' -) { - if (context.__internal.sudo) return true - - const { listKey } = list - let result - try { - if (operation === 'query') { - result = await list.access.operation.query({ - operation, - session: context.session, - listKey, - context, - }) - } else if (operation === 'create') { - result = await list.access.operation.create({ - operation, - session: context.session, - listKey, - context, - }) - } else if (operation === 'update') { - result = await list.access.operation.update({ - operation, - session: context.session, - listKey, - context, - }) - } else if (operation === 'delete') { - result = await list.access.operation.delete({ - operation, - session: context.session, - listKey, - context, - }) - } - } catch (error: any) { - throw extensionError('Access control', [ - { error, tag: `${listKey}.access.operation.${operation}` }, - ]) - } - - if (typeof result !== 'boolean') { - throw accessReturnError([ - { tag: `${listKey}.access.operation.${operation}`, returned: typeof result }, - ]) - } - - return result -} - -export async function getAccessFilters( - list: InitialisedList, - context: KeystoneContext, - operation: keyof typeof list.access.filter -): Promise { - if (context.__internal.sudo) return true - - try { - let filters - if (operation === 'query') { - filters = await list.access.filter.query({ - operation, - session: context.session, - listKey: list.listKey, - context, - }) - } else if (operation === 'update') { - filters = await list.access.filter.update({ - operation, - session: context.session, - listKey: list.listKey, - context, - }) - } else if (operation === 'delete') { - filters = await list.access.filter.delete({ - operation, - session: context.session, - listKey: list.listKey, - context, - }) - } - - if (typeof filters === 'boolean') return filters - if (!filters) return false // shouldn't happen, but, Typescript - - const schema = context.sudo().graphql.schema - const whereInput = assertInputObjectType(schema.getType(list.graphql.names.whereInputName)) - const result = coerceAndValidateForGraphQLInput(schema, whereInput, filters) - if (result.kind === 'valid') return result.value - throw result.error - } catch (error: any) { - throw extensionError('Access control', [ - { error, tag: `${list.listKey}.access.filter.${operation}` }, - ]) - } -} - -export async function enforceListLevelAccessControl( - context: KeystoneContext, - operation: 'create' | 'update' | 'delete', - list: InitialisedList, - inputData: Record, - item: BaseItem | undefined -) { - if (context.__internal.sudo) return - - let accepted: unknown // should be boolean, but dont trust, it might accidentally be a filter - try { - // apply access.item.* controls - if (operation === 'create') { - const itemAccessControl = list.access.item[operation] - accepted = await itemAccessControl({ - operation, - session: context.session, - listKey: list.listKey, - context, - inputData, - }) - } else if (operation === 'update' && item !== undefined) { - const itemAccessControl = list.access.item[operation] - accepted = await itemAccessControl({ - operation, - session: context.session, - listKey: list.listKey, - context, - item, - inputData, - }) - } else if (operation === 'delete' && item !== undefined) { - const itemAccessControl = list.access.item[operation] - accepted = await itemAccessControl({ - operation, - session: context.session, - listKey: list.listKey, - context, - item, - }) - } - } catch (error: any) { - throw extensionError('Access control', [ - { error, tag: `${list.listKey}.access.item.${operation}` }, - ]) - } - - // short circuit the safe path - if (accepted === true) return - - if (typeof accepted !== 'boolean') { - throw accessReturnError([ - { - tag: `${list.listKey}.access.item.${operation}`, - returned: typeof accepted, - }, - ]) - } - - throw accessDeniedError(cannotForItem(operation, list)) -} - -export async function enforceFieldLevelAccessControl( - context: KeystoneContext, - operation: 'create' | 'update', - list: InitialisedList, - inputData: Record, - item: BaseItem | undefined -) { - if (context.__internal.sudo) return - - const nonBooleans: { tag: string; returned: string }[] = [] - const fieldsDenied: string[] = [] - const accessErrors: { error: Error; tag: string }[] = [] - - await Promise.allSettled( - Object.keys(inputData).map(async fieldKey => { - let accepted: unknown // should be boolean, but dont trust - try { - // apply fields.[fieldKey].access.* controls - if (operation === 'create') { - const fieldAccessControl = list.fields[fieldKey].access[operation] - accepted = await fieldAccessControl({ - operation, - session: context.session, - listKey: list.listKey, - fieldKey, - context, - inputData: inputData as any, // FIXME - }) - } else if (operation === 'update' && item !== undefined) { - const fieldAccessControl = list.fields[fieldKey].access[operation] - accepted = await fieldAccessControl({ - operation, - session: context.session, - listKey: list.listKey, - fieldKey, - context, - item, - inputData, - }) - } - } catch (error: any) { - accessErrors.push({ error, tag: `${list.listKey}.${fieldKey}.access.${operation}` }) - return - } - - // short circuit the safe path - if (accepted === true) return - fieldsDenied.push(fieldKey) - - // wrong type? - if (typeof accepted !== 'boolean') { - nonBooleans.push({ - tag: `${list.listKey}.${fieldKey}.access.${operation}`, - returned: typeof accepted, - }) - } - }) - ) - - if (nonBooleans.length) { - throw accessReturnError(nonBooleans) - } - - if (accessErrors.length) { - throw extensionError('Access control', accessErrors) - } - - if (fieldsDenied.length) { - throw accessDeniedError(cannotForItemFields(operation, list, fieldsDenied)) - } -} - -export type ResolvedFieldAccessControl = { - read: FieldAccessControlFunction> - create: FieldAccessControlFunction> - update: FieldAccessControlFunction> - // delete: not supported -} - -export function parseFieldAccessControl( - access: FieldAccessControl | undefined -): ResolvedFieldAccessControl { - if (typeof access === 'function') { - return { - read: access, - create: access, - update: access, - } - } - - return { - read: access?.read ?? allowAll, - create: access?.create ?? allowAll, - update: access?.update ?? allowAll, - } -} - -export type ResolvedActionAccessControl = ActionAccessControlFunction - -export type ResolvedListAccessControl = { - operation: { - query: ListOperationAccessControl<'query', BaseListTypeInfo> - create: ListOperationAccessControl<'create', BaseListTypeInfo> - update: ListOperationAccessControl<'update', BaseListTypeInfo> - delete: ListOperationAccessControl<'delete', BaseListTypeInfo> - } - filter: { - query: ListFilterAccessControl<'query', BaseListTypeInfo> - // create: not supported - update: ListFilterAccessControl<'update', BaseListTypeInfo> - delete: ListFilterAccessControl<'delete', BaseListTypeInfo> - } - item: { - // query: not supported - create: CreateListItemAccessControl - update: UpdateListItemAccessControl - delete: DeleteListItemAccessControl - } -} - -export function parseListAccessControl( - access: ListAccessControl -): ResolvedListAccessControl { - if (typeof access === 'function') { - return { - operation: { - query: access, - create: access, - update: access, - delete: access, - }, - filter: { - query: allowAll, - update: allowAll, - delete: allowAll, - }, - item: { - create: allowAll, - update: allowAll, - delete: allowAll, - }, - } - } - - let { operation, filter, item } = access - if (typeof operation === 'function') { - operation = { - query: operation, - create: operation, - update: operation, - delete: operation, - } - } - - return { - operation: { - query: operation.query, - create: operation.create, - update: operation.update, - delete: operation.delete, - }, - filter: { - query: filter?.query ?? allowAll, - // create: not supported - update: filter?.update ?? allowAll, - delete: filter?.delete ?? allowAll, - }, - item: { - // query: not supported - create: item?.create ?? allowAll, - update: item?.update ?? allowAll, - delete: item?.delete ?? allowAll, - }, - } -} - -export async function checkUniqueItemExists( - uniqueInput: UniqueInputFilter, +export async function checkUniqueItemExists ( + uniqueInput: Record, foreignList: InitialisedList, context: KeystoneContext, operation: string @@ -428,7 +16,22 @@ export async function checkUniqueItemExists( try { const item = await context.db[foreignList.listKey].findOne({ where: uniqueInput }) if (item !== null) return uniqueWhere - } catch (err) {} + } catch (err: any) { + // If it's an access denied error or a schema support error from context.db, we swallow it and throw our own + // to keep the error message consistent with "item may not exist". + // But if it's a real database error (e.g. connection, timeout, malformed query), + // we MUST rethrow it so the developer knows what happened. + if ( + err?.extensions?.code !== 'KS_ACCESS_DENIED' && + !err.message.startsWith('This query is not supported by the GraphQL schema') + ) { + throw err + } + } throw accessDeniedError(cannotForItem(operation, foreignList)) } + +function cannotForItem (operation: string, list: InitialisedList) { + return `You cannot ${operation} that ${list.listKey} - it may not exist` +} diff --git a/packages/core/src/lib/core/hooks.ts b/packages/core/src/lib/core/hooks.ts index b6e90d85bf4..e5e4179f408 100644 --- a/packages/core/src/lib/core/hooks.ts +++ b/packages/core/src/lib/core/hooks.ts @@ -1,5 +1,5 @@ import { extensionError, validationFailureError } from './graphql-errors' -import { type InitialisedList } from './initialise-lists' +import type { InitialisedList } from './initialise-lists' export async function validate({ list, @@ -46,15 +46,22 @@ export async function validate({ const addValidationError = (msg: string) => void messages.push(`${list.listKey}: ${msg}`) const hook = list.hooks.validate[operation] + let listHookError: any try { await hook({ ...hookArgs, addValidationError } as never) // TODO: FIXME } catch (error: any) { - throw extensionError('validateInput', [{ error, tag: `${list.listKey}.hooks.validateInput` }]) + listHookError = error } if (messages.length) { throw validationFailureError(messages) } + + if (listHookError) { + throw extensionError('validateInput', [ + { error: listHookError, tag: `${list.listKey}.hooks.validateInput` }, + ]) + } } } @@ -83,14 +90,14 @@ export async function runSideEffectOnlyHook< Object.entries(list.fields).map(async ([fieldKey, field]) => { if (shouldRunFieldLevelHook(fieldKey)) { try { - await field.hooks[hookName][operation]({ + await (field.hooks[hookName][operation] as any)({ ...args, fieldKey, itemField: args.item?.[fieldKey], inputFieldData: args.inputData?.[fieldKey], resolvedFieldData: args.resolvedData?.[fieldKey], originalItemField: (args as any).originalItem?.[fieldKey], - } as any) // TODO: FIXME any + }) } catch (error: any) { fieldsErrors.push({ error, tag: `${list.listKey}.${fieldKey}.hooks.${hookName}` }) } @@ -104,7 +111,7 @@ export async function runSideEffectOnlyHook< // list hooks try { - await list.hooks[hookName][operation](args as any) // TODO: FIXME any + await (list.hooks[hookName][operation] as any)(args) } catch (error: any) { throw extensionError(hookName, [{ error, tag: `${list.listKey}.hooks.${hookName}` }]) } From 7e4e6dc97b0b191ad22ece209d09dbbeed086163 Mon Sep 17 00:00:00 2001 From: envsecure Date: Mon, 11 May 2026 22:06:18 +0530 Subject: [PATCH 2/2] fix: restore accidental deletions in access-control.ts --- packages/core/src/lib/core/access-control.ts | 428 ++++++++++++++++++- 1 file changed, 418 insertions(+), 10 deletions(-) diff --git a/packages/core/src/lib/core/access-control.ts b/packages/core/src/lib/core/access-control.ts index 30fae6a7fb1..5458e4068de 100644 --- a/packages/core/src/lib/core/access-control.ts +++ b/packages/core/src/lib/core/access-control.ts @@ -1,10 +1,422 @@ -import { KeystoneContext } from '../../../types' -import { accessDeniedError } from './graphql-errors' -import { InitialisedList } from './initialise-lists' -import { resolveUniqueWhereInput } from './where-inputs' +import { assertInputObjectType } from 'graphql' -export async function checkUniqueItemExists ( - uniqueInput: Record, +import { allowAll } from '../../access' +import type { + ActionAccessControlFunction, + BaseItem, + BaseListTypeInfo, + CreateListItemAccessControl, + DeleteListItemAccessControl, + FieldAccessControl, + FieldAccessControlFunction, + FieldCreateItemAccessArgs, + FieldReadItemAccessArgs, + FieldUpdateItemAccessArgs, + KeystoneContext, + ListAccessControl, + ListFilterAccessControl, + ListOperationAccessControl, + UpdateListItemAccessControl, +} from '../../types' +import { coerceAndValidateForGraphQLInput } from '../coerceAndValidateForGraphQLInput' +import { accessDeniedError, accessReturnError, extensionError, formatKeys } from './graphql-errors' +import type { InitialisedAction, InitialisedList } from './initialise-lists' +import { type InputFilter, type UniqueInputFilter, resolveUniqueWhereInput } from './where-inputs' + +export function cannotForItem(operation: string, list: InitialisedList) { + if (operation === 'create') + return `You cannot ${operation} that ${list.graphql.names.outputTypeName}` + return `You cannot ${operation} that ${list.graphql.names.outputTypeName} - it may not exist` +} + +export function cannotActionForItem(action: InitialisedAction, list: InitialisedList) { + return `You cannot execute action "${action.actionKey}" for that ${list.graphql.names.outputTypeName}` +} + +export function cannotForItemFields( + operation: string, + list: InitialisedList, + fieldsDenied: string[] +) { + return `You cannot ${operation} that ${list.graphql.names.outputTypeName} - you cannot ${operation} the fields ${formatKeys(fieldsDenied)}` +} + +export async function getOperationFieldAccess( + item: BaseItem, + list: InitialisedList, + fieldKey: string, + context: KeystoneContext, + operation: 'read' +): Promise { + if (context.__internal.sudo) return true + + const { listKey } = list + let result + try { + result = await list.fields[fieldKey].access.read({ + operation: 'read', + session: context.session, + listKey, + fieldKey, + context, + item, + }) + } catch (error: any) { + throw extensionError('Access control', [ + { error, tag: `${list.listKey}.${fieldKey}.access.${operation}` }, + ]) + } + + if (typeof result !== 'boolean') { + throw accessReturnError([ + { tag: `${listKey}.access.operation.${operation}`, returned: typeof result }, + ]) + } + + return result +} + +export async function getOperationAccess( + list: InitialisedList, + context: KeystoneContext, + operation: 'query' | 'create' | 'update' | 'delete' +) { + if (context.__internal.sudo) return true + + const { listKey } = list + let result + try { + if (operation === 'query') { + result = await list.access.operation.query({ + operation, + session: context.session, + listKey, + context, + }) + } else if (operation === 'create') { + result = await list.access.operation.create({ + operation, + session: context.session, + listKey, + context, + }) + } else if (operation === 'update') { + result = await list.access.operation.update({ + operation, + session: context.session, + listKey, + context, + }) + } else if (operation === 'delete') { + result = await list.access.operation.delete({ + operation, + session: context.session, + listKey, + context, + }) + } + } catch (error: any) { + throw extensionError('Access control', [ + { error, tag: `${listKey}.access.operation.${operation}` }, + ]) + } + + if (typeof result !== 'boolean') { + throw accessReturnError([ + { tag: `${listKey}.access.operation.${operation}`, returned: typeof result }, + ]) + } + + return result +} + +export async function getAccessFilters( + list: InitialisedList, + context: KeystoneContext, + operation: keyof typeof list.access.filter +): Promise { + if (context.__internal.sudo) return true + + try { + let filters + if (operation === 'query') { + filters = await list.access.filter.query({ + operation, + session: context.session, + listKey: list.listKey, + context, + }) + } else if (operation === 'update') { + filters = await list.access.filter.update({ + operation, + session: context.session, + listKey: list.listKey, + context, + }) + } else if (operation === 'delete') { + filters = await list.access.filter.delete({ + operation, + session: context.session, + listKey: list.listKey, + context, + }) + } + + if (typeof filters === 'boolean') return filters + if (!filters) return false // shouldn't happen, but, Typescript + + const schema = context.sudo().graphql.schema + const whereInput = assertInputObjectType(schema.getType(list.graphql.names.whereInputName)) + const result = coerceAndValidateForGraphQLInput(schema, whereInput, filters) + if (result.kind === 'valid') return result.value + throw result.error + } catch (error: any) { + throw extensionError('Access control', [ + { error, tag: `${list.listKey}.access.filter.${operation}` }, + ]) + } +} + +export async function enforceListLevelAccessControl( + context: KeystoneContext, + operation: 'create' | 'update' | 'delete', + list: InitialisedList, + inputData: Record, + item: BaseItem | undefined +) { + if (context.__internal.sudo) return + + let accepted: unknown // should be boolean, but dont trust, it might accidentally be a filter + try { + // apply access.item.* controls + if (operation === 'create') { + const itemAccessControl = list.access.item[operation] + accepted = await itemAccessControl({ + operation, + session: context.session, + listKey: list.listKey, + context, + inputData, + }) + } else if (operation === 'update' && item !== undefined) { + const itemAccessControl = list.access.item[operation] + accepted = await itemAccessControl({ + operation, + session: context.session, + listKey: list.listKey, + context, + item, + inputData, + }) + } else if (operation === 'delete' && item !== undefined) { + const itemAccessControl = list.access.item[operation] + accepted = await itemAccessControl({ + operation, + session: context.session, + listKey: list.listKey, + context, + item, + }) + } + } catch (error: any) { + throw extensionError('Access control', [ + { error, tag: `${list.listKey}.access.item.${operation}` }, + ]) + } + + // short circuit the safe path + if (accepted === true) return + + if (typeof accepted !== 'boolean') { + throw accessReturnError([ + { + tag: `${list.listKey}.access.item.${operation}`, + returned: typeof accepted, + }, + ]) + } + + throw accessDeniedError(cannotForItem(operation, list)) +} + +export async function enforceFieldLevelAccessControl( + context: KeystoneContext, + operation: 'create' | 'update', + list: InitialisedList, + inputData: Record, + item: BaseItem | undefined +) { + if (context.__internal.sudo) return + + const nonBooleans: { tag: string; returned: string }[] = [] + const fieldsDenied: string[] = [] + const accessErrors: { error: Error; tag: string }[] = [] + + await Promise.allSettled( + Object.keys(inputData).map(async fieldKey => { + let accepted: unknown // should be boolean, but dont trust + try { + // apply fields.[fieldKey].access.* controls + if (operation === 'create') { + const fieldAccessControl = list.fields[fieldKey].access[operation] + accepted = await fieldAccessControl({ + operation, + session: context.session, + listKey: list.listKey, + fieldKey, + context, + inputData: inputData as any, // FIXME + }) + } else if (operation === 'update' && item !== undefined) { + const fieldAccessControl = list.fields[fieldKey].access[operation] + accepted = await fieldAccessControl({ + operation, + session: context.session, + listKey: list.listKey, + fieldKey, + context, + item, + inputData, + }) + } + } catch (error: any) { + accessErrors.push({ error, tag: `${list.listKey}.${fieldKey}.access.${operation}` }) + return + } + + // short circuit the safe path + if (accepted === true) return + fieldsDenied.push(fieldKey) + + // wrong type? + if (typeof accepted !== 'boolean') { + nonBooleans.push({ + tag: `${list.listKey}.${fieldKey}.access.${operation}`, + returned: typeof accepted, + }) + } + }) + ) + + if (nonBooleans.length) { + throw accessReturnError(nonBooleans) + } + + if (accessErrors.length) { + throw extensionError('Access control', accessErrors) + } + + if (fieldsDenied.length) { + throw accessDeniedError(cannotForItemFields(operation, list, fieldsDenied)) + } +} + +export type ResolvedFieldAccessControl = { + read: FieldAccessControlFunction> + create: FieldAccessControlFunction> + update: FieldAccessControlFunction> + // delete: not supported +} + +export function parseFieldAccessControl( + access: FieldAccessControl | undefined +): ResolvedFieldAccessControl { + if (typeof access === 'function') { + return { + read: access, + create: access, + update: access, + } + } + + return { + read: access?.read ?? allowAll, + create: access?.create ?? allowAll, + update: access?.update ?? allowAll, + } +} + +export type ResolvedActionAccessControl = ActionAccessControlFunction + +export type ResolvedListAccessControl = { + operation: { + query: ListOperationAccessControl<'query', BaseListTypeInfo> + create: ListOperationAccessControl<'create', BaseListTypeInfo> + update: ListOperationAccessControl<'update', BaseListTypeInfo> + delete: ListOperationAccessControl<'delete', BaseListTypeInfo> + } + filter: { + query: ListFilterAccessControl<'query', BaseListTypeInfo> + // create: not supported + update: ListFilterAccessControl<'update', BaseListTypeInfo> + delete: ListFilterAccessControl<'delete', BaseListTypeInfo> + } + item: { + // query: not supported + create: CreateListItemAccessControl + update: UpdateListItemAccessControl + delete: DeleteListItemAccessControl + } +} + +export function parseListAccessControl( + access: ListAccessControl +): ResolvedListAccessControl { + if (typeof access === 'function') { + return { + operation: { + query: access, + create: access, + update: access, + delete: access, + }, + filter: { + query: allowAll, + update: allowAll, + delete: allowAll, + }, + item: { + create: allowAll, + update: allowAll, + delete: allowAll, + }, + } + } + + let { operation, filter, item } = access + if (typeof operation === 'function') { + operation = { + query: operation, + create: operation, + update: operation, + delete: operation, + } + } + + return { + operation: { + query: operation.query, + create: operation.create, + update: operation.update, + delete: operation.delete, + }, + filter: { + query: filter?.query ?? allowAll, + // create: not supported + update: filter?.update ?? allowAll, + delete: filter?.delete ?? allowAll, + }, + item: { + // query: not supported + create: item?.create ?? allowAll, + update: item?.update ?? allowAll, + delete: item?.delete ?? allowAll, + }, + } +} + +export async function checkUniqueItemExists( + uniqueInput: UniqueInputFilter, foreignList: InitialisedList, context: KeystoneContext, operation: string @@ -31,7 +443,3 @@ export async function checkUniqueItemExists ( throw accessDeniedError(cannotForItem(operation, foreignList)) } - -function cannotForItem (operation: string, list: InitialisedList) { - return `You cannot ${operation} that ${list.listKey} - it may not exist` -}