@dismissible/nestjs-dismissible 0.0.2-canary.c91edbc.0 → 0.0.2-canary.d2f56d7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -67
- package/package.json +3 -3
- package/src/api/dismissible-item-response.dto.ts +0 -8
- package/src/api/dismissible-item.mapper.spec.ts +0 -12
- package/src/api/dismissible-item.mapper.ts +2 -8
- package/src/api/index.ts +3 -0
- package/src/api/use-cases/dismiss/dismiss.controller.spec.ts +1 -2
- package/src/api/use-cases/dismiss/dismiss.controller.ts +8 -8
- package/src/api/use-cases/get-or-create/get-or-create.controller.spec.ts +2 -42
- package/src/api/use-cases/get-or-create/get-or-create.controller.ts +10 -56
- package/src/api/use-cases/get-or-create/index.ts +0 -1
- package/src/api/use-cases/restore/restore.controller.spec.ts +1 -2
- package/src/api/use-cases/restore/restore.controller.ts +8 -8
- package/src/api/validation/index.ts +2 -0
- package/src/api/validation/param-validation.pipe.spec.ts +317 -0
- package/src/api/validation/param-validation.pipe.ts +42 -0
- package/src/api/validation/param.decorators.ts +32 -0
- package/src/core/dismissible-core.service.spec.ts +3 -45
- package/src/core/dismissible-core.service.ts +10 -27
- package/src/core/dismissible.service.spec.ts +23 -16
- package/src/core/dismissible.service.ts +28 -11
- package/src/core/hook-runner.service.spec.ts +369 -19
- package/src/core/hook-runner.service.ts +17 -17
- package/src/core/index.ts +0 -1
- package/src/core/lifecycle-hook.interface.ts +8 -8
- package/src/core/service-responses.interface.ts +9 -9
- package/src/dismissible.module.integration.spec.ts +685 -0
- package/src/dismissible.module.ts +6 -10
- package/src/events/dismissible.events.ts +16 -39
- package/src/index.ts +1 -0
- package/src/request/request-context.decorator.ts +1 -0
- package/src/request/request-context.interface.ts +6 -0
- package/src/response/http-exception-filter.spec.ts +213 -0
- package/src/testing/factories.ts +5 -8
- package/src/utils/dismissible.helper.ts +2 -2
- package/src/validation/dismissible-input.dto.ts +47 -0
- package/src/validation/index.ts +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.spec.json +12 -0
- package/src/api/use-cases/get-or-create/get-or-create.request.dto.ts +0 -17
- package/src/core/create-options.ts +0 -9
|
@@ -2,12 +2,11 @@ import { mock } from 'ts-jest-mocker';
|
|
|
2
2
|
import { ForbiddenException } from '@nestjs/common';
|
|
3
3
|
import { HookRunner } from './hook-runner.service';
|
|
4
4
|
import { IDismissibleLifecycleHook } from './lifecycle-hook.interface';
|
|
5
|
-
import { BaseMetadata } from '@dismissible/nestjs-dismissible-item';
|
|
6
5
|
import { createTestItem, createTestContext } from '../testing/factories';
|
|
7
6
|
import { IDismissibleLogger } from '@dismissible/nestjs-logger';
|
|
8
7
|
|
|
9
8
|
describe('HookRunner', () => {
|
|
10
|
-
let hookRunner: HookRunner
|
|
9
|
+
let hookRunner: HookRunner;
|
|
11
10
|
let mockLogger: jest.Mocked<IDismissibleLogger>;
|
|
12
11
|
|
|
13
12
|
const testUserId = 'test-user-id';
|
|
@@ -45,7 +44,7 @@ describe('HookRunner', () => {
|
|
|
45
44
|
it('should execute hooks in priority order (low to high) for pre-hooks', async () => {
|
|
46
45
|
const executionOrder: number[] = [];
|
|
47
46
|
|
|
48
|
-
const hook1: IDismissibleLifecycleHook
|
|
47
|
+
const hook1: IDismissibleLifecycleHook = {
|
|
49
48
|
priority: 10,
|
|
50
49
|
onBeforeRequest: jest.fn(async () => {
|
|
51
50
|
executionOrder.push(10);
|
|
@@ -53,7 +52,7 @@ describe('HookRunner', () => {
|
|
|
53
52
|
}),
|
|
54
53
|
};
|
|
55
54
|
|
|
56
|
-
const hook2: IDismissibleLifecycleHook
|
|
55
|
+
const hook2: IDismissibleLifecycleHook = {
|
|
57
56
|
priority: 5,
|
|
58
57
|
onBeforeRequest: jest.fn(async () => {
|
|
59
58
|
executionOrder.push(5);
|
|
@@ -61,7 +60,7 @@ describe('HookRunner', () => {
|
|
|
61
60
|
}),
|
|
62
61
|
};
|
|
63
62
|
|
|
64
|
-
const hook3: IDismissibleLifecycleHook
|
|
63
|
+
const hook3: IDismissibleLifecycleHook = {
|
|
65
64
|
priority: 15,
|
|
66
65
|
onBeforeRequest: jest.fn(async () => {
|
|
67
66
|
executionOrder.push(15);
|
|
@@ -78,21 +77,21 @@ describe('HookRunner', () => {
|
|
|
78
77
|
it('should execute hooks in reverse priority order for post-hooks', async () => {
|
|
79
78
|
const executionOrder: number[] = [];
|
|
80
79
|
|
|
81
|
-
const hook1: IDismissibleLifecycleHook
|
|
80
|
+
const hook1: IDismissibleLifecycleHook = {
|
|
82
81
|
priority: 10,
|
|
83
82
|
onAfterRequest: jest.fn(async () => {
|
|
84
83
|
executionOrder.push(10);
|
|
85
84
|
}),
|
|
86
85
|
};
|
|
87
86
|
|
|
88
|
-
const hook2: IDismissibleLifecycleHook
|
|
87
|
+
const hook2: IDismissibleLifecycleHook = {
|
|
89
88
|
priority: 5,
|
|
90
89
|
onAfterRequest: jest.fn(async () => {
|
|
91
90
|
executionOrder.push(5);
|
|
92
91
|
}),
|
|
93
92
|
};
|
|
94
93
|
|
|
95
|
-
const hook3: IDismissibleLifecycleHook
|
|
94
|
+
const hook3: IDismissibleLifecycleHook = {
|
|
96
95
|
priority: 15,
|
|
97
96
|
onAfterRequest: jest.fn(async () => {
|
|
98
97
|
executionOrder.push(15);
|
|
@@ -106,7 +105,7 @@ describe('HookRunner', () => {
|
|
|
106
105
|
});
|
|
107
106
|
|
|
108
107
|
it('should block operation when pre-hook returns proceed: false', async () => {
|
|
109
|
-
const blockingHook: IDismissibleLifecycleHook
|
|
108
|
+
const blockingHook: IDismissibleLifecycleHook = {
|
|
110
109
|
onBeforeRequest: jest.fn(async () => ({
|
|
111
110
|
proceed: false,
|
|
112
111
|
reason: 'Rate limit exceeded',
|
|
@@ -121,7 +120,7 @@ describe('HookRunner', () => {
|
|
|
121
120
|
});
|
|
122
121
|
|
|
123
122
|
it('should apply mutations from pre-hooks', async () => {
|
|
124
|
-
const mutatingHook: IDismissibleLifecycleHook
|
|
123
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
125
124
|
onBeforeRequest: jest.fn(async () => ({
|
|
126
125
|
proceed: true,
|
|
127
126
|
mutations: {
|
|
@@ -139,7 +138,7 @@ describe('HookRunner', () => {
|
|
|
139
138
|
});
|
|
140
139
|
|
|
141
140
|
it('should pass mutations through multiple hooks', async () => {
|
|
142
|
-
const hook1: IDismissibleLifecycleHook
|
|
141
|
+
const hook1: IDismissibleLifecycleHook = {
|
|
143
142
|
priority: 1,
|
|
144
143
|
onBeforeRequest: jest.fn(async (itemId) => ({
|
|
145
144
|
proceed: true,
|
|
@@ -147,7 +146,7 @@ describe('HookRunner', () => {
|
|
|
147
146
|
})),
|
|
148
147
|
};
|
|
149
148
|
|
|
150
|
-
const hook2: IDismissibleLifecycleHook
|
|
149
|
+
const hook2: IDismissibleLifecycleHook = {
|
|
151
150
|
priority: 2,
|
|
152
151
|
onBeforeRequest: jest.fn(async (itemId) => ({
|
|
153
152
|
proceed: true,
|
|
@@ -160,11 +159,70 @@ describe('HookRunner', () => {
|
|
|
160
159
|
|
|
161
160
|
expect(result.id).toBe('original-hook1-hook2');
|
|
162
161
|
});
|
|
162
|
+
|
|
163
|
+
it('should apply context mutations from pre-hooks', async () => {
|
|
164
|
+
const context = createTestContext();
|
|
165
|
+
|
|
166
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
167
|
+
onBeforeRequest: jest.fn(async () => ({
|
|
168
|
+
proceed: true,
|
|
169
|
+
mutations: {
|
|
170
|
+
context: { authorizationHeader: 'Bearer mutated-token' },
|
|
171
|
+
},
|
|
172
|
+
})),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
176
|
+
const result = await hookRunner.runPreRequest('test-id', testUserId, context);
|
|
177
|
+
|
|
178
|
+
expect(result.context).toEqual({ ...context, authorizationHeader: 'Bearer mutated-token' });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should ignore context mutation when no context provided', async () => {
|
|
182
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
183
|
+
onBeforeRequest: jest.fn(async () => ({
|
|
184
|
+
proceed: true,
|
|
185
|
+
mutations: {
|
|
186
|
+
context: { authorizationHeader: 'Bearer token' },
|
|
187
|
+
},
|
|
188
|
+
})),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
192
|
+
const result = await hookRunner.runPreRequest('test-id', testUserId, undefined);
|
|
193
|
+
|
|
194
|
+
expect(result.context).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should handle hooks with default priority (undefined)', async () => {
|
|
198
|
+
const executionOrder: string[] = [];
|
|
199
|
+
|
|
200
|
+
const hook1: IDismissibleLifecycleHook = {
|
|
201
|
+
priority: 5,
|
|
202
|
+
onBeforeRequest: jest.fn(async () => {
|
|
203
|
+
executionOrder.push('priority-5');
|
|
204
|
+
return { proceed: true };
|
|
205
|
+
}),
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const hook2: IDismissibleLifecycleHook = {
|
|
209
|
+
// No priority - defaults to 0
|
|
210
|
+
onBeforeRequest: jest.fn(async () => {
|
|
211
|
+
executionOrder.push('priority-default');
|
|
212
|
+
return { proceed: true };
|
|
213
|
+
}),
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
hookRunner = new HookRunner([hook1, hook2], mockLogger);
|
|
217
|
+
await hookRunner.runPreRequest('test-id', testUserId, createTestContext());
|
|
218
|
+
|
|
219
|
+
expect(executionOrder).toEqual(['priority-default', 'priority-5']);
|
|
220
|
+
});
|
|
163
221
|
});
|
|
164
222
|
|
|
165
223
|
describe('error handling', () => {
|
|
166
224
|
it('should throw error from pre-hook', async () => {
|
|
167
|
-
const errorHook: IDismissibleLifecycleHook
|
|
225
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
168
226
|
onBeforeRequest: jest.fn(async () => {
|
|
169
227
|
throw new Error('Hook error');
|
|
170
228
|
}),
|
|
@@ -179,8 +237,24 @@ describe('HookRunner', () => {
|
|
|
179
237
|
expect(mockLogger.error).toHaveBeenCalled();
|
|
180
238
|
});
|
|
181
239
|
|
|
240
|
+
it('should throw non-Error from pre-hook', async () => {
|
|
241
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
242
|
+
onBeforeRequest: jest.fn(async () => {
|
|
243
|
+
throw 'string error';
|
|
244
|
+
}),
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
248
|
+
|
|
249
|
+
await expect(
|
|
250
|
+
hookRunner.runPreRequest('test-id', testUserId, createTestContext()),
|
|
251
|
+
).rejects.toBe('string error');
|
|
252
|
+
|
|
253
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
254
|
+
});
|
|
255
|
+
|
|
182
256
|
it('should log but not throw errors from post-hooks', async () => {
|
|
183
|
-
const errorHook: IDismissibleLifecycleHook
|
|
257
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
184
258
|
onAfterRequest: jest.fn(async () => {
|
|
185
259
|
throw new Error('Post-hook error');
|
|
186
260
|
}),
|
|
@@ -194,6 +268,22 @@ describe('HookRunner', () => {
|
|
|
194
268
|
|
|
195
269
|
expect(mockLogger.error).toHaveBeenCalled();
|
|
196
270
|
});
|
|
271
|
+
|
|
272
|
+
it('should log but not throw non-Error from post-hooks', async () => {
|
|
273
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
274
|
+
onAfterRequest: jest.fn(async () => {
|
|
275
|
+
throw 'string post-hook error';
|
|
276
|
+
}),
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
280
|
+
|
|
281
|
+
await expect(
|
|
282
|
+
hookRunner.runPostRequest('test-id', createTestItem(), testUserId, createTestContext()),
|
|
283
|
+
).resolves.not.toThrow();
|
|
284
|
+
|
|
285
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
286
|
+
});
|
|
197
287
|
});
|
|
198
288
|
|
|
199
289
|
describe('throwIfBlocked', () => {
|
|
@@ -209,6 +299,17 @@ describe('HookRunner', () => {
|
|
|
209
299
|
).toThrow(ForbiddenException);
|
|
210
300
|
});
|
|
211
301
|
|
|
302
|
+
it('should throw ForbiddenException with default reason when blocked without reason', () => {
|
|
303
|
+
expect(() =>
|
|
304
|
+
HookRunner.throwIfBlocked({
|
|
305
|
+
proceed: false,
|
|
306
|
+
id: 'test',
|
|
307
|
+
userId: testUserId,
|
|
308
|
+
context: createTestContext(),
|
|
309
|
+
}),
|
|
310
|
+
).toThrow('Operation blocked by lifecycle hook');
|
|
311
|
+
});
|
|
312
|
+
|
|
212
313
|
it('should not throw when not blocked', () => {
|
|
213
314
|
expect(() =>
|
|
214
315
|
HookRunner.throwIfBlocked({
|
|
@@ -226,7 +327,7 @@ describe('HookRunner', () => {
|
|
|
226
327
|
const item = createTestItem();
|
|
227
328
|
const context = createTestContext();
|
|
228
329
|
|
|
229
|
-
const hook: IDismissibleLifecycleHook
|
|
330
|
+
const hook: IDismissibleLifecycleHook = {
|
|
230
331
|
onBeforeGet: jest.fn().mockResolvedValue({ proceed: true }),
|
|
231
332
|
};
|
|
232
333
|
|
|
@@ -240,7 +341,7 @@ describe('HookRunner', () => {
|
|
|
240
341
|
const item = createTestItem();
|
|
241
342
|
const context = createTestContext();
|
|
242
343
|
|
|
243
|
-
const blockingHook: IDismissibleLifecycleHook
|
|
344
|
+
const blockingHook: IDismissibleLifecycleHook = {
|
|
244
345
|
onBeforeGet: jest.fn().mockResolvedValue({
|
|
245
346
|
proceed: false,
|
|
246
347
|
reason: 'Item is in invalid state',
|
|
@@ -258,7 +359,7 @@ describe('HookRunner', () => {
|
|
|
258
359
|
const item = createTestItem();
|
|
259
360
|
const context = createTestContext();
|
|
260
361
|
|
|
261
|
-
const hook: IDismissibleLifecycleHook
|
|
362
|
+
const hook: IDismissibleLifecycleHook = {
|
|
262
363
|
onAfterGet: jest.fn(),
|
|
263
364
|
};
|
|
264
365
|
|
|
@@ -272,7 +373,7 @@ describe('HookRunner', () => {
|
|
|
272
373
|
const item = createTestItem();
|
|
273
374
|
const context = createTestContext();
|
|
274
375
|
|
|
275
|
-
const mutatingHook: IDismissibleLifecycleHook
|
|
376
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
276
377
|
onBeforeGet: jest.fn().mockResolvedValue({
|
|
277
378
|
proceed: true,
|
|
278
379
|
mutations: {
|
|
@@ -286,10 +387,259 @@ describe('HookRunner', () => {
|
|
|
286
387
|
|
|
287
388
|
expect(result.id).toBe('mutated-id');
|
|
288
389
|
});
|
|
390
|
+
|
|
391
|
+
it('should apply userId mutation from onBeforeGet hook', async () => {
|
|
392
|
+
const item = createTestItem();
|
|
393
|
+
const context = createTestContext();
|
|
394
|
+
|
|
395
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
396
|
+
onBeforeGet: jest.fn().mockResolvedValue({
|
|
397
|
+
proceed: true,
|
|
398
|
+
mutations: {
|
|
399
|
+
userId: 'mutated-user-id',
|
|
400
|
+
},
|
|
401
|
+
}),
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
405
|
+
const result = await hookRunner.runPreGet('test-id', item, testUserId, context);
|
|
406
|
+
|
|
407
|
+
expect(result.userId).toBe('mutated-user-id');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should apply context mutation from onBeforeGet hook', async () => {
|
|
411
|
+
const item = createTestItem();
|
|
412
|
+
const context = createTestContext();
|
|
413
|
+
|
|
414
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
415
|
+
onBeforeGet: jest.fn().mockResolvedValue({
|
|
416
|
+
proceed: true,
|
|
417
|
+
mutations: {
|
|
418
|
+
context: { authorizationHeader: 'Bearer custom-token' },
|
|
419
|
+
},
|
|
420
|
+
}),
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
424
|
+
const result = await hookRunner.runPreGet('test-id', item, testUserId, context);
|
|
425
|
+
|
|
426
|
+
expect(result.context).toEqual({ ...context, authorizationHeader: 'Bearer custom-token' });
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('should ignore context mutation when no context provided to onBeforeGet', async () => {
|
|
430
|
+
const item = createTestItem();
|
|
431
|
+
|
|
432
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
433
|
+
onBeforeGet: jest.fn().mockResolvedValue({
|
|
434
|
+
proceed: true,
|
|
435
|
+
mutations: {
|
|
436
|
+
context: { authorizationHeader: 'Bearer token' },
|
|
437
|
+
},
|
|
438
|
+
}),
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
442
|
+
const result = await hookRunner.runPreGet('test-id', item, testUserId, undefined);
|
|
443
|
+
|
|
444
|
+
expect(result.context).toBeUndefined();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('should throw error from onBeforeGet hook', async () => {
|
|
448
|
+
const item = createTestItem();
|
|
449
|
+
const context = createTestContext();
|
|
450
|
+
|
|
451
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
452
|
+
onBeforeGet: jest.fn().mockRejectedValue(new Error('Get hook error')),
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
456
|
+
|
|
457
|
+
await expect(hookRunner.runPreGet('test-id', item, testUserId, context)).rejects.toThrow(
|
|
458
|
+
'Get hook error',
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should throw non-Error from onBeforeGet hook', async () => {
|
|
465
|
+
const item = createTestItem();
|
|
466
|
+
const context = createTestContext();
|
|
467
|
+
|
|
468
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
469
|
+
onBeforeGet: jest.fn().mockRejectedValue('string error'),
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
473
|
+
|
|
474
|
+
await expect(hookRunner.runPreGet('test-id', item, testUserId, context)).rejects.toBe(
|
|
475
|
+
'string error',
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
describe('runPreCreate, runPreDismiss, runPreRestore mutations', () => {
|
|
483
|
+
it('should apply mutations from onBeforeCreate hook', async () => {
|
|
484
|
+
const context = createTestContext();
|
|
485
|
+
|
|
486
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
487
|
+
onBeforeCreate: jest.fn().mockResolvedValue({
|
|
488
|
+
proceed: true,
|
|
489
|
+
mutations: {
|
|
490
|
+
id: 'mutated-create-id',
|
|
491
|
+
userId: 'mutated-create-user',
|
|
492
|
+
},
|
|
493
|
+
}),
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
497
|
+
const result = await hookRunner.runPreCreate('original-id', testUserId, context);
|
|
498
|
+
|
|
499
|
+
expect(result.id).toBe('mutated-create-id');
|
|
500
|
+
expect(result.userId).toBe('mutated-create-user');
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('should apply context mutation from onBeforeCreate when context is provided', async () => {
|
|
504
|
+
const context = createTestContext();
|
|
505
|
+
|
|
506
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
507
|
+
onBeforeCreate: jest.fn().mockResolvedValue({
|
|
508
|
+
proceed: true,
|
|
509
|
+
mutations: {
|
|
510
|
+
context: { authorizationHeader: 'Bearer create-token' },
|
|
511
|
+
},
|
|
512
|
+
}),
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
516
|
+
const result = await hookRunner.runPreCreate('test-id', testUserId, context);
|
|
517
|
+
|
|
518
|
+
expect(result.context).toEqual({ ...context, authorizationHeader: 'Bearer create-token' });
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('should block operation when onBeforeDismiss returns proceed: false', async () => {
|
|
522
|
+
const blockingHook: IDismissibleLifecycleHook = {
|
|
523
|
+
onBeforeDismiss: jest.fn().mockResolvedValue({
|
|
524
|
+
proceed: false,
|
|
525
|
+
reason: 'Cannot dismiss this item',
|
|
526
|
+
}),
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
hookRunner = new HookRunner([blockingHook], mockLogger);
|
|
530
|
+
const result = await hookRunner.runPreDismiss('test-id', testUserId, createTestContext());
|
|
531
|
+
|
|
532
|
+
expect(result.proceed).toBe(false);
|
|
533
|
+
expect(result.reason).toBe('Cannot dismiss this item');
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('should apply mutations from onBeforeRestore hook', async () => {
|
|
537
|
+
const context = createTestContext();
|
|
538
|
+
|
|
539
|
+
const mutatingHook: IDismissibleLifecycleHook = {
|
|
540
|
+
onBeforeRestore: jest.fn().mockResolvedValue({
|
|
541
|
+
proceed: true,
|
|
542
|
+
mutations: {
|
|
543
|
+
id: 'mutated-restore-id',
|
|
544
|
+
},
|
|
545
|
+
}),
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
hookRunner = new HookRunner([mutatingHook], mockLogger);
|
|
549
|
+
const result = await hookRunner.runPreRestore('original-id', testUserId, context);
|
|
550
|
+
|
|
551
|
+
expect(result.id).toBe('mutated-restore-id');
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('should throw error from onBeforeCreate hook', async () => {
|
|
555
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
556
|
+
onBeforeCreate: jest.fn().mockRejectedValue(new Error('Create hook error')),
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
560
|
+
|
|
561
|
+
await expect(
|
|
562
|
+
hookRunner.runPreCreate('test-id', testUserId, createTestContext()),
|
|
563
|
+
).rejects.toThrow('Create hook error');
|
|
564
|
+
|
|
565
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('should throw non-Error from onBeforeDismiss hook', async () => {
|
|
569
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
570
|
+
onBeforeDismiss: jest.fn().mockRejectedValue('dismiss string error'),
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
574
|
+
|
|
575
|
+
await expect(
|
|
576
|
+
hookRunner.runPreDismiss('test-id', testUserId, createTestContext()),
|
|
577
|
+
).rejects.toBe('dismiss string error');
|
|
578
|
+
|
|
579
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
describe('post-hook error handling', () => {
|
|
584
|
+
it('should log but not throw Error from onAfterCreate', async () => {
|
|
585
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
586
|
+
onAfterCreate: jest.fn().mockRejectedValue(new Error('After create error')),
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
590
|
+
|
|
591
|
+
await expect(
|
|
592
|
+
hookRunner.runPostCreate('test-id', createTestItem(), testUserId, createTestContext()),
|
|
593
|
+
).resolves.not.toThrow();
|
|
594
|
+
|
|
595
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('should log but not throw non-Error from onAfterDismiss', async () => {
|
|
599
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
600
|
+
onAfterDismiss: jest.fn().mockRejectedValue('dismiss post error'),
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
604
|
+
|
|
605
|
+
await expect(
|
|
606
|
+
hookRunner.runPostDismiss('test-id', createTestItem(), testUserId, createTestContext()),
|
|
607
|
+
).resolves.not.toThrow();
|
|
608
|
+
|
|
609
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('should log but not throw Error from onAfterRestore', async () => {
|
|
613
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
614
|
+
onAfterRestore: jest.fn().mockRejectedValue(new Error('After restore error')),
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
618
|
+
|
|
619
|
+
await expect(
|
|
620
|
+
hookRunner.runPostRestore('test-id', createTestItem(), testUserId, createTestContext()),
|
|
621
|
+
).resolves.not.toThrow();
|
|
622
|
+
|
|
623
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('should log but not throw non-Error from onAfterGet', async () => {
|
|
627
|
+
const errorHook: IDismissibleLifecycleHook = {
|
|
628
|
+
onAfterGet: jest.fn().mockRejectedValue('get post error'),
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
hookRunner = new HookRunner([errorHook], mockLogger);
|
|
632
|
+
|
|
633
|
+
await expect(
|
|
634
|
+
hookRunner.runPostGet('test-id', createTestItem(), testUserId, createTestContext()),
|
|
635
|
+
).resolves.not.toThrow();
|
|
636
|
+
|
|
637
|
+
expect(mockLogger.error).toHaveBeenCalled();
|
|
638
|
+
});
|
|
289
639
|
});
|
|
290
640
|
|
|
291
641
|
describe('all hook methods', () => {
|
|
292
|
-
let allMethodsHook: IDismissibleLifecycleHook
|
|
642
|
+
let allMethodsHook: IDismissibleLifecycleHook;
|
|
293
643
|
|
|
294
644
|
beforeEach(() => {
|
|
295
645
|
allMethodsHook = {
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
IHookResult,
|
|
6
6
|
} from './lifecycle-hook.interface';
|
|
7
7
|
import { DISMISSIBLE_LOGGER, IDismissibleLogger } from '@dismissible/nestjs-logger';
|
|
8
|
-
import {
|
|
8
|
+
import { DismissibleItemDto } from '@dismissible/nestjs-dismissible-item';
|
|
9
9
|
import { IRequestContext } from '../request/request-context.interface';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -32,13 +32,13 @@ export interface IHookRunResult {
|
|
|
32
32
|
* Service responsible for running lifecycle hooks.
|
|
33
33
|
*/
|
|
34
34
|
@Injectable()
|
|
35
|
-
export class HookRunner
|
|
36
|
-
private readonly sortedHooks: IDismissibleLifecycleHook
|
|
35
|
+
export class HookRunner {
|
|
36
|
+
private readonly sortedHooks: IDismissibleLifecycleHook[];
|
|
37
37
|
|
|
38
38
|
constructor(
|
|
39
39
|
@Optional()
|
|
40
40
|
@Inject(DISMISSIBLE_HOOKS)
|
|
41
|
-
hooks: IDismissibleLifecycleHook
|
|
41
|
+
hooks: IDismissibleLifecycleHook[] = [],
|
|
42
42
|
@Inject(DISMISSIBLE_LOGGER)
|
|
43
43
|
private readonly logger: IDismissibleLogger,
|
|
44
44
|
) {
|
|
@@ -68,7 +68,7 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
|
|
|
68
68
|
*/
|
|
69
69
|
async runPostRequest(
|
|
70
70
|
itemId: string,
|
|
71
|
-
item: DismissibleItemDto
|
|
71
|
+
item: DismissibleItemDto,
|
|
72
72
|
userId: string,
|
|
73
73
|
context?: IRequestContext,
|
|
74
74
|
): Promise<void> {
|
|
@@ -85,7 +85,7 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
|
|
|
85
85
|
*/
|
|
86
86
|
async runPreGet(
|
|
87
87
|
itemId: string,
|
|
88
|
-
item: DismissibleItemDto
|
|
88
|
+
item: DismissibleItemDto,
|
|
89
89
|
userId: string,
|
|
90
90
|
context?: IRequestContext,
|
|
91
91
|
): Promise<IHookRunResult> {
|
|
@@ -97,7 +97,7 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
|
|
|
97
97
|
*/
|
|
98
98
|
async runPostGet(
|
|
99
99
|
itemId: string,
|
|
100
|
-
item: DismissibleItemDto
|
|
100
|
+
item: DismissibleItemDto,
|
|
101
101
|
userId: string,
|
|
102
102
|
context?: IRequestContext,
|
|
103
103
|
): Promise<void> {
|
|
@@ -124,7 +124,7 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
|
|
|
124
124
|
*/
|
|
125
125
|
async runPostCreate(
|
|
126
126
|
itemId: string,
|
|
127
|
-
item: DismissibleItemDto
|
|
127
|
+
item: DismissibleItemDto,
|
|
128
128
|
userId: string,
|
|
129
129
|
context?: IRequestContext,
|
|
130
130
|
): Promise<void> {
|
|
@@ -151,7 +151,7 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
|
|
|
151
151
|
*/
|
|
152
152
|
async runPostDismiss(
|
|
153
153
|
itemId: string,
|
|
154
|
-
item: DismissibleItemDto
|
|
154
|
+
item: DismissibleItemDto,
|
|
155
155
|
userId: string,
|
|
156
156
|
context?: IRequestContext,
|
|
157
157
|
): Promise<void> {
|
|
@@ -178,7 +178,7 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
|
|
|
178
178
|
*/
|
|
179
179
|
async runPostRestore(
|
|
180
180
|
itemId: string,
|
|
181
|
-
item: DismissibleItemDto
|
|
181
|
+
item: DismissibleItemDto,
|
|
182
182
|
userId: string,
|
|
183
183
|
context?: IRequestContext,
|
|
184
184
|
): Promise<void> {
|
|
@@ -189,7 +189,7 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
|
|
|
189
189
|
* Internal method to run pre-hooks.
|
|
190
190
|
*/
|
|
191
191
|
private async runPreHooks(
|
|
192
|
-
hookName: keyof IDismissibleLifecycleHook
|
|
192
|
+
hookName: keyof IDismissibleLifecycleHook,
|
|
193
193
|
itemId: string,
|
|
194
194
|
userId: string,
|
|
195
195
|
context?: IRequestContext,
|
|
@@ -266,9 +266,9 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
|
|
|
266
266
|
* Unlike standard pre-hooks, these receive the item for inspection/access control.
|
|
267
267
|
*/
|
|
268
268
|
private async runPreHooksWithItem(
|
|
269
|
-
hookName: keyof IDismissibleLifecycleHook
|
|
269
|
+
hookName: keyof IDismissibleLifecycleHook,
|
|
270
270
|
itemId: string,
|
|
271
|
-
item: DismissibleItemDto
|
|
271
|
+
item: DismissibleItemDto,
|
|
272
272
|
userId: string,
|
|
273
273
|
context?: IRequestContext,
|
|
274
274
|
): Promise<IHookRunResult> {
|
|
@@ -280,7 +280,7 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
|
|
|
280
280
|
const hookFn = hook[hookName] as
|
|
281
281
|
| ((
|
|
282
282
|
itemId: string,
|
|
283
|
-
item: DismissibleItemDto
|
|
283
|
+
item: DismissibleItemDto,
|
|
284
284
|
userId: string,
|
|
285
285
|
context?: IRequestContext,
|
|
286
286
|
) => Promise<IHookResult> | IHookResult)
|
|
@@ -345,9 +345,9 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
|
|
|
345
345
|
* Post-hooks run in reverse priority order.
|
|
346
346
|
*/
|
|
347
347
|
private async runPostHooks(
|
|
348
|
-
hookName: keyof IDismissibleLifecycleHook
|
|
348
|
+
hookName: keyof IDismissibleLifecycleHook,
|
|
349
349
|
itemId: string,
|
|
350
|
-
item: DismissibleItemDto
|
|
350
|
+
item: DismissibleItemDto,
|
|
351
351
|
userId: string,
|
|
352
352
|
context?: IRequestContext,
|
|
353
353
|
): Promise<void> {
|
|
@@ -358,7 +358,7 @@ export class HookRunner<TMetadata extends BaseMetadata = BaseMetadata> {
|
|
|
358
358
|
const hookFn = hook[hookName] as
|
|
359
359
|
| ((
|
|
360
360
|
itemId: string,
|
|
361
|
-
item: DismissibleItemDto
|
|
361
|
+
item: DismissibleItemDto,
|
|
362
362
|
userId: string,
|
|
363
363
|
context?: IRequestContext,
|
|
364
364
|
) => Promise<void> | void)
|
package/src/core/index.ts
CHANGED