Skip to content

Commit

Permalink
Merge pull request nestjs#1163 from nestjs/feature/dynamic-create
Browse files Browse the repository at this point in the history
feature(core) instantiate class dynamically (ModuleRef)
  • Loading branch information
kamilmysliwiec authored Oct 19, 2018
2 parents d49a265 + 6ac9968 commit 92cd57a
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 54 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"postinstall": "opencollective",
"copy-docs": "gulp copy-docs",
"prepare:npm": "npm run build:lib && npm run copy-docs",
"prepare:rc": "npm run build:lib && npm run copy-docs",
"prepare:next": "npm run build:lib && npm run copy-docs",
"prepare:beta": "npm run build:lib && npm run copy-docs",
"publish":
"npm run prepare:npm && ./node_modules/.bin/lerna publish --exact -m \"chore(@nestjs) publish %s release\"",
"publish:rc":
Expand Down Expand Up @@ -151,6 +154,7 @@
"packages/microservices/microservices-module.ts",
"packages/core/middleware/middleware-module.ts",
"packages/core/injector/module-ref.ts",
"packages/core/injector/container-scanner.ts",
"packages/common/cache/**/*",
"packages/common/serializer/**/*",
"packages/common/services/logger.service.ts"
Expand Down
8 changes: 8 additions & 0 deletions packages/core/errors/exceptions/invalid-class.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { INVALID_CLASS_MESSAGE } from '../messages';
import { RuntimeException } from './runtime.exception';

export class InvalidClassException extends RuntimeException {
constructor(value: any) {
super(INVALID_CLASS_MESSAGE`${value}`);
}
}
3 changes: 3 additions & 0 deletions packages/core/errors/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export const INVALID_MODULE_MESSAGE = (text, scope: string) =>
export const UNKNOWN_EXPORT_MESSAGE = (text, module: string) =>
`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.`;

export const INVALID_CLASS_MESSAGE = (text, value: any) =>
`ModuleRef cannot instantiate class (${value} is not constructable).`;

export const INVALID_MIDDLEWARE_CONFIGURATION = `Invalid middleware configuration passed inside the module 'configure()' method.`;
export const UNKNOWN_REQUEST_MAPPING = `Request mapping properties not defined in the @RequestMapping() annotation!`;
export const UNHANDLED_RUNTIME_EXCEPTION = `Unhandled Runtime Exception.`;
Expand Down
65 changes: 65 additions & 0 deletions packages/core/injector/container-scanner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Type } from '@nestjs/common';
import { isFunction } from '@nestjs/common/utils/shared.utils';
import { UnknownElementException } from '../errors/exceptions/unknown-element.exception';
import { InstanceWrapper, NestContainer } from './container';
import { Module } from './module';

