Skip to content

Commit 62e0283

Browse files
feature(core) instantiate class dynamically (ModuleRef)
1 parent 89d84d2 commit 62e0283

8 files changed

Lines changed: 175 additions & 81 deletions

File tree

package.json

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
"scripts": {
66
"coverage": "nyc report --reporter=text-lcov | coveralls",
77
"precommit": "lint-staged",
8-
"test": "nyc --require ts-node/register mocha packages/**/*.spec.ts --reporter spec",
9-
"integration-test": "mocha integration/**/*.spec.ts --reporter spec --require ts-node/register",
10-
"lint": "tslint -p tsconfig.json -c tslint.json \"packages/**/*.ts\" -e \"*.spec.ts\"",
11-
"format": "prettier **/**/*.ts --ignore-path ./.prettierignore --write && git status",
8+
"test":
9+
"nyc --require ts-node/register mocha packages/**/*.spec.ts --reporter spec",
10+
"integration-test":
11+
"mocha integration/**/*.spec.ts --reporter spec --require ts-node/register",
12+
"lint":
13+
"tslint -p tsconfig.json -c tslint.json \"packages/**/*.ts\" -e \"*.spec.ts\"",
14+
"format":
15+
"prettier **/**/*.ts --ignore-path ./.prettierignore --write && git status",
1216
"build": "gulp build && gulp move",
1317
"build:lib": "gulp build --dist bundle",
1418
"postinstall": "opencollective",
@@ -17,11 +21,16 @@
1721
"prepare:rc": "npm run build:lib && npm run copy-docs",
1822
"prepare:next": "npm run build:lib && npm run copy-docs",
1923
"prepare:beta": "npm run build:lib && npm run copy-docs",
20-
"publish": "npm run prepare && ./node_modules/.bin/lerna publish --exact -m \"chore(@nestjs) publish %s release\"",
21-
"publish:rc": "npm run prepare && ./node_modules/.bin/lerna publish --npm-tag=rc -m \"chore(@nestjs) publish %s release\"",
22-
"publish:next": "npm run prepare && ./node_modules/.bin/lerna publish --npm-tag=next --skip-git -m \"chore(@nestjs) publish %s release\"",
23-
"publish:beta": "npm run prepare && ./node_modules/.bin/lerna publish --npm-tag=beta -m \"chore(@nestjs) publish %s release\"",
24-
"publish:test": "npm run prepare && ./node_modules/.bin/lerna publish --npm-tag=test --skip-git -m \"chore(@nestjs) publish %s release\""
24+
"publish":
25+
"npm run prepare && ./node_modules/.bin/lerna publish --exact -m \"chore(@nestjs) publish %s release\"",
26+
"publish:rc":
27+
"npm run prepare && ./node_modules/.bin/lerna publish --npm-tag=rc -m \"chore(@nestjs) publish %s release\"",
28+
"publish:next":
29+
"npm run prepare && ./node_modules/.bin/lerna publish --npm-tag=next --skip-git -m \"chore(@nestjs) publish %s release\"",
30+
"publish:beta":
31+
"npm run prepare && ./node_modules/.bin/lerna publish --npm-tag=beta -m \"chore(@nestjs) publish %s release\"",
32+
"publish:test":
33+
"npm run prepare && ./node_modules/.bin/lerna publish --npm-tag=test --skip-git -m \"chore(@nestjs) publish %s release\""
2534
},
2635
"engines": {
2736
"node": ">= 8.9.0"
@@ -130,9 +139,7 @@
130139
}
131140
},
132141
"nyc": {
133-
"include": [
134-
"packages/**/*.ts"
135-
],
142+
"include": ["packages/**/*.ts"],
136143
"exclude": [
137144
"node_modules/",
138145
"packages/**/*.spec.ts",
@@ -147,27 +154,18 @@
147154
"packages/microservices/microservices-module.ts",
148155
"packages/core/middleware/middleware-module.ts",
149156
"packages/core/injector/module-ref.ts",
157+
"packages/core/injector/container-scanner.ts",
150158
"packages/common/cache/**/*",
151159
"packages/common/serializer/**/*",
152160
"packages/common/services/logger.service.ts"
153161
],
154-
"extension": [
155-
".ts"
156-
],
157-
"require": [
158-
"ts-node/register"
159-
],
160-
"reporter": [
161-
"text-summary",
162-
"html"
163-
],
162+
"extension": [".ts"],
163+
"require": ["ts-node/register"],
164+
"reporter": ["text-summary", "html"],
164165
"sourceMap": true,
165166
"instrument": true
166167
},
167168
"lint-staged": {
168-
"packages/**/*.{ts,json}": [
169-
"npm run format",
170-
"git add"
171-
]
169+
"packages/**/*.{ts,json}": ["npm run format", "git add"]
172170
}
173171
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { INVALID_CLASS_MESSAGE } from '../messages';
2+
import { RuntimeException } from './runtime.exception';
3+
4+
export class InvalidClassException extends RuntimeException {
5+
constructor(value: any) {
6+
super(INVALID_CLASS_MESSAGE`${value}`);
7+
}
8+
}

