Skip to content

Commit

Permalink
Merge pull request #1172 from nestjs/feature/property-injection
Browse files Browse the repository at this point in the history
feature(core) property based injection (composition)
  • Loading branch information
kamilmysliwiec committed Oct 19, 2018
2 parents 82bf25b + 313e226 commit a2ed524
Show file tree
Hide file tree
Showing 24 changed files with 456 additions and 100 deletions.
5 changes: 2 additions & 3 deletions integration/injector/e2e/circular-modules.spec.ts
@@ -1,10 +1,9 @@
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { Test, TestingModuleBuilder } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { CircularModule } from '../src/circular-modules/circular.module';
import { InputService } from '../src/circular-modules/input.service';
import { CircularService } from '../src/circular-modules/circular.service';
import { InputModule } from '../src/circular-modules/input.module';
import { InputService } from '../src/circular-modules/input.service';

describe('Circular dependency (modules)', () => {
it('should resolve circular dependency between providers', async () => {
Expand Down
20 changes: 20 additions & 0 deletions integration/injector/e2e/circular-property-injection.spec.ts
@@ -0,0 +1,20 @@
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { CircularPropertiesModule } from '../src/circular-properties/circular-properties.module';
import { CircularService } from '../src/circular-properties/circular.service';
import { InputPropertiesModule } from '../src/circular-properties/input-properties.module';
import { InputService } from '../src/circular-properties/input.service';

describe('Circular properties dependency (modules)', () => {
it('should resolve circular dependency between providers', async () => {
const builder = Test.createTestingModule({
imports: [CircularPropertiesModule, InputPropertiesModule],
});
const testingModule = await builder.compile();
const inputService = testingModule.get<InputService>(InputService);
const circularService = testingModule.get<CircularService>(CircularService);

expect(inputService.service).to.be.eql(circularService);
expect(circularService.service).to.be.eql(inputService);
});
});
5 changes: 2 additions & 3 deletions integration/injector/e2e/circular.spec.ts
@@ -1,9 +1,8 @@
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { Test, TestingModuleBuilder } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { CircularModule } from '../src/circular/circular.module';
import { InputService } from '../src/circular/input.service';
import { CircularService } from '../src/circular/circular.service';
import { InputService } from '../src/circular/input.service';

describe('Circular dependency', () => {
it('should resolve circular dependency between providers', async () => {
Expand Down
15 changes: 8 additions & 7 deletions integration/injector/e2e/injector.spec.ts
@@ -1,13 +1,14 @@
import { RuntimeException } from '@nestjs/core/errors/exceptions/runtime.exception';
import { UnknownExportException } from '@nestjs/core/errors/exceptions/unknown-export.exception';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { Test, TestingModuleBuilder } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import {
DYNAMIC_TOKEN,
DYNAMIC_VALUE,
NestDynamicModule,
} from '../src/dynamic/dynamic.module';
import { ExportsModule } from '../src/exports/exports.module';
import { TestingModule } from '@nestjs/testing/testing-module';
import { UnknownExportException } from '@nestjs/core/errors/exceptions/unknown-export.exception';
import { UndefinedDependencyException } from '@nestjs/core/errors/exceptions/undefined-dependency.exception';
import { InjectModule } from '../src/inject/inject.module';
import { RuntimeException } from '@nestjs/core/errors/exceptions/runtime.exception';
import { NestDynamicModule, DYNAMIC_TOKEN, DYNAMIC_VALUE } from '../src/dynamic/dynamic.module';

describe('Injector', () => {
describe('when "providers" and "exports" properties are inconsistent', () => {
Expand Down
18 changes: 18 additions & 0 deletions integration/injector/e2e/property-injection.spec.ts
@@ -0,0 +1,18 @@
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { DependencyService } from '../src/properties/dependency.service';
import { PropertiesModule } from '../src/properties/properties.module';
import { PropertiesService } from '../src/properties/properties.service';

describe('Injector', () => {
it('should resolve property-based dependencies', async () => {
const builder = Test.createTestingModule({
imports: [PropertiesModule],
});
const app = await builder.compile();
const dependency = app.get(DependencyService);

expect(app.get(PropertiesService).service).to.be.eql(dependency);
expect(app.get(PropertiesService).token).to.be.true;
});
});
@@ -0,0 +1,10 @@
import { forwardRef, Module } from '@nestjs/common';
import { CircularService } from './circular.service';
import { InputPropertiesModule } from './input-properties.module';

@Module({
imports: [forwardRef(() => InputPropertiesModule)],
providers: [CircularService],
exports: [CircularService],
})
export class CircularPropertiesModule {}
@@ -0,0 +1,8 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { InputService } from './input.service';

@Injectable()
export class CircularService {
@Inject(forwardRef(() => InputService))
public readonly service: InputService;
}
@@ -0,0 +1,10 @@
import { forwardRef, Module } from '@nestjs/common';
import { CircularPropertiesModule } from './circular-properties.module';
import { InputService } from './input.service';

@Module({
imports: [forwardRef(() => CircularPropertiesModule)],
providers: [InputService],
exports: [InputService],
})
export class InputPropertiesModule {}
8 changes: 8 additions & 0 deletions integration/injector/src/circular-properties/input.service.ts
@@ -0,0 +1,8 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { CircularService } from './circular.service';

@Injectable()
export class InputService {
@Inject(forwardRef(() => CircularService))
public readonly service: CircularService;
}
4 changes: 4 additions & 0 deletions integration/injector/src/properties/dependency.service.ts
@@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class DependencyService {}
15 changes: 15 additions & 0 deletions integration/injector/src/properties/properties.module.ts
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { DependencyService } from './dependency.service';
import { PropertiesService } from './properties.service';

@Module({
providers: [
DependencyService,
PropertiesService,
{
provide: 'token',
useValue: true,
},
],
})
export class PropertiesModule {}
8 changes: 8 additions & 0 deletions integration/injector/src/properties/properties.service.ts
@@ -0,0 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { DependencyService } from './dependency.service';

@Injectable()
export class PropertiesService {
@Inject() service: DependencyService;
@Inject('token') token: boolean;
}
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -153,6 +153,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
3 changes: 3 additions & 0 deletions packages/common/constants.ts
Expand Up @@ -13,6 +13,9 @@ export const PATH_METADATA = 'path';
export const PARAMTYPES_METADATA = 'design:paramtypes';
export const SELF_DECLARED_DEPS_METADATA = 'self:paramtypes';
export const OPTIONAL_DEPS_METADATA = 'optional:paramtypes';
export const PROPERTY_DEPS_METADATA = 'self:properties_metadata';
export const OPTIONAL_PROPERTY_DEPS_METADATA = 'optional:properties_metadata';

export const METHOD_METADATA = 'method';
export const ROUTE_ARGS_METADATA = '__routeArguments__';
export const CUSTOM_ROUTE_AGRS_METADATA = '__customRouteArgs__';
Expand Down
35 changes: 27 additions & 8 deletions packages/common/decorators/core/inject.decorator.ts
@@ -1,16 +1,35 @@
import { SELF_DECLARED_DEPS_METADATA } from '../../constants';
import { isFunction } from '../../utils/shared.utils';
import {
PROPERTY_DEPS_METADATA,
SELF_DECLARED_DEPS_METADATA,
} from '../../constants';
import { isFunction, isUndefined } from '../../utils/shared.utils';

/**
* Injects provider which has to be available in the current injector (module) scope.
* Providers are recognized by types or tokens.
*/
export function Inject<T = any>(token: T): ParameterDecorator {
return (target, key, index) => {
const args = Reflect.getMetadata(SELF_DECLARED_DEPS_METADATA, target) || [];
const type = isFunction(token) ? (token as any as Function).name : token;
export function Inject<T = any>(token?: T) {
return (target: Object, key: string | symbol, index?: number) => {
token = token || Reflect.getMetadata('design:type', target, key);
const type =
token && isFunction(token) ? ((token as any) as Function).name : token;

args.push({ index, param: type });
Reflect.defineMetadata(SELF_DECLARED_DEPS_METADATA, args, target);
if (!isUndefined(index)) {
const dependencies =
Reflect.getMetadata(SELF_DECLARED_DEPS_METADATA, target) || [];

dependencies.push({ index, param: type });
Reflect.defineMetadata(SELF_DECLARED_DEPS_METADATA, dependencies, target);
return;
}
const properties =
Reflect.getMetadata(PROPERTY_DEPS_METADATA, target.constructor) || [];

properties.push({ key, type });
Reflect.defineMetadata(
PROPERTY_DEPS_METADATA,
properties,
target.constructor,
);
};
}
27 changes: 22 additions & 5 deletions packages/common/decorators/core/optional.decorator.ts
@@ -1,11 +1,28 @@
import { OPTIONAL_DEPS_METADATA } from '../../constants';
import {
OPTIONAL_DEPS_METADATA,
OPTIONAL_PROPERTY_DEPS_METADATA,
} from '../../constants';
import { isUndefined } from '../../utils/shared.utils';

/**
* Sets dependency as an optional one.
*/
export function Optional(): ParameterDecorator {
return (target, key, index) => {
const args = Reflect.getMetadata(OPTIONAL_DEPS_METADATA, target) || [];
Reflect.defineMetadata(OPTIONAL_DEPS_METADATA, [...args, index], target);
export function Optional() {
return (target: Object, key: string | symbol, index?: number) => {
if (!isUndefined(index)) {
const args = Reflect.getMetadata(OPTIONAL_DEPS_METADATA, target) || [];
Reflect.defineMetadata(OPTIONAL_DEPS_METADATA, [...args, index], target);
return;
}
const properties =
Reflect.getMetadata(
OPTIONAL_PROPERTY_DEPS_METADATA,
target.constructor,
) || [];
Reflect.defineMetadata(
OPTIONAL_PROPERTY_DEPS_METADATA,
[...properties, key],
target.constructor,
);
};
}
8 changes: 8 additions & 0 deletions packages/core/errors/exceptions/invalid-class.exception.ts
@@ -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}`);
}
}
16 changes: 12 additions & 4 deletions packages/core/errors/messages.ts
@@ -1,4 +1,5 @@
import { Type } from '@nestjs/common';
import { isNil } from '@nestjs/common/utils/shared.utils';
import {
InjectorDependency,
InjectorDependencyContext,
Expand All @@ -17,14 +18,18 @@ export const UNKNOWN_DEPENDENCIES_MESSAGE = (
type: string,
unknownDependencyContext: InjectorDependencyContext,
) => {
const { index, dependencies } = unknownDependencyContext;
const { index, dependencies, key } = unknownDependencyContext;
let message = `Nest can't resolve dependencies of the ${type}`;
message += ` (`;

const dependenciesName = dependencies.map(getDependencyName);
if (isNil(index)) {
message += `. Please make sure that the "${key}" property is available in the current context.`;
return message;
}
const dependenciesName = (dependencies || []).map(getDependencyName);
dependenciesName[index] = '?';
message += dependenciesName.join(', ');

message += ` (`;
message += dependenciesName.join(', ');
message += `). Please make sure that the argument at index [${index}] is available in the current context.`;
return message;
};
Expand All @@ -38,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
@@ -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>;
}
}

0 comments on commit a2ed524

Please sign in to comment.