Ability to get the "previous" state in the AfterChange Hook (#1115)
Co-authored-by: Alessio Gravili <alessio@bonfireleads.com> Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
@@ -38,7 +38,7 @@ const ExampleHooks: CollectionConfig = {
|
||||
slug: 'example-hooks',
|
||||
fields: [
|
||||
{ name: 'name', type: 'text'},
|
||||
]
|
||||
],
|
||||
hooks: {
|
||||
beforeOperation: [(args) => {...}],
|
||||
beforeValidate: [(args) => {...}],
|
||||
@@ -56,7 +56,7 @@ const ExampleHooks: CollectionConfig = {
|
||||
afterRefresh: [(args) => {...}],
|
||||
afterMe: [(args) => {...}],
|
||||
afterForgotPassword: [(args) => {...}],
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -121,6 +121,7 @@ import { CollectionAfterChangeHook } from 'payload/types';
|
||||
const afterChangeHook: CollectionAfterChangeHook = async ({
|
||||
doc, // full document data
|
||||
req, // full express request
|
||||
previousDoc, // document data before updating the collection
|
||||
operation, // name of the operation ie. 'create', 'update'
|
||||
}) => {
|
||||
return doc;
|
||||
|
||||
@@ -56,15 +56,17 @@ All field-level hooks are formatted to accept the same arguments, although some
|
||||
|
||||
Field Hooks receive one `args` argument that contains the following properties:
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | -------------|
|
||||
| **`data`** | The data passed to update the document within `create` and `update` operations, and the full document itself in the `afterRead` hook. |
|
||||
| **`findMany`** | Boolean to denote if this hook is running against finding one, or finding many within the `afterRead` hook. |
|
||||
| **`operation`** | A string relating to which operation the field type is currently executing within. Useful within `beforeValidate`, `beforeChange`, and `afterChange` hooks to differentiate between `create` and `update` operations. |
|
||||
| **`originalDoc`** | The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. |
|
||||
| **`req`** | The Express `request` object. It is mocked for Local API operations. |
|
||||
| **`siblingData`** | The sibling data passed to a field that the hook is running against. |
|
||||
| **`value`** | The value of the field. |
|
||||
| Option | Description |
|
||||
|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`data`** | The data passed to update the document within `create` and `update` operations, and the full document itself in the `afterRead` hook. |
|
||||
| **`siblingData`** | The sibling data passed to a field that the hook is running against. |
|
||||
| **`findMany`** | Boolean to denote if this hook is running against finding one, or finding many within the `afterRead` hook. |
|
||||
| **`operation`** | A string relating to which operation the field type is currently executing within. Useful within `beforeValidate`, `beforeChange`, and `afterChange` hooks to differentiate between `create` and `update` operations. |
|
||||
| **`originalDoc`** | The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. |
|
||||
| **`previousDoc`** | The document before changes were applied, only in `afterChange` hooks. |
|
||||
| **`previousSiblingDoc`** | The sibling data from the previous document in `afterChange` hook. |
|
||||
| **`req`** | The Express `request` object. It is mocked for Local API operations. |
|
||||
| **`value`** | The value of the field. |
|
||||
|
||||
#### Return value
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ import { GlobalAfterChangeHook } from 'payload/types'
|
||||
|
||||
const afterChangeHook: GlobalAfterChangeHook = async ({
|
||||
doc, // full document data
|
||||
previousDoc, // document data before updating the collection
|
||||
req, // full express request
|
||||
}) => {
|
||||
return data;
|
||||
|
||||
@@ -79,6 +79,7 @@ export type BeforeChangeHook<T extends TypeWithID = any> = (args: {
|
||||
export type AfterChangeHook<T extends TypeWithID = any> = (args: {
|
||||
doc: T;
|
||||
req: PayloadRequest;
|
||||
previousDoc: T,
|
||||
/**
|
||||
* Hook operation being performed
|
||||
*/
|
||||
|
||||
@@ -252,6 +252,7 @@ async function create(incomingArgs: Arguments): Promise<Document> {
|
||||
result = await afterChange({
|
||||
data,
|
||||
doc: result,
|
||||
previousDoc: {},
|
||||
entityConfig: collectionConfig,
|
||||
operation: 'create',
|
||||
req,
|
||||
@@ -266,6 +267,7 @@ async function create(incomingArgs: Arguments): Promise<Document> {
|
||||
|
||||
result = await hook({
|
||||
doc: result,
|
||||
previousDoc: {},
|
||||
req: args.req,
|
||||
operation: 'create',
|
||||
}) || result;
|
||||
|
||||
@@ -94,6 +94,16 @@ async function restoreVersion<T extends TypeWithID = any>(args: Arguments): Prom
|
||||
if (!doc && !hasWherePolicy) throw new NotFound();
|
||||
if (!doc && hasWherePolicy) throw new Forbidden();
|
||||
|
||||
// /////////////////////////////////////
|
||||
// fetch previousDoc
|
||||
// /////////////////////////////////////
|
||||
|
||||
const previousDoc = await payload.findByID({
|
||||
collection: collectionConfig.slug,
|
||||
id: parentDocID,
|
||||
depth,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Update
|
||||
// /////////////////////////////////////
|
||||
@@ -145,6 +155,7 @@ async function restoreVersion<T extends TypeWithID = any>(args: Arguments): Prom
|
||||
result = await afterChange({
|
||||
data: result,
|
||||
doc: result,
|
||||
previousDoc,
|
||||
entityConfig: collectionConfig,
|
||||
operation: 'update',
|
||||
req,
|
||||
@@ -160,6 +171,7 @@ async function restoreVersion<T extends TypeWithID = any>(args: Arguments): Prom
|
||||
result = await hook({
|
||||
doc: result,
|
||||
req,
|
||||
previousDoc,
|
||||
operation: 'update',
|
||||
}) || result;
|
||||
}, Promise.resolve());
|
||||
|
||||
@@ -308,6 +308,7 @@ async function update(incomingArgs: Arguments): Promise<Document> {
|
||||
result = await afterChange({
|
||||
data,
|
||||
doc: result,
|
||||
previousDoc: originalDoc,
|
||||
entityConfig: collectionConfig,
|
||||
operation: 'update',
|
||||
req,
|
||||
@@ -322,6 +323,7 @@ async function update(incomingArgs: Arguments): Promise<Document> {
|
||||
|
||||
result = await hook({
|
||||
doc: result,
|
||||
previousDoc: originalDoc,
|
||||
req,
|
||||
operation: 'update',
|
||||
}) || result;
|
||||
|
||||
@@ -16,6 +16,10 @@ export type FieldHookArgs<T extends TypeWithID = any, P = any, S = any> = {
|
||||
findMany?: boolean
|
||||
/** The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. */
|
||||
originalDoc?: T,
|
||||
/** The document before changes were applied, only in `afterChange` hooks. */
|
||||
previousDoc?: T,
|
||||
/** The sibling data from the previous document in `afterChange` hook. */
|
||||
previousSiblingDoc?: T,
|
||||
/** A string relating to which operation the field type is currently executing within. Useful within beforeValidate, beforeChange, and afterChange hooks to differentiate between create and update operations. */
|
||||
operation?: 'create' | 'read' | 'update' | 'delete',
|
||||
/** The Express request object. It is mocked for Local API operations. */
|
||||
@@ -24,6 +28,7 @@ export type FieldHookArgs<T extends TypeWithID = any, P = any, S = any> = {
|
||||
siblingData: Partial<S>
|
||||
/** The value of the field. */
|
||||
value?: P,
|
||||
previousValue?: P,
|
||||
}
|
||||
|
||||
export type FieldHook<T extends TypeWithID = any, P = any, S = any> = (args: FieldHookArgs<T, P, S>) => Promise<P> | P;
|
||||
|
||||
@@ -7,6 +7,7 @@ import deepCopyObject from '../../../utilities/deepCopyObject';
|
||||
type Args = {
|
||||
data: Record<string, unknown>
|
||||
doc: Record<string, unknown>
|
||||
previousDoc: Record<string, unknown>
|
||||
entityConfig: SanitizedCollectionConfig | SanitizedGlobalConfig
|
||||
operation: 'create' | 'update'
|
||||
req: PayloadRequest
|
||||
@@ -15,6 +16,7 @@ type Args = {
|
||||
export const afterChange = async ({
|
||||
data,
|
||||
doc: incomingDoc,
|
||||
previousDoc,
|
||||
entityConfig,
|
||||
operation,
|
||||
req,
|
||||
@@ -24,9 +26,11 @@ export const afterChange = async ({
|
||||
await traverseFields({
|
||||
data,
|
||||
doc,
|
||||
previousDoc,
|
||||
fields: entityConfig.fields,
|
||||
operation,
|
||||
req,
|
||||
previousSiblingDoc: previousDoc,
|
||||
siblingDoc: doc,
|
||||
siblingData: data,
|
||||
});
|
||||
|
||||
@@ -6,6 +6,8 @@ import { traverseFields } from './traverseFields';
|
||||
type Args = {
|
||||
data: Record<string, unknown>
|
||||
doc: Record<string, unknown>
|
||||
previousDoc: Record<string, unknown>
|
||||
previousSiblingDoc: Record<string, unknown>
|
||||
field: Field | TabAsField
|
||||
operation: 'create' | 'update'
|
||||
req: PayloadRequest
|
||||
@@ -19,6 +21,8 @@ type Args = {
|
||||
export const promise = async ({
|
||||
data,
|
||||
doc,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
field,
|
||||
operation,
|
||||
req,
|
||||
@@ -34,6 +38,9 @@ export const promise = async ({
|
||||
const hookedValue = await currentHook({
|
||||
value: siblingData[field.name],
|
||||
originalDoc: doc,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
previousValue: previousDoc[field.name],
|
||||
data,
|
||||
siblingData,
|
||||
operation,
|
||||
@@ -53,6 +60,8 @@ export const promise = async ({
|
||||
await traverseFields({
|
||||
data,
|
||||
doc,
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc[field.name] as Record<string, unknown>,
|
||||
fields: field.fields,
|
||||
operation,
|
||||
req,
|
||||
@@ -72,6 +81,8 @@ export const promise = async ({
|
||||
promises.push(traverseFields({
|
||||
data,
|
||||
doc,
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc[field.name]?.[i] || {} as Record<string, unknown>,
|
||||
fields: field.fields,
|
||||
operation,
|
||||
req,
|
||||
@@ -96,6 +107,8 @@ export const promise = async ({
|
||||
promises.push(traverseFields({
|
||||
data,
|
||||
doc,
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc[field.name]?.[i] || {} as Record<string, unknown>,
|
||||
fields: block.fields,
|
||||
operation,
|
||||
req,
|
||||
@@ -115,6 +128,8 @@ export const promise = async ({
|
||||
await traverseFields({
|
||||
data,
|
||||
doc,
|
||||
previousDoc,
|
||||
previousSiblingDoc: { ...previousSiblingDoc },
|
||||
fields: field.fields,
|
||||
operation,
|
||||
req,
|
||||
@@ -128,10 +143,12 @@ export const promise = async ({
|
||||
case 'tab': {
|
||||
let tabSiblingData = siblingData;
|
||||
let tabSiblingDoc = siblingDoc;
|
||||
let tabPreviousSiblingDoc = siblingDoc;
|
||||
|
||||
if (tabHasName(field)) {
|
||||
tabSiblingData = siblingData[field.name] as Record<string, unknown>;
|
||||
tabSiblingDoc = siblingDoc[field.name] as Record<string, unknown>;
|
||||
tabPreviousSiblingDoc = previousDoc[field.name] as Record<string, unknown>;
|
||||
}
|
||||
|
||||
await traverseFields({
|
||||
@@ -140,6 +157,8 @@ export const promise = async ({
|
||||
fields: field.fields,
|
||||
operation,
|
||||
req,
|
||||
previousSiblingDoc: tabPreviousSiblingDoc,
|
||||
previousDoc,
|
||||
siblingData: tabSiblingData,
|
||||
siblingDoc: tabSiblingDoc,
|
||||
});
|
||||
@@ -151,6 +170,8 @@ export const promise = async ({
|
||||
await traverseFields({
|
||||
data,
|
||||
doc,
|
||||
previousDoc,
|
||||
previousSiblingDoc: { ...previousSiblingDoc },
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
operation,
|
||||
req,
|
||||
|
||||
@@ -5,6 +5,8 @@ import { PayloadRequest } from '../../../express/types';
|
||||
type Args = {
|
||||
data: Record<string, unknown>
|
||||
doc: Record<string, unknown>
|
||||
previousDoc: Record<string, unknown>
|
||||
previousSiblingDoc: Record<string, unknown>
|
||||
fields: (Field | TabAsField)[]
|
||||
operation: 'create' | 'update'
|
||||
req: PayloadRequest
|
||||
@@ -15,6 +17,8 @@ type Args = {
|
||||
export const traverseFields = async ({
|
||||
data,
|
||||
doc,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
fields,
|
||||
operation,
|
||||
req,
|
||||
@@ -27,6 +31,8 @@ export const traverseFields = async ({
|
||||
promises.push(promise({
|
||||
data,
|
||||
doc,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
field,
|
||||
operation,
|
||||
req,
|
||||
|
||||
@@ -24,6 +24,7 @@ export type BeforeChangeHook = (args: {
|
||||
|
||||
export type AfterChangeHook = (args: {
|
||||
doc: any;
|
||||
previousDoc: any;
|
||||
req: PayloadRequest;
|
||||
}) => any;
|
||||
|
||||
|
||||
@@ -58,6 +58,15 @@ async function restoreVersion<T extends TypeWithVersion<T> = any>(args: Argument
|
||||
|
||||
rawVersion = rawVersion.toJSON({ virtuals: true });
|
||||
|
||||
// /////////////////////////////////////
|
||||
// fetch previousDoc
|
||||
// /////////////////////////////////////
|
||||
|
||||
const previousDoc = await payload.findGlobal({
|
||||
slug: globalConfig.slug,
|
||||
depth,
|
||||
});
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Update global
|
||||
// /////////////////////////////////////
|
||||
@@ -118,6 +127,7 @@ async function restoreVersion<T extends TypeWithVersion<T> = any>(args: Argument
|
||||
result = await afterChange({
|
||||
data: result,
|
||||
doc: result,
|
||||
previousDoc,
|
||||
entityConfig: globalConfig,
|
||||
operation: 'update',
|
||||
req,
|
||||
@@ -132,6 +142,7 @@ async function restoreVersion<T extends TypeWithVersion<T> = any>(args: Argument
|
||||
|
||||
result = await hook({
|
||||
doc: result,
|
||||
previousDoc,
|
||||
req,
|
||||
}) || result;
|
||||
}, Promise.resolve());
|
||||
|
||||
@@ -239,6 +239,7 @@ async function update<T extends TypeWithID = any>(args: Args): Promise<T> {
|
||||
|
||||
global = await hook({
|
||||
doc: global,
|
||||
previousDoc: originalDoc,
|
||||
req,
|
||||
}) || global;
|
||||
}, Promise.resolve());
|
||||
@@ -250,6 +251,7 @@ async function update<T extends TypeWithID = any>(args: Args): Promise<T> {
|
||||
global = await afterChange({
|
||||
data,
|
||||
doc: global,
|
||||
previousDoc: originalDoc,
|
||||
entityConfig: globalConfig,
|
||||
operation: 'update',
|
||||
req,
|
||||
@@ -264,6 +266,7 @@ async function update<T extends TypeWithID = any>(args: Args): Promise<T> {
|
||||
|
||||
global = await hook({
|
||||
doc: global,
|
||||
previousDoc: originalDoc,
|
||||
req,
|
||||
}) || result;
|
||||
}, Promise.resolve());
|
||||
|
||||
@@ -13,7 +13,12 @@ const Hooks: CollectionConfig = {
|
||||
hooks: {
|
||||
beforeValidate: [({ data }) => validateHookOrder('collectionBeforeValidate', data)],
|
||||
beforeChange: [({ data }) => validateHookOrder('collectionBeforeChange', data)],
|
||||
afterChange: [({ doc }) => validateHookOrder('collectionAfterChange', doc)],
|
||||
afterChange: [({ doc, previousDoc }) => {
|
||||
if (!previousDoc) {
|
||||
throw new Error('previousDoc is missing in afterChange hook');
|
||||
}
|
||||
return validateHookOrder('collectionAfterChange', doc);
|
||||
}],
|
||||
beforeRead: [({ doc }) => validateHookOrder('collectionBeforeRead', doc)],
|
||||
afterRead: [({ doc }) => validateHookOrder('collectionAfterRead', doc)],
|
||||
},
|
||||
@@ -49,8 +54,14 @@ const Hooks: CollectionConfig = {
|
||||
type: 'checkbox',
|
||||
hooks: {
|
||||
afterChange: [
|
||||
({ data }) => {
|
||||
({ data, previousDoc, previousSiblingDoc }) => {
|
||||
data.fieldAfterChange = true;
|
||||
if (!previousDoc) {
|
||||
throw new Error('previousDoc is missing in afterChange hook');
|
||||
}
|
||||
if (!previousSiblingDoc) {
|
||||
throw new Error('previousSiblingDoc is missing in afterChange hook');
|
||||
}
|
||||
validateHookOrder('fieldAfterChange', data);
|
||||
return true;
|
||||
},
|
||||
|
||||
1
test/hooks/mocks/emptyModule.js
Normal file
1
test/hooks/mocks/emptyModule.js
Normal file
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
Reference in New Issue
Block a user