From 0acaf8a7f735ed1166364622903967a181a1c93d Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Mon, 27 Jan 2025 14:41:35 -0500 Subject: [PATCH] fix: field paths within hooks (#10638) Field paths within hooks are not correct. For example, an unnamed tab containing a group field and nested text field should have the path: - `myGroupField.myTextField` However, within hooks that path is formatted as: - `_index-1.myGroupField.myTextField` The leading index shown above should not exist, as this field is considered top-level since it is located within an unnamed tab. This discrepancy is only evident through the APIs themselves, such as when creating a request with invalid data and reading the validation errors in the response. Form state contains proper field paths, which is ultimately why this issue was never caught. This is because within the admin panel we merge the API response with the current form state, obscuring the underlying issue. This becomes especially obvious in #10580, where we no longer initialize validation errors within form state until the form has been submitted, and instead rely solely on the API response for the initial error state. Here's comprehensive example of how field paths _should_ be formatted: ``` { // ... fields: [ { // path: 'topLevelNamedField' // schemaPath: 'topLevelNamedField' // indexPath: '' name: 'topLevelNamedField', type: 'text', }, { // path: 'array' // schemaPath: 'array' // indexPath: '' name: 'array', type: 'array', fields: [ { // path: 'array.[n].fieldWithinArray' // schemaPath: 'array.fieldWithinArray' // indexPath: '' name: 'fieldWithinArray', type: 'text', }, { // path: 'array.[n].nestedArray' // schemaPath: 'array.nestedArray' // indexPath: '' name: 'nestedArray', type: 'array', fields: [ { // path: 'array.[n].nestedArray.[n].fieldWithinNestedArray' // schemaPath: 'array.nestedArray.fieldWithinNestedArray' // indexPath: '' name: 'fieldWithinNestedArray', type: 'text', }, ], }, { // path: 'array.[n]._index-2' // schemaPath: 'array._index-2' // indexPath: '2' type: 'row', fields: [ { // path: 'array.[n].fieldWithinRowWithinArray' // schemaPath: 'array._index-2.fieldWithinRowWithinArray' // indexPath: '' name: 'fieldWithinRowWithinArray', type: 'text', }, ], }, ], }, { // path: '_index-2' // schemaPath: '_index-2' // indexPath: '2' type: 'row', fields: [ { // path: 'fieldWithinRow' // schemaPath: '_index-2.fieldWithinRow' // indexPath: '' name: 'fieldWithinRow', type: 'text', }, ], }, { // path: '_index-3' // schemaPath: '_index-3' // indexPath: '3' type: 'tabs', tabs: [ { // path: '_index-3-0' // schemaPath: '_index-3-0' // indexPath: '3-0' label: 'Unnamed Tab', fields: [ { // path: 'fieldWithinUnnamedTab' // schemaPath: '_index-3-0.fieldWithinUnnamedTab' // indexPath: '' name: 'fieldWithinUnnamedTab', type: 'text', }, { // path: '_index-3-0-1' // schemaPath: '_index-3-0-1' // indexPath: '3-0-1' type: 'tabs', tabs: [ { // path: '_index-3-0-1-0' // schemaPath: '_index-3-0-1-0' // indexPath: '3-0-1-0' label: 'Nested Unnamed Tab', fields: [ { // path: 'fieldWithinNestedUnnamedTab' // schemaPath: '_index-3-0-1-0.fieldWithinNestedUnnamedTab' // indexPath: '' name: 'fieldWithinNestedUnnamedTab', type: 'text', }, ], }, ], }, ], }, { // path: 'namedTab' // schemaPath: '_index-3.namedTab' // indexPath: '' label: 'Named Tab', name: 'namedTab', fields: [ { // path: 'namedTab.fieldWithinNamedTab' // schemaPath: '_index-3.namedTab.fieldWithinNamedTab' // indexPath: '' name: 'fieldWithinNamedTab', type: 'text', }, ], }, ], }, ] } ``` --- packages/payload/src/admin/RichText.ts | 2 +- .../src/collections/operations/find.ts | 2 +- packages/payload/src/fields/config/types.ts | 1 + packages/payload/src/fields/getFieldPaths.ts | 46 ++- .../src/fields/hooks/afterChange/index.ts | 5 +- .../src/fields/hooks/afterChange/promise.ts | 90 +++-- .../hooks/afterChange/traverseFields.ts | 15 +- .../src/fields/hooks/afterRead/index.ts | 5 +- .../src/fields/hooks/afterRead/promise.ts | 108 +++--- .../fields/hooks/afterRead/traverseFields.ts | 15 +- .../src/fields/hooks/beforeChange/index.ts | 6 +- .../src/fields/hooks/beforeChange/promise.ts | 106 +++--- .../hooks/beforeChange/traverseFields.ts | 15 +- .../src/fields/hooks/beforeDuplicate/index.ts | 6 +- .../fields/hooks/beforeDuplicate/promise.ts | 164 ++++++--- .../hooks/beforeDuplicate/traverseFields.ts | 16 +- .../src/fields/hooks/beforeValidate/index.ts | 5 +- .../fields/hooks/beforeValidate/promise.ts | 79 ++-- .../hooks/beforeValidate/traverseFields.ts | 17 +- .../src/fields/utilities/getFieldPaths.ts | 0 packages/payload/src/index.ts | 2 +- ...tFormattedLabel.ts => getLabelFromPath.ts} | 2 +- packages/richtext-lexical/src/index.ts | 28 +- .../recursivelyPopulateFieldsForGraphQL.ts | 5 +- .../addFieldStatePromise.ts | 9 +- .../traverseFields.ts | 7 +- .../buildFieldSchemaMap/traverseFields.ts | 6 +- test/_community/payload-types.ts | 21 +- test/database/config.ts | 28 +- test/database/int.spec.ts | 20 +- test/database/payload-types.ts | 31 ++ test/database/shared.ts | 1 + test/fields/collections/Tabs/index.ts | 2 + test/fields/int.spec.ts | 11 +- .../hooks/collections/BeforeValidate/index.ts | 2 +- test/hooks/collections/Data/index.ts | 2 - test/hooks/collections/FieldPaths/index.ts | 214 +++++++++++ test/hooks/config.ts | 3 + test/hooks/e2e.spec.ts | 2 +- test/hooks/int.spec.ts | 97 ++++- test/hooks/payload-types.ts | 343 ++++++++++++++++++ test/hooks/{collectionSlugs.ts => shared.ts} | 1 + tsconfig.base.json | 2 +- 43 files changed, 1224 insertions(+), 318 deletions(-) create mode 100644 packages/payload/src/fields/utilities/getFieldPaths.ts rename packages/payload/src/utilities/{getFormattedLabel.ts => getLabelFromPath.ts} (88%) create mode 100644 test/hooks/collections/FieldPaths/index.ts rename test/hooks/{collectionSlugs.ts => shared.ts} (54%) diff --git a/packages/payload/src/admin/RichText.ts b/packages/payload/src/admin/RichText.ts index 9bed6b8725..7c25daa6b9 100644 --- a/packages/payload/src/admin/RichText.ts +++ b/packages/payload/src/admin/RichText.ts @@ -116,7 +116,7 @@ export type BaseRichTextHookArgs< field: FieldAffectingData /** The global which the field belongs to. If the field belongs to a collection, this will be null. */ global: null | SanitizedGlobalConfig - + indexPath: number[] /** The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. */ originalDoc?: TData /** diff --git a/packages/payload/src/collections/operations/find.ts b/packages/payload/src/collections/operations/find.ts index 71387ca9c1..4e1c05969d 100644 --- a/packages/payload/src/collections/operations/find.ts +++ b/packages/payload/src/collections/operations/find.ts @@ -239,7 +239,7 @@ export const findOperation = async < doc._isLocked = !!lockedDoc doc._userEditing = lockedDoc ? lockedDoc?.user?.value : null } - } catch (error) { + } catch (_err) { for (const doc of result.docs) { doc._isLocked = false doc._userEditing = null diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index ea5d973b89..039c1ce2af 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -166,6 +166,7 @@ export type FieldHookArgs({ fields: collection?.fields || global?.fields, global, operation, - path: [], + parentIndexPath: '', + parentPath: '', + parentSchemaPath: '', previousDoc, previousSiblingDoc: previousDoc, req, - schemaPath: [], siblingData: data, siblingDoc: incomingDoc, }) diff --git a/packages/payload/src/fields/hooks/afterChange/promise.ts b/packages/payload/src/fields/hooks/afterChange/promise.ts index ac32a7c163..7c1dbe6e0f 100644 --- a/packages/payload/src/fields/hooks/afterChange/promise.ts +++ b/packages/payload/src/fields/hooks/afterChange/promise.ts @@ -7,7 +7,7 @@ import type { Field, TabAsField } from '../../config/types.js' import { MissingEditorProp } from '../../../errors/index.js' import { fieldAffectsData, tabHasName } from '../../config/types.js' -import { getFieldPaths } from '../../getFieldPaths.js' +import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js' import { traverseFields } from './traverseFields.js' type Args = { @@ -19,14 +19,9 @@ type Args = { fieldIndex: number global: null | SanitizedGlobalConfig operation: 'create' | 'update' - /** - * The parent's path - */ - parentPath: (number | string)[] - /** - * The parent's schemaPath (path without indexes). - */ - parentSchemaPath: string[] + parentIndexPath: string + parentPath: string + parentSchemaPath: string previousDoc: JsonObject previousSiblingDoc: JsonObject req: PayloadRequest @@ -46,6 +41,7 @@ export const promise = async ({ fieldIndex, global, operation, + parentIndexPath, parentPath, parentSchemaPath, previousDoc, @@ -54,15 +50,17 @@ export const promise = async ({ siblingData, siblingDoc, }: Args): Promise => { - const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({ + const { indexPath, path, schemaPath } = getFieldPaths({ field, index: fieldIndex, - parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data - parentPath: parentPath.join('.'), - parentSchemaPath: parentSchemaPath.join('.'), + parentIndexPath, + parentPath, + parentSchemaPath, }) - const fieldPath = _fieldPath ? _fieldPath.split('.') : [] - const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : [] + + const pathSegments = path ? path.split('.') : [] + const schemaPathSegments = schemaPath ? schemaPath.split('.') : [] + const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : [] if (fieldAffectsData(field)) { // Execute hooks @@ -76,14 +74,15 @@ export const promise = async ({ data, field, global, + indexPath: indexPathSegments, operation, originalDoc: doc, - path: fieldPath, + path: pathSegments, previousDoc, previousSiblingDoc, previousValue: previousDoc[field.name], req, - schemaPath: fieldSchemaPath, + schemaPath: schemaPathSegments, siblingData, value: siblingDoc[field.name], }) @@ -102,7 +101,7 @@ export const promise = async ({ if (Array.isArray(rows)) { const promises = [] - rows.forEach((row, i) => { + rows.forEach((row, rowIndex) => { promises.push( traverseFields({ collection, @@ -112,18 +111,20 @@ export const promise = async ({ fields: field.fields, global, operation, - path: [...fieldPath, i], + parentIndexPath: '', + parentPath: path + '.' + rowIndex, + parentSchemaPath: schemaPath, previousDoc, - previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as JsonObject), + previousSiblingDoc: previousDoc?.[field.name]?.[rowIndex] || ({} as JsonObject), req, - schemaPath: fieldSchemaPath, - siblingData: siblingData?.[field.name]?.[i] || {}, + siblingData: siblingData?.[field.name]?.[rowIndex] || {}, siblingDoc: row ? { ...row } : {}, }), ) }) await Promise.all(promises) } + break } @@ -132,7 +133,8 @@ export const promise = async ({ if (Array.isArray(rows)) { const promises = [] - rows.forEach((row, i) => { + + rows.forEach((row, rowIndex) => { const block = field.blocks.find( (blockType) => blockType.slug === (row as JsonObject).blockType, ) @@ -147,17 +149,19 @@ export const promise = async ({ fields: block.fields, global, operation, - path: [...fieldPath, i], + parentIndexPath: '', + parentPath: path + '.' + rowIndex, + parentSchemaPath: schemaPath + '.' + block.slug, previousDoc, - previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as JsonObject), + previousSiblingDoc: previousDoc?.[field.name]?.[rowIndex] || ({} as JsonObject), req, - schemaPath: fieldSchemaPath, - siblingData: siblingData?.[field.name]?.[i] || {}, + siblingData: siblingData?.[field.name]?.[rowIndex] || {}, siblingDoc: row ? { ...row } : {}, }), ) } }) + await Promise.all(promises) } @@ -174,17 +178,19 @@ export const promise = async ({ fields: field.fields, global, operation, - path: fieldPath, + parentIndexPath: indexPath, + parentPath, + parentSchemaPath: schemaPath, previousDoc, previousSiblingDoc: { ...previousSiblingDoc }, req, - schemaPath: fieldSchemaPath, siblingData: siblingData || {}, siblingDoc: { ...siblingDoc }, }) break } + case 'group': { await traverseFields({ collection, @@ -194,11 +200,12 @@ export const promise = async ({ fields: field.fields, global, operation, - path: fieldPath, + parentIndexPath: '', + parentPath: path, + parentSchemaPath: schemaPath, previousDoc, previousSiblingDoc: previousDoc[field.name] as JsonObject, req, - schemaPath: fieldSchemaPath, siblingData: (siblingData?.[field.name] as JsonObject) || {}, siblingDoc: siblingDoc[field.name] as JsonObject, }) @@ -210,6 +217,7 @@ export const promise = async ({ if (!field?.editor) { throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor } + if (typeof field?.editor === 'function') { throw new Error('Attempted to access unsanitized rich text editor.') } @@ -226,14 +234,15 @@ export const promise = async ({ data, field, global, + indexPath: indexPathSegments, operation, originalDoc: doc, - path: fieldPath, + path: pathSegments, previousDoc, previousSiblingDoc, previousValue: previousDoc[field.name], req, - schemaPath: fieldSchemaPath, + schemaPath: schemaPathSegments, siblingData, value: siblingDoc[field.name], }) @@ -251,7 +260,9 @@ export const promise = async ({ let tabSiblingDoc = siblingDoc let tabPreviousSiblingDoc = siblingDoc - if (tabHasName(field)) { + const isNamedTab = tabHasName(field) + + if (isNamedTab) { tabSiblingData = (siblingData[field.name] as JsonObject) ?? {} tabSiblingDoc = (siblingDoc[field.name] as JsonObject) ?? {} tabPreviousSiblingDoc = (previousDoc[field.name] as JsonObject) ?? {} @@ -265,11 +276,12 @@ export const promise = async ({ fields: field.fields, global, operation, - path: fieldPath, + parentIndexPath: isNamedTab ? '' : indexPath, + parentPath: isNamedTab ? path : parentPath, + parentSchemaPath: schemaPath, previousDoc, previousSiblingDoc: tabPreviousSiblingDoc, req, - schemaPath: fieldSchemaPath, siblingData: tabSiblingData, siblingDoc: tabSiblingDoc, }) @@ -286,14 +298,16 @@ export const promise = async ({ fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), global, operation, - path: fieldPath, + parentIndexPath: indexPath, + parentPath: path, + parentSchemaPath: schemaPath, previousDoc, previousSiblingDoc: { ...previousSiblingDoc }, req, - schemaPath: fieldSchemaPath, siblingData: siblingData || {}, siblingDoc: { ...siblingDoc }, }) + break } diff --git a/packages/payload/src/fields/hooks/afterChange/traverseFields.ts b/packages/payload/src/fields/hooks/afterChange/traverseFields.ts index 223ba0ed33..273729e656 100644 --- a/packages/payload/src/fields/hooks/afterChange/traverseFields.ts +++ b/packages/payload/src/fields/hooks/afterChange/traverseFields.ts @@ -14,11 +14,12 @@ type Args = { fields: (Field | TabAsField)[] global: null | SanitizedGlobalConfig operation: 'create' | 'update' - path: (number | string)[] + parentIndexPath: string + parentPath: string + parentSchemaPath: string previousDoc: JsonObject previousSiblingDoc: JsonObject req: PayloadRequest - schemaPath: string[] siblingData: JsonObject siblingDoc: JsonObject } @@ -31,11 +32,12 @@ export const traverseFields = async ({ fields, global, operation, - path, + parentIndexPath, + parentPath, + parentSchemaPath, previousDoc, previousSiblingDoc, req, - schemaPath, siblingData, siblingDoc, }: Args): Promise => { @@ -52,8 +54,9 @@ export const traverseFields = async ({ fieldIndex, global, operation, - parentPath: path, - parentSchemaPath: schemaPath, + parentIndexPath, + parentPath, + parentSchemaPath, previousDoc, previousSiblingDoc, req, diff --git a/packages/payload/src/fields/hooks/afterRead/index.ts b/packages/payload/src/fields/hooks/afterRead/index.ts index d345e1c83a..d9a659df69 100644 --- a/packages/payload/src/fields/hooks/afterRead/index.ts +++ b/packages/payload/src/fields/hooks/afterRead/index.ts @@ -83,11 +83,12 @@ export async function afterRead(args: Args): Promise global, locale, overrideAccess, - path: [], + parentIndexPath: '', + parentPath: '', + parentSchemaPath: '', populate, populationPromises, req, - schemaPath: [], select, selectMode: select ? getSelectMode(select) : undefined, showHiddenFields, diff --git a/packages/payload/src/fields/hooks/afterRead/promise.ts b/packages/payload/src/fields/hooks/afterRead/promise.ts index 509160a4df..5a36fdcf91 100644 --- a/packages/payload/src/fields/hooks/afterRead/promise.ts +++ b/packages/payload/src/fields/hooks/afterRead/promise.ts @@ -14,7 +14,7 @@ import type { Field, TabAsField } from '../../config/types.js' import { MissingEditorProp } from '../../../errors/index.js' import { fieldAffectsData, tabHasName } from '../../config/types.js' import { getDefaultValue } from '../../getDefaultValue.js' -import { getFieldPaths } from '../../getFieldPaths.js' +import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js' import { relationshipPopulationPromise } from './relationshipPopulationPromise.js' import { traverseFields } from './traverseFields.js' @@ -37,14 +37,9 @@ type Args = { global: null | SanitizedGlobalConfig locale: null | string overrideAccess: boolean - /** - * The parent's path. - */ - parentPath: (number | string)[] - /** - * The parent's schemaPath (path without indexes). - */ - parentSchemaPath: string[] + parentIndexPath: string + parentPath: string + parentSchemaPath: string populate?: PopulateType populationPromises: Promise[] req: PayloadRequest @@ -80,6 +75,7 @@ export const promise = async ({ global, locale, overrideAccess, + parentIndexPath, parentPath, parentSchemaPath, populate, @@ -92,15 +88,17 @@ export const promise = async ({ triggerAccessControl = true, triggerHooks = true, }: Args): Promise => { - const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({ + const { indexPath, path, schemaPath } = getFieldPaths({ field, index: fieldIndex, - parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data - parentPath: parentPath.join('.'), - parentSchemaPath: parentSchemaPath.join('.'), + parentIndexPath, + parentPath, + parentSchemaPath, }) - const fieldPath = _fieldPath ? _fieldPath.split('.') : [] - const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : [] + + const pathSegments = path ? path.split('.') : [] + const schemaPathSegments = schemaPath ? schemaPath.split('.') : [] + const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : [] if ( fieldAffectsData(field) && @@ -111,6 +109,7 @@ export const promise = async ({ delete siblingDoc[field.name] } + // Strip unselected fields if (fieldAffectsData(field) && select && selectMode) { if (selectMode === 'include') { if (!select[field.name]) { @@ -246,12 +245,13 @@ export const promise = async ({ field, findMany, global, + indexPath: indexPathSegments, operation: 'read', originalDoc: doc, overrideAccess, - path: fieldPath, + path: pathSegments, req, - schemaPath: fieldSchemaPath, + schemaPath: schemaPathSegments, showHiddenFields, siblingData: siblingDoc, value, @@ -275,12 +275,13 @@ export const promise = async ({ field, findMany, global, + indexPath: indexPathSegments, operation: 'read', originalDoc: doc, overrideAccess, - path: fieldPath, + path: pathSegments, req, - schemaPath: fieldSchemaPath, + schemaPath: schemaPathSegments, showHiddenFields, siblingData: siblingDoc, value: siblingDoc[field.name], @@ -361,7 +362,7 @@ export const promise = async ({ } if (Array.isArray(rows)) { - rows.forEach((row, i) => { + rows.forEach((row, rowIndex) => { traverseFields({ collection, context, @@ -377,11 +378,12 @@ export const promise = async ({ global, locale, overrideAccess, - path: [...fieldPath, i], + parentIndexPath: '', + parentPath: path + '.' + rowIndex, + parentSchemaPath: schemaPath, populate, populationPromises, req, - schemaPath: fieldSchemaPath, select: typeof arraySelect === 'object' ? arraySelect : undefined, selectMode, showHiddenFields, @@ -393,7 +395,7 @@ export const promise = async ({ } else if (!shouldHoistLocalizedValue && typeof rows === 'object' && rows !== null) { Object.values(rows).forEach((localeRows) => { if (Array.isArray(localeRows)) { - localeRows.forEach((row, i) => { + localeRows.forEach((row, rowIndex) => { traverseFields({ collection, context, @@ -409,11 +411,12 @@ export const promise = async ({ global, locale, overrideAccess, - path: [...fieldPath, i], + parentIndexPath: '', + parentPath: path + '.' + rowIndex, + parentSchemaPath: schemaPath, populate, populationPromises, req, - schemaPath: fieldSchemaPath, showHiddenFields, siblingDoc: (row as JsonObject) || {}, triggerAccessControl, @@ -434,7 +437,7 @@ export const promise = async ({ let blocksSelect = select?.[field.name] if (Array.isArray(rows)) { - rows.forEach((row, i) => { + rows.forEach((row, rowIndex) => { const block = field.blocks.find( (blockType) => blockType.slug === (row as JsonObject).blockType, ) @@ -487,11 +490,12 @@ export const promise = async ({ global, locale, overrideAccess, - path: [...fieldPath, i], + parentIndexPath: '', + parentPath: path + '.' + rowIndex, + parentSchemaPath: schemaPath + '.' + block.slug, populate, populationPromises, req, - schemaPath: fieldSchemaPath, select: typeof blockSelect === 'object' ? blockSelect : undefined, selectMode: blockSelectMode, showHiddenFields, @@ -504,7 +508,7 @@ export const promise = async ({ } else if (!shouldHoistLocalizedValue && typeof rows === 'object' && rows !== null) { Object.values(rows).forEach((localeRows) => { if (Array.isArray(localeRows)) { - localeRows.forEach((row, i) => { + localeRows.forEach((row, rowIndex) => { const block = field.blocks.find( (blockType) => blockType.slug === (row as JsonObject).blockType, ) @@ -525,11 +529,12 @@ export const promise = async ({ global, locale, overrideAccess, - path: [...fieldPath, i], + parentIndexPath: '', + parentPath: path + '.' + rowIndex, + parentSchemaPath: schemaPath + '.' + block.slug, populate, populationPromises, req, - schemaPath: fieldSchemaPath, showHiddenFields, siblingDoc: (row as JsonObject) || {}, triggerAccessControl, @@ -563,11 +568,12 @@ export const promise = async ({ global, locale, overrideAccess, - path: fieldPath, + parentIndexPath: indexPath, + parentPath, + parentSchemaPath: schemaPath, populate, populationPromises, req, - schemaPath: fieldSchemaPath, select, selectMode, showHiddenFields, @@ -578,8 +584,10 @@ export const promise = async ({ break } + case 'group': { let groupDoc = siblingDoc[field.name] as JsonObject + if (typeof siblingDoc[field.name] !== 'object') { groupDoc = {} } @@ -601,11 +609,12 @@ export const promise = async ({ global, locale, overrideAccess, - path: fieldPath, + parentIndexPath: '', + parentPath: path, + parentSchemaPath: schemaPath, populate, populationPromises, req, - schemaPath: fieldSchemaPath, select: typeof groupSelect === 'object' ? groupSelect : undefined, selectMode, showHiddenFields, @@ -621,6 +630,7 @@ export const promise = async ({ if (!field?.editor) { throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor } + if (typeof field?.editor === 'function') { throw new Error('Attempted to access unsanitized rich text editor.') } @@ -652,15 +662,16 @@ export const promise = async ({ findMany, flattenLocales, global, + indexPath: indexPathSegments, locale, operation: 'read', originalDoc: doc, overrideAccess, - path: fieldPath, + path: pathSegments, populate, populationPromises, req, - schemaPath: fieldSchemaPath, + schemaPath: schemaPathSegments, showHiddenFields, siblingData: siblingDoc, triggerAccessControl, @@ -689,15 +700,16 @@ export const promise = async ({ findMany, flattenLocales, global, + indexPath: indexPathSegments, locale, operation: 'read', originalDoc: doc, overrideAccess, - path: fieldPath, + path: pathSegments, populate, populationPromises, req, - schemaPath: fieldSchemaPath, + schemaPath: schemaPathSegments, showHiddenFields, siblingData: siblingDoc, triggerAccessControl, @@ -717,8 +729,12 @@ export const promise = async ({ case 'tab': { let tabDoc = siblingDoc let tabSelect: SelectType | undefined - if (tabHasName(field)) { + + const isNamedTab = tabHasName(field) + + if (isNamedTab) { tabDoc = siblingDoc[field.name] as JsonObject + if (typeof siblingDoc[field.name] !== 'object') { tabDoc = {} } @@ -745,11 +761,12 @@ export const promise = async ({ global, locale, overrideAccess, - path: fieldPath, + parentIndexPath: isNamedTab ? '' : indexPath, + parentPath: isNamedTab ? path : parentPath, + parentSchemaPath: schemaPath, populate, populationPromises, req, - schemaPath: fieldSchemaPath, select: tabSelect, selectMode, showHiddenFields, @@ -777,11 +794,12 @@ export const promise = async ({ global, locale, overrideAccess, - path: fieldPath, + parentIndexPath: indexPath, + parentPath: path, + parentSchemaPath: schemaPath, populate, populationPromises, req, - schemaPath: fieldSchemaPath, select, selectMode, showHiddenFields, @@ -789,9 +807,9 @@ export const promise = async ({ triggerAccessControl, triggerHooks, }) + break } - default: { break } diff --git a/packages/payload/src/fields/hooks/afterRead/traverseFields.ts b/packages/payload/src/fields/hooks/afterRead/traverseFields.ts index 81894caf8b..7f5028a19c 100644 --- a/packages/payload/src/fields/hooks/afterRead/traverseFields.ts +++ b/packages/payload/src/fields/hooks/afterRead/traverseFields.ts @@ -30,11 +30,12 @@ type Args = { global: null | SanitizedGlobalConfig locale: null | string overrideAccess: boolean - path: (number | string)[] + parentIndexPath: string + parentPath: string + parentSchemaPath: string populate?: PopulateType populationPromises: Promise[] req: PayloadRequest - schemaPath: string[] select?: SelectType selectMode?: SelectMode showHiddenFields: boolean @@ -58,11 +59,12 @@ export const traverseFields = ({ global, locale, overrideAccess, - path, + parentIndexPath, + parentPath, + parentSchemaPath, populate, populationPromises, req, - schemaPath, select, selectMode, showHiddenFields, @@ -88,8 +90,9 @@ export const traverseFields = ({ global, locale, overrideAccess, - parentPath: path, - parentSchemaPath: schemaPath, + parentIndexPath, + parentPath, + parentSchemaPath, populate, populationPromises, req, diff --git a/packages/payload/src/fields/hooks/beforeChange/index.ts b/packages/payload/src/fields/hooks/beforeChange/index.ts index 7c33ec5152..9359b5c37b 100644 --- a/packages/payload/src/fields/hooks/beforeChange/index.ts +++ b/packages/payload/src/fields/hooks/beforeChange/index.ts @@ -28,6 +28,7 @@ export type Args = { * - Transform data for storage * - Unflatten locales. The input `data` is the normal document for one locale. The output result will become the document with locales. */ + export const beforeChange = async ({ id, collection, @@ -56,9 +57,10 @@ export const beforeChange = async ({ global, mergeLocaleActions, operation, - path: [], + parentIndexPath: '', + parentPath: '', + parentSchemaPath: '', req, - schemaPath: [], siblingData: data, siblingDoc: doc, siblingDocWithLocales: docWithLocales, diff --git a/packages/payload/src/fields/hooks/beforeChange/promise.ts b/packages/payload/src/fields/hooks/beforeChange/promise.ts index 2f0641d64e..1e827684fd 100644 --- a/packages/payload/src/fields/hooks/beforeChange/promise.ts +++ b/packages/payload/src/fields/hooks/beforeChange/promise.ts @@ -4,14 +4,14 @@ import type { ValidationFieldError } from '../../../errors/index.js' import type { SanitizedGlobalConfig } from '../../../globals/config/types.js' import type { RequestContext } from '../../../index.js' import type { JsonObject, Operation, PayloadRequest } from '../../../types/index.js' -import type { BaseValidateOptions, Field, TabAsField } from '../../config/types.js' +import type { Field, TabAsField } from '../../config/types.js' import { MissingEditorProp } from '../../../errors/index.js' import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js' -import { getFormattedLabel } from '../../../utilities/getFormattedLabel.js' +import { getLabelFromPath } from '../../../utilities/getLabelFromPath.js' import { getTranslatedLabel } from '../../../utilities/getTranslatedLabel.js' import { fieldAffectsData, tabHasName } from '../../config/types.js' -import { getFieldPaths } from '../../getFieldPaths.js' +import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js' import { getExistingRowDoc } from './getExistingRowDoc.js' import { traverseFields } from './traverseFields.js' @@ -23,23 +23,14 @@ type Args = { docWithLocales: JsonObject errors: ValidationFieldError[] field: Field | TabAsField - /** - * The index of the field as it appears in the parent's fields array. This is used to construct the field path / schemaPath - * for unnamed fields like rows and collapsibles. - */ fieldIndex: number global: null | SanitizedGlobalConfig id?: number | string mergeLocaleActions: (() => Promise)[] operation: Operation - /** - * The parent's path. - */ - parentPath: (number | string)[] - /** - * The parent's schemaPath (path without indexes). - */ - parentSchemaPath: string[] + parentIndexPath: string + parentPath: string + parentSchemaPath: string req: PayloadRequest siblingData: JsonObject siblingDoc: JsonObject @@ -68,6 +59,7 @@ export const promise = async ({ global, mergeLocaleActions, operation, + parentIndexPath, parentPath, parentSchemaPath, req, @@ -76,6 +68,14 @@ export const promise = async ({ siblingDocWithLocales, skipValidation, }: Args): Promise => { + const { indexPath, path, schemaPath } = getFieldPaths({ + field, + index: fieldIndex, + parentIndexPath, + parentPath, + parentSchemaPath, + }) + const passesCondition = field.admin?.condition ? Boolean(field.admin.condition(data, siblingData, { user: req.user })) : true @@ -84,15 +84,9 @@ export const promise = async ({ const defaultLocale = localization ? localization?.defaultLocale : 'en' const operationLocale = req.locale || defaultLocale - const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({ - field, - index: fieldIndex, - parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data - parentPath: parentPath.join('.'), - parentSchemaPath: parentSchemaPath.join('.'), - }) - const fieldPath = _fieldPath ? _fieldPath.split('.') : [] - const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : [] + const pathSegments = path ? path.split('.') : [] + const schemaPathSegments = schemaPath ? schemaPath.split('.') : [] + const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : [] if (fieldAffectsData(field)) { // skip validation if the field is localized and the incoming data is null @@ -113,13 +107,14 @@ export const promise = async ({ data, field, global, + indexPath: indexPathSegments, operation, originalDoc: doc, - path: fieldPath, + path: pathSegments, previousSiblingDoc: siblingDoc, previousValue: siblingDoc[field.name], req, - schemaPath: parentSchemaPath, + schemaPath: schemaPathSegments, siblingData, siblingDocWithLocales, value: siblingData[field.name], @@ -163,16 +158,17 @@ export const promise = async ({ if (typeof validationResult === 'string') { const label = getTranslatedLabel(field?.label || field?.name, req.i18n) + const parentPathSegments = parentPath ? parentPath.split('.') : [] const fieldLabel = - Array.isArray(parentPath) && parentPath.length > 0 - ? getFormattedLabel([...parentPath, label]) + Array.isArray(parentPathSegments) && parentPathSegments.length > 0 + ? getLabelFromPath(parentPathSegments.concat(label)) : label errors.push({ label: fieldLabel, message: validationResult, - path: fieldPath.join('.'), + path, }) } } @@ -216,7 +212,8 @@ export const promise = async ({ if (Array.isArray(rows)) { const promises = [] - rows.forEach((row, i) => { + + rows.forEach((row, rowIndex) => { promises.push( traverseFields({ id, @@ -230,9 +227,10 @@ export const promise = async ({ global, mergeLocaleActions, operation, - path: [...fieldPath, i], + parentIndexPath: '', + parentPath: path + '.' + rowIndex, + parentSchemaPath: schemaPath, req, - schemaPath: fieldSchemaPath, siblingData: row as JsonObject, siblingDoc: getExistingRowDoc(row as JsonObject, siblingDoc[field.name]), siblingDocWithLocales: getExistingRowDoc( @@ -254,8 +252,10 @@ export const promise = async ({ const rows = siblingData[field.name] if (Array.isArray(rows)) { const promises = [] - rows.forEach((row, i) => { + + rows.forEach((row, rowIndex) => { const rowSiblingDoc = getExistingRowDoc(row as JsonObject, siblingDoc[field.name]) + const rowSiblingDocWithLocales = getExistingRowDoc( row as JsonObject, siblingDocWithLocales ? siblingDocWithLocales[field.name] : {}, @@ -278,9 +278,10 @@ export const promise = async ({ global, mergeLocaleActions, operation, - path: [...fieldPath, i], + parentIndexPath: '', + parentPath: path + '.' + rowIndex, + parentSchemaPath: schemaPath + '.' + block.slug, req, - schemaPath: fieldSchemaPath, siblingData: row as JsonObject, siblingDoc: rowSiblingDoc, siblingDocWithLocales: rowSiblingDocWithLocales, @@ -310,9 +311,10 @@ export const promise = async ({ global, mergeLocaleActions, operation, - path: fieldPath, + parentIndexPath: indexPath, + parentPath, + parentSchemaPath: schemaPath, req, - schemaPath: fieldSchemaPath, siblingData, siblingDoc, siblingDocWithLocales, @@ -326,9 +328,11 @@ export const promise = async ({ if (typeof siblingData[field.name] !== 'object') { siblingData[field.name] = {} } + if (typeof siblingDoc[field.name] !== 'object') { siblingDoc[field.name] = {} } + if (typeof siblingDocWithLocales[field.name] !== 'object') { siblingDocWithLocales[field.name] = {} } @@ -345,9 +349,10 @@ export const promise = async ({ global, mergeLocaleActions, operation, - path: fieldPath, + parentIndexPath: '', + parentPath: path, + parentSchemaPath: schemaPath, req, - schemaPath: fieldSchemaPath, siblingData: siblingData[field.name] as JsonObject, siblingDoc: siblingDoc[field.name] as JsonObject, siblingDocWithLocales: siblingDocWithLocales[field.name] as JsonObject, @@ -356,6 +361,7 @@ export const promise = async ({ break } + case 'point': { // Transform point data for storage if ( @@ -379,6 +385,7 @@ export const promise = async ({ if (!field?.editor) { throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor } + if (typeof field?.editor === 'function') { throw new Error('Attempted to access unsanitized rich text editor.') } @@ -397,14 +404,15 @@ export const promise = async ({ errors, field, global, + indexPath: indexPathSegments, mergeLocaleActions, operation, originalDoc: doc, - path: fieldPath, + path: pathSegments, previousSiblingDoc: siblingDoc, previousValue: siblingDoc[field.name], req, - schemaPath: parentSchemaPath, + schemaPath: schemaPathSegments, siblingData, siblingDocWithLocales, skipValidation, @@ -425,13 +433,17 @@ export const promise = async ({ let tabSiblingDoc = siblingDoc let tabSiblingDocWithLocales = siblingDocWithLocales - if (tabHasName(field)) { + const isNamedTab = tabHasName(field) + + if (isNamedTab) { if (typeof siblingData[field.name] !== 'object') { siblingData[field.name] = {} } + if (typeof siblingDoc[field.name] !== 'object') { siblingDoc[field.name] = {} } + if (typeof siblingDocWithLocales[field.name] !== 'object') { siblingDocWithLocales[field.name] = {} } @@ -453,9 +465,10 @@ export const promise = async ({ global, mergeLocaleActions, operation, - path: fieldPath, + parentIndexPath: isNamedTab ? '' : indexPath, + parentPath: isNamedTab ? path : parentPath, + parentSchemaPath: schemaPath, req, - schemaPath: fieldSchemaPath, siblingData: tabSiblingData, siblingDoc: tabSiblingDoc, siblingDocWithLocales: tabSiblingDocWithLocales, @@ -478,9 +491,10 @@ export const promise = async ({ global, mergeLocaleActions, operation, - path: fieldPath, + parentIndexPath: indexPath, + parentPath: path, + parentSchemaPath: schemaPath, req, - schemaPath: fieldSchemaPath, siblingData, siblingDoc, siblingDocWithLocales, diff --git a/packages/payload/src/fields/hooks/beforeChange/traverseFields.ts b/packages/payload/src/fields/hooks/beforeChange/traverseFields.ts index 626c2a5474..c75b3e865a 100644 --- a/packages/payload/src/fields/hooks/beforeChange/traverseFields.ts +++ b/packages/payload/src/fields/hooks/beforeChange/traverseFields.ts @@ -25,9 +25,10 @@ type Args = { id?: number | string mergeLocaleActions: (() => Promise)[] operation: Operation - path: (number | string)[] + parentIndexPath: string + parentPath: string + parentSchemaPath: string req: PayloadRequest - schemaPath: string[] siblingData: JsonObject /** * The original siblingData (not modified by any hooks) @@ -60,9 +61,10 @@ export const traverseFields = async ({ global, mergeLocaleActions, operation, - path, + parentIndexPath, + parentPath, + parentSchemaPath, req, - schemaPath, siblingData, siblingDoc, siblingDocWithLocales, @@ -85,8 +87,9 @@ export const traverseFields = async ({ global, mergeLocaleActions, operation, - parentPath: path, - parentSchemaPath: schemaPath, + parentIndexPath, + parentPath, + parentSchemaPath, req, siblingData, siblingDoc, diff --git a/packages/payload/src/fields/hooks/beforeDuplicate/index.ts b/packages/payload/src/fields/hooks/beforeDuplicate/index.ts index 5679b18015..7eea0c3af3 100644 --- a/packages/payload/src/fields/hooks/beforeDuplicate/index.ts +++ b/packages/payload/src/fields/hooks/beforeDuplicate/index.ts @@ -2,7 +2,6 @@ import type { SanitizedCollectionConfig } from '../../../collections/config/type import type { RequestContext } from '../../../index.js' import type { JsonObject, PayloadRequest } from '../../../types/index.js' -import { deepCopyObjectSimple } from '../../../utilities/deepCopyObject.js' import { traverseFields } from './traverseFields.js' type Args = { @@ -35,9 +34,10 @@ export const beforeDuplicate = async ({ doc, fields: collection?.fields, overrideAccess, - path: [], + parentIndexPath: '', + parentPath: '', + parentSchemaPath: '', req, - schemaPath: [], siblingDoc: doc, }) diff --git a/packages/payload/src/fields/hooks/beforeDuplicate/promise.ts b/packages/payload/src/fields/hooks/beforeDuplicate/promise.ts index 9c3d43e092..cec3cac67d 100644 --- a/packages/payload/src/fields/hooks/beforeDuplicate/promise.ts +++ b/packages/payload/src/fields/hooks/beforeDuplicate/promise.ts @@ -3,8 +3,8 @@ import type { RequestContext } from '../../../index.js' import type { JsonObject, PayloadRequest } from '../../../types/index.js' import type { Field, FieldHookArgs, TabAsField } from '../../config/types.js' -import { fieldAffectsData, tabHasName } from '../../config/types.js' -import { getFieldPaths } from '../../getFieldPaths.js' +import { fieldAffectsData } from '../../config/types.js' +import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js' import { runBeforeDuplicateHooks } from './runHook.js' import { traverseFields } from './traverseFields.js' @@ -16,8 +16,9 @@ type Args = { fieldIndex: number id?: number | string overrideAccess: boolean - parentPath: (number | string)[] - parentSchemaPath: string[] + parentIndexPath: string + parentPath: string + parentSchemaPath: string req: PayloadRequest siblingDoc: JsonObject } @@ -30,40 +31,25 @@ export const promise = async ({ field, fieldIndex, overrideAccess, + parentIndexPath, parentPath, parentSchemaPath, req, siblingDoc, }: Args): Promise => { - const { localization } = req.payload.config - - const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({ + const { indexPath, path, schemaPath } = getFieldPaths({ field, index: fieldIndex, - parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data - parentPath: parentPath.join('.'), - parentSchemaPath: parentSchemaPath.join('.'), + parentIndexPath, + parentPath, + parentSchemaPath, }) - const fieldPath = _fieldPath ? _fieldPath.split('.') : [] - const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : [] - // Handle unnamed tabs - if (field.type === 'tab' && !tabHasName(field)) { - await traverseFields({ - id, - collection, - context, - doc, - fields: field.fields, - overrideAccess, - path: fieldPath, - req, - schemaPath: fieldSchemaPath, - siblingDoc, - }) + const { localization } = req.payload.config - return - } + const pathSegments = path ? path.split('.') : [] + const schemaPathSegments = schemaPath ? schemaPath.split('.') : [] + const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : [] if (fieldAffectsData(field)) { let fieldData = siblingDoc?.[field.name] @@ -82,11 +68,12 @@ export const promise = async ({ data: doc, field, global: undefined, - path: fieldPath, + indexPath: indexPathSegments, + path: pathSegments, previousSiblingDoc: siblingDoc, previousValue: siblingDoc[field.name]?.[locale], req, - schemaPath: parentSchemaPath, + schemaPath: schemaPathSegments, siblingData: siblingDoc, siblingDocWithLocales: siblingDoc, value: siblingDoc[field.name]?.[locale], @@ -114,11 +101,12 @@ export const promise = async ({ data: doc, field, global: undefined, - path: fieldPath, + indexPath: indexPathSegments, + path: pathSegments, previousSiblingDoc: siblingDoc, previousValue: siblingDoc[field.name], req, - schemaPath: parentSchemaPath, + schemaPath: schemaPathSegments, siblingData: siblingDoc, siblingDocWithLocales: siblingDoc, value: siblingDoc[field.name], @@ -150,7 +138,8 @@ export const promise = async ({ if (Array.isArray(rows)) { const promises = [] - rows.forEach((row, i) => { + + rows.forEach((row, rowIndex) => { promises.push( traverseFields({ id, @@ -159,22 +148,26 @@ export const promise = async ({ doc, fields: field.fields, overrideAccess, - path: [...fieldPath, i], + parentIndexPath: '', + parentPath: path + '.' + rowIndex, + parentSchemaPath: schemaPath, req, - schemaPath: fieldSchemaPath, siblingDoc: row, }), ) }) } + break } + case 'blocks': { const rows = fieldData[locale] if (Array.isArray(rows)) { const promises = [] - rows.forEach((row, i) => { + + rows.forEach((row, rowIndex) => { const blockTypeToMatch = row.blockType const block = field.blocks.find( @@ -189,9 +182,10 @@ export const promise = async ({ doc, fields: block.fields, overrideAccess, - path: [...fieldPath, i], + parentIndexPath: '', + parentPath: path + '.' + rowIndex, + parentSchemaPath: schemaPath + '.' + block.slug, req, - schemaPath: fieldSchemaPath, siblingDoc: row, }), ) @@ -201,7 +195,6 @@ export const promise = async ({ } case 'group': - case 'tab': { promises.push( traverseFields({ @@ -211,9 +204,10 @@ export const promise = async ({ doc, fields: field.fields, overrideAccess, - path: fieldSchemaPath, + parentIndexPath: '', + parentPath: path, + parentSchemaPath: schemaPath, req, - schemaPath: fieldSchemaPath, siblingDoc: fieldData[locale], }), ) @@ -235,7 +229,8 @@ export const promise = async ({ if (Array.isArray(rows)) { const promises = [] - rows.forEach((row, i) => { + + rows.forEach((row, rowIndex) => { promises.push( traverseFields({ id, @@ -244,23 +239,28 @@ export const promise = async ({ doc, fields: field.fields, overrideAccess, - path: [...fieldPath, i], + parentIndexPath: '', + parentPath: path + '.' + rowIndex, + parentSchemaPath: schemaPath, req, - schemaPath: fieldSchemaPath, siblingDoc: row, }), ) }) + await Promise.all(promises) } + break } + case 'blocks': { const rows = siblingDoc[field.name] if (Array.isArray(rows)) { const promises = [] - rows.forEach((row, i) => { + + rows.forEach((row, rowIndex) => { const blockTypeToMatch = row.blockType const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch) @@ -275,28 +275,28 @@ export const promise = async ({ doc, fields: block.fields, overrideAccess, - path: [...fieldPath, i], + parentIndexPath: '', + parentPath: path + '.' + rowIndex, + parentSchemaPath: schemaPath + '.' + block.slug, req, - schemaPath: fieldSchemaPath, siblingDoc: row, }), ) } }) + await Promise.all(promises) } break } - case 'group': - - case 'tab': { + case 'group': { if (typeof siblingDoc[field.name] !== 'object') { siblingDoc[field.name] = {} } - const groupDoc = siblingDoc[field.name] as Record + const groupDoc = siblingDoc[field.name] as JsonObject await traverseFields({ id, @@ -305,10 +305,35 @@ export const promise = async ({ doc, fields: field.fields, overrideAccess, - path: fieldPath, + parentIndexPath: '', + parentPath: path, + parentSchemaPath: schemaPath, req, - schemaPath: fieldSchemaPath, - siblingDoc: groupDoc as JsonObject, + siblingDoc: groupDoc, + }) + + break + } + + case 'tab': { + if (typeof siblingDoc[field.name] !== 'object') { + siblingDoc[field.name] = {} + } + + const tabDoc = siblingDoc[field.name] as JsonObject + + await traverseFields({ + id, + collection, + context, + doc, + fields: field.fields, + overrideAccess, + parentIndexPath: '', + parentPath: path, + parentSchemaPath: schemaPath, + req, + siblingDoc: tabDoc, }) break @@ -327,9 +352,31 @@ export const promise = async ({ doc, fields: field.fields, overrideAccess, - path: fieldPath, + parentIndexPath: indexPath, + parentPath, + parentSchemaPath: schemaPath, + req, + siblingDoc, + }) + + break + } + + // Unnamed Tab + // @ts-expect-error `fieldAffectsData` inferred return type doesn't account for TabAsField + case 'tab': { + await traverseFields({ + id, + collection, + context, + doc, + // @ts-expect-error `fieldAffectsData` inferred return type doesn't account for TabAsField + fields: field.fields, + overrideAccess, + parentIndexPath: indexPath, + parentPath, + parentSchemaPath: schemaPath, req, - schemaPath: fieldSchemaPath, siblingDoc, }) @@ -344,9 +391,10 @@ export const promise = async ({ doc, fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })), overrideAccess, - path: fieldPath, + parentIndexPath: indexPath, + parentPath: path, + parentSchemaPath: schemaPath, req, - schemaPath: fieldSchemaPath, siblingDoc, }) diff --git a/packages/payload/src/fields/hooks/beforeDuplicate/traverseFields.ts b/packages/payload/src/fields/hooks/beforeDuplicate/traverseFields.ts index 6e9b6b04c6..b94047870f 100644 --- a/packages/payload/src/fields/hooks/beforeDuplicate/traverseFields.ts +++ b/packages/payload/src/fields/hooks/beforeDuplicate/traverseFields.ts @@ -12,9 +12,10 @@ type Args = { fields: (Field | TabAsField)[] id?: number | string overrideAccess: boolean - path: (number | string)[] + parentIndexPath: string + parentPath: string + parentSchemaPath: string req: PayloadRequest - schemaPath: string[] siblingDoc: JsonObject } @@ -25,12 +26,14 @@ export const traverseFields = async ({ doc, fields, overrideAccess, - path, + parentIndexPath, + parentPath, + parentSchemaPath, req, - schemaPath, siblingDoc, }: Args): Promise => { const promises = [] + fields.forEach((field, fieldIndex) => { promises.push( promise({ @@ -41,8 +44,9 @@ export const traverseFields = async ({ field, fieldIndex, overrideAccess, - parentPath: path, - parentSchemaPath: schemaPath, + parentIndexPath, + parentPath, + parentSchemaPath, req, siblingDoc, }), diff --git a/packages/payload/src/fields/hooks/beforeValidate/index.ts b/packages/payload/src/fields/hooks/beforeValidate/index.ts index 590417316a..b390d7a1c5 100644 --- a/packages/payload/src/fields/hooks/beforeValidate/index.ts +++ b/packages/payload/src/fields/hooks/beforeValidate/index.ts @@ -47,9 +47,10 @@ export const beforeValidate = async ({ global, operation, overrideAccess, - path: [], + parentIndexPath: '', + parentPath: '', + parentSchemaPath: '', req, - schemaPath: [], siblingData: incomingData, siblingDoc: doc, }) diff --git a/packages/payload/src/fields/hooks/beforeValidate/promise.ts b/packages/payload/src/fields/hooks/beforeValidate/promise.ts index e109b20b2d..fede4caed9 100644 --- a/packages/payload/src/fields/hooks/beforeValidate/promise.ts +++ b/packages/payload/src/fields/hooks/beforeValidate/promise.ts @@ -8,7 +8,7 @@ import type { Field, TabAsField } from '../../config/types.js' import { MissingEditorProp } from '../../../errors/index.js' import { fieldAffectsData, tabHasName, valueIsValueWithRelation } from '../../config/types.js' import { getDefaultValue } from '../../getDefaultValue.js' -import { getFieldPaths } from '../../getFieldPaths.js' +import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js' import { cloneDataFromOriginalDoc } from '../beforeChange/cloneDataFromOriginalDoc.js' import { getExistingRowDoc } from '../beforeChange/getExistingRowDoc.js' import { traverseFields } from './traverseFields.js' @@ -27,8 +27,9 @@ type Args = { id?: number | string operation: 'create' | 'update' overrideAccess: boolean - parentPath: (number | string)[] - parentSchemaPath: string[] + parentIndexPath: string + parentPath: string + parentSchemaPath: string req: PayloadRequest siblingData: JsonObject /** @@ -55,21 +56,24 @@ export const promise = async ({ global, operation, overrideAccess, + parentIndexPath, parentPath, parentSchemaPath, req, siblingData, siblingDoc, }: Args): Promise => { - const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({ + const { indexPath, path, schemaPath } = getFieldPaths({ field, index: fieldIndex, - parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data - parentPath: parentPath.join('.'), - parentSchemaPath: parentSchemaPath.join('.'), + parentIndexPath, + parentPath, + parentSchemaPath, }) - const fieldPath = _fieldPath ? _fieldPath.split('.') : [] - const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : [] + + const pathSegments = path ? path.split('.') : [] + const schemaPathSegments = schemaPath ? schemaPath.split('.') : [] + const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : [] if (fieldAffectsData(field)) { if (field.name === 'id') { @@ -271,14 +275,15 @@ export const promise = async ({ data, field, global, + indexPath: indexPathSegments, operation, originalDoc: doc, overrideAccess, - path: fieldPath, + path: pathSegments, previousSiblingDoc: siblingDoc, previousValue: siblingDoc[field.name], req, - schemaPath: fieldSchemaPath, + schemaPath: schemaPathSegments, siblingData, value: siblingData[field.name], }) @@ -325,7 +330,8 @@ export const promise = async ({ if (Array.isArray(rows)) { const promises = [] - rows.forEach((row, i) => { + + rows.forEach((row, rowIndex) => { promises.push( traverseFields({ id, @@ -337,14 +343,16 @@ export const promise = async ({ global, operation, overrideAccess, - path: [...fieldPath, i], + parentIndexPath: '', + parentPath: path + '.' + rowIndex, + parentSchemaPath: schemaPath, req, - schemaPath: fieldSchemaPath, siblingData: row as JsonObject, siblingDoc: getExistingRowDoc(row as JsonObject, siblingDoc[field.name]), }), ) }) + await Promise.all(promises) } break @@ -355,7 +363,8 @@ export const promise = async ({ if (Array.isArray(rows)) { const promises = [] - rows.forEach((row, i) => { + + rows.forEach((row, rowIndex) => { const rowSiblingDoc = getExistingRowDoc(row as JsonObject, siblingDoc[field.name]) const blockTypeToMatch = (row as JsonObject).blockType || rowSiblingDoc.blockType const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch) @@ -374,15 +383,17 @@ export const promise = async ({ global, operation, overrideAccess, - path: [...fieldPath, i], + parentIndexPath: '', + parentPath: path + '.' + rowIndex, + parentSchemaPath: schemaPath + '.' + block.slug, req, - schemaPath: fieldSchemaPath, siblingData: row as JsonObject, siblingDoc: rowSiblingDoc, }), ) } }) + await Promise.all(promises) } @@ -401,19 +412,22 @@ export const promise = async ({ global, operation, overrideAccess, - path: fieldPath, + parentIndexPath: indexPath, + parentPath, + parentSchemaPath: schemaPath, req, - schemaPath: fieldSchemaPath, siblingData, siblingDoc, }) break } + case 'group': { if (typeof siblingData[field.name] !== 'object') { siblingData[field.name] = {} } + if (typeof siblingDoc[field.name] !== 'object') { siblingDoc[field.name] = {} } @@ -431,9 +445,10 @@ export const promise = async ({ global, operation, overrideAccess, - path: fieldPath, + parentIndexPath: '', + parentPath: path, + parentSchemaPath: schemaPath, req, - schemaPath: fieldSchemaPath, siblingData: groupData as JsonObject, siblingDoc: groupDoc as JsonObject, }) @@ -445,6 +460,7 @@ export const promise = async ({ if (!field?.editor) { throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor } + if (typeof field?.editor === 'function') { throw new Error('Attempted to access unsanitized rich text editor.') } @@ -461,14 +477,15 @@ export const promise = async ({ data, field, global, + indexPath: indexPathSegments, operation, originalDoc: doc, overrideAccess, - path: fieldPath, + path: pathSegments, previousSiblingDoc: siblingDoc, previousValue: siblingData[field.name], req, - schemaPath: fieldSchemaPath, + schemaPath: schemaPathSegments, siblingData, value: siblingData[field.name], }) @@ -484,10 +501,14 @@ export const promise = async ({ case 'tab': { let tabSiblingData let tabSiblingDoc - if (tabHasName(field)) { + + const isNamedTab = tabHasName(field) + + if (isNamedTab) { if (typeof siblingData[field.name] !== 'object') { siblingData[field.name] = {} } + if (typeof siblingDoc[field.name] !== 'object') { siblingDoc[field.name] = {} } @@ -509,9 +530,10 @@ export const promise = async ({ global, operation, overrideAccess, - path: fieldPath, + parentIndexPath: isNamedTab ? '' : indexPath, + parentPath: isNamedTab ? path : parentPath, + parentSchemaPath: schemaPath, req, - schemaPath: fieldSchemaPath, siblingData: tabSiblingData, siblingDoc: tabSiblingDoc, }) @@ -530,9 +552,10 @@ export const promise = async ({ global, operation, overrideAccess, - path: fieldPath, + parentIndexPath: indexPath, + parentPath: path, + parentSchemaPath: schemaPath, req, - schemaPath: fieldSchemaPath, siblingData, siblingDoc, }) diff --git a/packages/payload/src/fields/hooks/beforeValidate/traverseFields.ts b/packages/payload/src/fields/hooks/beforeValidate/traverseFields.ts index cc8fa9a25a..8f1a29f5e0 100644 --- a/packages/payload/src/fields/hooks/beforeValidate/traverseFields.ts +++ b/packages/payload/src/fields/hooks/beforeValidate/traverseFields.ts @@ -19,9 +19,10 @@ type Args = { id?: number | string operation: 'create' | 'update' overrideAccess: boolean - path: (number | string)[] + parentIndexPath: string + parentPath: string + parentSchemaPath: string req: PayloadRequest - schemaPath: string[] siblingData: JsonObject /** * The original siblingData (not modified by any hooks) @@ -39,13 +40,15 @@ export const traverseFields = async ({ global, operation, overrideAccess, - path, + parentIndexPath, + parentPath, + parentSchemaPath, req, - schemaPath, siblingData, siblingDoc, }: Args): Promise => { const promises = [] + fields.forEach((field, fieldIndex) => { promises.push( promise({ @@ -59,13 +62,15 @@ export const traverseFields = async ({ global, operation, overrideAccess, - parentPath: path, - parentSchemaPath: schemaPath, + parentIndexPath, + parentPath, + parentSchemaPath, req, siblingData, siblingDoc, }), ) }) + await Promise.all(promises) } diff --git a/packages/payload/src/fields/utilities/getFieldPaths.ts b/packages/payload/src/fields/utilities/getFieldPaths.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index c1916b34c5..43229471c4 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -1152,6 +1152,7 @@ export { type ServerOnlyFieldProperties, } from './fields/config/client.js' export { sanitizeFields } from './fields/config/sanitize.js' + export type { AdminClient, ArrayField, @@ -1428,7 +1429,6 @@ export { deleteCollectionVersions } from './versions/deleteCollectionVersions.js export { enforceMaxVersions } from './versions/enforceMaxVersions.js' export { getLatestCollectionVersion } from './versions/getLatestCollectionVersion.js' export { getLatestGlobalVersion } from './versions/getLatestGlobalVersion.js' - export { saveVersion } from './versions/saveVersion.js' export type { SchedulePublishTaskInput } from './versions/schedule/types.js' export type { TypeWithVersion } from './versions/types.js' diff --git a/packages/payload/src/utilities/getFormattedLabel.ts b/packages/payload/src/utilities/getLabelFromPath.ts similarity index 88% rename from packages/payload/src/utilities/getFormattedLabel.ts rename to packages/payload/src/utilities/getLabelFromPath.ts index 7e1241e16d..fbf4bec5fc 100644 --- a/packages/payload/src/utilities/getFormattedLabel.ts +++ b/packages/payload/src/utilities/getLabelFromPath.ts @@ -1,4 +1,4 @@ -export const getFormattedLabel = (path: (number | string)[]): string => { +export const getLabelFromPath = (path: (number | string)[]): string => { return path .filter((pathSegment) => !(typeof pathSegment === 'string' && pathSegment.includes('_index'))) .reduce((acc, part) => { diff --git a/packages/richtext-lexical/src/index.ts b/packages/richtext-lexical/src/index.ts index 170e5e3977..1c4940d044 100644 --- a/packages/richtext-lexical/src/index.ts +++ b/packages/richtext-lexical/src/index.ts @@ -190,6 +190,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte context: _context, data, global, + indexPath, operation, originalDoc, path, @@ -198,6 +199,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte req, schemaPath, } = args + let { value } = args if (finalSanitizedEditorConfig?.features?.hooks?.afterChange?.length) { for (const hook of finalSanitizedEditorConfig.features.hooks.afterChange) { @@ -292,11 +294,12 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte fields: subFields, global, operation, - path, + parentIndexPath: indexPath.join('-'), + parentPath: path.join('.'), + parentSchemaPath: schemaPath.join('.'), previousDoc, previousSiblingDoc: { ...nodePreviousSiblingDoc }, req, - schemaPath, siblingData: nodeSiblingData || {}, siblingDoc: { ...nodeSiblingDoc }, }) @@ -322,6 +325,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte findMany, flattenLocales, global, + indexPath, locale, originalDoc, overrideAccess, @@ -334,6 +338,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte triggerAccessControl, triggerHooks, } = args + let { value } = args if (finalSanitizedEditorConfig?.features?.hooks?.afterRead?.length) { @@ -408,11 +413,12 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte global, locale: locale!, overrideAccess: overrideAccess!, - path, + parentIndexPath: indexPath.join('-'), + parentPath: path.join('.'), + parentSchemaPath: schemaPath.join('.'), populate, populationPromises: populationPromises!, req, - schemaPath, showHiddenFields: showHiddenFields!, siblingDoc: nodeSliblingData, triggerAccessControl, @@ -435,6 +441,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte errors, field, global, + indexPath, mergeLocaleActions, operation, originalDoc, @@ -446,6 +453,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte siblingDocWithLocales, skipValidation, } = args + let { value } = args if (finalSanitizedEditorConfig?.features?.hooks?.beforeChange?.length) { @@ -566,9 +574,10 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte global, mergeLocaleActions: mergeLocaleActions!, operation: operation!, - path, + parentIndexPath: indexPath.join('-'), + parentPath: path.join('.'), + parentSchemaPath: schemaPath.join('.'), req, - schemaPath, siblingData: nodeSiblingData, siblingDoc: nodePreviousSiblingDoc, siblingDocWithLocales: nodeSiblingDocWithLocales ?? {}, @@ -620,6 +629,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte context, data, global, + indexPath, operation, originalDoc, overrideAccess, @@ -628,6 +638,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte req, schemaPath, } = args + let { value } = args if (finalSanitizedEditorConfig?.features?.hooks?.beforeValidate?.length) { for (const hook of finalSanitizedEditorConfig.features.hooks.beforeValidate) { @@ -755,9 +766,10 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte global, operation, overrideAccess: overrideAccess!, - path, + parentIndexPath: indexPath.join('-'), + parentPath: path.join('.'), + parentSchemaPath: schemaPath.join('.'), req, - schemaPath, siblingData: nodeSiblingData, siblingDoc: nodeSiblingDoc, }) diff --git a/packages/richtext-lexical/src/populateGraphQL/recursivelyPopulateFieldsForGraphQL.ts b/packages/richtext-lexical/src/populateGraphQL/recursivelyPopulateFieldsForGraphQL.ts index 44bcdf0d1a..d7c212937e 100644 --- a/packages/richtext-lexical/src/populateGraphQL/recursivelyPopulateFieldsForGraphQL.ts +++ b/packages/richtext-lexical/src/populateGraphQL/recursivelyPopulateFieldsForGraphQL.ts @@ -59,10 +59,11 @@ export const recursivelyPopulateFieldsForGraphQL = ({ global: null, // Pass from core? This is only needed for hooks, so we can leave this null for now locale: req.locale!, overrideAccess, - path: [], + parentIndexPath: '', + parentPath: '', + parentSchemaPath: '', populationPromises, // This is not the same as populationPromises passed into this recurseNestedFields. These are just promises resolved at the very end. req, - schemaPath: [], showHiddenFields, siblingDoc, triggerHooks: false, diff --git a/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts b/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts index b1642e52a9..d84ffe1f2a 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts @@ -365,7 +365,6 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom } const parentPath = path + '.' + i - const rowSchemaPath = schemaPath + '.' + block.slug if (block) { row.id = row?.id || new ObjectId().toHexString() @@ -435,7 +434,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom parentIndexPath: '', parentPassesCondition: passesCondition, parentPath, - parentSchemaPath: rowSchemaPath, + parentSchemaPath: schemaPath + '.' + block.slug, permissions: fieldPermissions === true ? fieldPermissions @@ -741,10 +740,10 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom includeSchema, omitParents, operation, - parentIndexPath: tabHasName(tab) ? '' : tabIndexPath, + parentIndexPath: isNamedTab ? '' : tabIndexPath, parentPassesCondition: passesCondition, - parentPath: tabHasName(tab) ? tabPath : parentPath, - parentSchemaPath: tabHasName(tab) ? tabSchemaPath : parentSchemaPath, + parentPath: isNamedTab ? tabPath : parentPath, + parentSchemaPath: isNamedTab ? tabSchemaPath : parentSchemaPath, permissions: childPermissions, preferences, previousFormState, diff --git a/packages/ui/src/utilities/buildClientFieldSchemaMap/traverseFields.ts b/packages/ui/src/utilities/buildClientFieldSchemaMap/traverseFields.ts index 42dfac268d..a238e1113e 100644 --- a/packages/ui/src/utilities/buildClientFieldSchemaMap/traverseFields.ts +++ b/packages/ui/src/utilities/buildClientFieldSchemaMap/traverseFields.ts @@ -131,8 +131,11 @@ export const traverseFields = ({ } break } + case 'tabs': field.tabs.map((tab, tabIndex) => { + const isNamedTab = tabHasName(tab) + const { indexPath: tabIndexPath, schemaPath: tabSchemaPath } = getFieldPaths({ field: { ...tab, @@ -151,8 +154,8 @@ export const traverseFields = ({ config, fields: tab.fields, i18n, - parentIndexPath: tabHasName(tab) ? '' : tabIndexPath, - parentSchemaPath: tabHasName(tab) ? tabSchemaPath : parentSchemaPath, + parentIndexPath: isNamedTab ? '' : tabIndexPath, + parentSchemaPath: isNamedTab ? tabSchemaPath : parentSchemaPath, payload, schemaMap, }) diff --git a/packages/ui/src/utilities/buildFieldSchemaMap/traverseFields.ts b/packages/ui/src/utilities/buildFieldSchemaMap/traverseFields.ts index 37df3098ee..cc620332d5 100644 --- a/packages/ui/src/utilities/buildFieldSchemaMap/traverseFields.ts +++ b/packages/ui/src/utilities/buildFieldSchemaMap/traverseFields.ts @@ -98,6 +98,8 @@ export const traverseFields = ({ case 'tabs': field.tabs.map((tab, tabIndex) => { + const isNamedTab = tabHasName(tab) + const { indexPath: tabIndexPath, schemaPath: tabSchemaPath } = getFieldPaths({ field: { ...tab, @@ -115,8 +117,8 @@ export const traverseFields = ({ config, fields: tab.fields, i18n, - parentIndexPath: tabHasName(tab) ? '' : tabIndexPath, - parentSchemaPath: tabHasName(tab) ? tabSchemaPath : parentSchemaPath, + parentIndexPath: isNamedTab ? '' : tabIndexPath, + parentSchemaPath: isNamedTab ? tabSchemaPath : parentSchemaPath, schemaMap, }) }) diff --git a/test/_community/payload-types.ts b/test/_community/payload-types.ts index 353f19cace..22f8b49173 100644 --- a/test/_community/payload-types.ts +++ b/test/_community/payload-types.ts @@ -70,7 +70,7 @@ export interface UserAuthOperations { export interface Post { id: string; title?: string | null; - richText?: { + content?: { root: { type: string; children: { @@ -217,7 +217,7 @@ export interface PayloadMigration { */ export interface PostsSelect { title?: T; - richText?: T; + content?: T; updatedAt?: T; createdAt?: T; _status?: T; @@ -340,23 +340,6 @@ export interface MenuSelect { createdAt?: T; globalType?: T; } -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "ContactBlock". - */ -export interface ContactBlock { - /** - * ... - */ - first: string; - /** - * ... - */ - two: string; - id?: string | null; - blockName?: string | null; - blockType: 'contact'; -} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "auth". diff --git a/test/database/config.ts b/test/database/config.ts index 83389c7014..eb5fd9df9e 100644 --- a/test/database/config.ts +++ b/test/database/config.ts @@ -8,7 +8,7 @@ import { v4 as uuid } from 'uuid' import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import { devUser } from '../credentials.js' -import { postsSlug } from './shared.js' +import { errorOnUnnamedFieldsSlug, postsSlug } from './shared.js' const defaultValueField: TextField = { name: 'defaultValue', @@ -156,6 +156,32 @@ export default buildConfigWithDefaults({ ], }, }, + { + slug: errorOnUnnamedFieldsSlug, + fields: [ + { + type: 'tabs', + tabs: [ + { + label: 'UnnamedTab', + fields: [ + { + name: 'groupWithinUnnamedTab', + type: 'group', + fields: [ + { + name: 'text', + type: 'text', + required: true, + }, + ], + }, + ], + }, + ], + }, + ], + }, { slug: 'default-values', fields: [ diff --git a/test/database/int.spec.ts b/test/database/int.spec.ts index c7daa70f0b..888624d1e2 100644 --- a/test/database/int.spec.ts +++ b/test/database/int.spec.ts @@ -26,7 +26,7 @@ import { devUser } from '../credentials.js' import { initPayloadInt } from '../helpers/initPayloadInt.js' import { isMongoose } from '../helpers/isMongoose.js' import removeFiles from '../helpers/removeFiles.js' -import { postsSlug } from './shared.js' +import { errorOnUnnamedFieldsSlug, postsSlug } from './shared.js' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) @@ -895,7 +895,7 @@ describe('database', () => { await expect(errorMessage).toBe('The following field is invalid: Title') }) - it('should return proper deeply nested field validation errors', async () => { + it('should return validation errors in response', async () => { try { await payload.create({ collection: postsSlug, @@ -921,6 +921,22 @@ describe('database', () => { ) } }) + + it('should return validation errors with proper field paths for unnamed fields', async () => { + try { + await payload.create({ + collection: errorOnUnnamedFieldsSlug, + data: { + groupWithinUnnamedTab: { + // @ts-expect-error + text: undefined, + }, + }, + }) + } catch (e: any) { + expect(e.data?.errors?.[0]?.path).toBe('groupWithinUnnamedTab.text') + } + }) }) describe('defaultValue', () => { diff --git a/test/database/payload-types.ts b/test/database/payload-types.ts index b9dc6d96a0..62a306a5e3 100644 --- a/test/database/payload-types.ts +++ b/test/database/payload-types.ts @@ -12,6 +12,7 @@ export interface Config { }; collections: { posts: Post; + 'error-on-unnamed-fields': ErrorOnUnnamedField; 'default-values': DefaultValue; 'relation-a': RelationA; 'relation-b': RelationB; @@ -30,6 +31,7 @@ export interface Config { collectionsJoins: {}; collectionsSelect: { posts: PostsSelect | PostsSelect; + 'error-on-unnamed-fields': ErrorOnUnnamedFieldsSelect | ErrorOnUnnamedFieldsSelect; 'default-values': DefaultValuesSelect | DefaultValuesSelect; 'relation-a': RelationASelect | RelationASelect; 'relation-b': RelationBSelect | RelationBSelect; @@ -114,6 +116,18 @@ export interface Post { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "error-on-unnamed-fields". + */ +export interface ErrorOnUnnamedField { + id: string; + groupWithinUnnamedTab: { + text: string; + }; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "default-values". @@ -355,6 +369,10 @@ export interface PayloadLockedDocument { relationTo: 'posts'; value: string | Post; } | null) + | ({ + relationTo: 'error-on-unnamed-fields'; + value: string | ErrorOnUnnamedField; + } | null) | ({ relationTo: 'default-values'; value: string | DefaultValue; @@ -482,6 +500,19 @@ export interface PostsSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "error-on-unnamed-fields_select". + */ +export interface ErrorOnUnnamedFieldsSelect { + groupWithinUnnamedTab?: + | T + | { + text?: T; + }; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "default-values_select". diff --git a/test/database/shared.ts b/test/database/shared.ts index f4dad31482..0c2fe99672 100644 --- a/test/database/shared.ts +++ b/test/database/shared.ts @@ -1 +1,2 @@ export const postsSlug = 'posts' +export const errorOnUnnamedFieldsSlug = 'error-on-unnamed-fields' diff --git a/test/fields/collections/Tabs/index.ts b/test/fields/collections/Tabs/index.ts index 8a7121c8bb..f0647dad7a 100644 --- a/test/fields/collections/Tabs/index.ts +++ b/test/fields/collections/Tabs/index.ts @@ -47,6 +47,8 @@ const TabsFields: CollectionConfig = { type: 'array', required: true, fields: [ + // path: 'array.n.text' + // schemaPath: '_index-1-0.array.text' { name: 'text', type: 'text', diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index ef90dd0afe..3d8b6a1a9a 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -30,6 +30,7 @@ import { clearAndSeedEverything } from './seed.js' import { arrayFieldsSlug, blockFieldsSlug, + collapsibleFieldsSlug, groupFieldsSlug, relationshipFieldsSlug, tabsFieldsSlug, @@ -487,7 +488,7 @@ describe('Fields', () => { }) describe('rows', () => { - it('show proper validation error message on text field within row field', async () => { + it('should show proper validation error message on text field within row field', async () => { await expect(async () => payload.create({ collection: 'row-fields', @@ -1677,7 +1678,7 @@ describe('Fields', () => { expect(res.id).toBe(doc.id) }) - it('show proper validation error on text field in nested array', async () => { + it('should show proper validation error on text field in nested array', async () => { await expect(async () => payload.create({ collection, @@ -1697,7 +1698,7 @@ describe('Fields', () => { ).rejects.toThrow('The following field is invalid: Items 1 > SubArray 1 > Second text field') }) - it('show proper validation error on text field in row field in nested array', async () => { + it('should show proper validation error on text field in row field in nested array', async () => { await expect(async () => payload.create({ collection, @@ -2506,10 +2507,10 @@ describe('Fields', () => { }) describe('collapsible', () => { - it('show proper validation error message for fields nested in collapsible', async () => { + it('should show proper validation error message for fields nested in collapsible', async () => { await expect(async () => payload.create({ - collection: 'collapsible-fields', + collection: collapsibleFieldsSlug, data: { text: 'required', group: { diff --git a/test/hooks/collections/BeforeValidate/index.ts b/test/hooks/collections/BeforeValidate/index.ts index a7967cff3e..17348d6c2c 100644 --- a/test/hooks/collections/BeforeValidate/index.ts +++ b/test/hooks/collections/BeforeValidate/index.ts @@ -1,6 +1,6 @@ import type { CollectionConfig } from 'payload' -import { beforeValidateSlug } from '../../collectionSlugs.js' +import { beforeValidateSlug } from '../../shared.js' export const BeforeValidateCollection: CollectionConfig = { slug: beforeValidateSlug, diff --git a/test/hooks/collections/Data/index.ts b/test/hooks/collections/Data/index.ts index 7aac1a9122..c7e526851d 100644 --- a/test/hooks/collections/Data/index.ts +++ b/test/hooks/collections/Data/index.ts @@ -18,7 +18,6 @@ export const DataHooks: CollectionConfig = { return args }, ], - beforeChange: [ ({ context, data, collection }) => { context['collection_beforeChange_collection'] = JSON.stringify(collection) @@ -69,7 +68,6 @@ export const DataHooks: CollectionConfig = { return value }, ], - afterRead: [ ({ collection, field, context }) => { return ( diff --git a/test/hooks/collections/FieldPaths/index.ts b/test/hooks/collections/FieldPaths/index.ts new file mode 100644 index 0000000000..e395b9335e --- /dev/null +++ b/test/hooks/collections/FieldPaths/index.ts @@ -0,0 +1,214 @@ +import type { CollectionConfig, Field, FieldHook, FieldHookArgs } from 'payload' + +import { fieldPathsSlug } from '../../shared.js' + +const attachPathsToDoc = ( + label: string, + { value, path, schemaPath, indexPath, data }: FieldHookArgs, +): any => { + if (!data) { + data = {} + } + + // attach values to data for `beforeRead` and `beforeChange` hooks + data[`${label}_FieldPaths`] = { + path, + schemaPath, + indexPath, + } + + return value +} + +const attachHooks = ( + fieldIdentifier: string, +): { + afterRead: FieldHook[] + beforeChange: FieldHook[] + beforeDuplicate: FieldHook[] + beforeValidate: FieldHook[] +} => ({ + beforeValidate: [(args) => attachPathsToDoc(`${fieldIdentifier}_beforeValidate`, args)], + beforeChange: [(args) => attachPathsToDoc(`${fieldIdentifier}_beforeChange`, args)], + afterRead: [(args) => attachPathsToDoc(`${fieldIdentifier}_afterRead`, args)], + beforeDuplicate: [(args) => attachPathsToDoc(`${fieldIdentifier}_beforeDuplicate`, args)], +}) + +const createFields = (fieldIdentifiers: string[]): Field[] => + fieldIdentifiers.reduce((acc, fieldIdentifier) => { + return [ + ...acc, + { + name: `${fieldIdentifier}_beforeValidate_FieldPaths`, + type: 'json', + }, + { + name: `${fieldIdentifier}_beforeChange_FieldPaths`, + type: 'json', + }, + { + name: `${fieldIdentifier}_afterRead_FieldPaths`, + type: 'json', + }, + { + name: `${fieldIdentifier}_beforeDuplicate_FieldPaths`, + type: 'json', + }, + ] + }, [] as Field[]) + +export const FieldPaths: CollectionConfig = { + slug: fieldPathsSlug, + fields: [ + { + // path: 'topLevelNamedField' + // schemaPath: 'topLevelNamedField' + // indexPath: '' + name: 'topLevelNamedField', + type: 'text', + hooks: attachHooks('topLevelNamedField'), + }, + { + // path: 'array' + // schemaPath: 'array' + // indexPath: '' + name: 'array', + type: 'array', + fields: [ + { + // path: 'array.[n].fieldWithinArray' + // schemaPath: 'array.fieldWithinArray' + // indexPath: '' + name: 'fieldWithinArray', + type: 'text', + hooks: attachHooks('fieldWithinArray'), + }, + { + // path: 'array.[n].nestedArray' + // schemaPath: 'array.nestedArray' + // indexPath: '' + name: 'nestedArray', + type: 'array', + fields: [ + { + // path: 'array.[n].nestedArray.[n].fieldWithinNestedArray' + // schemaPath: 'array.nestedArray.fieldWithinNestedArray' + // indexPath: '' + name: 'fieldWithinNestedArray', + type: 'text', + hooks: attachHooks('fieldWithinNestedArray'), + }, + ], + }, + { + // path: 'array.[n]._index-2' + // schemaPath: 'array._index-2' + // indexPath: '' + type: 'row', + fields: [ + { + // path: 'array.[n].fieldWithinRowWithinArray' + // schemaPath: 'array._index-2.fieldWithinRowWithinArray' + // indexPath: '' + name: 'fieldWithinRowWithinArray', + type: 'text', + hooks: attachHooks('fieldWithinRowWithinArray'), + }, + ], + }, + ], + }, + { + // path: '_index-2' + // schemaPath: '_index-2' + // indexPath: '2' + type: 'row', + fields: [ + { + // path: 'fieldWithinRow' + // schemaPath: '_index-2.fieldWithinRow' + // indexPath: '' + name: 'fieldWithinRow', + type: 'text', + hooks: attachHooks('fieldWithinRow'), + }, + ], + }, + { + // path: '_index-3' + // schemaPath: '_index-3' + // indexPath: '3' + type: 'tabs', + tabs: [ + { + // path: '_index-3-0' + // schemaPath: '_index-3-0' + // indexPath: '3-0' + label: 'Unnamed Tab', + fields: [ + { + // path: 'fieldWithinUnnamedTab' + // schemaPath: '_index-3-0.fieldWithinUnnamedTab' + // indexPath: '' + name: 'fieldWithinUnnamedTab', + type: 'text', + hooks: attachHooks('fieldWithinUnnamedTab'), + }, + { + // path: '_index-3-0-1' + // schemaPath: '_index-3-0-1' + // indexPath: '3-0-1' + type: 'tabs', + tabs: [ + { + // path: '_index-3-0-1-0' + // schemaPath: '_index-3-0-1-0' + // indexPath: '3-0-1-0' + label: 'Nested Unnamed Tab', + fields: [ + { + // path: 'fieldWithinNestedUnnamedTab' + // schemaPath: '_index-3-0-1-0.fieldWithinNestedUnnamedTab' + // indexPath: '' + name: 'fieldWithinNestedUnnamedTab', + type: 'text', + hooks: attachHooks('fieldWithinNestedUnnamedTab'), + }, + ], + }, + ], + }, + ], + }, + { + // path: 'namedTab' + // schemaPath: '_index-3.namedTab' + // indexPath: '' + label: 'Named Tab', + name: 'namedTab', + fields: [ + { + // path: 'namedTab.fieldWithinNamedTab' + // schemaPath: '_index-3.namedTab.fieldWithinNamedTab' + // indexPath: '' + name: 'fieldWithinNamedTab', + type: 'text', + hooks: attachHooks('fieldWithinNamedTab'), + }, + ], + }, + ], + }, + // create fields for the hooks to save data to + ...createFields([ + 'topLevelNamedField', + 'fieldWithinArray', + 'fieldWithinNestedArray', + 'fieldWithinRowWithinArray', + 'fieldWithinRow', + 'fieldWithinUnnamedTab', + 'fieldWithinNestedUnnamedTab', + 'fieldWithinNamedTab', + ]), + ], +} diff --git a/test/hooks/config.ts b/test/hooks/config.ts index d6699cd884..a3247abcae 100644 --- a/test/hooks/config.ts +++ b/test/hooks/config.ts @@ -13,12 +13,14 @@ import { BeforeValidateCollection } from './collections/BeforeValidate/index.js' import ChainingHooks from './collections/ChainingHooks/index.js' import ContextHooks from './collections/ContextHooks/index.js' import { DataHooks } from './collections/Data/index.js' +import { FieldPaths } from './collections/FieldPaths/index.js' import Hooks, { hooksSlug } from './collections/Hook/index.js' import NestedAfterReadHooks from './collections/NestedAfterReadHooks/index.js' import Relations from './collections/Relations/index.js' import TransformHooks from './collections/Transform/index.js' import Users, { seedHooksUsers } from './collections/Users/index.js' import { DataHooksGlobal } from './globals/Data/index.js' + export const HooksConfig: Promise = buildConfigWithDefaults({ admin: { importMap: { @@ -37,6 +39,7 @@ export const HooksConfig: Promise = buildConfigWithDefaults({ Relations, Users, DataHooks, + FieldPaths, ], globals: [DataHooksGlobal], endpoints: [ diff --git a/test/hooks/e2e.spec.ts b/test/hooks/e2e.spec.ts index ef9a68c56f..759c78eded 100644 --- a/test/hooks/e2e.spec.ts +++ b/test/hooks/e2e.spec.ts @@ -12,7 +12,7 @@ import { ensureCompilationIsDone, initPageConsoleErrorCatch, saveDocAndAssert } import { AdminUrlUtil } from '../helpers/adminUrlUtil.js' import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' import { TEST_TIMEOUT_LONG } from '../playwright.config.js' -import { beforeValidateSlug } from './collectionSlugs.js' +import { beforeValidateSlug } from './shared.js' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) diff --git a/test/hooks/int.spec.ts b/test/hooks/int.spec.ts index a4ce5ba694..8d21609870 100644 --- a/test/hooks/int.spec.ts +++ b/test/hooks/int.spec.ts @@ -21,7 +21,7 @@ import { import { relationsSlug } from './collections/Relations/index.js' import { transformSlug } from './collections/Transform/index.js' import { hooksUsersSlug } from './collections/Users/index.js' -import { beforeValidateSlug } from './collectionSlugs.js' +import { beforeValidateSlug, fieldPathsSlug } from './shared.js' import { HooksConfig } from './config.js' import { dataHooksGlobalSlug } from './globals/Data/index.js' @@ -517,6 +517,101 @@ describe('Hooks', () => { expect(doc.field_globalAndField).toStrictEqual(globalAndFieldString + globalAndFieldString) }) + + it('should pass correct field paths through field hooks', async () => { + const formatExpectedFieldPaths = ( + fieldIdentifier: string, + { + path, + schemaPath, + }: { + path: string[] + schemaPath: string[] + }, + ) => ({ + [`${fieldIdentifier}_beforeValidate_FieldPaths`]: { + path, + schemaPath, + }, + [`${fieldIdentifier}_beforeChange_FieldPaths`]: { + path, + schemaPath, + }, + [`${fieldIdentifier}_afterRead_FieldPaths`]: { + path, + schemaPath, + }, + [`${fieldIdentifier}_beforeDuplicate_FieldPaths`]: { + path, + schemaPath, + }, + }) + + const originalDoc = await payload.create({ + collection: fieldPathsSlug, + data: { + topLevelNamedField: 'Test', + array: [ + { + fieldWithinArray: 'Test', + nestedArray: [ + { + fieldWithinNestedArray: 'Test', + fieldWithinNestedRow: 'Test', + }, + ], + }, + ], + fieldWithinRow: 'Test', + fieldWithinUnnamedTab: 'Test', + namedTab: { + fieldWithinNamedTab: 'Test', + }, + fieldWithinNestedUnnamedTab: 'Test', + }, + }) + + // duplicate the doc to ensure that the beforeDuplicate hook is run + const doc = await payload.duplicate({ + id: originalDoc.id, + collection: fieldPathsSlug, + }) + + expect(doc).toMatchObject({ + ...formatExpectedFieldPaths('topLevelNamedField', { + path: ['topLevelNamedField'], + schemaPath: ['topLevelNamedField'], + }), + ...formatExpectedFieldPaths('fieldWithinArray', { + path: ['array', '0', 'fieldWithinArray'], + schemaPath: ['array', 'fieldWithinArray'], + }), + ...formatExpectedFieldPaths('fieldWithinNestedArray', { + path: ['array', '0', 'nestedArray', '0', 'fieldWithinNestedArray'], + schemaPath: ['array', 'nestedArray', 'fieldWithinNestedArray'], + }), + ...formatExpectedFieldPaths('fieldWithinRowWithinArray', { + path: ['array', '0', 'fieldWithinRowWithinArray'], + schemaPath: ['array', '_index-2', 'fieldWithinRowWithinArray'], + }), + ...formatExpectedFieldPaths('fieldWithinRow', { + path: ['fieldWithinRow'], + schemaPath: ['_index-2', 'fieldWithinRow'], + }), + ...formatExpectedFieldPaths('fieldWithinUnnamedTab', { + path: ['fieldWithinUnnamedTab'], + schemaPath: ['_index-3-0', 'fieldWithinUnnamedTab'], + }), + ...formatExpectedFieldPaths('fieldWithinNestedUnnamedTab', { + path: ['fieldWithinNestedUnnamedTab'], + schemaPath: ['_index-3-0-1-0', 'fieldWithinNestedUnnamedTab'], + }), + ...formatExpectedFieldPaths('fieldWithinNamedTab', { + path: ['namedTab', 'fieldWithinNamedTab'], + schemaPath: ['_index-3', 'namedTab', 'fieldWithinNamedTab'], + }), + }) + }) }) describe('config level after error hook', () => { diff --git a/test/hooks/payload-types.ts b/test/hooks/payload-types.ts index 7427e2bb24..73c4857ba2 100644 --- a/test/hooks/payload-types.ts +++ b/test/hooks/payload-types.ts @@ -22,6 +22,7 @@ export interface Config { relations: Relation; 'hooks-users': HooksUser; 'data-hooks': DataHook; + 'field-paths': FieldPath; 'payload-locked-documents': PayloadLockedDocument; 'payload-preferences': PayloadPreference; 'payload-migrations': PayloadMigration; @@ -39,6 +40,7 @@ export interface Config { relations: RelationsSelect | RelationsSelect; 'hooks-users': HooksUsersSelect | HooksUsersSelect; 'data-hooks': DataHooksSelect | DataHooksSelect; + 'field-paths': FieldPathsSelect | FieldPathsSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; @@ -236,6 +238,286 @@ export interface DataHook { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "field-paths". + */ +export interface FieldPath { + id: string; + topLevelNamedField?: string | null; + array?: + | { + fieldWithinArray?: string | null; + nestedArray?: + | { + fieldWithinNestedArray?: string | null; + id?: string | null; + }[] + | null; + id?: string | null; + }[] + | null; + fieldWithinRow?: string | null; + fieldWithinUnnamedTab?: string | null; + fieldWithinNestedUnnamedTab?: string | null; + namedTab?: { + fieldWithinNamedTab?: string | null; + }; + topLevelNamedField_beforeValidate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + topLevelNamedField_beforeChange_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + topLevelNamedField_afterRead_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + topLevelNamedField_beforeDuplicate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinArray_beforeValidate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinArray_beforeChange_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinArray_afterRead_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinArray_beforeDuplicate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNestedArray_beforeValidate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNestedArray_beforeChange_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNestedArray_afterRead_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNestedArray_beforeDuplicate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinRow_beforeValidate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinRow_beforeChange_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinRow_afterRead_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinRow_beforeDuplicate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinUnnamedTab_beforeValidate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinUnnamedTab_beforeChange_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinUnnamedTab_afterRead_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinUnnamedTab_beforeDuplicate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNestedUnnamedTab_beforeValidate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNestedUnnamedTab_beforeChange_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNestedUnnamedTab_afterRead_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNestedUnnamedTab_beforeDuplicate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNamedTab_beforeValidate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNamedTab_beforeChange_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNamedTab_afterRead_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + fieldWithinNamedTab_beforeDuplicate_FieldPaths?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-locked-documents". @@ -286,6 +568,10 @@ export interface PayloadLockedDocument { | ({ relationTo: 'data-hooks'; value: string | DataHook; + } | null) + | ({ + relationTo: 'field-paths'; + value: string | FieldPath; } | null); globalSlug?: string | null; user: { @@ -470,6 +756,63 @@ export interface DataHooksSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "field-paths_select". + */ +export interface FieldPathsSelect { + topLevelNamedField?: T; + array?: + | T + | { + fieldWithinArray?: T; + nestedArray?: + | T + | { + fieldWithinNestedArray?: T; + id?: T; + }; + id?: T; + }; + fieldWithinRow?: T; + fieldWithinUnnamedTab?: T; + fieldWithinNestedUnnamedTab?: T; + namedTab?: + | T + | { + fieldWithinNamedTab?: T; + }; + topLevelNamedField_beforeValidate_FieldPaths?: T; + topLevelNamedField_beforeChange_FieldPaths?: T; + topLevelNamedField_afterRead_FieldPaths?: T; + topLevelNamedField_beforeDuplicate_FieldPaths?: T; + fieldWithinArray_beforeValidate_FieldPaths?: T; + fieldWithinArray_beforeChange_FieldPaths?: T; + fieldWithinArray_afterRead_FieldPaths?: T; + fieldWithinArray_beforeDuplicate_FieldPaths?: T; + fieldWithinNestedArray_beforeValidate_FieldPaths?: T; + fieldWithinNestedArray_beforeChange_FieldPaths?: T; + fieldWithinNestedArray_afterRead_FieldPaths?: T; + fieldWithinNestedArray_beforeDuplicate_FieldPaths?: T; + fieldWithinRow_beforeValidate_FieldPaths?: T; + fieldWithinRow_beforeChange_FieldPaths?: T; + fieldWithinRow_afterRead_FieldPaths?: T; + fieldWithinRow_beforeDuplicate_FieldPaths?: T; + fieldWithinUnnamedTab_beforeValidate_FieldPaths?: T; + fieldWithinUnnamedTab_beforeChange_FieldPaths?: T; + fieldWithinUnnamedTab_afterRead_FieldPaths?: T; + fieldWithinUnnamedTab_beforeDuplicate_FieldPaths?: T; + fieldWithinNestedUnnamedTab_beforeValidate_FieldPaths?: T; + fieldWithinNestedUnnamedTab_beforeChange_FieldPaths?: T; + fieldWithinNestedUnnamedTab_afterRead_FieldPaths?: T; + fieldWithinNestedUnnamedTab_beforeDuplicate_FieldPaths?: T; + fieldWithinNamedTab_beforeValidate_FieldPaths?: T; + fieldWithinNamedTab_beforeChange_FieldPaths?: T; + fieldWithinNamedTab_afterRead_FieldPaths?: T; + fieldWithinNamedTab_beforeDuplicate_FieldPaths?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-locked-documents_select". diff --git a/test/hooks/collectionSlugs.ts b/test/hooks/shared.ts similarity index 54% rename from test/hooks/collectionSlugs.ts rename to test/hooks/shared.ts index 6a4647bffa..141f1aca63 100644 --- a/test/hooks/collectionSlugs.ts +++ b/test/hooks/shared.ts @@ -1 +1,2 @@ export const beforeValidateSlug = 'before-validate' +export const fieldPathsSlug = 'field-paths' diff --git a/tsconfig.base.json b/tsconfig.base.json index c461af5dcb..06a27a30e1 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -31,7 +31,7 @@ } ], "paths": { - "@payload-config": ["./test/admin/config.ts"], + "@payload-config": ["./test/hooks/config.ts"], "@payloadcms/live-preview": ["./packages/live-preview/src"], "@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"], "@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],