Skip to content

Commit 9e0f066

Browse files
committed
feat(json-api-nestjs): Allow call interceptor for each operation in atomic endpoint
1 parent 8e498a8 commit 9e0f066

File tree

7 files changed

+197
-10
lines changed

7 files changed

+197
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {
2+
CallHandler,
3+
ExecutionContext,
4+
NestInterceptor,
5+
Injectable,
6+
} from '@nestjs/common';
7+
import { Observable } from 'rxjs';
8+
9+
@Injectable()
10+
export class AtomicInterceptor<T> implements NestInterceptor {
11+
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<T> {
12+
const isAtomic = context.getArgByIndex(3);
13+
if (isAtomic) {
14+
console.log('call from atomic operation');
15+
}
16+
return next.handle();
17+
}
18+
}

libs/json-api/json-api-nestjs/README.md

+16
Original file line numberDiff line numberDiff line change
@@ -409,4 +409,20 @@ but [Atomic operation](https://jsonapi.org/ext/atomic/) allow for one request.
409409

410410
```
411411
**tmpId** - is params for operation **add**, should be unique for all operations.
412+
If you have Interceptor you can check call it from **AtomicOperation**
412413

414+
415+
416+
```ts
417+
@Injectable()
418+
export class AtomicInterceptor<T> implements NestInterceptor {
419+
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<T> {
420+
const isAtomic = context.getArgByIndex(3)
421+
if (isAtomic) {
422+
console.log('call from atomic operation')
423+
}
424+
return next.handle();
425+
}
426+
}
427+
```
428+
**isAtomic** - is array of params of method

libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts

+36-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { DynamicModule, Module } from '@nestjs/common';
1+
import { AsyncLocalStorage } from 'async_hooks';
2+
import {
3+
DynamicModule,
4+
Inject,
5+
MiddlewareConsumer,
6+
Module,
7+
NestModule,
8+
} from '@nestjs/common';
29
import { DiscoveryModule } from '@nestjs/core';
310

411
import { OperationController } from './controllers';
@@ -11,9 +18,10 @@ import {
1118
AsyncIterate,
1219
} from './factory';
1320
import { ModuleOptions } from '../../types';
21+
import { MAP_CONTROLLER_INTERCEPTORS, OPTIONS } from './constants';
1422

1523
@Module({})
16-
export class AtomicOperationModule {
24+
export class AtomicOperationModule implements NestModule {
1725
static forRoot(
1826
options: ModuleOptions,
1927
entityModules: DynamicModule[],
@@ -30,8 +38,34 @@ export class AtomicOperationModule {
3038
MapControllerEntity(options.entities, entityModules),
3139
MapEntityNameToEntity(options.entities),
3240
ZodInputOperation(options.connectionName),
41+
{
42+
provide: MAP_CONTROLLER_INTERCEPTORS,
43+
useValue: new Map(),
44+
},
45+
{
46+
provide: OPTIONS,
47+
useValue: options.options,
48+
},
49+
{
50+
provide: AsyncLocalStorage,
51+
useValue: new AsyncLocalStorage(),
52+
},
3353
],
3454
imports: [DiscoveryModule, commonModule],
3555
};
3656
}
57+
@Inject(AsyncLocalStorage) private readonly als!: AsyncLocalStorage<any>;
58+
59+
configure(consumer: MiddlewareConsumer) {
60+
consumer
61+
.apply((req: any, res: any, next: any) => {
62+
const store = {
63+
req: req,
64+
res: res,
65+
next: next,
66+
};
67+
this.als.run(store, () => next());
68+
})
69+
.forRoutes('*');
70+
}
3771
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
export const MAP_CONTROLLER_ENTITY = Symbol('MAP_CONTROLLER_ENTITY');
2+
export const MAP_CONTROLLER_INTERCEPTORS = Symbol(
3+
'MAP_CONTROLLER_INTERCEPTORS'
4+
);
25
export const MAP_ENTITY = Symbol('MAP_ENTITY');
36
export const ZOD_INPUT_OPERATION = Symbol('ZOD_INPUT_OPERATION');
47
export const ASYNC_ITERATOR_FACTORY = Symbol('ASYNC_ITERATOR_FACTORY');
58
export const KEY_MAIN_INPUT_SCHEMA = 'atomic:operations';
69
export const KEY_MAIN_OUTPUT_SCHEMA = 'atomic:results';
10+
export const OPTIONS = Symbol('OPTIONS');

libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts

+103-6
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ import {
1212
} from '@nestjs/common/constants';
1313
import { Module } from '@nestjs/core/injector/module';
1414
import { ArgumentMetadata } from '@nestjs/common/interfaces/features/pipe-transform.interface';
15-
import { ModuleRef } from '@nestjs/core';
15+
import { ApplicationConfig, ModuleRef, NestContainer } from '@nestjs/core';
1616
import { DataSource } from 'typeorm';
1717

18-
import { ParamsForExecute } from '../types';
18+
import { MapControllerInterceptor, ParamsForExecute } from '../types';
1919
import { CURRENT_DATA_SOURCE_TOKEN } from '../../../constants';
2020
import {
2121
ASYNC_ITERATOR_FACTORY,
2222
KEY_MAIN_INPUT_SCHEMA,
23+
MAP_CONTROLLER_INTERCEPTORS,
2324
OPTIONS,
2425
} from '../constants';
2526
import { IterateFactory } from '../factory';
@@ -31,6 +32,13 @@ import {
3132
ValidateQueryError,
3233
} from '../../../types';
3334
import { ObjectTyped } from '../../../helper';
35+
import {
36+
InterceptorsConsumer,
37+
InterceptorsContextCreator,
38+
} from '@nestjs/core/interceptors';
39+
import { Controller } from '@nestjs/common/interfaces';
40+
import { lastValueFrom } from 'rxjs';
41+
import { AsyncLocalStorage } from 'async_hooks';
3442

3543
export function isZodError(
3644
param: string | unknown
@@ -46,11 +54,34 @@ export function isZodError(
4654
@Injectable()
4755
export class ExecuteService {
4856
@Inject(CURRENT_DATA_SOURCE_TOKEN) private readonly dataSource!: DataSource;
49-
@Inject(ModuleRef) private readonly moduleRef!: ModuleRef;
57+
@Inject(ModuleRef) private readonly moduleRef!: ModuleRef & {
58+
container: NestContainer;
59+
applicationConfig: ApplicationConfig;
60+
_moduleKey: string;
61+
};
5062
@Inject(ASYNC_ITERATOR_FACTORY) private asyncIteratorFactory!: IterateFactory<
5163
ExecuteService['runOneOperation']
5264
>;
5365
@Inject(OPTIONS) private options!: ConfigParam;
66+
@Inject(MAP_CONTROLLER_INTERCEPTORS)
67+
private mapControllerInterceptor!: MapControllerInterceptor;
68+
69+
@Inject(AsyncLocalStorage) private asyncLocalStorage!: AsyncLocalStorage<any>;
70+
71+
private _interceptorsContextCreator!: InterceptorsContextCreator;
72+
73+
get interceptorsContextCreator() {
74+
if (!this._interceptorsContextCreator) {
75+
this._interceptorsContextCreator = new InterceptorsContextCreator(
76+
this.moduleRef.container,
77+
this.moduleRef.applicationConfig
78+
);
79+
}
80+
81+
return this._interceptorsContextCreator;
82+
}
83+
84+
private interceptorsConsumer = new InterceptorsConsumer();
5485

5586
async run(params: ParamsForExecute[], tmpIds: (string | number)[]) {
5687
if (
@@ -79,7 +110,7 @@ export class ExecuteService {
79110

80111
private async executeOperations(
81112
params: ParamsForExecute[],
82-
tmpIds: (string | number)[]
113+
tmpIds: (string | number)[] = []
83114
) {
84115
const iterateParams = this.asyncIteratorFactory.createIterator(
85116
params as Parameters<ExecuteService['runOneOperation']>,
@@ -101,9 +132,39 @@ export class ExecuteService {
101132
const paramsForExecute = item as unknown as ParamsForExecute['params'];
102133

103134
const itemReplace = this.replaceTmpIds(paramsForExecute, tmpIdsMap);
135+
const body = itemReplace.at(-1);
136+
const currentTmpId = tmpIds[i];
137+
138+
if (methodName === 'postOne' && currentTmpId && body) {
139+
if (typeof body === 'object' && 'attributes' in body) {
140+
body['id'] = `${currentTmpId}`;
141+
itemReplace[itemReplace.length - 1];
142+
}
143+
}
104144

105-
// @ts-ignore
106-
const result = await controller[methodName](...itemReplace);
145+
const interceptors = this.getInterceptorsArray(
146+
controller,
147+
controller[methodName],
148+
currentParams.module
149+
);
150+
151+
const result$: any = await this.interceptorsConsumer.intercept(
152+
interceptors,
153+
[
154+
...Object.values(this.asyncLocalStorage.getStore() || {}),
155+
itemReplace,
156+
],
157+
controller,
158+
// @ts-ignore
159+
controller[methodName],
160+
// @ts-ignore
161+
async () => controller[methodName](...itemReplace)
162+
);
163+
164+
const result =
165+
interceptors.length === 0
166+
? await result$
167+
: await lastValueFrom(result$);
107168

108169
if (tmpIds[i] && result && !Array.isArray(result.data) && result.data) {
109170
tmpIdsMap[tmpIds[i]] = result.data.id;
@@ -120,6 +181,41 @@ export class ExecuteService {
120181
return resultArray;
121182
}
122183

184+
private getInterceptorsArray(
185+
controller: Controller,
186+
callback: (...arg: any) => any,
187+
module: ParamsForExecute['module']
188+
) {
189+
let controllerFromMap = this.mapControllerInterceptor.get(controller);
190+
191+
if (!controllerFromMap) {
192+
controllerFromMap = new Map();
193+
this.mapControllerInterceptor.set(controller, controllerFromMap);
194+
}
195+
196+
const interceptorsFromMap = controllerFromMap.get(callback);
197+
198+
if (interceptorsFromMap) {
199+
return interceptorsFromMap;
200+
}
201+
202+
const interceptorsForController = this.interceptorsContextCreator.create(
203+
controller,
204+
callback,
205+
module.token
206+
);
207+
208+
const interceptorsForMethode = new Set(
209+
Reflect.getMetadata(INTERCEPTORS_METADATA, callback) || []
210+
);
211+
212+
const resultInterceptors = interceptorsForController.filter((i) =>
213+
interceptorsForMethode.has(i.constructor)
214+
);
215+
controllerFromMap.set(callback, resultInterceptors);
216+
return resultInterceptors;
217+
}
218+
123219
private replaceTmpIds<T extends ParamsForExecute['params']>(
124220
inputParams: T,
125221
tmpIdsMap: Record<string | number, string | number>
@@ -162,6 +258,7 @@ export class ExecuteService {
162258
}
163259
return acum;
164260
},
261+
// @ts-ignore
165262
{ ...relationships }
166263
);
167264

libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { Test } from '@nestjs/testing';
22
import { ModulesContainer } from '@nestjs/core';
3-
import { MAP_ENTITY, MAP_CONTROLLER_ENTITY } from '../constants';
3+
import {
4+
MAP_ENTITY,
5+
MAP_CONTROLLER_ENTITY,
6+
OPTIONS,
7+
MAP_CONTROLLER_INTERCEPTORS,
8+
} from '../constants';
49
import { Operation } from '../utils';
510
import { ExplorerService } from './explorer.service';
611

@@ -31,6 +36,14 @@ describe('ExplorerService', () => {
3136
provide: MAP_CONTROLLER_ENTITY,
3237
useValue: new Map([[EntityName, ControllerName]]),
3338
},
39+
{
40+
provide: MAP_CONTROLLER_INTERCEPTORS,
41+
useValue: new Map(),
42+
},
43+
{
44+
provide: OPTIONS,
45+
useValue: {},
46+
},
3447
],
3548
}).compile();
3649

libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import { Type } from '@nestjs/common';
1+
import { NestInterceptor, Type } from '@nestjs/common';
22
import { Module } from '@nestjs/core/injector/module';
3+
import { Controller } from '@nestjs/common/interfaces';
34
import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type';
45
import { Entity, MethodName } from '../../../types';
56
import { JsonBaseController } from '../../../mixin/controller/json-base.controller';
67

8+
export type MapControllerInterceptor = Map<
9+
Controller,
10+
Map<(...arg: any) => any, NestInterceptor[]>
11+
>;
712
export type MapController = Map<EntityClassOrSchema, Type<any>>;
813
export type MapEntity = Map<string, EntityClassOrSchema>;
914

0 commit comments

Comments
 (0)