Skip to content

Commit 38dca3d

Browse files
fix(core): fix di related issues (request scope, modules hierarchy)
1 parent eb44443 commit 38dca3d

11 files changed

Lines changed: 171 additions & 39 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { INestApplication } from '@nestjs/common';
2+
import { Test } from '@nestjs/testing';
3+
import { expect } from 'chai';
4+
import * as request from 'supertest';
5+
import { RequestChainModule } from '../src/request-chain/request-chain.module';
6+
import { RequestChainService } from '../src/request-chain/request-chain.service';
7+
8+
describe('Request scope (modules propagation)', () => {
9+
let server;
10+
let app: INestApplication;
11+
12+
before(async () => {
13+
const module = await Test.createTestingModule({
14+
imports: [RequestChainModule],
15+
}).compile();
16+
17+
app = module.createNestApplication();
18+
server = app.getHttpServer();
19+
await app.init();
20+
});
21+
22+
describe('when service from parent module is request scoped', () => {
23+
before(async () => {
24+
const performHttpCall = end =>
25+
request(server)
26+
.get('/hello')
27+
.end((err, res) => {
28+
if (err) return end(err);
29+
end();
30+
});
31+
await new Promise(resolve => performHttpCall(resolve));
32+
await new Promise(resolve => performHttpCall(resolve));
33+
await new Promise(resolve => performHttpCall(resolve));
34+
});
35+
36+
it(`should not fail`, async () => {
37+
expect(RequestChainService.COUNTER).to.be.eql(3);
38+
});
39+
});
40+
41+
after(async () => {
42+
await app.close();
43+
});
44+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Module } from '@nestjs/common';
2+
import { HelperService } from './helper.service';
3+
4+
@Module({
5+
providers: [HelperService],
6+
exports: [HelperService],
7+
})
8+
export class HelperModule {}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Injectable, Scope } from '@nestjs/common';
2+
3+
@Injectable({ scope: Scope.REQUEST })
4+
export class HelperService {
5+
public noop() {}
6+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Controller, Get } from '@nestjs/common';
2+
import { RequestChainService } from './request-chain.service';
3+
4+
@Controller('hello')
5+
export class RequestChainController {
6+
constructor(private readonly chainService: RequestChainService) {}
7+
8+
@Get()
9+
greeting(): void {
10+
this.chainService.call();
11+
}
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Module } from '@nestjs/common';
2+
import { HelperModule } from './helper/helper.module';
3+
import { RequestChainController } from './request-chain.controller';
4+
import { RequestChainService } from './request-chain.service';
5+
6+
@Module({
7+
imports: [HelperModule],
8+
providers: [RequestChainService],
9+
controllers: [RequestChainController],
10+
})
11+
export class RequestChainModule {}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { HelperService } from './helper/helper.service';
3+
4+
@Injectable()
5+
export class RequestChainService {
6+
static COUNTER = 0;
7+
constructor(private readonly helperService: HelperService) {
8+
helperService.noop();
9+
RequestChainService.COUNTER += 1;
10+
}
11+
12+
call() {
13+
this.helperService.noop();
14+
}
15+
}

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

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import { ForbiddenException, ParamData } from '@nestjs/common';
22
import { CUSTOM_ROUTE_AGRS_METADATA } from '@nestjs/common/constants';
3-
import { Controller, PipeTransform } from '@nestjs/common/interfaces';
3+
import {
4+
ContextType,
5+
Controller,
6+
PipeTransform,
7+
} from '@nestjs/common/interfaces';
48
import { isEmpty, isFunction } from '@nestjs/common/utils/shared.utils';
9+
import { ExternalExceptionFilterContext } from '../exceptions/external-exception-filter-context';
510
import { FORBIDDEN_MESSAGE } from '../guards/constants';
611
import { GuardsConsumer } from '../guards/guards-consumer';
712
import { GuardsContextCreator } from '../guards/guards-context-creator';
13+
import { STATIC_CONTEXT } from '../injector/constants';
814
import { NestContainer } from '../injector/container';
915
import { Module } from '../injector/module';
1016
import { ModulesContainer } from '../injector/modules-container';
1117
import { InterceptorsConsumer } from '../interceptors/interceptors-consumer';
1218
import { InterceptorsContextCreator } from '../interceptors/interceptors-context-creator';
1319
import { PipesConsumer } from '../pipes/pipes-consumer';
1420
import { PipesContextCreator } from '../pipes/pipes-context-creator';
15-
import { ExternalExceptionFilterContext } from '../exceptions/external-exception-filter-context';
16-
import { STATIC_CONTEXT } from '../injector/constants';
1721
import { ContextUtils, ParamProperties } from './context-utils';
1822
import { ExternalErrorProxy } from './external-proxy';
1923
import { HandlerMetadataStorage } from './handler-metadata-storage';
@@ -80,7 +84,10 @@ export class ExternalContextCreator {
8084
);
8185
}
8286

