Skip to content

Commit d5833e3

Browse files
feature(@nestjs/core) extract helpers to context utils class
1 parent deffcdc commit d5833e3

14 files changed

Lines changed: 614 additions & 191 deletions

File tree

packages/common/services/logger.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class Logger implements LoggerService {
1919
private static readonly yellow = clc.xterm(3);
2020

2121
constructor(
22-
@Optional() private readonly context: string,
22+
@Optional() private readonly context?: string,
2323
@Optional() private readonly isTimeDiffEnabled = false,
2424
) {}
2525

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,75 @@
1+
import { ParseIntPipe } from '@nestjs/common';
2+
import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants';
13
import { expect } from 'chai';
24
import { createRouteParamDecorator } from '../../decorators/http/create-route-param-metadata.decorator';
3-
import { CUSTOM_ROUTE_AGRS_METADATA } from '../../constants';
45

56
describe('createRouteParamDecorator', () => {
6-
let key;
7-
let reflector;
87
let result;
98

109
beforeEach(() => {
11-
key = 'key';
12-
reflector = (data, req, res, next) => true;
13-
result = createRouteParamDecorator(reflector);
10+
const fn = (data, req) => true;
11+
result = createRouteParamDecorator(fn);
1412
});
1513
it('should return a function as a first element', () => {
1614
expect(result).to.be.a('function');
1715
});
16+
describe('returned decorator', () => {
17+
const factoryFn = (data, req) => true;
18+
const Decorator = createRouteParamDecorator(factoryFn);
19+
20+
describe('when 0 pipes have been passed', () => {
21+
const data = 'test';
22+
class Test {
23+
public test(@Decorator(data) param) {}
24+
}
25+
it('should enhance param with "data"', () => {
26+
const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test');
27+
const key = Object.keys(metadata)[0];
28+
expect(metadata[key]).to.be.eql({
29+
data: 'test',
30+
factory: factoryFn,
31+
index: 0,
32+
pipes: [],
33+
});
34+
});
35+
});
36+
37+
describe('when > 0 pipes have been passed', () => {
38+
const data = 'test';
39+
const pipe = new ParseIntPipe();
40+
class Test {
41+
public test(
42+
@Decorator(data, pipe)
43+
param,
44+
) {}
45+
46+
public testNoData(@Decorator(pipe) param) {}
47+
}
48+
it('should enhance param with "data" and ParseIntPipe', () => {
49+
const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test');
50+
const key = Object.keys(metadata)[0];
51+
expect(metadata[key]).to.be.eql({
52+
data: 'test',
53+
factory: factoryFn,
54+
index: 0,
55+
pipes: [pipe],
56+
});
57+
});
58+
59+
it('should enhance param with ParseIntPipe', () => {
60+
const metadata = Reflect.getMetadata(
61+
ROUTE_ARGS_METADATA,
62+
Test,
63+
'testNoData',
64+
);
65+
const key = Object.keys(metadata)[0];
66+
expect(metadata[key]).to.be.eql({
67+
data: undefined,
68+
factory: factoryFn,
69+
index: 0,
70+
pipes: [pipe],
71+
});
72+
});
73+
});
74+
});
1875
});

packages/common/test/decorators/route-params.decorator.spec.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import 'reflect-metadata';
1+
import { Param } from '@nestjs/common';
22
import { expect } from 'chai';
3+
import 'reflect-metadata';
4+
import { Body, Query } from '../../decorators';
35
import { RequestMethod } from '../../enums/request-method.enum';
4-
import { Get, Post, Delete, All, Put, Patch } from '../../index';
6+
import { All, Delete, Get, Patch, Post, Put } from '../../index';
57

68
describe('@Get', () => {
79
const requestPath = 'test';
@@ -13,7 +15,7 @@ describe('@Get', () => {
1315
it('should enhance class with expected request metadata', () => {
1416
class Test {
1517
@Get(requestPath)
16-
public static test() {}
18+
public static test(@Param('id') params) {}
1719
}
1820

1921
const path = Reflect.getMetadata('path', Test.test);
@@ -56,7 +58,7 @@ describe('@Post', () => {
5658
it('should set path on "/" by default', () => {
5759
class Test {
5860
@Post()
59-
public static test() {}
61+
public static test(@Query() query) {}
6062
}
6163
const path = Reflect.getMetadata('path', Test.test);
6264
expect(path).to.be.eql('/');
@@ -73,7 +75,7 @@ describe('@Delete', () => {
7375
it('should enhance class with expected request metadata', () => {
7476
class Test {
7577
@Delete(requestPath)
76-
public static test() {}
78+
public static test(@Body() body) {}
7779
}
7880

7981
const path = Reflect.getMetadata('path', Test.test);
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { ParamData } from '@nestjs/common';
2+
import { PARAMTYPES_METADATA } from '@nestjs/common/constants';
3+
import { Controller, PipeTransform } from '@nestjs/common/interfaces';
4+
5+
export interface ParamProperties<T = any, IExtractor extends Function = any> {
6+
index: number;
7+
type: T | string;
8+
data: ParamData;
9+
pipes: PipeTransform[];
10+
extractValue: IExtractor;
11+
}
12+
13+
export class ContextUtils {
14+
public mapParamType(key: string): string {
15+
const keyPair = key.split(':');
16+
return keyPair[0];
17+
}
18+
19+
public reflectCallbackParamtypes(
20+
instance: Controller,
21+
methodName: string,
22+
): any[] {
23+
return Reflect.getMetadata(PARAMTYPES_METADATA, instance, methodName);
24+
}
25+
26+
public reflectCallbackMetadata<T = any>(
27+
instance: Controller,
28+
methodName: string,
29+
metadataKey: string,
30+
): T {
31+
return Reflect.getMetadata(metadataKey, instance.constructor, methodName);
32+
}
33+
34+
public getArgumentsLength<T>(keys: string[], metadata: T): number {
35+
return Math.max(...keys.map(key => metadata[key].index)) + 1;
36+
}
37+
38+
public createNullArray(length: number): any[] {
39+
return Array.apply(null, { length }).fill(null);
40+
}
41+
42+
public mergeParamsMetatypes(
43+
paramsProperties: ParamProperties[],
44+
paramtypes: any[],
45+
): (ParamProperties & { metatype?: any })[] {
46+
if (!paramtypes) {
47+
return paramsProperties;
48+
}
49+
return paramsProperties.map(param => ({
50+
...param,
51+
metatype: paramtypes[param.index],
52+
}));
53+
}
54+
}

packages/core/helpers/external-context-creator.ts

Lines changed: 142 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { ForbiddenException } from '@nestjs/common';
2-
import { Controller } from '@nestjs/common/interfaces';
1+
import { ForbiddenException, ParamData } from '@nestjs/common';
2+
import { CUSTOM_ROUTE_AGRS_METADATA } from '@nestjs/common/constants';
3+
import { Controller, Transform } from '@nestjs/common/interfaces';
4+
import { isFunction, isUndefined } from '@nestjs/common/utils/shared.utils';
35
import 'reflect-metadata';
46
import { FORBIDDEN_MESSAGE } from '../guards/constants';
57
import { GuardsConsumer } from '../guards/guards-consumer';
@@ -8,29 +10,81 @@ import { Module } from '../injector/module';
810
import { ModulesContainer } from '../injector/modules-container';
911
import { InterceptorsConsumer } from '../interceptors/interceptors-consumer';
1012
import { InterceptorsContextCreator } from '../interceptors/interceptors-context-creator';
13+
import { PipesConsumer } from '../pipes/pipes-consumer';
14+
import { PipesContextCreator } from '../pipes/pipes-context-creator';
15+
import { ContextUtils, ParamProperties } from './context-utils';
16+
17+
export interface ParamsMetadata {
18+
[prop: number]: {
19+
index: number;
20+
data?: ParamData;
21+
};
22+
}
23+
24+
export interface ParamsFactory {
25+
exchangeKeyForValue(type: number, data: ParamData, args: any): any;
26+
}
1127

1228
export class ExternalContextCreator {
29+
private readonly contextUtils = new ContextUtils();
30+
1331
constructor(
1432
private readonly guardsContextCreator: GuardsContextCreator,
1533
private readonly guardsConsumer: GuardsConsumer,
1634
private readonly interceptorsContextCreator: InterceptorsContextCreator,
1735
private readonly interceptorsConsumer: InterceptorsConsumer,
1836
private readonly modulesContainer: ModulesContainer,
37+
private readonly pipesContextCreator: PipesContextCreator,
38+
private readonly pipesConsumer: PipesConsumer,
1939
) {}
2040

21-
public create(
41+
public create<T extends ParamsMetadata = ParamsMetadata>(
2242
instance: Controller,
2343
callback: (...args) => any,
2444
methodName: string,
45+
metadataKey?: string,
46+
paramsFactory?: ParamsFactory,
2547
) {
2648
const module = this.findContextModuleName(instance.constructor);
49+
const pipes = this.pipesContextCreator.create(instance, callback, module);
50+
const paramtypes = this.contextUtils.reflectCallbackParamtypes(
51+
instance,
52+
methodName,
53+
);
2754
const guards = this.guardsContextCreator.create(instance, callback, module);
2855
const interceptors = this.interceptorsContextCreator.create(
2956
instance,
3057
callback,
3158
module,
3259
);
60+
61+
const metadata =
62+
this.contextUtils.reflectCallbackMetadata<T>(
63+
instance,
64+
methodName,
65+
metadataKey || '',
66+
) || {};
67+
const keys = Object.keys(metadata);
68+
const argsLength = this.contextUtils.getArgumentsLength(keys, metadata);
69+
const paramsMetadata = paramsFactory
70+
? this.exchangeKeysForValues(keys, metadata, module, paramsFactory)
71+
: null;
72+
73+
const paramsOptions = paramsMetadata
74+
? this.contextUtils.mergeParamsMetatypes(paramsMetadata, paramtypes)
75+
: [];
76+
const fnApplyPipes = this.createPipesFn(pipes, paramsOptions);
77+
78+
const handler = (initialArgs, ...args) => async () => {
79+
if (fnApplyPipes) {
80+
await fnApplyPipes(initialArgs, ...args);
81+
return callback.apply(instance, initialArgs);
82+
}
83+
return callback.apply(instance, args);
84+
};
85+
3386
return async (...args) => {
87+
const initialArgs = this.contextUtils.createNullArray(argsLength);
3488
const canActivate = await this.guardsConsumer.tryActivate(
3589
guards,
3690
args,
@@ -40,14 +94,14 @@ export class ExternalContextCreator {
4094
if (!canActivate) {
4195
throw new ForbiddenException(FORBIDDEN_MESSAGE);
4296
}
43-
const handler = () => callback.apply(instance, args);
44-
return await this.interceptorsConsumer.intercept(
97+
const result = await this.interceptorsConsumer.intercept(
4598
interceptors,
4699
args,
47100
instance,
48101
callback,
49-
handler,
102+
handler(initialArgs, ...args),
50103
);
104+
return await this.transformToResult(result);
51105
};
52106
}
53107

@@ -71,4 +125,86 @@ export class ExternalContextCreator {
71125
);
72126
return !!hasComponent;
73127
}
128+
129+
public exchangeKeysForValues<TMetadata = any>(
130+
keys: string[],
131+
metadata: TMetadata,
132+
moduleContext: string,
133+
paramsFactory: ParamsFactory,
134+
): ParamProperties[] {
135+
this.pipesContextCreator.setModuleContext(moduleContext);
136+
return keys.map(key => {
137+
const { index, data, pipes: pipesCollection } = metadata[key];
138+
const pipes = this.pipesContextCreator.createConcreteContext(
139+
pipesCollection,
140+
);
141+
const type = this.contextUtils.mapParamType(key);
142+
143+
if (key.includes(CUSTOM_ROUTE_AGRS_METADATA)) {
144+
const { factory } = metadata[key];
145+
const customExtractValue = this.getCustomFactory(factory, data);
146+
return { index, extractValue: customExtractValue, type, data, pipes };
147+
}
148+
const numericType = Number(type);
149+
const extractValue = (...args) =>
150+
paramsFactory.exchangeKeyForValue(numericType, data, args);
151+
152+
return { index, extractValue, type: numericType, data, pipes };
153+
});
154+
}
155+
156+
public getCustomFactory(factory: (...args) => void, data): (...args) => any {
157+
return !isUndefined(factory) && isFunction(factory)
158+
? (...args) => factory(data, args)
159+
: () => null;
160+
}
161+
162+
public createPipesFn(
163+
pipes: any[],
164+
paramsOptions: (ParamProperties & { metatype?: any })[],
165+
) {
166+
const pipesFn = async (args, ...gqlArgs) => {
167+
await Promise.all(
168+
paramsOptions.map(async param => {
169+
const {
170+
index,
171+
extractValue,
172+
type,
173+
data,
174+
metatype,
175+
pipes: paramPipes,
176+
} = param;
177+
const value = extractValue(...gqlArgs);
178+
179+
args[index] = await this.getParamValue(
180+
value,
181+
{ metatype, type, data },
182+
pipes.concat(paramPipes),
183+
);
184+
}),
185+
);
186+
};
187+
return paramsOptions.length ? pipesFn : null;
188+
}
189+
190+
public async getParamValue<T>(
191+
value: T,
192+
{ metatype, type, data },
193+
transforms: Transform<any>[],
194+
): Promise<any> {
195+
return await this.pipesConsumer.apply(
196+
value,
197+
{ metatype, type, data },
198+
transforms,
199+
);
200+
}
201+
202+
public async transformToResult(resultOrDeffered) {
203+
if (resultOrDeffered instanceof Promise) {
204+
return await resultOrDeffered;
205+
} else if (resultOrDeffered && isFunction(resultOrDeffered.subscribe)) {
206+
return await resultOrDeffered.toPromise();
207+
}
208+
return resultOrDeffered;
209+
}
74210
}

0 commit comments

Comments
 (0)