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:
Elliot DeNolf
2022-09-12 15:48:50 -07:00
committed by GitHub
parent 94d355ba5e
commit d5ccd45b53
16 changed files with 97 additions and 13 deletions

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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
*/

View File

@@ -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;

View File

@@ -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());

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
});

View File

@@ -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,

View File

@@ -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,

View File

@@ -24,6 +24,7 @@ export type BeforeChangeHook = (args: {
export type AfterChangeHook = (args: {
doc: any;
previousDoc: any;
req: PayloadRequest;
}) => any;

View File

@@ -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());

View File

@@ -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());

View File

@@ -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;
},

View File

@@ -0,0 +1 @@
export default {}