83-
public create<T extends ParamsMetadata = ParamsMetadata>(
87+
public create<
88+
TParamsMetadata extends ParamsMetadata = ParamsMetadata,
89+
TContext extends ContextType = ContextType
90+
>(
8491
instance: Controller,
8592
callback: (...args: any[]) => any,
8693
methodName: string,
@@ -93,14 +100,12 @@ export class ExternalContextCreator {
93100
guards: true,
94101
filters: true,
95102
},
103+
contextType: TContext = 'http' as TContext,
96104
) {
97105
const module = this.getContextModuleName(instance.constructor);
98-
const { argsLength, paramtypes, getParamsMetadata } = this.getMetadata<T>(
99-
instance,
100-
methodName,
101-
metadataKey,
102-
paramsFactory,
103-
);
106+
const { argsLength, paramtypes, getParamsMetadata } = this.getMetadata<
107+
TParamsMetadata
108+
>(instance, methodName, metadataKey, paramsFactory);
104109
const pipes = this.pipesContextCreator.create(
105110
instance,
106111
callback,
@@ -138,7 +143,7 @@ export class ExternalContextCreator {
138143
: [];
139144

140145
const fnCanActivate = options.guards
141-
? this.createGuardsFn(guards, instance, callback)
146+
? this.createGuardsFn(guards, instance, callback, contextType)
142147
: null;
143148
const fnApplyPipes = this.createPipesFn(pipes, paramsOptions);
144149
const handler = (initialArgs: any[], ...args: any[]) => async () => {
@@ -159,6 +164,7 @@ export class ExternalContextCreator {
159164
instance,
160165
callback,
161166
handler(initialArgs, ...args),
167+
contextType,
162168
);
163169
return this.transformToResult(result);
164170
};
@@ -316,17 +322,19 @@ export class ExternalContextCreator {
316322
return resultOrDeffered;
317323
}
318324

319-
public createGuardsFn(
325+
public createGuardsFn<TContext extends ContextType = ContextType>(
320326
guards: any[],
321327
instance: Controller,
322328
callback: (...args: any[]) => any,
329+
contextType?: TContext,
323330
): Function | null {
324331
const canActivateFn = async (args: any[]) => {
325-
const canActivate = await this.guardsConsumer.tryActivate(
332+
const canActivate = await this.guardsConsumer.tryActivate<TContext>(
326333
guards,
327334
args,
328335
instance,
329336
callback,
337+
contextType,
330338
);
331339
if (!canActivate) {
332340
throw new ForbiddenException(FORBIDDEN_MESSAGE);

packages/core/injector/injector.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -358,10 +358,8 @@ export class Injector {
358358
wrapper,
359359
contextId,
360360
inquirer,
361+
keyOrIndex,
361362
);
362-
isString(keyOrIndex)
363-
? wrapper.addPropertiesMetadata(keyOrIndex, instanceWrapper)
364-
: wrapper.addCtorMetadata(keyOrIndex, instanceWrapper);
365363

366364
return this.resolveComponentHost(
367365
module,
@@ -419,6 +417,7 @@ export class Injector {
419417
wrapper: InstanceWrapper<T>,
420418
contextId = STATIC_CONTEXT,
421419
inquirer?: InstanceWrapper,
420+
keyOrIndex?: string | number,
422421
): Promise<InstanceWrapper<T>> {
423422
const { name } = dependencyContext;
424423
if (wrapper && wrapper.name === name) {
@@ -428,23 +427,28 @@ export class Injector {
428427
module,
429428
);
430429
}
431-
const scanInExports = () =>
432-
this.lookupComponentInExports(
433-
dependencyContext,
434-
module,
435-
wrapper,
436-
contextId,
437-
inquirer,
438-
);
439-
return providers.has(name) ? providers.get(name) : scanInExports();
430+
if (providers.has(name)) {
431+
const instanceWrapper = providers.get(name);
432+
this.addDependencyMetadata(keyOrIndex, wrapper, instanceWrapper);
433+
return instanceWrapper;
434+
}
435+
return this.lookupComponentInParentModules(
436+
dependencyContext,
437+
module,
438+
wrapper,
439+
contextId,
440+
inquirer,
441+
keyOrIndex,
442+
);
440443
}
441444

442-
public async lookupComponentInExports<T = any>(
445+
public async lookupComponentInParentModules<T = any>(
443446
dependencyContext: InjectorDependencyContext,
444447
module: Module,
445448
wrapper: InstanceWrapper<T>,
446449
contextId = STATIC_CONTEXT,
447450
inquirer?: InstanceWrapper,
451+
keyOrIndex?: string | number,
448452
) {
449453
const instanceWrapper = await this.lookupComponentInImports(
450454
module,
@@ -453,6 +457,7 @@ export class Injector {
453457
[],
454458
contextId,
455459
inquirer,
460+
keyOrIndex,
456461
);
457462
if (isNil(instanceWrapper)) {
458463
throw new UnknownDependenciesException(
@@ -471,6 +476,7 @@ export class Injector {
471476
moduleRegistry: any[] = [],
472477
contextId = STATIC_CONTEXT,
473478
inquirer?: InstanceWrapper,
479+
keyOrIndex?: string | number,
474480
isTraversing?: boolean,
475481
): Promise<any> {
476482
let instanceWrapperRef: InstanceWrapper = null;
@@ -499,14 +505,17 @@ export class Injector {
499505
moduleRegistry,
500506
contextId,
501507
inquirer,
508+
keyOrIndex,
502509
true,
503510
);
504511
if (instanceRef) {
512+
this.addDependencyMetadata(keyOrIndex, wrapper, instanceRef);
505513
return instanceRef;
506514
}
507515
continue;
508516
}
509517
instanceWrapperRef = providers.get(name);
518+
this.addDependencyMetadata(keyOrIndex, wrapper, instanceWrapperRef);
510519

511520
const inquirerId = this.getInquirerId(inquirer);
512521
const instanceHost = instanceWrapperRef.getInstanceByContextId(
@@ -752,4 +761,14 @@ export class Injector {
752761
) {
753762
return param === INQUIRER && parentInquirer;
754763
}
764+
765+
private addDependencyMetadata(
766+
keyOrIndex: number | string,
767+
hostWrapper: InstanceWrapper,
768+
instanceWrapper: InstanceWrapper,
769+
) {
770+
isString(keyOrIndex)
771+
? hostWrapper.addPropertiesMetadata(keyOrIndex, instanceWrapper)
772+
: hostWrapper.addCtorMetadata(keyOrIndex, instanceWrapper);
773+
}
755774
}

packages/core/injector/module-token-factory.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ export class ModuleTokenFactory {
1313
const moduleScope = this.reflectScope(metatype);
1414
const isSingleScoped = moduleScope === true;
1515
const opaqueToken = {
16-
module: metatype,
16+
module: this.getModuleName(metatype),
1717
dynamic: this.getDynamicMetadataToken(dynamicModuleMetadata),
1818
scope: isSingleScoped ? this.getScopeStack(scope) : moduleScope,
1919
};
20-
return hash(opaqueToken);
20+
return hash(opaqueToken, { ignoreUnknown: true });
2121
}
2222

2323
public getDynamicMetadataToken(
@@ -45,6 +45,10 @@ export class ModuleTokenFactory {
4545
return stack.map(module => module.name);
4646
}
4747

48+
public getModuleName(metatype: Type<any>): string {
49+
return metatype.name;
50+
}
51+
4852
private reflectScope(metatype: Type<any>) {
4953
const scope = Reflect.getMetadata(SHARED_MODULE_METADATA, metatype);
5054
return scope ? scope : 'global';

packages/core/test/injector/injector.spec.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -266,12 +266,12 @@ describe('Injector', () => {
266266
describe('lookupComponent', () => {
267267
let lookupComponentInImports: sinon.SinonStub;
268268
const metatype = { name: 'test', metatype: { name: 'test' } };
269-
const wrapper: any = {
269+
const wrapper = new InstanceWrapper({
270270
name: 'Test',
271-
metatype,
271+
metatype: metatype as any,
272272
instance: null,
273273
isResolved: false,
274-
};
274+
});
275275
beforeEach(() => {
276276
lookupComponentInImports = sinon.stub();
277277
(injector as any).lookupComponentInImports = lookupComponentInImports;
@@ -303,10 +303,9 @@ describe('Injector', () => {
303303
collection as any,
304304
null,
305305
{ name, index: 0, dependencies: [] },
306-
{
307-
...wrapper,
306+
Object.assign(wrapper, {
308307
name,
309-
},
308+
}),
310309
);
311310
expect(result).to.eventually.be.rejected;
312311
});
@@ -374,7 +373,7 @@ describe('Injector', () => {
374373
const result = await injector.lookupComponentInImports(
375374
module as any,
376375
null,
377-
null,
376+
new InstanceWrapper(),
378377
);
379378
expect(result).to.be.eq(null);
380379
});
@@ -443,7 +442,7 @@ describe('Injector', () => {
443442
await injector.lookupComponentInImports(
444443
module as any,
445444
metatype as any,
446-
null,
445+
new InstanceWrapper(),
447446
);
448447
expect(loadProvider.called).to.be.true;
449448
});

0 commit comments

Comments
 (0)