packages/core/errors/messages.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ export const INVALID_MODULE_MESSAGE = (text, scope: string) =>
3838
export const UNKNOWN_EXPORT_MESSAGE = (text, module: string) =>
3939
`Nest cannot export a component/module that is not a part of the currently processed module (${module}). Please verify whether each exported unit is available in this particular context.`;
4040

41+
export const INVALID_CLASS_MESSAGE = (text, value: any) =>
42+
`ModuleRef cannot instantiate class (${value} is not constructable).`;
43+
4144
export const INVALID_MIDDLEWARE_CONFIGURATION = `Invalid middleware configuration passed inside the module 'configure()' method.`;
4245
export const UNKNOWN_REQUEST_MAPPING = `Request mapping properties not defined in the @RequestMapping() annotation!`;
4346
export const UNHANDLED_RUNTIME_EXCEPTION = `Unhandled Runtime Exception.`;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Type } from '@nestjs/common';
2+
import { isFunction } from '@nestjs/common/utils/shared.utils';
3+
import { UnknownElementException } from '../errors/exceptions/unknown-element.exception';
4+
import { InstanceWrapper, NestContainer } from './container';
5+
import { Module } from './module';
6+
7+
export class ContainerScanner {
8+
private flatContainer: Partial<Module>;
9+
10+
constructor(private readonly container: NestContainer) {}
11+
12+
public find<TInput = any, TResult = TInput>(
13+
typeOrToken: Type<TInput> | string | symbol,
14+
): TResult {
15+
this.initFlatContainer();
16+
return this.findInstanceByPrototypeOrToken<TInput, TResult>(
17+
typeOrToken,
18+
this.flatContainer,
19+
);
20+
}
21+
22+
public findInstanceByPrototypeOrToken<TInput = any, TResult = TInput>(
23+
metatypeOrToken: Type<TInput> | string | symbol,
24+
contextModule: Partial<Module>,
25+
): TResult {
26+
const dependencies = new Map([
27+
...contextModule.components,
28+
...contextModule.routes,
29+
...contextModule.injectables,
30+
]);
31+
const name = isFunction(metatypeOrToken)
32+
? (metatypeOrToken as Function).name
33+
: metatypeOrToken;
34+
const instanceWrapper = dependencies.get(name as string);
35+
if (!instanceWrapper) {
36+
throw new UnknownElementException();
37+
}
38+
return (instanceWrapper as InstanceWrapper<any>).instance;
39+
}
40+
41+
private initFlatContainer() {
42+
if (this.flatContainer) {
43+
return undefined;
44+
}
45+
const modules = this.container.getModules();
46+
const initialValue = {
47+
components: [],
48+
routes: [],
49+
injectables: [],
50+
};
51+
const merge = <T = any>(
52+
initial: Map<string, T> | T[],
53+
arr: Map<string, T>,
54+
) => [...initial, ...arr];
55+
56+
this.flatContainer = ([...modules.values()].reduce(
57+
(current, next) => ({
58+
components: merge(current.components, next.components),
59+
routes: merge(current.routes, next.routes),
60+
injectables: merge(current.injectables, next.injectables),
61+
}),
62+
initialValue,
63+
) as any) as Partial<Module>;
64+
}
65+
}

