Skip to content

Commit d60ea6e

Browse files
authored
fix: previous value undefined for nested fields in afterChange (#14582)
### What? Updates the `previousValue` data sent to the `afterChange` hook to accomodate nested paths. ### Why? Currently the `afterChange` hook returns this: ```ts previousValue: previousDoc?.[field.name], ``` For any nested fields, this breaks because it does not match the structure of the incoming data. ### How? Uses the path segments to correctly extract the relevant data. **Reported by client.**
1 parent 8f7ef35 commit d60ea6e

File tree

5 files changed

+165
-2
lines changed

5 files changed

+165
-2
lines changed

packages/payload/src/fields/hooks/afterChange/promise.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ export const promise = async ({
7070
const pathSegments = path ? path.split('.') : []
7171
const schemaPathSegments = schemaPath ? schemaPath.split('.') : []
7272
const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : []
73+
const getNestedValue = (data: JsonObject, path: string[]) =>
74+
path.reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), data)
7375

7476
if (fieldAffectsData(field)) {
7577
// Execute hooks
@@ -88,12 +90,12 @@ export const promise = async ({
8890
path: pathSegments,
8991
previousDoc,
9092
previousSiblingDoc,
91-
previousValue: previousDoc?.[field.name],
93+
previousValue: getNestedValue(previousDoc, pathSegments) ?? previousDoc?.[field.name],
9294
req,
9395
schemaPath: schemaPathSegments,
9496
siblingData,
9597
siblingFields: siblingFields!,
96-
value: siblingDoc?.[field.name],
98+
value: getNestedValue(siblingDoc, pathSegments) ?? siblingDoc?.[field.name],
9799
})
98100

99101
if (hookedValue !== undefined) {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { CollectionConfig } from 'payload'
2+
export const nestedAfterChangeHooksSlug = 'nested-after-change-hooks'
3+
4+
const NestedAfterChangeHooks: CollectionConfig = {
5+
slug: nestedAfterChangeHooksSlug,
6+
fields: [
7+
{
8+
type: 'text',
9+
name: 'text',
10+
},
11+
{
12+
type: 'group',
13+
name: 'group',
14+
fields: [
15+
{
16+
type: 'array',
17+
name: 'array',
18+
fields: [
19+
{
20+
type: 'text',
21+
name: 'nestedAfterChange',
22+
hooks: {
23+
afterChange: [
24+
({ previousValue, operation }) => {
25+
console.log(previousValue)
26+
if (operation === 'update' && typeof previousValue === 'undefined') {
27+
throw new Error('previousValue is missing in nested beforeChange hook')
28+
}
29+
},
30+
],
31+
},
32+
},
33+
],
34+
},
35+
],
36+
},
37+
],
38+
}
39+
40+
export default NestedAfterChangeHooks

test/hooks/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import ContextHooks from './collections/ContextHooks/index.js'
1515
import { DataHooks } from './collections/Data/index.js'
1616
import { FieldPaths } from './collections/FieldPaths/index.js'
1717
import Hooks, { hooksSlug } from './collections/Hook/index.js'
18+
import NestedAfterChangeHooks from './collections/NestedAfterChangeHook/index.js'
1819
import NestedAfterReadHooks from './collections/NestedAfterReadHooks/index.js'
1920
import Relations from './collections/Relations/index.js'
2021
import TransformHooks from './collections/Transform/index.js'
@@ -36,6 +37,7 @@ export const HooksConfig: Promise<SanitizedConfig> = buildConfigWithDefaults({
3637
TransformHooks,
3738
Hooks,
3839
NestedAfterReadHooks,
40+
NestedAfterChangeHooks,
3941
ChainingHooks,
4042
Relations,
4143
Users,

test/hooks/int.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { chainingHooksSlug } from './collections/ChainingHooks/index.js'
1414
import { contextHooksSlug } from './collections/ContextHooks/index.js'
1515
import { dataHooksSlug } from './collections/Data/index.js'
1616
import { hooksSlug } from './collections/Hook/index.js'
17+
import { nestedAfterChangeHooksSlug } from './collections/NestedAfterChangeHook/index.js'
1718
import {
1819
generatedAfterReadText,
1920
nestedAfterReadHooksSlug,
@@ -328,6 +329,40 @@ describe('Hooks', () => {
328329

329330
expect(retrievedDoc.value).toEqual('data from REST API')
330331
})
332+
333+
it('should populate previousValue in nested afterChange hooks', async () => {
334+
// this collection will throw an error if previousValue is not defined in nested afterChange hook
335+
const nestedAfterChangeDoc = await payload.create({
336+
collection: nestedAfterChangeHooksSlug,
337+
data: {
338+
text: 'initial',
339+
group: {
340+
array: [
341+
{
342+
nestedAfterChange: 'initial',
343+
},
344+
],
345+
},
346+
},
347+
})
348+
349+
const updatedDoc = await payload.update({
350+
collection: 'nested-after-change-hooks',
351+
id: nestedAfterChangeDoc.id,
352+
data: {
353+
text: 'updated',
354+
group: {
355+
array: [
356+
{
357+
nestedAfterChange: 'updated',
358+
},
359+
],
360+
},
361+
},
362+
})
363+
364+
expect(updatedDoc).toBeDefined()
365+
})
331366
})
332367

333368
describe('auth collection hooks', () => {

test/hooks/payload-types.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,14 @@ export interface Config {
7474
transforms: Transform;
7575
hooks: Hook;
7676
'nested-after-read-hooks': NestedAfterReadHook;
77+
'nested-after-change-hooks': NestedAfterChangeHook;
7778
'chaining-hooks': ChainingHook;
7879
relations: Relation;
7980
'hooks-users': HooksUser;
8081
'data-hooks': DataHook;
8182
'field-paths': FieldPath;
8283
'value-hooks': ValueHook;
84+
'payload-kv': PayloadKv;
8385
'payload-locked-documents': PayloadLockedDocument;
8486
'payload-preferences': PayloadPreference;
8587
'payload-migrations': PayloadMigration;
@@ -93,12 +95,14 @@ export interface Config {
9395
transforms: TransformsSelect<false> | TransformsSelect<true>;
9496
hooks: HooksSelect<false> | HooksSelect<true>;
9597
'nested-after-read-hooks': NestedAfterReadHooksSelect<false> | NestedAfterReadHooksSelect<true>;
98+
'nested-after-change-hooks': NestedAfterChangeHooksSelect<false> | NestedAfterChangeHooksSelect<true>;
9699
'chaining-hooks': ChainingHooksSelect<false> | ChainingHooksSelect<true>;
97100
relations: RelationsSelect<false> | RelationsSelect<true>;
98101
'hooks-users': HooksUsersSelect<false> | HooksUsersSelect<true>;
99102
'data-hooks': DataHooksSelect<false> | DataHooksSelect<true>;
100103
'field-paths': FieldPathsSelect<false> | FieldPathsSelect<true>;
101104
'value-hooks': ValueHooksSelect<false> | ValueHooksSelect<true>;
105+
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
102106
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
103107
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
104108
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@@ -251,6 +255,24 @@ export interface Relation {
251255
updatedAt: string;
252256
createdAt: string;
253257
}
258+
/**
259+
* This interface was referenced by `Config`'s JSON-Schema
260+
* via the `definition` "nested-after-change-hooks".
261+
*/
262+
export interface NestedAfterChangeHook {
263+
id: string;
264+
text?: string | null;
265+
group?: {
266+
array?:
267+
| {
268+
nestedAfterChange?: string | null;
269+
id?: string | null;
270+
}[]
271+
| null;
272+
};
273+
updatedAt: string;
274+
createdAt: string;
275+
}
254276
/**
255277
* This interface was referenced by `Config`'s JSON-Schema
256278
* via the `definition` "chaining-hooks".
@@ -278,6 +300,13 @@ export interface HooksUser {
278300
hash?: string | null;
279301
loginAttempts?: number | null;
280302
lockUntil?: string | null;
303+
sessions?:
304+
| {
305+
id: string;
306+
createdAt?: string | null;
307+
expiresAt: string;
308+
}[]
309+
| null;
281310
password?: string | null;
282311
}
283312
/**
@@ -625,6 +654,23 @@ export interface ValueHook {
625654
updatedAt: string;
626655
createdAt: string;
627656
}
657+
/**
658+
* This interface was referenced by `Config`'s JSON-Schema
659+
* via the `definition` "payload-kv".
660+
*/
661+
export interface PayloadKv {
662+
id: string;
663+
key: string;
664+
data:
665+
| {
666+
[k: string]: unknown;
667+
}
668+
| unknown[]
669+
| string
670+
| number
671+
| boolean
672+
| null;
673+
}
628674
/**
629675
* This interface was referenced by `Config`'s JSON-Schema
630676
* via the `definition` "payload-locked-documents".
@@ -660,6 +706,10 @@ export interface PayloadLockedDocument {
660706
relationTo: 'nested-after-read-hooks';
661707
value: string | NestedAfterReadHook;
662708
} | null)
709+
| ({
710+
relationTo: 'nested-after-change-hooks';
711+
value: string | NestedAfterChangeHook;
712+
} | null)
663713
| ({
664714
relationTo: 'chaining-hooks';
665715
value: string | ChainingHook;
@@ -817,6 +867,25 @@ export interface NestedAfterReadHooksSelect<T extends boolean = true> {
817867
updatedAt?: T;
818868
createdAt?: T;
819869
}
870+
/**
871+
* This interface was referenced by `Config`'s JSON-Schema
872+
* via the `definition` "nested-after-change-hooks_select".
873+
*/
874+
export interface NestedAfterChangeHooksSelect<T extends boolean = true> {
875+
text?: T;
876+
group?:
877+
| T
878+
| {
879+
array?:
880+
| T
881+
| {
882+
nestedAfterChange?: T;
883+
id?: T;
884+
};
885+
};
886+
updatedAt?: T;
887+
createdAt?: T;
888+
}
820889
/**
821890
* This interface was referenced by `Config`'s JSON-Schema
822891
* via the `definition` "chaining-hooks_select".
@@ -851,6 +920,13 @@ export interface HooksUsersSelect<T extends boolean = true> {
851920
hash?: T;
852921
loginAttempts?: T;
853922
lockUntil?: T;
923+
sessions?:
924+
| T
925+
| {
926+
id?: T;
927+
createdAt?: T;
928+
expiresAt?: T;
929+
};
854930
}
855931
/**
856932
* This interface was referenced by `Config`'s JSON-Schema
@@ -940,6 +1016,14 @@ export interface ValueHooksSelect<T extends boolean = true> {
9401016
updatedAt?: T;
9411017
createdAt?: T;
9421018
}
1019+
/**
1020+
* This interface was referenced by `Config`'s JSON-Schema
1021+
* via the `definition` "payload-kv_select".
1022+
*/
1023+
export interface PayloadKvSelect<T extends boolean = true> {
1024+
key?: T;
1025+
data?: T;
1026+
}
9431027
/**
9441028
* This interface was referenced by `Config`'s JSON-Schema
9451029
* via the `definition` "payload-locked-documents_select".

0 commit comments

Comments
 (0)