fix: handle undefined values in afterChange hooks when read:false and create:true on the field level access for parents and siblings (#12664)

<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

-->

### What?

Fixes a bug where `afterChange` hooks would attempt to access values for
fields that are `read: false` but `create: true`, resulting in
`undefined` values and unexpected behavior.

### Why?

In scenarios where access control allows field creation (`create: true`)
but disallows reading it (`read: false`), hooks like `afterChange` would
still attempt to operate on `undefined` values from `siblingDoc` or
`previousDoc`, potentially causing errors or skipped logic.

### How?

Adds safe optional chaining and fallback object initialization in
`promise.ts` for:
- `previousDoc[field.name]`
- `siblingDoc[field.name]`
- Group, Array, and Block field traversals

This ensures that these values are treated as empty objects or arrays
where appropriate to prevent runtime errors during traversal or hook
execution.

Fixes https://github.com/payloadcms/payload/issues/12660

---------

Co-authored-by: Niall Bambury <niall.bambury@cuckoo.co>
This commit is contained in:
iamacup
2025-07-18 13:34:54 +01:00
committed by GitHub
parent c08b2aea89
commit 46d8a26b0d

View File

@@ -88,12 +88,12 @@ export const promise = async ({
path: pathSegments,
previousDoc,
previousSiblingDoc,
previousValue: previousDoc[field.name!],
previousValue: previousDoc?.[field.name!],
req,
schemaPath: schemaPathSegments,
siblingData,
siblingFields: siblingFields!,
value: siblingDoc[field.name!],
value: siblingDoc?.[field.name!],
})
if (hookedValue !== undefined) {
@@ -226,10 +226,10 @@ export const promise = async ({
parentPath: path,
parentSchemaPath: schemaPath,
previousDoc,
previousSiblingDoc: previousDoc[field.name] as JsonObject,
previousSiblingDoc: (previousDoc?.[field.name] as JsonObject) || {},
req,
siblingData: (siblingData?.[field.name] as JsonObject) || {},
siblingDoc: siblingDoc[field.name] as JsonObject,
siblingDoc: (siblingDoc?.[field.name] as JsonObject) || {},
})
} else {
await traverseFields({
@@ -282,11 +282,11 @@ export const promise = async ({
path: pathSegments,
previousDoc,
previousSiblingDoc,
previousValue: previousDoc[field.name],
previousValue: previousDoc?.[field.name],
req,
schemaPath: schemaPathSegments,
siblingData,
value: siblingDoc[field.name],
value: siblingDoc?.[field.name],
})
if (hookedValue !== undefined) {
@@ -305,9 +305,9 @@ export const promise = async ({
const isNamedTab = tabHasName(field)
if (isNamedTab) {
tabSiblingData = (siblingData[field.name] as JsonObject) ?? {}
tabSiblingDoc = (siblingDoc[field.name] as JsonObject) ?? {}
tabPreviousSiblingDoc = (previousDoc[field.name] as JsonObject) ?? {}
tabSiblingData = (siblingData?.[field.name] ?? {}) as JsonObject
tabSiblingDoc = (siblingDoc?.[field.name] ?? {}) as JsonObject
tabPreviousSiblingDoc = (previousDoc?.[field.name] ?? {}) as JsonObject
}
await traverseFields({