packages/core/injector/injector.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export class Injector {
120120

121121
public async loadInstance<T>(
122122
wrapper: InstanceWrapper<T>,
123-
collection,
123+
collection: Map<string, InstanceWrapper<any>>,
124124
module: Module,
125125
) {
126126
if (wrapper.isPending) {
@@ -129,19 +129,19 @@ export class Injector {
129129
const done = this.applyDoneHook(wrapper);
130130
const { name, inject } = wrapper;
131131

132-
const targetMetatype = collection.get(name);
133-
if (isUndefined(targetMetatype)) {
132+
const targetWrapper = collection.get(name);
133+
if (isUndefined(targetWrapper)) {
134134
throw new RuntimeException();
135135
}
136-
if (targetMetatype.isResolved) {
137-
return void 0;
136+
if (targetWrapper.isResolved) {
137+
return undefined;
138138
}
139139
await this.resolveConstructorParams<T>(
140140
wrapper,
141141
module,
142142
inject,
143143
async instances =>
144-
this.instantiateClass(instances, wrapper, targetMetatype, done),
144+
this.instantiateClass(instances, wrapper, targetWrapper, done),
145145
);
146146
}
147147

Lines changed: 39 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,61 @@
11
import { Type } from '@nestjs/common';
2-
import { isFunction } from '@nestjs/common/utils/shared.utils';
3-
import { UnknownElementException } from '../errors/exceptions/unknown-element.exception';
4-
import { InstanceWrapper, NestContainer } from './container';
2+
import { NestContainer } from './container';
3+
import { ContainerScanner } from './container-scanner';
4+
import { Injector } from './injector';
55
import { Module } from './module';
66

77
export abstract class ModuleRef {
8-
private flattenModuleFixture: Partial<Module>;
8+
private readonly injector = new Injector();
9+
private readonly containerScanner: ContainerScanner;
910

10-
constructor(protected readonly container: NestContainer) {}
11+
constructor(protected readonly container: NestContainer) {
12+
this.containerScanner = new ContainerScanner(container);
13+
}
1114

1215
public abstract get<TInput = any, TResult = TInput>(
1316
typeOrToken: Type<TInput> | string | symbol,
1417
options?: { strict: boolean },
1518
): TResult;
1619

20+
public abstract create<T = any>(type: Type<T>): Promise<T>;
21+
1722
protected find<TInput = any, TResult = TInput>(
1823
typeOrToken: Type<TInput> | string | symbol,
1924
): TResult {
20-
this.initFlattenModule();
21-
return this.findInstanceByPrototypeOrToken<TInput, TResult>(
22-
typeOrToken,
23-
this.flattenModuleFixture,
24-
);
25+
return this.containerScanner.find<TInput, TResult>(typeOrToken);
26+
}
27+
28+
protected async instantiateClass<T = any>(
29+
type: Type<T>,
30+
module: Module,
31+
): Promise<T> {
32+
const wrapper = {
33+
name: type.name,
34+
metatype: type,
35+
instance: undefined,
36+
isResolved: false,
37+
};
38+
return new Promise<T>(async (resolve, reject) => {
39+
try {
40+
await this.injector.resolveConstructorParams<T>(
41+
wrapper,
42+
module,
43+
undefined,
44+
async instances => resolve(new type(...instances)),
45+
);
46+
} catch (err) {
47+
reject(err);
48+
}
49+
});
2550
}
2651

2752
protected findInstanceByPrototypeOrToken<TInput = any, TResult = TInput>(
2853
metatypeOrToken: Type<TInput> | string | symbol,
2954
contextModule: Partial<Module>,
3055
): TResult {
31-
const dependencies = new Map([
32-
...contextModule.components,
33-
...contextModule.routes,
34-
...contextModule.injectables,
35-
]);
36-
const name = isFunction(metatypeOrToken)
37-
? (metatypeOrToken as any).name
38-
: metatypeOrToken;
39-
const instanceWrapper = dependencies.get(name);
40-
if (!instanceWrapper) {
41-
throw new UnknownElementException();
42-
}
43-
return (instanceWrapper as InstanceWrapper<any>).instance;
44-
}
45-
46-
private initFlattenModule() {
47-
if (this.flattenModuleFixture) {
48-
return void 0;
49-
}
50-
const modules = this.container.getModules();
51-
const initialValue = {
52-
components: [],
53-
routes: [],
54-
injectables: [],
55-
};
56-
this.flattenModuleFixture = [...modules.values()].reduce(
57-
(flatten, curr) => ({
58-
components: [...flatten.components, ...curr.components],
59-
routes: [...flatten.routes, ...curr.routes],
60-
injectables: [...flatten.injectables, ...curr.injectables],
61-
}),
62-
initialValue,
63-
) as any;
56+
return this.containerScanner.findInstanceByPrototypeOrToken<
57+
TInput,
58+
TResult
59+
>(metatypeOrToken, contextModule);
6460
}
6561
}

packages/core/injector/module.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
isSymbol,
1414
isUndefined,
1515
} from '@nestjs/common/utils/shared.utils';
16+
import { InvalidClassException } from '../errors/exceptions/invalid-class.exception';
1617
import { RuntimeException } from '../errors/exceptions/runtime.exception';
1718
import { UnknownExportException } from '../errors/exceptions/unknown-export.exception';
1819
import { ApplicationReferenceHost } from '../helpers/application-ref-host';
@@ -364,6 +365,13 @@ export class Module {
364365
self,
365366
);
366367
}
368+
369+
public async create<T = any>(type: Type<T>): Promise<T> {
370+
if (!(type && isFunction(type) && type.prototype)) {
371+
throw new InvalidClassException(type);
372+
}
373+
return this.instantiateClass<T>(type, self);
374+
}
367375
};
368376
}
369377
}

