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..5458e4068de 100644 --- a/packages/core/src/lib/core/access-control.ts +++ b/packages/core/src/lib/core/access-control.ts @@ -428,7 +428,18 @@ 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)) } 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}` }]) }