feat: global beforeOperation hook (#13768)
Adds support for the `beforeOperation` hook on globals. Runs before all
other hooks to either modify the arguments that operations receive, or
perform side-effects before an operation begins.
```ts
import type { GlobalConfig } from 'payload'
const MyGlobal: GlobalConfig = {
// ...
hooks: {
beforeOperation: []
}
}
```
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211317005907890
This commit is contained in:
@@ -67,8 +67,6 @@ export const CollectionWithHooks: CollectionConfig = {
|
|||||||
|
|
||||||
The `beforeOperation` hook can be used to modify the arguments that operations accept or execute side-effects that run before an operation begins.
|
The `beforeOperation` hook can be used to modify the arguments that operations accept or execute side-effects that run before an operation begins.
|
||||||
|
|
||||||
Available Collection operations include `create`, `read`, `update`, `delete`, `login`, `refresh`, and `forgotPassword`.
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import type { CollectionBeforeOperationHook } from 'payload'
|
import type { CollectionBeforeOperationHook } from 'payload'
|
||||||
|
|
||||||
@@ -83,12 +81,12 @@ const beforeOperationHook: CollectionBeforeOperationHook = async ({
|
|||||||
|
|
||||||
The following arguments are provided to the `beforeOperation` hook:
|
The following arguments are provided to the `beforeOperation` hook:
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. |
|
| **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. Available options include: `autosave`, `count`, `countVersions`, `create`, `delete`, `forgotPassword`, `login`, `read`, `readDistinct`, `refresh`, `resetPassword`, `restoreVersion`, and `update`. |
|
||||||
| **`context`** | Custom context passed between Hooks. [More details](./context). |
|
| **`context`** | Custom context passed between Hooks. [More details](./context). |
|
||||||
| **`operation`** | The name of the operation that this hook is running within. |
|
| **`operation`** | The name of the operation that this hook is running within. |
|
||||||
| **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. |
|
| **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. |
|
||||||
|
|
||||||
### beforeValidate
|
### beforeValidate
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const GlobalWithHooks: GlobalConfig = {
|
|||||||
// ...
|
// ...
|
||||||
// highlight-start
|
// highlight-start
|
||||||
hooks: {
|
hooks: {
|
||||||
|
beforeOperation: [(args) => {...}],
|
||||||
beforeValidate: [(args) => {...}],
|
beforeValidate: [(args) => {...}],
|
||||||
beforeChange: [(args) => {...}],
|
beforeChange: [(args) => {...}],
|
||||||
beforeRead: [(args) => {...}],
|
beforeRead: [(args) => {...}],
|
||||||
@@ -48,6 +49,31 @@ const GlobalWithHooks: GlobalConfig = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### beforeOperation
|
||||||
|
|
||||||
|
The `beforeOperation` hook can be used to modify the arguments that operations accept or execute side-effects that run before an operation begins.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { GlobalBeforeOperationHook } from 'payload'
|
||||||
|
|
||||||
|
const beforeOperationHook: GlobalBeforeOperationHook = async ({
|
||||||
|
args,
|
||||||
|
operation,
|
||||||
|
req,
|
||||||
|
}) => {
|
||||||
|
return args // return modified operation arguments as necessary
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The following arguments are provided to the `beforeOperation` hook:
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| **`global`** | The [Global](../configuration/globals) in which this Hook is running against. Available operation include: `countVersions`, `read`, `restoreVersion`, and `update`. |
|
||||||
|
| **`context`** | Custom context passed between Hooks. [More details](./context). |
|
||||||
|
| **`operation`** | The name of the operation that this hook is running within. |
|
||||||
|
| **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. |
|
||||||
|
|
||||||
### beforeValidate
|
### beforeValidate
|
||||||
|
|
||||||
Runs during the `update` operation. This hook allows you to add or format data before the incoming data is validated server-side.
|
Runs during the `update` operation. This hook allows you to add or format data before the incoming data is validated server-side.
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ type CreateOrUpdateOperation = Extract<HookOperationType, 'create' | 'update'>
|
|||||||
|
|
||||||
export type BeforeOperationHook = (args: {
|
export type BeforeOperationHook = (args: {
|
||||||
args?: any
|
args?: any
|
||||||
/** The collection which this hook is being run on */
|
/**
|
||||||
|
* The collection which this hook is being run on
|
||||||
|
*/
|
||||||
collection: SanitizedCollectionConfig
|
collection: SanitizedCollectionConfig
|
||||||
context: RequestContext
|
context: RequestContext
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const sanitizeGlobal = async (
|
|||||||
if (global._sanitized) {
|
if (global._sanitized) {
|
||||||
return global as SanitizedGlobalConfig
|
return global as SanitizedGlobalConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
global._sanitized = true
|
global._sanitized = true
|
||||||
|
|
||||||
global.label = global.label || toWords(global.slug)
|
global.label = global.label || toWords(global.slug)
|
||||||
@@ -33,12 +34,15 @@ export const sanitizeGlobal = async (
|
|||||||
// /////////////////////////////////
|
// /////////////////////////////////
|
||||||
|
|
||||||
global.endpoints = global.endpoints ?? []
|
global.endpoints = global.endpoints ?? []
|
||||||
|
|
||||||
if (!global.hooks) {
|
if (!global.hooks) {
|
||||||
global.hooks = {}
|
global.hooks = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.access) {
|
if (!global.access) {
|
||||||
global.access = {}
|
global.access = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.admin) {
|
if (!global.admin) {
|
||||||
global.admin = {}
|
global.admin = {}
|
||||||
}
|
}
|
||||||
@@ -46,6 +50,7 @@ export const sanitizeGlobal = async (
|
|||||||
if (!global.access.read) {
|
if (!global.access.read) {
|
||||||
global.access.read = defaultAccess
|
global.access.read = defaultAccess
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.access.update) {
|
if (!global.access.update) {
|
||||||
global.access.update = defaultAccess
|
global.access.update = defaultAccess
|
||||||
}
|
}
|
||||||
@@ -53,15 +58,19 @@ export const sanitizeGlobal = async (
|
|||||||
if (!global.hooks.beforeValidate) {
|
if (!global.hooks.beforeValidate) {
|
||||||
global.hooks.beforeValidate = []
|
global.hooks.beforeValidate = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.hooks.beforeChange) {
|
if (!global.hooks.beforeChange) {
|
||||||
global.hooks.beforeChange = []
|
global.hooks.beforeChange = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.hooks.afterChange) {
|
if (!global.hooks.afterChange) {
|
||||||
global.hooks.afterChange = []
|
global.hooks.afterChange = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.hooks.beforeRead) {
|
if (!global.hooks.beforeRead) {
|
||||||
global.hooks.beforeRead = []
|
global.hooks.beforeRead = []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.hooks.afterRead) {
|
if (!global.hooks.afterRead) {
|
||||||
global.hooks.afterRead = []
|
global.hooks.afterRead = []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,22 @@ export type AfterReadHook = (args: {
|
|||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
}) => any
|
}) => any
|
||||||
|
|
||||||
|
export type HookOperationType = 'countVersions' | 'read' | 'restoreVersion' | 'update'
|
||||||
|
|
||||||
|
export type BeforeOperationHook = (args: {
|
||||||
|
args?: any
|
||||||
|
context: RequestContext
|
||||||
|
/**
|
||||||
|
* The Global which this hook is being run on
|
||||||
|
* */
|
||||||
|
global: SanitizedGlobalConfig
|
||||||
|
/**
|
||||||
|
* Hook operation being performed
|
||||||
|
*/
|
||||||
|
operation: HookOperationType
|
||||||
|
req: PayloadRequest
|
||||||
|
}) => any
|
||||||
|
|
||||||
export type GlobalAdminOptions = {
|
export type GlobalAdminOptions = {
|
||||||
/**
|
/**
|
||||||
* Custom admin components
|
* Custom admin components
|
||||||
@@ -187,6 +203,7 @@ export type GlobalConfig<TSlug extends GlobalSlug = any> = {
|
|||||||
afterChange?: AfterChangeHook[]
|
afterChange?: AfterChangeHook[]
|
||||||
afterRead?: AfterReadHook[]
|
afterRead?: AfterReadHook[]
|
||||||
beforeChange?: BeforeChangeHook[]
|
beforeChange?: BeforeChangeHook[]
|
||||||
|
beforeOperation?: BeforeOperationHook[]
|
||||||
beforeRead?: BeforeReadHook[]
|
beforeRead?: BeforeReadHook[]
|
||||||
beforeValidate?: BeforeValidateHook[]
|
beforeValidate?: BeforeValidateHook[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,23 @@ export const countGlobalVersionsOperation = async <TSlug extends GlobalSlug>(
|
|||||||
const req = args.req!
|
const req = args.req!
|
||||||
const { payload } = req
|
const { payload } = req
|
||||||
|
|
||||||
|
// /////////////////////////////////////
|
||||||
|
// beforeOperation - Global
|
||||||
|
// /////////////////////////////////////
|
||||||
|
|
||||||
|
if (global.hooks?.beforeOperation?.length) {
|
||||||
|
for (const hook of global.hooks.beforeOperation) {
|
||||||
|
args =
|
||||||
|
(await hook({
|
||||||
|
args,
|
||||||
|
context: req.context,
|
||||||
|
global,
|
||||||
|
operation: 'countVersions',
|
||||||
|
req,
|
||||||
|
})) || args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
// Access
|
// Access
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
|
|||||||
@@ -52,6 +52,23 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
|
|||||||
} = args
|
} = args
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// /////////////////////////////////////
|
||||||
|
// beforeOperation - Global
|
||||||
|
// /////////////////////////////////////
|
||||||
|
|
||||||
|
if (globalConfig.hooks?.beforeOperation?.length) {
|
||||||
|
for (const hook of globalConfig.hooks.beforeOperation) {
|
||||||
|
args =
|
||||||
|
(await hook({
|
||||||
|
args,
|
||||||
|
context: args.req.context,
|
||||||
|
global: globalConfig,
|
||||||
|
operation: 'read',
|
||||||
|
req: args.req,
|
||||||
|
})) || args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
// Retrieve and execute access
|
// Retrieve and execute access
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
|
|||||||
@@ -31,6 +31,23 @@ export const restoreVersionOperation = async <T extends TypeWithVersion<T> = any
|
|||||||
try {
|
try {
|
||||||
const shouldCommit = await initTransaction(req)
|
const shouldCommit = await initTransaction(req)
|
||||||
|
|
||||||
|
// /////////////////////////////////////
|
||||||
|
// beforeOperation - Global
|
||||||
|
// /////////////////////////////////////
|
||||||
|
|
||||||
|
if (globalConfig.hooks?.beforeOperation?.length) {
|
||||||
|
for (const hook of globalConfig.hooks.beforeOperation) {
|
||||||
|
args =
|
||||||
|
(await hook({
|
||||||
|
args,
|
||||||
|
context: req.context,
|
||||||
|
global: globalConfig,
|
||||||
|
operation: 'restoreVersion',
|
||||||
|
req,
|
||||||
|
})) || args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
// Access
|
// Access
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
|
|||||||
@@ -77,6 +77,23 @@ export const updateOperation = async <
|
|||||||
try {
|
try {
|
||||||
const shouldCommit = !disableTransaction && (await initTransaction(req))
|
const shouldCommit = !disableTransaction && (await initTransaction(req))
|
||||||
|
|
||||||
|
// /////////////////////////////////////
|
||||||
|
// beforeOperation - Global
|
||||||
|
// /////////////////////////////////////
|
||||||
|
|
||||||
|
if (globalConfig.hooks?.beforeOperation?.length) {
|
||||||
|
for (const hook of globalConfig.hooks.beforeOperation) {
|
||||||
|
args =
|
||||||
|
(await hook({
|
||||||
|
args,
|
||||||
|
context: args.req.context,
|
||||||
|
global: globalConfig,
|
||||||
|
operation: 'update',
|
||||||
|
req: args.req,
|
||||||
|
})) || args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let { data } = args
|
let { data } = args
|
||||||
|
|
||||||
const shouldSaveDraft = Boolean(draftArg && globalConfig.versions?.drafts)
|
const shouldSaveDraft = Boolean(draftArg && globalConfig.versions?.drafts)
|
||||||
|
|||||||
@@ -1575,6 +1575,7 @@ export type {
|
|||||||
AfterChangeHook as GlobalAfterChangeHook,
|
AfterChangeHook as GlobalAfterChangeHook,
|
||||||
AfterReadHook as GlobalAfterReadHook,
|
AfterReadHook as GlobalAfterReadHook,
|
||||||
BeforeChangeHook as GlobalBeforeChangeHook,
|
BeforeChangeHook as GlobalBeforeChangeHook,
|
||||||
|
BeforeOperationHook as GlobalBeforeOperationHook,
|
||||||
BeforeReadHook as GlobalBeforeReadHook,
|
BeforeReadHook as GlobalBeforeReadHook,
|
||||||
BeforeValidateHook as GlobalBeforeValidateHook,
|
BeforeValidateHook as GlobalBeforeValidateHook,
|
||||||
DataFromGlobalSlug,
|
DataFromGlobalSlug,
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ describe('Hooks', () => {
|
|||||||
const document = await payload.create({
|
const document = await payload.create({
|
||||||
collection: contextHooksSlug,
|
collection: contextHooksSlug,
|
||||||
context: {
|
context: {
|
||||||
secretValue: 'data from local API',
|
secretValue: 'data from Local API',
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
value: 'wrongvalue',
|
value: 'wrongvalue',
|
||||||
@@ -289,28 +289,28 @@ describe('Hooks', () => {
|
|||||||
collection: contextHooksSlug,
|
collection: contextHooksSlug,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(retrievedDoc.value).toEqual('data from local API')
|
expect(retrievedDoc.value).toEqual('data from Local API')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should pass context from local API to global hooks', async () => {
|
it('should pass context from Local API to global hooks', async () => {
|
||||||
const globalDocument = await payload.findGlobal({
|
const globalDocument = await payload.findGlobal({
|
||||||
slug: dataHooksGlobalSlug,
|
slug: dataHooksGlobalSlug,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(globalDocument.field_globalAndField).not.toEqual('data from local API context')
|
expect(globalDocument.field_globalAndField).not.toEqual('data from Local API context')
|
||||||
|
|
||||||
const globalDocumentWithContext = await payload.findGlobal({
|
const globalDocumentWithContext = await payload.findGlobal({
|
||||||
slug: dataHooksGlobalSlug,
|
slug: dataHooksGlobalSlug,
|
||||||
context: {
|
context: {
|
||||||
field_beforeChange_GlobalAndField_override: 'data from local API context',
|
field_beforeChange_GlobalAndField_override: 'data from Local API context',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(globalDocumentWithContext.field_globalAndField).toEqual('data from local API context')
|
expect(globalDocumentWithContext.field_globalAndField).toEqual('data from Local API context')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should pass context from rest API to hooks', async () => {
|
it('should pass context from REST API to hooks', async () => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
context_secretValue: 'data from rest API',
|
context_secretValue: 'data from REST API',
|
||||||
})
|
})
|
||||||
// send context as query params. It will be parsed by the beforeOperation hook
|
// send context as query params. It will be parsed by the beforeOperation hook
|
||||||
const { doc } = await restClient
|
const { doc } = await restClient
|
||||||
@@ -326,7 +326,7 @@ describe('Hooks', () => {
|
|||||||
id: doc.id,
|
id: doc.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(retrievedDoc.value).toEqual('data from rest API')
|
expect(retrievedDoc.value).toEqual('data from REST API')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -426,15 +426,19 @@ describe('Hooks', () => {
|
|||||||
expect(JSON.parse(doc.collection_beforeOperation_collection)).toStrictEqual(
|
expect(JSON.parse(doc.collection_beforeOperation_collection)).toStrictEqual(
|
||||||
sanitizedHooksCollection,
|
sanitizedHooksCollection,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(JSON.parse(doc.collection_beforeChange_collection)).toStrictEqual(
|
expect(JSON.parse(doc.collection_beforeChange_collection)).toStrictEqual(
|
||||||
sanitizedHooksCollection,
|
sanitizedHooksCollection,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(JSON.parse(doc.collection_afterChange_collection)).toStrictEqual(
|
expect(JSON.parse(doc.collection_afterChange_collection)).toStrictEqual(
|
||||||
sanitizedHooksCollection,
|
sanitizedHooksCollection,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(JSON.parse(doc.collection_afterRead_collection)).toStrictEqual(
|
expect(JSON.parse(doc.collection_afterRead_collection)).toStrictEqual(
|
||||||
sanitizedHooksCollection,
|
sanitizedHooksCollection,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(JSON.parse(doc.collection_afterOperation_collection)).toStrictEqual(
|
expect(JSON.parse(doc.collection_afterOperation_collection)).toStrictEqual(
|
||||||
sanitizedHooksCollection,
|
sanitizedHooksCollection,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user