Skip to content

Commit 33686c6

Browse files
stefgootzenjacobsfletchAlessioGr
authored
feat: add afterOperation hook (#2697)
* feat: add afterOperation hook for Find operation * docs: change #afterOperation to #afteroperation * chore: extract afterOperation in function * chore: implement afterChange in operations * docs: use proper CollectionAfterOperationHook * chore: remove outdated info * chore: types afterOperation hook * chore: improves afterOperation tests * docs: updates description of afterOperation hook * chore: improve typings * chore: improve types * chore: rename index.tsx => index.ts --------- Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com> Co-authored-by: Alessio Gravili <alessio@gravili.de>
1 parent 6d6acbc commit 33686c6

File tree

20 files changed

+426
-67
lines changed

20 files changed

+426
-67
lines changed

docs/hooks/collections.mdx

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Collections feature the ability to define the following hooks:
1616
- [afterRead](#afterread)
1717
- [beforeDelete](#beforedelete)
1818
- [afterDelete](#afterdelete)
19+
- [afterOperation](#afteroperation)
1920

2021
Additionally, `auth`-enabled collections feature the following hooks:
2122

@@ -31,6 +32,7 @@ Additionally, `auth`-enabled collections feature the following hooks:
3132
All collection Hook properties accept arrays of synchronous or asynchronous functions. Each Hook type receives specific arguments and has the ability to modify specific outputs.
3233

3334
`collections/exampleHooks.js`
35+
3436
```ts
3537
import { CollectionConfig } from 'payload/types';
3638

@@ -48,6 +50,7 @@ export const ExampleHooks: CollectionConfig = {
4850
afterChange: [(args) => {...}],
4951
afterRead: [(args) => {...}],
5052
afterDelete: [(args) => {...}],
53+
afterOperation: [(args) => {...}],
5154

5255
// Auth-enabled hooks
5356
beforeLogin: [(args) => {...}],
@@ -62,19 +65,19 @@ export const ExampleHooks: CollectionConfig = {
6265

6366
### beforeOperation
6467

65-
The `beforeOperation` Hook type can be used to modify the arguments that operations accept or execute side-effects that run before an operation begins.
68+
The `beforeOperation` hook can be used to modify the arguments that operations accept or execute side-effects that run before an operation begins.
6669

67-
Available Collection operations include `create`, `read`, `update`, `delete`, `login`, `refresh` and `forgotPassword`.
70+
Available Collection operations include `create`, `read`, `update`, `delete`, `login`, `refresh`, and `forgotPassword`.
6871

6972
```ts
70-
import { CollectionBeforeOperationHook } from 'payload/types';
73+
import { CollectionBeforeOperationHook } from "payload/types";
7174

7275
const beforeOperationHook: CollectionBeforeOperationHook = async ({
73-
args, // Original arguments passed into the operation
76+
args, // original arguments passed into the operation
7477
operation, // name of the operation
7578
}) => {
76-
return args; // Return operation arguments as necessary
77-
}
79+
return args; // return modified operation arguments as necessary
80+
};
7881
```
7982

8083
### beforeValidate
@@ -88,7 +91,7 @@ Please do note that this does not run before the client-side validation. If you
8891
3. `validate` runs on the server
8992

9093
```ts
91-
import { CollectionBeforeOperationHook } from 'payload/types';
94+
import { CollectionBeforeOperationHook } from "payload/types";
9295

9396
const beforeValidateHook: CollectionBeforeValidateHook = async ({
9497
data, // incoming data to update or create with
@@ -97,15 +100,15 @@ const beforeValidateHook: CollectionBeforeValidateHook = async ({
97100
originalDoc, // original document
98101
}) => {
99102
return data; // Return data to either create or update a document with
100-
}
103+
};
101104
```
102105

103106
### beforeChange
104107

105108
Immediately following validation, `beforeChange` hooks will run within `create` and `update` operations. At this stage, you can be confident that the data that will be saved to the document is valid in accordance to your field validations. You can optionally modify the shape of data to be saved.
106109

107110
```ts
108-
import { CollectionBeforeChangeHook } from 'payload/types';
111+
import { CollectionBeforeChangeHook } from "payload/types";
109112

110113
const beforeChangeHook: CollectionBeforeChangeHook = async ({
111114
data, // incoming data to update or create with
@@ -114,15 +117,15 @@ const beforeChangeHook: CollectionBeforeChangeHook = async ({
114117
originalDoc, // original document
115118
}) => {
116119
return data; // Return data to either create or update a document with
117-
}
120+
};
118121
```
119122

120123
### afterChange
121124

122125
After a document is created or updated, the `afterChange` hook runs. This hook is helpful to recalculate statistics such as total sales within a global, syncing user profile changes to a CRM, and more.
123126

124127
```ts
125-
import { CollectionAfterChangeHook } from 'payload/types';
128+
import { CollectionAfterChangeHook } from "payload/types";
126129

127130
const afterChangeHook: CollectionAfterChangeHook = async ({
128131
doc, // full document data
@@ -131,31 +134,31 @@ const afterChangeHook: CollectionAfterChangeHook = async ({
131134
operation, // name of the operation ie. 'create', 'update'
132135
}) => {
133136
return doc;
134-
}
137+
};
135138
```
136139

137140
### beforeRead
138141

139142
Runs before `find` and `findByID` operations are transformed for output by `afterRead`. This hook fires before hidden fields are removed and before localized fields are flattened into the requested locale. Using this Hook will provide you with all locales and all hidden fields via the `doc` argument.
140143

141144
```ts
142-
import { CollectionBeforeReadHook } from 'payload/types';
145+
import { CollectionBeforeReadHook } from "payload/types";
143146

144147
const beforeReadHook: CollectionBeforeReadHook = async ({
145148
doc, // full document data
146149
req, // full express request
147150
query, // JSON formatted query
148151
}) => {
149152
return doc;
150-
}
153+
};
151154
```
152155

153156
### afterRead
154157

155158
Runs as the last step before documents are returned. Flattens locales, hides protected fields, and removes fields that users do not have access to.
156159

157160
```ts
158-
import { CollectionAfterReadHook } from 'payload/types';
161+
import { CollectionAfterReadHook } from "payload/types";
159162

160163
const afterReadHook: CollectionAfterReadHook = async ({
161164
doc, // full document data
@@ -164,7 +167,7 @@ const afterReadHook: CollectionAfterReadHook = async ({
164167
findMany, // boolean to denote if this hook is running against finding one, or finding many
165168
}) => {
166169
return doc;
167-
}
170+
};
168171
```
169172

170173
### beforeDelete
@@ -194,19 +197,37 @@ const afterDeleteHook: CollectionAfterDeleteHook = async ({
194197
}) => {...}
195198
```
196199

200+
### afterOperation
201+
202+
The `afterOperation` hook can be used to modify the result of operations or execute side-effects that run after an operation has completed.
203+
204+
Available Collection operations include `create`, `find`, `findByID`, `update`, `updateByID`, `delete`, `deleteByID`, `login`, `refresh`, and `forgotPassword`.
205+
206+
```ts
207+
import { CollectionAfterOperationHook } from "payload/types";
208+
209+
const afterOperationHook: CollectionAfterOperationHook = async ({
210+
args, // arguments passed into the operation
211+
operation, // name of the operation
212+
result, // the result of the operation, before modifications
213+
}) => {
214+
return result; // return modified result as necessary
215+
};
216+
```
217+
197218
### beforeLogin
198219

199220
For auth-enabled Collections, this hook runs during `login` operations where a user with the provided credentials exist, but before a token is generated and added to the response. You can optionally modify the user that is returned, or throw an error in order to deny the login operation.
200221

201222
```ts
202-
import { CollectionBeforeLoginHook } from 'payload/types';
223+
import { CollectionBeforeLoginHook } from "payload/types";
203224

204225
const beforeLoginHook: CollectionBeforeLoginHook = async ({
205226
req, // full express request
206227
user, // user being logged in
207228
}) => {
208229
return user;
209-
}
230+
};
210231
```
211232

212233
### afterLogin
@@ -267,15 +288,15 @@ const afterMeHook: CollectionAfterMeHook = async ({
267288
For auth-enabled Collections, this hook runs after successful `forgotPassword` operations. Returned values are discarded.
268289

269290
```ts
270-
import { CollectionAfterForgotPasswordHook } from 'payload/types';
291+
import { CollectionAfterForgotPasswordHook } from "payload/types";
271292

272293
const afterLoginHook: CollectionAfterForgotPasswordHook = async ({
273294
req, // full express request
274295
user, // user being logged in
275296
token, // user token
276297
}) => {
277298
return user;
278-
}
299+
};
279300
```
280301

281302
## TypeScript
@@ -298,5 +319,5 @@ import type {
298319
CollectionAfterRefreshHook,
299320
CollectionAfterMeHook,
300321
CollectionAfterForgotPasswordHook,
301-
} from 'payload/types';
322+
} from "payload/types";
302323
```

src/auth/operations/forgotPassword.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Document } from 'mongoose';
33
import { APIError } from '../../errors';
44
import { PayloadRequest } from '../../express/types';
55
import { Collection } from '../../collections/config/types';
6+
import { buildAfterOperation } from '../../collections/operations/utils';
67

78
export type Arguments = {
89
collection: Collection
@@ -128,6 +129,16 @@ async function forgotPassword(incomingArgs: Arguments): Promise<string | null> {
128129
await hook({ args, context: req.context });
129130
}, Promise.resolve());
130131

132+
// /////////////////////////////////////
133+
// afterOperation - Collection
134+
// /////////////////////////////////////
135+
136+
token = await buildAfterOperation({
137+
operation: 'forgotPassword',
138+
args,
139+
result: token,
140+
});
141+
131142
return token;
132143
}
133144

src/auth/operations/login.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { User } from '../types';
1010
import { Collection } from '../../collections/config/types';
1111
import { afterRead } from '../../fields/hooks/afterRead';
1212
import unlock from './unlock';
13+
import { buildAfterOperation } from '../../collections/operations/utils';
1314
import { incrementLoginAttempts } from '../strategies/local/incrementLoginAttempts';
1415
import { authenticateLocalStrategy } from '../strategies/local/authenticate';
1516
import { getFieldsToSign } from './getFieldsToSign';
@@ -206,15 +207,28 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
206207
}) || user;
207208
}, Promise.resolve());
208209

209-
// /////////////////////////////////////
210-
// Return results
211-
// /////////////////////////////////////
212210

213-
return {
211+
let result: Result & { user: GeneratedTypes['collections'][TSlug] } = {
214212
token,
215213
user,
216214
exp: (jwt.decode(token) as jwt.JwtPayload).exp,
217215
};
216+
217+
// /////////////////////////////////////
218+
// afterOperation - Collection
219+
// /////////////////////////////////////
220+
221+
result = await buildAfterOperation<GeneratedTypes['collections'][TSlug]>({
222+
operation: 'login',
223+
args,
224+
result,
225+
});
226+
227+
// /////////////////////////////////////
228+
// Return results
229+
// /////////////////////////////////////
230+
231+
return result;
218232
}
219233

220234
export default login;

src/auth/operations/refresh.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import url from 'url';
12
import jwt from 'jsonwebtoken';
23
import { Response } from 'express';
3-
import url from 'url';
44
import { Collection, BeforeOperationHook } from '../../collections/config/types';
55
import { Forbidden } from '../../errors';
66
import getCookieExpiration from '../../utilities/getCookieExpiration';
77
import { Document } from '../../types';
88
import { PayloadRequest } from '../../express/types';
9+
import { buildAfterOperation } from '../../collections/operations/utils';
910
import { getFieldsToSign } from './getFieldsToSign';
1011

1112
export type Result = {
@@ -97,7 +98,7 @@ async function refresh(incomingArgs: Arguments): Promise<Result> {
9798
args.res.cookie(`${config.cookiePrefix}-token`, refreshedToken, cookieOptions);
9899
}
99100

100-
let response: Result = {
101+
let result: Result = {
101102
user,
102103
refreshedToken,
103104
exp,
@@ -110,20 +111,31 @@ async function refresh(incomingArgs: Arguments): Promise<Result> {
110111
await collectionConfig.hooks.afterRefresh.reduce(async (priorHook, hook) => {
111112
await priorHook;
112113

113-
response = (await hook({
114+
result = (await hook({
114115
req: args.req,
115116
res: args.res,
116117
exp,
117118
token: refreshedToken,
118119
context: args.req.context,
119-
})) || response;
120+
})) || result;
120121
}, Promise.resolve());
121122

123+
124+
// /////////////////////////////////////
125+
// afterOperation - Collection
126+
// /////////////////////////////////////
127+
128+
result = await buildAfterOperation({
129+
operation: 'refresh',
130+
args,
131+
result,
132+
});
133+
122134
// /////////////////////////////////////
123135
// Return results
124136
// /////////////////////////////////////
125137

126-
return response;
138+
return result;
127139
}
128140

129141
export default refresh;

src/collections/config/defaults.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const defaults = {
2929
afterRead: [],
3030
beforeDelete: [],
3131
afterDelete: [],
32+
afterOperation: [],
3233
beforeLogin: [],
3334
afterLogin: [],
3435
afterLogout: [],

src/collections/config/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ const collectionSchema = joi.object().keys({
9898
afterRead: joi.array().items(joi.func()),
9999
beforeDelete: joi.array().items(joi.func()),
100100
afterDelete: joi.array().items(joi.func()),
101+
afterOperation: joi.array().items(joi.func()),
101102
beforeLogin: joi.array().items(joi.func()),
102103
afterLogin: joi.array().items(joi.func()),
103104
afterLogout: joi.array().items(joi.func()),

src/collections/config/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { IncomingUploadType, Upload } from '../../uploads/types';
1212
import { IncomingCollectionVersions, SanitizedCollectionVersions } from '../../versions/types';
1313
import { BuildQueryArgs } from '../../mongoose/buildQuery';
1414
import { CustomPreviewButtonProps, CustomPublishButtonProps, CustomSaveButtonProps, CustomSaveDraftButtonProps } from '../../admin/components/elements/types';
15+
import { AfterOperationArg, AfterOperationMap } from '../operations/utils';
1516
import type { Props as ListProps } from '../../admin/components/views/collections/List/types';
1617
import type { Props as EditProps } from '../../admin/components/views/collections/Edit/types';
1718

@@ -123,6 +124,13 @@ export type AfterDeleteHook<T extends TypeWithID = any> = (args: {
123124
context: RequestContext;
124125
}) => any;
125126

127+
128+
export type AfterOperationHook<
129+
T extends TypeWithID = any,
130+
> = (
131+
arg: AfterOperationArg<T>,
132+
) => Promise<ReturnType<AfterOperationMap<T>[keyof AfterOperationMap<T>]>>;
133+
126134
export type AfterErrorHook = (err: Error, res: unknown, context: RequestContext) => { response: any, status: number } | void;
127135

128136
export type BeforeLoginHook<T extends TypeWithID = any> = (args: {
@@ -314,6 +322,7 @@ export type CollectionConfig = {
314322
afterMe?: AfterMeHook[];
315323
afterRefresh?: AfterRefreshHook[];
316324
afterForgotPassword?: AfterForgotPasswordHook[];
325+
afterOperation?: AfterOperationHook[];
317326
};
318327
/**
319328
* Custom rest api endpoints

0 commit comments

Comments
 (0)