export class ContainerScanner {
private flatContainer: Partial<Module>;

constructor(private readonly container: NestContainer) {}

public find<TInput = any, TResult = TInput>(
typeOrToken: Type<TInput> | string | symbol,
): TResult {
this.initFlatContainer();
return this.findInstanceByPrototypeOrToken<TInput, TResult>(
typeOrToken,
this.flatContainer,
);
}

public findInstanceByPrototypeOrToken<TInput = any, TResult = TInput>(
metatypeOrToken: Type<TInput> | string | symbol,
contextModule: Partial<Module>,
): TResult {
const dependencies = new Map([
...contextModule.components,
...contextModule.routes,
...contextModule.injectables,
]);
const name = isFunction(metatypeOrToken)
? (metatypeOrToken as Function).name
: metatypeOrToken;
const instanceWrapper = dependencies.get(name as string);
if (!instanceWrapper) {
throw new UnknownElementException();
}
return (instanceWrapper as InstanceWrapper<any>).instance;
}

private initFlatContainer() {
if (this.flatContainer) {
return undefined;
}
const modules = this.container.getModules();
const initialValue = {
components: [],
routes: [],
injectables: [],
};
const merge = <T = any>(
initial: Map<string, T> | T[],
arr: Map<string, T>,
) => [...initial, ...arr];

this.flatContainer = ([...modules.values()].reduce(
(current, next) => ({
components: merge(current.components, next.components),
routes: merge(current.routes, next.routes),
injectables: merge(current.injectables, next.injectables),
}),
initialValue,
) as any) as Partial<Module>;
}
}
10 changes: 5 additions & 5 deletions packages/core/injector/injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export class Injector {

public async loadInstance<T>(
wrapper: InstanceWrapper<T>,
collection,
collection: Map<string, InstanceWrapper<any>>,
module: Module,
) {
if (wrapper.isPending) {
Expand All @@ -146,19 +146,19 @@ export class Injector {
const done = this.applyDoneHook(wrapper);
const { name, inject } = wrapper;

const targetMetatype = collection.get(name);
if (isUndefined(targetMetatype)) {
const targetWrapper = collection.get(name);
if (isUndefined(targetWrapper)) {
throw new RuntimeException();
}
if (targetMetatype.isResolved) {
if (targetWrapper.isResolved) {
return undefined;
}
const callback = async instances => {
const properties = await this.resolveProperties(wrapper, module, inject);
const instance = await this.instantiateClass(
instances,
wrapper,
targetMetatype,
targetWrapper,
);
this.applyProperties(instance, properties);
done();
Expand Down
82 changes: 39 additions & 43 deletions packages/core/injector/module-ref.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,61 @@
import { Type } from '@nestjs/common';
import { isFunction } from '@nestjs/common/utils/shared.utils';
import { UnknownElementException } from '../errors/exceptions/unknown-element.exception';
import { InstanceWrapper, NestContainer } from './container';
import { NestContainer } from './container';
import { ContainerScanner } from './container-scanner';
import { Injector } from './injector';
import { Module } from './module';

export abstract class ModuleRef {
private flattenModuleFixture: Partial<Module>;
private readonly injector = new Injector();
private readonly containerScanner: ContainerScanner;

constructor(protected readonly container: NestContainer) {}
constructor(protected readonly container: NestContainer) {
this.containerScanner = new ContainerScanner(container);
}

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

public abstract create<T = any>(type: Type<T>): Promise<T>;

protected find<TInput = any, TResult = TInput>(
typeOrToken: Type<TInput> | string | symbol,
): TResult {
this.initFlattenModule();
return this.findInstanceByPrototypeOrToken<TInput, TResult>(
typeOrToken,
this.flattenModuleFixture,
);
return this.containerScanner.find<TInput, TResult>(typeOrToken);
}

protected async instantiateClass<T = any>(
type: Type<T>,
module: Module,
): Promise<T> {
const wrapper = {
name: type.name,
metatype: type,
instance: undefined,
isResolved: false,
};
return new Promise<T>(async (resolve, reject) => {
try {
await this.injector.resolveConstructorParams<T>(
wrapper,
module,
undefined,
async instances => resolve(new type(...instances)),
);
} catch (err) {
reject(err);
}
});
}

protected findInstanceByPrototypeOrToken<TInput = any, TResult = TInput>(
metatypeOrToken: Type<TInput> | string | symbol,
contextModule: Partial<Module>,
): TResult {
const dependencies = new Map([
...contextModule.components,
...contextModule.routes,
...contextModule.injectables,
]);
const name = isFunction(metatypeOrToken)
? (metatypeOrToken as any).name
: metatypeOrToken;
const instanceWrapper = dependencies.get(name);
if (!instanceWrapper) {
throw new UnknownElementException();
}
return (instanceWrapper as InstanceWrapper<any>).instance;
}

private initFlattenModule() {
if (this.flattenModuleFixture) {
return void 0;
}
const modules = this.container.getModules();
const initialValue = {
components: [],
routes: [],
injectables: [],
};
this.flattenModuleFixture = [...modules.values()].reduce(
(flatten, curr) => ({
components: [...flatten.components, ...curr.components],
routes: [...flatten.routes, ...curr.routes],
injectables: [...flatten.injectables, ...curr.injectables],
}),
initialValue,
) as any;
return this.containerScanner.findInstanceByPrototypeOrToken<
TInput,
TResult
>(metatypeOrToken, contextModule);
}
}
8 changes: 8 additions & 0 deletions packages/core/injector/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
isSymbol,
isUndefined,
} from '@nestjs/common/utils/shared.utils';
import { InvalidClassException } from '../errors/exceptions/invalid-class.exception';
import { RuntimeException } from '../errors/exceptions/runtime.exception';
import { UnknownExportException } from '../errors/exceptions/unknown-export.exception';
import { ApplicationReferenceHost } from '../helpers/application-ref-host';
Expand Down Expand Up @@ -364,6 +365,13 @@ export class Module {
self,
);
}

public async create<T = any>(type: Type<T>): Promise<T> {
if (!(type && isFunction(type) && type.prototype)) {
throw new InvalidClassException(type);
}
return this.instantiateClass<T>(type, self);
}
};
}
}
28 changes: 22 additions & 6 deletions packages/core/nest-application-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@ import { isNil, isUndefined } from '@nestjs/common/utils/shared.utils';
import iterate from 'iterare';
import { UnknownModuleException } from './errors/exceptions/unknown-module.exception';
import { NestContainer } from './injector/container';
import { ContainerScanner } from './injector/container-scanner';
import { Module } from './injector/module';
import { ModuleRef } from './injector/module-ref';
import { ModuleTokenFactory } from './injector/module-token-factory';

export class NestApplicationContext extends ModuleRef
implements INestApplicationContext {
export class NestApplicationContext implements INestApplicationContext {
private readonly moduleTokenFactory = new ModuleTokenFactory();
private readonly containerScanner: ContainerScanner;

constructor(
container: NestContainer,
protected readonly container: NestContainer,
private readonly scope: Type<any>[],
protected contextModule: Module,
private contextModule: Module,
) {
super(container);
this.containerScanner = new ContainerScanner(container);
}

public selectContextModule() {
Expand Down Expand Up @@ -168,4 +168,20 @@ export class NestApplicationContext extends ModuleRef
(instance as OnApplicationBootstrap).onApplicationBootstrap,
);
}

protected find<TInput = any, TResult = TInput>(
typeOrToken: Type<TInput> | string | symbol,
): TResult {
return this.containerScanner.find<TInput, TResult>(typeOrToken);
}

protected findInstanceByPrototypeOrToken<TInput = any, TResult = TInput>(
metatypeOrToken: Type<TInput> | string | symbol,
contextModule: Partial<Module>,
): TResult {
return this.containerScanner.findInstanceByPrototypeOrToken<
TInput,
TResult
>(metatypeOrToken, contextModule);
}
}

0 comments on commit 92cd57a

Please sign in to comment.