packages/core/nest-application-context.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,20 @@ import { isNil, isUndefined } from '@nestjs/common/utils/shared.utils';
1111
import iterate from 'iterare';
1212
import { UnknownModuleException } from './errors/exceptions/unknown-module.exception';
1313
import { NestContainer } from './injector/container';
14+
import { ContainerScanner } from './injector/container-scanner';
1415
import { Module } from './injector/module';
15-
import { ModuleRef } from './injector/module-ref';
1616
import { ModuleTokenFactory } from './injector/module-token-factory';
1717

18-
export class NestApplicationContext extends ModuleRef
19-
implements INestApplicationContext {
18+
export class NestApplicationContext implements INestApplicationContext {
2019
private readonly moduleTokenFactory = new ModuleTokenFactory();
20+
private readonly containerScanner: ContainerScanner;
2121

2222
constructor(
23-
container: NestContainer,
23+
protected readonly container: NestContainer,
2424
private readonly scope: Type<any>[],
25-
protected contextModule: Module,
25+
private contextModule: Module,
2626
) {
27-
super(container);
27+
this.containerScanner = new ContainerScanner(container);
2828
}
2929

3030
public selectContextModule() {
@@ -168,4 +168,20 @@ export class NestApplicationContext extends ModuleRef
168168
(instance as OnApplicationBootstrap).onApplicationBootstrap,
169169
);
170170
}
171+
172+
protected find<TInput = any, TResult = TInput>(
173+
typeOrToken: Type<TInput> | string | symbol,
174+
): TResult {
175+
return this.containerScanner.find<TInput, TResult>(typeOrToken);
176+
}
177+
178+
protected findInstanceByPrototypeOrToken<TInput = any, TResult = TInput>(
179+
metatypeOrToken: Type<TInput> | string | symbol,
180+
contextModule: Partial<Module>,
181+
): TResult {
182+
return this.containerScanner.findInstanceByPrototypeOrToken<
183+
TInput,
184+
TResult
185+
>(metatypeOrToken, contextModule);
186+
}
171187
}

0 commit comments

Comments
 (0)