feat: add graph inspector implementation

This commit is contained in:
Kamil Myśliwiec
2022-11-21 11:00:11 +01:00
parent ba60d35d47
commit a89615b124
48 changed files with 2218 additions and 541 deletions

View File

@@ -206,17 +206,16 @@
"**/*.js", "**/*.js",
"**/*.d.ts", "**/*.d.ts",
"**/*.spec.ts", "**/*.spec.ts",
"packages/**/*.spec.ts",
"packages/**/adapters/*.ts", "packages/**/adapters/*.ts",
"packages/**/nest-*.ts", "packages/**/nest-*.ts",
"packages/**/test/**/*.ts", "packages/**/test/**/*.ts",
"packages/core/errors/**/*", "packages/core/errors/**/*",
"packages/common/exceptions/*.ts", "packages/common/exceptions/*.ts",
"packages/common/http/*.ts",
"packages/common/utils/load-package.util.ts", "packages/common/utils/load-package.util.ts",
"packages/microservices/exceptions/", "packages/microservices/exceptions/",
"packages/microservices/microservices-module.ts", "packages/microservices/microservices-module.ts",
"packages/core/middleware/middleware-module.ts", "packages/core/middleware/middleware-module.ts",
"packages/core/discovery/discovery-service.ts",
"packages/core/injector/module-ref.ts", "packages/core/injector/module-ref.ts",
"packages/core/injector/instance-links-host.ts", "packages/core/injector/instance-links-host.ts",
"packages/core/helpers/context-id-factory.ts", "packages/core/helpers/context-id-factory.ts",

View File

@@ -4,6 +4,7 @@ export const MODULE_METADATA = {
CONTROLLERS: 'controllers', CONTROLLERS: 'controllers',
EXPORTS: 'exports', EXPORTS: 'exports',
}; };
export const GLOBAL_MODULE_METADATA = '__module:global__'; export const GLOBAL_MODULE_METADATA = '__module:global__';
export const HOST_METADATA = 'host'; export const HOST_METADATA = 'host';
export const PATH_METADATA = 'path'; export const PATH_METADATA = 'path';
@@ -20,12 +21,23 @@ export const CUSTOM_ROUTE_ARGS_METADATA = '__customRouteArgs__';
* @deprecated Use `CUSTOM_ROUTE_ARGS_METADATA` instead * @deprecated Use `CUSTOM_ROUTE_ARGS_METADATA` instead
*/ */
export const CUSTOM_ROUTE_AGRS_METADATA = CUSTOM_ROUTE_ARGS_METADATA; export const CUSTOM_ROUTE_AGRS_METADATA = CUSTOM_ROUTE_ARGS_METADATA;
export const EXCEPTION_FILTERS_METADATA = '__exceptionFilters__';
export const FILTER_CATCH_EXCEPTIONS = '__filterCatchExceptions__'; export const FILTER_CATCH_EXCEPTIONS = '__filterCatchExceptions__';
export const PIPES_METADATA = '__pipes__'; export const PIPES_METADATA = '__pipes__';
export const GUARDS_METADATA = '__guards__'; export const GUARDS_METADATA = '__guards__';
export const RENDER_METADATA = '__renderTemplate__';
export const INTERCEPTORS_METADATA = '__interceptors__'; export const INTERCEPTORS_METADATA = '__interceptors__';
export const EXCEPTION_FILTERS_METADATA = '__exceptionFilters__';
export const ENHANCER_KEY_TO_SUBTYPE_MAP = {
[GUARDS_METADATA]: 'guard',
[INTERCEPTORS_METADATA]: 'interceptor',
[PIPES_METADATA]: 'pipe',
[EXCEPTION_FILTERS_METADATA]: 'filter',
} as const;
export type EnhancerSubtype =
typeof ENHANCER_KEY_TO_SUBTYPE_MAP[keyof typeof ENHANCER_KEY_TO_SUBTYPE_MAP];
export const RENDER_METADATA = '__renderTemplate__';
export const HTTP_CODE_METADATA = '__httpCode__'; export const HTTP_CODE_METADATA = '__httpCode__';
export const MODULE_PATH = '__module_path__'; export const MODULE_PATH = '__module_path__';
export const HEADERS_METADATA = '__headers__'; export const HEADERS_METADATA = '__headers__';

View File

@@ -1,3 +1,5 @@
import { EnhancerSubtype } from '@nestjs/common/constants';
export const MESSAGES = { export const MESSAGES = {
APPLICATION_START: `Starting Nest application...`, APPLICATION_START: `Starting Nest application...`,
APPLICATION_READY: `Nest application successfully started`, APPLICATION_READY: `Nest application successfully started`,
@@ -12,3 +14,15 @@ export const APP_INTERCEPTOR = 'APP_INTERCEPTOR';
export const APP_PIPE = 'APP_PIPE'; export const APP_PIPE = 'APP_PIPE';
export const APP_GUARD = 'APP_GUARD'; export const APP_GUARD = 'APP_GUARD';
export const APP_FILTER = 'APP_FILTER'; export const APP_FILTER = 'APP_FILTER';
export const ENHANCER_TOKEN_TO_SUBTYPE_MAP: Record<
| typeof APP_GUARD
| typeof APP_PIPE
| typeof APP_FILTER
| typeof APP_INTERCEPTOR,
EnhancerSubtype
> = {
[APP_GUARD]: 'guard',
[APP_INTERCEPTOR]: 'interceptor',
[APP_PIPE]: 'pipe',
[APP_FILTER]: 'filter',
} as const;

View File

@@ -1,5 +1,8 @@
import { DynamicModule, Provider } from '@nestjs/common'; import { DynamicModule, Provider } from '@nestjs/common';
import { GLOBAL_MODULE_METADATA } from '@nestjs/common/constants'; import {
EnhancerSubtype,
GLOBAL_MODULE_METADATA,
} from '@nestjs/common/constants';
import { Injectable } from '@nestjs/common/interfaces/injectable.interface'; import { Injectable } from '@nestjs/common/interfaces/injectable.interface';
import { Type } from '@nestjs/common/interfaces/type.interface'; import { Type } from '@nestjs/common/interfaces/type.interface';
import { ApplicationConfig } from '../application-config'; import { ApplicationConfig } from '../application-config';
@@ -8,6 +11,7 @@ import {
UndefinedForwardRefException, UndefinedForwardRefException,
UnknownModuleException, UnknownModuleException,
} from '../errors/exceptions'; } from '../errors/exceptions';
import { SerializedGraph } from '../inspector/serialized-graph';
import { REQUEST } from '../router/request/request-constants'; import { REQUEST } from '../router/request/request-constants';
import { ModuleCompiler } from './compiler'; import { ModuleCompiler } from './compiler';
import { ContextId } from './instance-wrapper'; import { ContextId } from './instance-wrapper';
@@ -27,12 +31,17 @@ export class NestContainer {
Partial<DynamicModule> Partial<DynamicModule>
>(); >();
private readonly internalProvidersStorage = new InternalProvidersStorage(); private readonly internalProvidersStorage = new InternalProvidersStorage();
private readonly _serializedGraph = new SerializedGraph();
private internalCoreModule: Module; private internalCoreModule: Module;
constructor( constructor(
private readonly _applicationConfig: ApplicationConfig = undefined, private readonly _applicationConfig: ApplicationConfig = undefined,
) {} ) {}
get serializedGraph(): SerializedGraph {
return this._serializedGraph;
}
get applicationConfig(): ApplicationConfig | undefined { get applicationConfig(): ApplicationConfig | undefined {
return this._applicationConfig; return this._applicationConfig;
} }
@@ -74,13 +83,11 @@ export class NestContainer {
moduleRef.token = token; moduleRef.token = token;
this.modules.set(token, moduleRef); this.modules.set(token, moduleRef);
await this.addDynamicMetadata( const updatedScope = [].concat(scope, type);
token, await this.addDynamicMetadata(token, dynamicMetadata, updatedScope);
dynamicMetadata,
[].concat(scope, type),
);
if (this.isGlobalModule(type, dynamicMetadata)) { if (this.isGlobalModule(type, dynamicMetadata)) {
moduleRef.isGlobal = true;
this.addGlobalModule(moduleRef); this.addGlobalModule(moduleRef);
} }
return moduleRef; return moduleRef;
@@ -155,6 +162,7 @@ export class NestContainer {
public addProvider( public addProvider(
provider: Provider, provider: Provider,
token: string, token: string,
enhancerSubtype?: EnhancerSubtype,
): string | symbol | Function { ): string | symbol | Function {
const moduleRef = this.modules.get(token); const moduleRef = this.modules.get(token);
if (!provider) { if (!provider) {
@@ -163,19 +171,20 @@ export class NestContainer {
if (!moduleRef) { if (!moduleRef) {
throw new UnknownModuleException(); throw new UnknownModuleException();
} }
return moduleRef.addProvider(provider); return moduleRef.addProvider(provider, enhancerSubtype) as Function;
} }
public addInjectable( public addInjectable(
injectable: Provider, injectable: Provider,
token: string, token: string,
enhancerSubtype: EnhancerSubtype,
host?: Type<Injectable>, host?: Type<Injectable>,
) { ) {
if (!this.modules.has(token)) { if (!this.modules.has(token)) {
throw new UnknownModuleException(); throw new UnknownModuleException();
} }
const moduleRef = this.modules.get(token); const moduleRef = this.modules.get(token);
moduleRef.addInjectable(injectable, host); return moduleRef.addInjectable(injectable, enhancerSubtype, host);
} }
public addExportedProvider(provider: Type<any>, token: string) { public addExportedProvider(provider: Type<any>, token: string) {
@@ -219,15 +228,16 @@ export class NestContainer {
target.addRelatedModule(globalModule); target.addRelatedModule(globalModule);
} }
public getDynamicMetadataByToken(token: string): Partial<DynamicModule>;
public getDynamicMetadataByToken<
K extends Exclude<keyof DynamicModule, 'global' | 'module'>,
>(token: string, metadataKey: K): DynamicModule[K];
public getDynamicMetadataByToken( public getDynamicMetadataByToken(
token: string, token: string,
metadataKey: keyof DynamicModule, metadataKey?: Exclude<keyof DynamicModule, 'global' | 'module'>,
) { ) {
const metadata = this.dynamicModulesMetadata.get(token); const metadata = this.dynamicModulesMetadata.get(token);
if (metadata && metadata[metadataKey]) { return metadataKey ? metadata?.[metadataKey] ?? [] : metadata;
return metadata[metadataKey] as any[];
}
return [];
} }
public registerCoreModuleRef(moduleRef: Module) { public registerCoreModuleRef(moduleRef: Module) {

View File

@@ -1,7 +1,8 @@
import { Logger } from '@nestjs/common'; import { Logger, LoggerService } from '@nestjs/common';
import { Controller } from '@nestjs/common/interfaces/controllers/controller.interface'; import { Controller } from '@nestjs/common/interfaces/controllers/controller.interface';
import { Injectable } from '@nestjs/common/interfaces/injectable.interface'; import { Injectable } from '@nestjs/common/interfaces/injectable.interface';
import { MODULE_INIT_MESSAGE } from '../helpers/messages'; import { MODULE_INIT_MESSAGE } from '../helpers/messages';
import { GraphInspector } from '../inspector/graph-inspector';
import { NestContainer } from './container'; import { NestContainer } from './container';
import { Injector } from './injector'; import { Injector } from './injector';
import { InternalCoreModule } from './internal-core-module/internal-core-module'; import { InternalCoreModule } from './internal-core-module/internal-core-module';
@@ -9,9 +10,11 @@ import { Module } from './module';
export class InstanceLoader { export class InstanceLoader {
protected readonly injector = new Injector(); protected readonly injector = new Injector();
constructor( constructor(
protected readonly container: NestContainer, protected readonly container: NestContainer,
private logger = new Logger(InstanceLoader.name, { protected readonly graphInspector: GraphInspector,
private logger: LoggerService = new Logger(InstanceLoader.name, {
timestamp: true, timestamp: true,
}), }),
) {} ) {}
@@ -42,7 +45,7 @@ export class InstanceLoader {
await this.createInstancesOfInjectables(moduleRef); await this.createInstancesOfInjectables(moduleRef);
await this.createInstancesOfControllers(moduleRef); await this.createInstancesOfControllers(moduleRef);
const { name } = moduleRef.metatype; const { name } = moduleRef;
this.isModuleWhitelisted(name) && this.isModuleWhitelisted(name) &&
this.logger.log(MODULE_INIT_MESSAGE`${name}`); this.logger.log(MODULE_INIT_MESSAGE`${name}`);
}), }),
@@ -60,7 +63,10 @@ export class InstanceLoader {
const { providers } = moduleRef; const { providers } = moduleRef;
const wrappers = [...providers.values()]; const wrappers = [...providers.values()];
await Promise.all( await Promise.all(
wrappers.map(item => this.injector.loadProvider(item, moduleRef)), wrappers.map(async item => {
await this.injector.loadProvider(item, moduleRef);
this.graphInspector.inspectInstanceWrapper(item, moduleRef);
}),
); );
} }
@@ -75,7 +81,10 @@ export class InstanceLoader {
const { controllers } = moduleRef; const { controllers } = moduleRef;
const wrappers = [...controllers.values()]; const wrappers = [...controllers.values()];
await Promise.all( await Promise.all(
wrappers.map(item => this.injector.loadController(item, moduleRef)), wrappers.map(async item => {
await this.injector.loadController(item, moduleRef);
this.graphInspector.inspectInstanceWrapper(item, moduleRef);
}),
); );
} }
@@ -90,7 +99,10 @@ export class InstanceLoader {
const { injectables } = moduleRef; const { injectables } = moduleRef;
const wrappers = [...injectables.values()]; const wrappers = [...injectables.values()];
await Promise.all( await Promise.all(
wrappers.map(item => this.injector.loadInjectable(item, moduleRef)), wrappers.map(async item => {
await this.injector.loadInjectable(item, moduleRef);
this.graphInspector.inspectInstanceWrapper(item, moduleRef);
}),
); );
} }

View File

@@ -8,6 +8,7 @@ import {
isUndefined, isUndefined,
} from '@nestjs/common/utils/shared.utils'; } from '@nestjs/common/utils/shared.utils';
import { iterate } from 'iterare'; import { iterate } from 'iterare';
import { EnhancerSubtype } from '../../common/constants';
import { STATIC_CONTEXT } from './constants'; import { STATIC_CONTEXT } from './constants';
import { import {
isClassProvider, isClassProvider,
@@ -60,6 +61,7 @@ export class InstanceWrapper<T = any> {
public readonly async?: boolean; public readonly async?: boolean;
public readonly host?: Module; public readonly host?: Module;
public readonly isAlias: boolean = false; public readonly isAlias: boolean = false;
public readonly subtype?: EnhancerSubtype;
public scope?: Scope = Scope.DEFAULT; public scope?: Scope = Scope.DEFAULT;
public metatype: Type<T> | Function; public metatype: Type<T> | Function;

View File

@@ -1,6 +1,7 @@
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { ExternalContextCreator } from '../../helpers/external-context-creator'; import { ExternalContextCreator } from '../../helpers/external-context-creator';
import { HttpAdapterHost } from '../../helpers/http-adapter-host'; import { HttpAdapterHost } from '../../helpers/http-adapter-host';
import { GraphInspector } from '../../inspector/graph-inspector';
import { DependenciesScanner } from '../../scanner'; import { DependenciesScanner } from '../../scanner';
import { ModuleCompiler } from '../compiler'; import { ModuleCompiler } from '../compiler';
import { NestContainer } from '../container'; import { NestContainer } from '../container';
@@ -15,7 +16,25 @@ export class InternalCoreModuleFactory {
scanner: DependenciesScanner, scanner: DependenciesScanner,
moduleCompiler: ModuleCompiler, moduleCompiler: ModuleCompiler,
httpAdapterHost: HttpAdapterHost, httpAdapterHost: HttpAdapterHost,
graphInspector: GraphInspector,
) { ) {
const lazyModuleLoaderFactory = () => {
const logger = new Logger(LazyModuleLoader.name, {
timestamp: false,
});
const instanceLoader = new InstanceLoader(
container,
graphInspector,
logger,
);
return new LazyModuleLoader(
scanner,
instanceLoader,
moduleCompiler,
container.getModules(),
);
};
return InternalCoreModule.register([ return InternalCoreModule.register([
{ {
provide: ExternalContextCreator, provide: ExternalContextCreator,
@@ -35,18 +54,7 @@ export class InternalCoreModuleFactory {
}, },
{ {
provide: LazyModuleLoader, provide: LazyModuleLoader,
useFactory: () => { useFactory: lazyModuleLoaderFactory,
const logger = new Logger(LazyModuleLoader.name, {
timestamp: false,
});
const instanceLoader = new InstanceLoader(container, logger);
return new LazyModuleLoader(
scanner,
instanceLoader,
moduleCompiler,
container.getModules(),
);
},
}, },
]); ]);
} }

View File

@@ -21,6 +21,7 @@ import {
isUndefined, isUndefined,
} from '@nestjs/common/utils/shared.utils'; } from '@nestjs/common/utils/shared.utils';
import { iterate } from 'iterare'; import { iterate } from 'iterare';
import { EnhancerSubtype } from '../../common/constants';
import { ApplicationConfig } from '../application-config'; import { ApplicationConfig } from '../application-config';
import { import {
InvalidClassException, InvalidClassException,
@@ -62,6 +63,7 @@ export class Module {
>(); >();
private readonly _exports = new Set<InstanceToken>(); private readonly _exports = new Set<InstanceToken>();
private _distance = 0; private _distance = 0;
private _isGlobal = false;
private _token: string; private _token: string;
constructor( constructor(
@@ -84,6 +86,18 @@ export class Module {
this._token = token; this._token = token;
} }
get name() {
return this.metatype.name;
}
get isGlobal() {
return this._isGlobal;
}
set isGlobal(global: boolean) {
this._isGlobal = global;
}
get providers(): Map<InstanceToken, InstanceWrapper<Injectable>> { get providers(): Map<InstanceToken, InstanceWrapper<Injectable>> {
return this._providers; return this._providers;
} }
@@ -199,6 +213,7 @@ export class Module {
public addInjectable<T extends Injectable>( public addInjectable<T extends Injectable>(
injectable: Provider, injectable: Provider,
enhancerSubtype: EnhancerSubtype,
host?: Type<T>, host?: Type<T>,
) { ) {
if (this.isCustomProvider(injectable)) { if (this.isCustomProvider(injectable)) {
@@ -214,6 +229,7 @@ export class Module {
isResolved: false, isResolved: false,
scope: getClassScope(injectable), scope: getClassScope(injectable),
durable: isDurable(injectable), durable: isDurable(injectable),
subtype: enhancerSubtype,
host: this, host: this,
}); });
this._injectables.set(injectable, instanceWrapper); this._injectables.set(injectable, instanceWrapper);
@@ -223,11 +239,17 @@ export class Module {
this._controllers.get(host) || this._providers.get(host); this._controllers.get(host) || this._providers.get(host);
hostWrapper && hostWrapper.addEnhancerMetadata(instanceWrapper); hostWrapper && hostWrapper.addEnhancerMetadata(instanceWrapper);
} }
return instanceWrapper;
} }
public addProvider(provider: Provider) { public addProvider(provider: Provider): Provider | InjectionToken;
public addProvider(
provider: Provider,
enhancerSubtype: EnhancerSubtype,
): Provider | InjectionToken;
public addProvider(provider: Provider, enhancerSubtype?: EnhancerSubtype) {
if (this.isCustomProvider(provider)) { if (this.isCustomProvider(provider)) {
return this.addCustomProvider(provider, this._providers); return this.addCustomProvider(provider, this._providers, enhancerSubtype);
} }
this._providers.set( this._providers.set(
provider, provider,
@@ -270,15 +292,16 @@ export class Module {
| ValueProvider | ValueProvider
| ExistingProvider, | ExistingProvider,
collection: Map<Function | string | symbol, any>, collection: Map<Function | string | symbol, any>,
enhancerSubtype?: EnhancerSubtype,
) { ) {
if (this.isCustomClass(provider)) { if (this.isCustomClass(provider)) {
this.addCustomClass(provider, collection); this.addCustomClass(provider, collection, enhancerSubtype);
} else if (this.isCustomValue(provider)) { } else if (this.isCustomValue(provider)) {
this.addCustomValue(provider, collection); this.addCustomValue(provider, collection, enhancerSubtype);
} else if (this.isCustomFactory(provider)) { } else if (this.isCustomFactory(provider)) {
this.addCustomFactory(provider, collection); this.addCustomFactory(provider, collection, enhancerSubtype);
} else if (this.isCustomUseExisting(provider)) { } else if (this.isCustomUseExisting(provider)) {
this.addCustomUseExisting(provider, collection); this.addCustomUseExisting(provider, collection, enhancerSubtype);
} }
return provider.provide; return provider.provide;
} }
@@ -306,6 +329,7 @@ export class Module {
public addCustomClass( public addCustomClass(
provider: ClassProvider, provider: ClassProvider,
collection: Map<InstanceToken, InstanceWrapper>, collection: Map<InstanceToken, InstanceWrapper>,
enhancerSubtype?: EnhancerSubtype,
) { ) {
let { scope, durable } = provider; let { scope, durable } = provider;
@@ -327,6 +351,7 @@ export class Module {
scope, scope,
durable, durable,
host: this, host: this,
subtype: enhancerSubtype,
}), }),
); );
} }
@@ -334,6 +359,7 @@ export class Module {
public addCustomValue( public addCustomValue(
provider: ValueProvider, provider: ValueProvider,
collection: Map<Function | string | symbol, InstanceWrapper>, collection: Map<Function | string | symbol, InstanceWrapper>,
enhancerSubtype?: EnhancerSubtype,
) { ) {
const { useValue: value, provide: providerToken } = provider; const { useValue: value, provide: providerToken } = provider;
collection.set( collection.set(
@@ -346,6 +372,7 @@ export class Module {
isResolved: true, isResolved: true,
async: value instanceof Promise, async: value instanceof Promise,
host: this, host: this,
subtype: enhancerSubtype,
}), }),
); );
} }
@@ -353,6 +380,7 @@ export class Module {
public addCustomFactory( public addCustomFactory(
provider: FactoryProvider, provider: FactoryProvider,
collection: Map<Function | string | symbol, InstanceWrapper>, collection: Map<Function | string | symbol, InstanceWrapper>,
enhancerSubtype?: EnhancerSubtype,
) { ) {
const { const {
useFactory: factory, useFactory: factory,
@@ -374,6 +402,7 @@ export class Module {
scope, scope,
durable, durable,
host: this, host: this,
subtype: enhancerSubtype,
}), }),
); );
} }
@@ -381,6 +410,7 @@ export class Module {
public addCustomUseExisting( public addCustomUseExisting(
provider: ExistingProvider, provider: ExistingProvider,
collection: Map<Function | string | symbol, InstanceWrapper>, collection: Map<Function | string | symbol, InstanceWrapper>,
enhancerSubtype?: EnhancerSubtype,
) { ) {
const { useExisting, provide: providerToken } = provider; const { useExisting, provide: providerToken } = provider;
collection.set( collection.set(
@@ -394,6 +424,7 @@ export class Module {
inject: [useExisting], inject: [useExisting],
host: this, host: this,
isAlias: true, isAlias: true,
subtype: enhancerSubtype,
}), }),
); );
} }

View File

@@ -0,0 +1,201 @@
import { NestContainer } from '../injector/container';
import { InstanceWrapper } from '../injector/instance-wrapper';
import { Module } from '../injector/module';
import { EnhancerMetadataCacheEntry } from './interfaces/enhancer-metadata-cache-entry.interface';
import { Entrypoint } from './interfaces/entrypoint.interface';
import { OrphanedEnhancerDefinition } from './interfaces/extras.interface';
import { ClassNode, Node } from './interfaces/node.interface';
import { SerializedGraph } from './serialized-graph';
export class GraphInspector {
private readonly graph: SerializedGraph;
private readonly enhancersMetadataCache =
new Array<EnhancerMetadataCacheEntry>();
constructor(private readonly container: NestContainer) {
this.graph = container.serializedGraph;
}
public inspectModules() {
const modules = this.container.getModules().values();
for (const moduleRef of modules) {
this.insertModuleNode(moduleRef);
this.insertClassNodes(moduleRef);
this.insertModuleToModuleEdges(moduleRef);
}
this.enhancersMetadataCache.forEach(entry =>
this.insertEnhancerEdge(entry),
);
}
public inspectInstanceWrapper<T = any>(
source: InstanceWrapper<T>,
moduleRef: Module,
) {
const ctorMetadata = source.getCtorMetadata();
ctorMetadata?.forEach((target, index) =>
this.insertClassToClassEdge(
source,
target,
moduleRef,
index,
'constructor',
),
);
const propertiesMetadata = source.getPropertiesMetadata();
propertiesMetadata?.forEach(({ key, wrapper: target }) =>
this.insertClassToClassEdge(source, target, moduleRef, key, 'property'),
);
}
public insertEnhancerMetadataCache(entry: EnhancerMetadataCacheEntry) {
this.enhancersMetadataCache.push(entry);
}
public insertOrphanedEnhancer(entry: OrphanedEnhancerDefinition) {
this.graph.insertOrphanedEnhancer(entry);
}
public insertAttachedEnhancer(wrapper: InstanceWrapper) {
const existingNode = this.graph.getNodeById(wrapper.id);
existingNode.metadata.global = true;
this.graph.insertAttachedEnhancer(existingNode.id);
}
public insertEntrypointDefinition<T>(definition: Entrypoint<T>) {
this.graph.insertEntrypoint(definition);
}
private insertModuleNode(moduleRef: Module) {
const dynamicMetadata = this.container.getDynamicMetadataByToken(
moduleRef.token,
);
const node: Node = {
id: moduleRef.id,
label: moduleRef.name,
metadata: {
type: 'module',
global: moduleRef.isGlobal,
dynamic: !!dynamicMetadata,
},
};
this.graph.insertNode(node);
}
private insertModuleToModuleEdges(moduleRef: Module) {
for (const targetModuleRef of moduleRef.imports) {
this.graph.insertEdge({
source: moduleRef.id,
target: targetModuleRef.id,
metadata: {
type: 'module-to-module',
sourceModuleName: moduleRef.name,
targetModuleName: targetModuleRef.name,
},
});
}
}
private insertEnhancerEdge(entry: EnhancerMetadataCacheEntry) {
const moduleRef = this.container.getModuleByKey(entry.moduleToken);
const sourceInstanceWrapper =
moduleRef.controllers.get(entry.classRef) ??
moduleRef.providers.get(entry.classRef);
const existingSourceNode = this.graph.getNodeById(
sourceInstanceWrapper.id,
) as ClassNode;
const enhancers = existingSourceNode.metadata.enhancers ?? [];
if (entry.enhancerInstanceWrapper) {
this.insertClassToClassEdge(
sourceInstanceWrapper,
entry.enhancerInstanceWrapper,
moduleRef,
undefined,
'decorator',
);
enhancers.push({
id: entry.enhancerInstanceWrapper.id,
methodKey: entry.methodKey,
});
} else {
const name =
entry.enhancerRef.constructor?.name ??
(entry.enhancerRef as Function).name;
enhancers.push({
name,
methodKey: entry.methodKey,
});
}
existingSourceNode.metadata.enhancers = enhancers;
}
private insertClassToClassEdge<T>(
source: InstanceWrapper<T>,
target: InstanceWrapper,
moduleRef: Module,
keyOrIndex: number | string | symbol | undefined,
injectionType: 'constructor' | 'property' | 'decorator',
) {
this.graph.insertEdge({
source: source.id,
target: target.id,
metadata: {
type: 'class-to-class',
sourceModuleName: moduleRef.name,
sourceClassName: source.name,
targetClassName: target.name,
sourceClassToken: source.token,
targetClassToken: target.token,
targetModuleName: target.host?.name,
keyOrIndex,
injectionType,
},
});
}
private insertClassNodes(moduleRef: Module) {
moduleRef.providers.forEach(value =>
this.insertClassNode(moduleRef, value, 'provider'),
);
moduleRef.middlewares.forEach(item =>
this.insertClassNode(moduleRef, item, 'middleware'),
);
moduleRef.injectables.forEach(value =>
this.insertClassNode(moduleRef, value, 'injectable'),
);
moduleRef.controllers.forEach(value =>
this.insertClassNode(moduleRef, value, 'controller'),
);
}
private insertClassNode(
moduleRef: Module,
wrapper: InstanceWrapper,
type: Exclude<Node['metadata']['type'], 'module'>,
) {
if (wrapper.metatype === moduleRef.metatype) {
return;
}
this.graph.insertNode({
id: wrapper.id,
label: wrapper.name,
parent: moduleRef.id,
metadata: {
type,
sourceModuleName: moduleRef.name,
durable: wrapper.isDependencyTreeDurable(),
static: wrapper.isDependencyTreeStatic(),
scope: wrapper.scope,
transient: wrapper.isTransient,
token: wrapper.token,
},
});
}
}

View File

@@ -0,0 +1,31 @@
import { InjectionToken } from '@nestjs/common';
type CommonEdgeMetadata = {
sourceModuleName: string;
targetModuleName: string;
};
type ModuleToModuleEdgeMetadata = {
type: 'module-to-module';
} & CommonEdgeMetadata;
type ClassToClassEdgeMetadata = {
type: 'class-to-class';
sourceClassName: string;
targetClassName: string;
sourceClassToken: InjectionToken;
targetClassToken: InjectionToken;
injectionType: 'constructor' | 'property' | 'decorator';
keyOrIndex?: string | number | symbol;
/**
* If true, indicates that this edge represents an internal providers connection
*/
internal?: boolean;
} & CommonEdgeMetadata;
export interface Edge {
id: string;
source: string;
target: string;
metadata: ModuleToModuleEdgeMetadata | ClassToClassEdgeMetadata;
}

View File

@@ -0,0 +1,13 @@
import { Type } from '@nestjs/common';
import { EnhancerSubtype } from '@nestjs/common/constants';
import { InstanceWrapper } from '../../injector/instance-wrapper';
export interface EnhancerMetadataCacheEntry {
targetNodeId?: string;
moduleToken: string;
classRef: Type;
methodKey: string | undefined;
enhancerRef?: unknown;
enhancerInstanceWrapper?: InstanceWrapper;
subtype: EnhancerSubtype;
}

View File

@@ -0,0 +1,23 @@
import { RequestMethod } from '@nestjs/common';
import { VersionValue } from '@nestjs/common/interfaces';
export type HttpEntrypointMetadata = {
path: string;
requestMethod: keyof typeof RequestMethod;
methodVersion?: VersionValue;
controllerVersion?: VersionValue;
};
export type MiddlewareEntrypointMetadata = {
path: string;
requestMethod: keyof typeof RequestMethod;
version?: VersionValue;
};
export type Entrypoint<T> = {
type: string;
methodName: string;
className: string;
classNodeId: string;
metadata: T;
};

View File

@@ -0,0 +1,21 @@
import { EnhancerSubtype } from '@nestjs/common/constants';
/**
* Enhancers attached through APP_PIPE, APP_GUARD, APP_INTERCEPTOR, and APP_FILTER tokens.
*/
export interface AttachedEnhancerDefinition {
nodeId: string;
}
/**
* Enhancers registered through "app.useGlobalPipes()", "app.useGlobalGuards()", "app.useGlobalInterceptors()", and "app.useGlobalFilters()" methods.
*/
export interface OrphanedEnhancerDefinition {
subtype: EnhancerSubtype;
ref: unknown;
}
export interface Extras {
orphanedEnhancers: Array<OrphanedEnhancerDefinition>;
attachedEnhancers: Array<AttachedEnhancerDefinition>;
}

View File

@@ -0,0 +1,43 @@
import { InjectionToken, Scope } from '@nestjs/common';
export type ModuleNode = {
metadata: {
type: 'module';
global: boolean;
dynamic: boolean;
};
};
export type ClassNode = {
parent: string;
metadata: {
type: 'provider' | 'controller' | 'middleware' | 'injectable';
sourceModuleName: string;
durable: boolean;
static: boolean;
transient: boolean;
scope: Scope;
token: InjectionToken;
/**
* Enhancers metadata collection
*/
enhancers?: Array<{ id: string } | { name: string; methodKey?: string }>;
/**
* Order in which globally registered enhancers are applied
*/
enhancerRegistrationOrder?: number;
/**
* If true, node is a globally registered enhancer
*/
global?: boolean;
/**
* If true, indicates that this node represents an internal provider
*/
internal?: boolean;
};
};
export type Node = {
id: string;
label: string;
} & (ClassNode | ModuleNode);

View File

@@ -0,0 +1,97 @@
import { InjectionToken } from '@nestjs/common';
import { randomUUID } from 'crypto';
import { ApplicationConfig } from '../application-config';
import { ExternalContextCreator } from '../helpers/external-context-creator';
import { HttpAdapterHost } from '../helpers/http-adapter-host';
import { INQUIRER } from '../injector/inquirer/inquirer-constants';
import { LazyModuleLoader } from '../injector/lazy-module-loader/lazy-module-loader';
import { ModuleRef } from '../injector/module-ref';
import { ModulesContainer } from '../injector/modules-container';
import { REQUEST } from '../router/request/request-constants';
import { Reflector } from '../services/reflector.service';
import { Edge } from './interfaces/edge.interface';
import { Entrypoint } from './interfaces/entrypoint.interface';
import {
Extras,
OrphanedEnhancerDefinition,
} from './interfaces/extras.interface';
import { Node } from './interfaces/node.interface';
const INTERNAL_PROVIDERS: Array<InjectionToken> = [
ApplicationConfig,
ModuleRef,
HttpAdapterHost,
LazyModuleLoader,
ExternalContextCreator,
ModulesContainer,
Reflector,
HttpAdapterHost.name,
Reflector.name,
REQUEST,
INQUIRER,
];
type WithOptionalId<T extends Record<'id', string>> = Omit<T, 'id'> &
Partial<Pick<T, 'id'>>;
export class SerializedGraph {
private readonly nodes = new Map<string, Node>();
private readonly edges = new Map<string, Edge>();
private readonly entrypoints = new Set<Entrypoint<unknown>>();
private readonly extras: Extras = {
orphanedEnhancers: [],
attachedEnhancers: [],
};
public insertNode(nodeDefinition: Node) {
if (
nodeDefinition.metadata.type === 'provider' &&
INTERNAL_PROVIDERS.includes(nodeDefinition.metadata.token)
) {
nodeDefinition.metadata = {
...nodeDefinition.metadata,
internal: true,
};
}
this.nodes.set(nodeDefinition.id, nodeDefinition);
return nodeDefinition;
}
public insertEdge(edgeDefinition: WithOptionalId<Edge>) {
if (
edgeDefinition.metadata.type === 'class-to-class' &&
(INTERNAL_PROVIDERS.includes(edgeDefinition.metadata.sourceClassToken) ||
INTERNAL_PROVIDERS.includes(edgeDefinition.metadata.targetClassToken))
) {
edgeDefinition.metadata = {
...edgeDefinition.metadata,
internal: true,
};
}
const id = edgeDefinition.id ?? randomUUID();
const edge = {
...edgeDefinition,
id,
};
this.edges.set(id, edge);
return edge;
}
public insertEntrypoint<T>(definition: Entrypoint<T>) {
this.entrypoints.add(definition);
}
public insertOrphanedEnhancer(entry: OrphanedEnhancerDefinition) {
this.extras.orphanedEnhancers.push(entry);
}
public insertAttachedEnhancer(nodeId: string) {
this.extras.attachedEnhancers.push({
nodeId,
});
}
public getNodeById(id: string) {
return this.nodes.get(id);
}
}

View File

@@ -2,8 +2,8 @@ import { HttpServer, VersioningType } from '@nestjs/common';
import { RequestMethod } from '@nestjs/common/enums/request-method.enum'; import { RequestMethod } from '@nestjs/common/enums/request-method.enum';
import { import {
MiddlewareConfiguration, MiddlewareConfiguration,
RouteInfo,
NestMiddleware, NestMiddleware,
RouteInfo,
} from '@nestjs/common/interfaces/middleware'; } from '@nestjs/common/interfaces/middleware';
import { import {
addLeadingSlash, addLeadingSlash,
@@ -19,6 +19,11 @@ import { NestContainer } from '../injector/container';
import { Injector } from '../injector/injector'; import { Injector } from '../injector/injector';
import { InstanceWrapper } from '../injector/instance-wrapper'; import { InstanceWrapper } from '../injector/instance-wrapper';
import { InstanceToken, Module } from '../injector/module'; import { InstanceToken, Module } from '../injector/module';
import { GraphInspector } from '../inspector/graph-inspector';
import {
Entrypoint,
MiddlewareEntrypointMetadata,
} from '../inspector/interfaces/entrypoint.interface';
import { REQUEST_CONTEXT_ID } from '../router/request/request-constants'; import { REQUEST_CONTEXT_ID } from '../router/request/request-constants';
import { RoutePathFactory } from '../router/route-path-factory'; import { RoutePathFactory } from '../router/route-path-factory';
import { RouterExceptionFilters } from '../router/router-exception-filters'; import { RouterExceptionFilters } from '../router/router-exception-filters';
@@ -40,6 +45,7 @@ export class MiddlewareModule {
private config: ApplicationConfig; private config: ApplicationConfig;
private container: NestContainer; private container: NestContainer;
private httpAdapter: HttpServer; private httpAdapter: HttpServer;
private graphInspector: GraphInspector;
constructor(private readonly routePathFactory: RoutePathFactory) {} constructor(private readonly routePathFactory: RoutePathFactory) {}
@@ -49,6 +55,7 @@ export class MiddlewareModule {
config: ApplicationConfig, config: ApplicationConfig,
injector: Injector, injector: Injector,
httpAdapter: HttpServer, httpAdapter: HttpServer,
graphInspector: GraphInspector,
) { ) {
const appRef = container.getHttpAdapterRef(); const appRef = container.getHttpAdapterRef();
this.routerExceptionFilter = new RouterExceptionFilters( this.routerExceptionFilter = new RouterExceptionFilters(
@@ -63,6 +70,7 @@ export class MiddlewareModule {
this.injector = injector; this.injector = injector;
this.container = container; this.container = container;
this.httpAdapter = httpAdapter; this.httpAdapter = httpAdapter;
this.graphInspector = graphInspector;
const modules = container.getModules(); const modules = container.getModules();
await this.resolveMiddleware(middlewareContainer, modules); await this.resolveMiddleware(middlewareContainer, modules);
@@ -174,6 +182,21 @@ export class MiddlewareModule {
if (instanceWrapper.isTransient) { if (instanceWrapper.isTransient) {
return; return;
} }
const middlewareDefinition: Entrypoint<MiddlewareEntrypointMetadata> = {
type: 'middleware',
methodName: 'use',
className: instanceWrapper.name,
classNodeId: instanceWrapper.id,
metadata: {
path: routeInfo.path,
requestMethod: RequestMethod[
routeInfo.method
] as keyof typeof RequestMethod,
version: routeInfo.version,
},
};
this.graphInspector.insertEntrypointDefinition(middlewareDefinition);
await this.bindHandler( await this.bindHandler(
instanceWrapper, instanceWrapper,
applicationRef, applicationRef,

View File

@@ -1,5 +1,5 @@
import { MODULE_PATH, PATH_METADATA } from '@nestjs/common/constants'; import { MODULE_PATH, PATH_METADATA } from '@nestjs/common/constants';
import { RouteInfo, Type, VersionValue } from '@nestjs/common/interfaces'; import { RouteInfo, Type } from '@nestjs/common/interfaces';
import { import {
addLeadingSlash, addLeadingSlash,
isString, isString,
@@ -8,14 +8,14 @@ import {
import { NestContainer } from '../injector/container'; import { NestContainer } from '../injector/container';
import { Module } from '../injector/module'; import { Module } from '../injector/module';
import { MetadataScanner } from '../metadata-scanner'; import { MetadataScanner } from '../metadata-scanner';
import { RouterExplorer } from '../router/router-explorer'; import { PathsExplorer } from '../router/paths-explorer';
import { targetModulesByContainer } from '../router/router-module'; import { targetModulesByContainer } from '../router/router-module';
export class RoutesMapper { export class RoutesMapper {
private readonly routerExplorer: RouterExplorer; private readonly pathsExplorer: PathsExplorer;
constructor(private readonly container: NestContainer) { constructor(private readonly container: NestContainer) {
this.routerExplorer = new RouterExplorer(new MetadataScanner(), container); this.pathsExplorer = new PathsExplorer(new MetadataScanner());
} }
public mapRouteToRouteInfo( public mapRouteToRouteInfo(
@@ -58,7 +58,7 @@ export class RoutesMapper {
controller: Type<any>, controller: Type<any>,
routePath: string, routePath: string,
): RouteInfo[] { ): RouteInfo[] {
const controllerPaths = this.routerExplorer.scanForPaths( const controllerPaths = this.pathsExplorer.scanForPaths(
Object.create(controller), Object.create(controller),
controller.prototype, controller.prototype,
); );

View File

@@ -13,9 +13,9 @@ import {
WebSocketAdapter, WebSocketAdapter,
} from '@nestjs/common'; } from '@nestjs/common';
import { import {
RouteInfo,
GlobalPrefixOptions, GlobalPrefixOptions,
NestApplicationOptions, NestApplicationOptions,
RouteInfo,
} from '@nestjs/common/interfaces'; } from '@nestjs/common/interfaces';
import { import {
CorsOptions, CorsOptions,
@@ -37,6 +37,7 @@ import { ApplicationConfig } from './application-config';
import { MESSAGES } from './constants'; import { MESSAGES } from './constants';
import { optionalRequire } from './helpers/optional-require'; import { optionalRequire } from './helpers/optional-require';
import { NestContainer } from './injector/container'; import { NestContainer } from './injector/container';
import { GraphInspector } from './inspector/graph-inspector';
import { MiddlewareContainer } from './middleware/container'; import { MiddlewareContainer } from './middleware/container';
import { MiddlewareModule } from './middleware/middleware-module'; import { MiddlewareModule } from './middleware/middleware-module';
import { NestApplicationContext } from './nest-application-context'; import { NestApplicationContext } from './nest-application-context';
@@ -80,6 +81,7 @@ export class NestApplication
container: NestContainer, container: NestContainer,
private readonly httpAdapter: HttpServer, private readonly httpAdapter: HttpServer,
private readonly config: ApplicationConfig, private readonly config: ApplicationConfig,
private readonly graphInspector: GraphInspector,
private readonly appOptions: NestApplicationOptions = {}, private readonly appOptions: NestApplicationOptions = {},
) { ) {
super(container); super(container);
@@ -92,6 +94,7 @@ export class NestApplication
this.container, this.container,
this.config, this.config,
this.injector, this.injector,
this.graphInspector,
); );
} }
@@ -152,6 +155,7 @@ export class NestApplication
this.config, this.config,
this.injector, this.injector,
this.httpAdapter, this.httpAdapter,
this.graphInspector,
); );
} }
@@ -217,6 +221,7 @@ export class NestApplication
const instance = new NestMicroservice( const instance = new NestMicroservice(
this.container, this.container,
microserviceOptions, microserviceOptions,
this.graphInspector,
applicationConfig, applicationConfig,
); );
instance.registerListeners(); instance.registerListeners();
@@ -365,21 +370,45 @@ export class NestApplication
public useGlobalFilters(...filters: ExceptionFilter[]): this { public useGlobalFilters(...filters: ExceptionFilter[]): this {
this.config.useGlobalFilters(...filters); this.config.useGlobalFilters(...filters);
filters.forEach(item =>
this.graphInspector.insertOrphanedEnhancer({
subtype: 'filter',
ref: item,
}),
);
return this; return this;
} }
public useGlobalPipes(...pipes: PipeTransform<any>[]): this { public useGlobalPipes(...pipes: PipeTransform<any>[]): this {
this.config.useGlobalPipes(...pipes); this.config.useGlobalPipes(...pipes);
pipes.forEach(item =>
this.graphInspector.insertOrphanedEnhancer({
subtype: 'pipe',
ref: item,
}),
);
return this; return this;
} }
public useGlobalInterceptors(...interceptors: NestInterceptor[]): this { public useGlobalInterceptors(...interceptors: NestInterceptor[]): this {
this.config.useGlobalInterceptors(...interceptors); this.config.useGlobalInterceptors(...interceptors);
interceptors.forEach(item =>
this.graphInspector.insertOrphanedEnhancer({
subtype: 'interceptor',
ref: item,
}),
);
return this; return this;
} }
public useGlobalGuards(...guards: CanActivate[]): this { public useGlobalGuards(...guards: CanActivate[]): this {
this.config.useGlobalGuards(...guards); this.config.useGlobalGuards(...guards);
guards.forEach(item =>
this.graphInspector.insertOrphanedEnhancer({
subtype: 'guard',
ref: item,
}),
);
return this; return this;
} }

View File

@@ -18,6 +18,7 @@ import { loadAdapter } from './helpers/load-adapter';
import { rethrow } from './helpers/rethrow'; import { rethrow } from './helpers/rethrow';
import { NestContainer } from './injector/container'; import { NestContainer } from './injector/container';
import { InstanceLoader } from './injector/instance-loader'; import { InstanceLoader } from './injector/instance-loader';
import { GraphInspector } from './inspector/graph-inspector';
import { MetadataScanner } from './metadata-scanner'; import { MetadataScanner } from './metadata-scanner';
import { NestApplication } from './nest-application'; import { NestApplication } from './nest-application';
import { NestApplicationContext } from './nest-application-context'; import { NestApplicationContext } from './nest-application-context';
@@ -73,15 +74,24 @@ export class NestFactoryStatic {
const applicationConfig = new ApplicationConfig(); const applicationConfig = new ApplicationConfig();
const container = new NestContainer(applicationConfig); const container = new NestContainer(applicationConfig);
const graphInspector = new GraphInspector(container);
this.setAbortOnError(serverOrOptions, options); this.setAbortOnError(serverOrOptions, options);
this.registerLoggerConfiguration(appOptions); this.registerLoggerConfiguration(appOptions);
await this.initialize(module, container, applicationConfig, httpServer); await this.initialize(
module,
container,
graphInspector,
applicationConfig,
httpServer,
);
const instance = new NestApplication( const instance = new NestApplication(
container, container,
httpServer, httpServer,
applicationConfig, applicationConfig,
graphInspector,
appOptions, appOptions,
); );
const target = this.createNestInstance(instance); const target = this.createNestInstance(instance);
@@ -108,12 +118,19 @@ export class NestFactoryStatic {
); );
const applicationConfig = new ApplicationConfig(); const applicationConfig = new ApplicationConfig();
const container = new NestContainer(applicationConfig); const container = new NestContainer(applicationConfig);
const graphInspector = new GraphInspector(container);
this.setAbortOnError(options); this.setAbortOnError(options);
this.registerLoggerConfiguration(options); this.registerLoggerConfiguration(options);
await this.initialize(module, container, applicationConfig); await this.initialize(module, container, graphInspector, applicationConfig);
return this.createNestInstance<INestMicroservice>( return this.createNestInstance<INestMicroservice>(
new NestMicroservice(container, options, applicationConfig), new NestMicroservice(
container,
options,
graphInspector,
applicationConfig,
),
); );
} }
@@ -131,10 +148,12 @@ export class NestFactoryStatic {
options?: NestApplicationContextOptions, options?: NestApplicationContextOptions,
): Promise<INestApplicationContext> { ): Promise<INestApplicationContext> {
const container = new NestContainer(); const container = new NestContainer();
const graphInspector = new GraphInspector(container);
this.setAbortOnError(options); this.setAbortOnError(options);
this.registerLoggerConfiguration(options); this.registerLoggerConfiguration(options);
await this.initialize(module, container); await this.initialize(module, container, graphInspector);
const modules = container.getModules().values(); const modules = container.getModules().values();
const root = modules.next().value; const root = modules.next().value;
@@ -155,14 +174,16 @@ export class NestFactoryStatic {
private async initialize( private async initialize(
module: any, module: any,
container: NestContainer, container: NestContainer,
graphInspector: GraphInspector,
config = new ApplicationConfig(), config = new ApplicationConfig(),
httpServer: HttpServer = null, httpServer: HttpServer = null,
) { ) {
const instanceLoader = new InstanceLoader(container); const instanceLoader = new InstanceLoader(container, graphInspector);
const metadataScanner = new MetadataScanner(); const metadataScanner = new MetadataScanner();
const dependenciesScanner = new DependenciesScanner( const dependenciesScanner = new DependenciesScanner(
container, container,
metadataScanner, metadataScanner,
graphInspector,
config, config,
); );
container.setHttpAdapter(httpServer); container.setHttpAdapter(httpServer);

View File

@@ -9,6 +9,7 @@ export class ReplLogger extends ConsoleLogger {
RouterExplorer.name, RouterExplorer.name,
NestApplication.name, NestApplication.name,
]; ];
log(_message: any, context?: string) { log(_message: any, context?: string) {
if (ReplLogger.ignoredContexts.includes(context)) { if (ReplLogger.ignoredContexts.includes(context)) {
return; return;

View File

@@ -0,0 +1,74 @@
import {
METHOD_METADATA,
PATH_METADATA,
VERSION_METADATA,
} from '@nestjs/common/constants';
import { RequestMethod } from '@nestjs/common/enums';
import { Controller } from '@nestjs/common/interfaces/controllers/controller.interface';
import { VersionValue } from '@nestjs/common/interfaces/version-options.interface';
import {
addLeadingSlash,
isString,
isUndefined,
} from '@nestjs/common/utils/shared.utils';
import { MetadataScanner } from '../metadata-scanner';
import { RouterProxyCallback } from './router-proxy';
export interface RouteDefinition {
path: string[];
requestMethod: RequestMethod;
targetCallback: RouterProxyCallback;
methodName: string;
version?: VersionValue;
}
export class PathsExplorer {
constructor(private readonly metadataScanner: MetadataScanner) {}
public scanForPaths(
instance: Controller,
prototype?: object,
): RouteDefinition[] {
const instancePrototype = isUndefined(prototype)
? Object.getPrototypeOf(instance)
: prototype;
return this.metadataScanner.scanFromPrototype<Controller, RouteDefinition>(
instance,
instancePrototype,
method => this.exploreMethodMetadata(instance, instancePrototype, method),
);
}
public exploreMethodMetadata(
instance: Controller,
prototype: object,
methodName: string,
): RouteDefinition {
const instanceCallback = instance[methodName];
const prototypeCallback = prototype[methodName];
const routePath = Reflect.getMetadata(PATH_METADATA, prototypeCallback);
if (isUndefined(routePath)) {
return null;
}
const requestMethod: RequestMethod = Reflect.getMetadata(
METHOD_METADATA,
prototypeCallback,
);
const version: VersionValue | undefined = Reflect.getMetadata(
VERSION_METADATA,
prototypeCallback,
);
const path = isString(routePath)
? [addLeadingSlash(routePath)]
: routePath.map((p: string) => addLeadingSlash(p));
return {
path,
requestMethod,
targetCallback: instanceCallback,
methodName,
version,
};
}
}

View File

@@ -1,9 +1,5 @@
import { HttpServer } from '@nestjs/common'; import { HttpServer } from '@nestjs/common';
import { import { PATH_METADATA } from '@nestjs/common/constants';
METHOD_METADATA,
PATH_METADATA,
VERSION_METADATA,
} from '@nestjs/common/constants';
import { RequestMethod, VersioningType } from '@nestjs/common/enums'; import { RequestMethod, VersioningType } from '@nestjs/common/enums';
import { InternalServerErrorException } from '@nestjs/common/exceptions'; import { InternalServerErrorException } from '@nestjs/common/exceptions';
import { Controller } from '@nestjs/common/interfaces/controllers/controller.interface'; import { Controller } from '@nestjs/common/interfaces/controllers/controller.interface';
@@ -12,7 +8,6 @@ import { VersionValue } from '@nestjs/common/interfaces/version-options.interfac
import { Logger } from '@nestjs/common/services/logger.service'; import { Logger } from '@nestjs/common/services/logger.service';
import { import {
addLeadingSlash, addLeadingSlash,
isString,
isUndefined, isUndefined,
} from '@nestjs/common/utils/shared.utils'; } from '@nestjs/common/utils/shared.utils';
import * as pathToRegexp from 'path-to-regexp'; import * as pathToRegexp from 'path-to-regexp';
@@ -32,6 +27,11 @@ import { NestContainer } from '../injector/container';
import { Injector } from '../injector/injector'; import { Injector } from '../injector/injector';
import { ContextId, InstanceWrapper } from '../injector/instance-wrapper'; import { ContextId, InstanceWrapper } from '../injector/instance-wrapper';
import { Module } from '../injector/module'; import { Module } from '../injector/module';
import { GraphInspector } from '../inspector/graph-inspector';
import {
Entrypoint,
HttpEntrypointMetadata,
} from '../inspector/interfaces/entrypoint.interface';
import { InterceptorsConsumer } from '../interceptors/interceptors-consumer'; import { InterceptorsConsumer } from '../interceptors/interceptors-consumer';
import { InterceptorsContextCreator } from '../interceptors/interceptors-context-creator'; import { InterceptorsContextCreator } from '../interceptors/interceptors-context-creator';
import { MetadataScanner } from '../metadata-scanner'; import { MetadataScanner } from '../metadata-scanner';
@@ -39,6 +39,7 @@ import { PipesConsumer } from '../pipes/pipes-consumer';
import { PipesContextCreator } from '../pipes/pipes-context-creator'; import { PipesContextCreator } from '../pipes/pipes-context-creator';
import { ExceptionsFilter } from './interfaces/exceptions-filter.interface'; import { ExceptionsFilter } from './interfaces/exceptions-filter.interface';
import { RoutePathMetadata } from './interfaces/route-path-metadata.interface'; import { RoutePathMetadata } from './interfaces/route-path-metadata.interface';
import { PathsExplorer } from './paths-explorer';
import { REQUEST_CONTEXT_ID } from './request/request-constants'; import { REQUEST_CONTEXT_ID } from './request/request-constants';
import { RouteParamsFactory } from './route-params-factory'; import { RouteParamsFactory } from './route-params-factory';
import { RoutePathFactory } from './route-path-factory'; import { RoutePathFactory } from './route-path-factory';
@@ -55,6 +56,7 @@ export interface RouteDefinition {
export class RouterExplorer { export class RouterExplorer {
private readonly executionContextCreator: RouterExecutionContext; private readonly executionContextCreator: RouterExecutionContext;
private readonly pathsExplorer: PathsExplorer;
private readonly routerMethodFactory = new RouterMethodFactory(); private readonly routerMethodFactory = new RouterMethodFactory();
private readonly logger = new Logger(RouterExplorer.name, { private readonly logger = new Logger(RouterExplorer.name, {
timestamp: true, timestamp: true,
@@ -62,14 +64,17 @@ export class RouterExplorer {
private readonly exceptionFiltersCache = new WeakMap(); private readonly exceptionFiltersCache = new WeakMap();
constructor( constructor(
private readonly metadataScanner: MetadataScanner, metadataScanner: MetadataScanner,
private readonly container: NestContainer, private readonly container: NestContainer,
private readonly injector?: Injector, private readonly injector: Injector,
private readonly routerProxy?: RouterProxy, private readonly routerProxy: RouterProxy,
private readonly exceptionsFilter?: ExceptionsFilter, private readonly exceptionsFilter: ExceptionsFilter,
private readonly config?: ApplicationConfig, config: ApplicationConfig,
private readonly routePathFactory?: RoutePathFactory, private readonly routePathFactory: RoutePathFactory,
private readonly graphInspector: GraphInspector,
) { ) {
this.pathsExplorer = new PathsExplorer(metadataScanner);
const routeParamsFactory = new RouteParamsFactory(); const routeParamsFactory = new RouteParamsFactory();
const pipesContextCreator = new PipesContextCreator(container, config); const pipesContextCreator = new PipesContextCreator(container, config);
const pipesConsumer = new PipesConsumer(); const pipesConsumer = new PipesConsumer();
@@ -101,7 +106,7 @@ export class RouterExplorer {
routePathMetadata: RoutePathMetadata, routePathMetadata: RoutePathMetadata,
) { ) {
const { instance } = instanceWrapper; const { instance } = instanceWrapper;
const routerPaths = this.scanForPaths(instance); const routerPaths = this.pathsExplorer.scanForPaths(instance);
this.applyPathsToRouterProxy( this.applyPathsToRouterProxy(
applicationRef, applicationRef,
routerPaths, routerPaths,
@@ -124,53 +129,6 @@ export class RouterExplorer {
return [addLeadingSlash(path)]; return [addLeadingSlash(path)];
} }
public scanForPaths(
instance: Controller,
prototype?: object,
): RouteDefinition[] {
const instancePrototype = isUndefined(prototype)
? Object.getPrototypeOf(instance)
: prototype;
return this.metadataScanner.scanFromPrototype<Controller, RouteDefinition>(
instance,
instancePrototype,
method => this.exploreMethodMetadata(instance, instancePrototype, method),
);
}
public exploreMethodMetadata(
instance: Controller,
prototype: object,
methodName: string,
): RouteDefinition {
const instanceCallback = instance[methodName];
const prototypeCallback = prototype[methodName];
const routePath = Reflect.getMetadata(PATH_METADATA, prototypeCallback);
if (isUndefined(routePath)) {
return null;
}
const requestMethod: RequestMethod = Reflect.getMetadata(
METHOD_METADATA,
prototypeCallback,
);
const version: VersionValue | undefined = Reflect.getMetadata(
VERSION_METADATA,
prototypeCallback,
);
const path = isString(routePath)
? [addLeadingSlash(routePath)]
: routePath.map((p: string) => addLeadingSlash(p));
return {
path,
requestMethod,
targetCallback: instanceCallback,
methodName,
version,
};
}
public applyPathsToRouterProxy<T extends HttpServer>( public applyPathsToRouterProxy<T extends HttpServer>(
router: T, router: T,
routeDefinitions: RouteDefinition[], routeDefinitions: RouteDefinition[],
@@ -255,7 +213,29 @@ export class RouterExplorer {
routePathMetadata, routePathMetadata,
requestMethod, requestMethod,
); );
pathsToRegister.forEach(path => routerMethodRef(path, routeHandler)); pathsToRegister.forEach(path => {
const entrypointDefinition: Entrypoint<HttpEntrypointMetadata> = {
type: 'http-endpoint',
methodName,
className: instanceWrapper.name,
classNodeId: instanceWrapper.id,
metadata: {
path,
requestMethod: RequestMethod[
requestMethod
] as keyof typeof RequestMethod,
methodVersion: routePathMetadata.methodVersion as VersionValue,
controllerVersion:
routePathMetadata.controllerVersion as VersionValue,
},
};
routerMethodRef(path, routeHandler);
this.graphInspector.insertEntrypointDefinition<HttpEntrypointMetadata>(
entrypointDefinition,
);
});
const pathsToLog = this.routePathFactory.create( const pathsToLog = this.routePathFactory.create(
{ {

View File

@@ -5,10 +5,10 @@ import {
VERSION_METADATA, VERSION_METADATA,
} from '@nestjs/common/constants'; } from '@nestjs/common/constants';
import { import {
Controller,
HttpServer, HttpServer,
Type, Type,
VersionValue, VersionValue,
Controller,
} from '@nestjs/common/interfaces'; } from '@nestjs/common/interfaces';
import { Logger } from '@nestjs/common/services/logger.service'; import { Logger } from '@nestjs/common/services/logger.service';
import { ApplicationConfig } from '../application-config'; import { ApplicationConfig } from '../application-config';
@@ -19,6 +19,7 @@ import {
import { NestContainer } from '../injector/container'; import { NestContainer } from '../injector/container';
import { Injector } from '../injector/injector'; import { Injector } from '../injector/injector';
import { InstanceWrapper } from '../injector/instance-wrapper'; import { InstanceWrapper } from '../injector/instance-wrapper';
import { GraphInspector } from '../inspector/graph-inspector';
import { MetadataScanner } from '../metadata-scanner'; import { MetadataScanner } from '../metadata-scanner';
import { Resolver } from './interfaces/resolver.interface'; import { Resolver } from './interfaces/resolver.interface';
import { RoutePathMetadata } from './interfaces/route-path-metadata.interface'; import { RoutePathMetadata } from './interfaces/route-path-metadata.interface';
@@ -40,6 +41,7 @@ export class RoutesResolver implements Resolver {
private readonly container: NestContainer, private readonly container: NestContainer,
private readonly applicationConfig: ApplicationConfig, private readonly applicationConfig: ApplicationConfig,
private readonly injector: Injector, private readonly injector: Injector,
graphInspector: GraphInspector,
) { ) {
const httpAdapterRef = container.getHttpAdapterRef(); const httpAdapterRef = container.getHttpAdapterRef();
this.routerExceptionsFilter = new RouterExceptionFilters( this.routerExceptionsFilter = new RouterExceptionFilters(
@@ -58,6 +60,7 @@ export class RoutesResolver implements Resolver {
this.routerExceptionsFilter, this.routerExceptionsFilter,
this.applicationConfig, this.applicationConfig,
this.routePathFactory, this.routePathFactory,
graphInspector,
); );
} }

View File

@@ -1,12 +1,9 @@
import { import { DynamicModule, ForwardReference, Provider } from '@nestjs/common';
DynamicModule,
flatten,
ForwardReference,
Provider,
} from '@nestjs/common';
import { import {
CATCH_WATERMARK, CATCH_WATERMARK,
CONTROLLER_WATERMARK, CONTROLLER_WATERMARK,
EnhancerSubtype,
ENHANCER_KEY_TO_SUBTYPE_MAP,
EXCEPTION_FILTERS_METADATA, EXCEPTION_FILTERS_METADATA,
GUARDS_METADATA, GUARDS_METADATA,
INJECTABLE_WATERMARK, INJECTABLE_WATERMARK,
@@ -38,7 +35,13 @@ import {
} from '@nestjs/common/utils/shared.utils'; } from '@nestjs/common/utils/shared.utils';
import { iterate } from 'iterare'; import { iterate } from 'iterare';
import { ApplicationConfig } from './application-config'; import { ApplicationConfig } from './application-config';
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from './constants'; import {
APP_FILTER,
APP_GUARD,
APP_INTERCEPTOR,
APP_PIPE,
ENHANCER_TOKEN_TO_SUBTYPE_MAP,
} from './constants';
import { CircularDependencyException } from './errors/exceptions/circular-dependency.exception'; import { CircularDependencyException } from './errors/exceptions/circular-dependency.exception';
import { InvalidClassModuleException } from './errors/exceptions/invalid-class-module.exception'; import { InvalidClassModuleException } from './errors/exceptions/invalid-class-module.exception';
import { InvalidModuleException } from './errors/exceptions/invalid-module.exception'; import { InvalidModuleException } from './errors/exceptions/invalid-module.exception';
@@ -48,6 +51,7 @@ import { NestContainer } from './injector/container';
import { InstanceWrapper } from './injector/instance-wrapper'; import { InstanceWrapper } from './injector/instance-wrapper';
import { InternalCoreModuleFactory } from './injector/internal-core-module/internal-core-module-factory'; import { InternalCoreModuleFactory } from './injector/internal-core-module/internal-core-module-factory';
import { Module } from './injector/module'; import { Module } from './injector/module';
import { GraphInspector } from './inspector/graph-inspector';
import { MetadataScanner } from './metadata-scanner'; import { MetadataScanner } from './metadata-scanner';
interface ApplicationProviderWrapper { interface ApplicationProviderWrapper {
@@ -64,6 +68,7 @@ export class DependenciesScanner {
constructor( constructor(
private readonly container: NestContainer, private readonly container: NestContainer,
private readonly metadataScanner: MetadataScanner, private readonly metadataScanner: MetadataScanner,
private readonly graphInspector: GraphInspector,
private readonly applicationConfig = new ApplicationConfig(), private readonly applicationConfig = new ApplicationConfig(),
) {} ) {}
@@ -75,6 +80,8 @@ export class DependenciesScanner {
this.addScopedEnhancersMetadata(); this.addScopedEnhancersMetadata();
this.container.bindGlobalScope(); this.container.bindGlobalScope();
this.graphInspector.inspectModules();
} }
public async scanForModules( public async scanForModules(
@@ -211,15 +218,15 @@ export class DependenciesScanner {
}); });
} }
public reflectDynamicMetadata(obj: Type<Injectable>, token: string) { public reflectDynamicMetadata(cls: Type<Injectable>, token: string) {
if (!obj || !obj.prototype) { if (!cls || !cls.prototype) {
return; return;
} }
this.reflectInjectables(obj, token, GUARDS_METADATA); this.reflectInjectables(cls, token, GUARDS_METADATA);
this.reflectInjectables(obj, token, INTERCEPTORS_METADATA); this.reflectInjectables(cls, token, INTERCEPTORS_METADATA);
this.reflectInjectables(obj, token, EXCEPTION_FILTERS_METADATA); this.reflectInjectables(cls, token, EXCEPTION_FILTERS_METADATA);
this.reflectInjectables(obj, token, PIPES_METADATA); this.reflectInjectables(cls, token, PIPES_METADATA);
this.reflectParamInjectables(obj, token, ROUTE_ARGS_METADATA); this.reflectParamInjectables(cls, token, ROUTE_ARGS_METADATA);
} }
public reflectExports(module: Type<unknown>, token: string) { public reflectExports(module: Type<unknown>, token: string) {
@@ -240,22 +247,37 @@ export class DependenciesScanner {
token: string, token: string,
metadataKey: string, metadataKey: string,
) { ) {
const controllerInjectables = this.reflectMetadata(metadataKey, component); const controllerInjectables = this.reflectMetadata<Type<Injectable>>(
const methodsInjectables = this.metadataScanner.scanFromPrototype( metadataKey,
component,
);
const methodInjectables = this.metadataScanner.scanFromPrototype<
Type,
{ methodKey: string; metadata: Type<Injectable>[] }
>(
null, null,
component.prototype, component.prototype,
this.reflectKeyMetadata.bind(this, component, metadataKey), this.reflectKeyMetadata.bind(this, component, metadataKey),
); );
const flattenMethodsInjectables = this.flatten(methodsInjectables); controllerInjectables.forEach(injectable =>
const combinedInjectables = [ this.insertInjectable(
...controllerInjectables, injectable,
...flattenMethodsInjectables, token,
].filter(isFunction); component,
const injectables = Array.from(new Set(combinedInjectables)); ENHANCER_KEY_TO_SUBTYPE_MAP[metadataKey],
),
injectables.forEach(injectable => );
this.insertInjectable(injectable, token, component), methodInjectables.forEach(({ methodKey, metadata }) =>
metadata.forEach(injectable =>
this.insertInjectable(
injectable,
token,
component,
ENHANCER_KEY_TO_SUBTYPE_MAP[metadataKey],
methodKey,
),
),
); );
} }
@@ -264,32 +286,60 @@ export class DependenciesScanner {
token: string, token: string,
metadataKey: string, metadataKey: string,
) { ) {
const paramsMetadata = this.metadataScanner.scanFromPrototype( const paramsMetadata = this.metadataScanner.scanFromPrototype<
null, Type,
component.prototype, {
method => Reflect.getMetadata(metadataKey, component, method), methodKey: string;
); metadata: Record<
const paramsInjectables = this.flatten(paramsMetadata).map( string,
(param: Record<string, any>) => {
flatten(Object.keys(param).map(k => param[k].pipes)).filter(isFunction), index: number;
); data: unknown;
flatten(paramsInjectables).forEach((injectable: Type<Injectable>) => pipes: Array<Type<PipeTransform> | PipeTransform>;
this.insertInjectable(injectable, token, component), }
); >;
}
>(null, component.prototype, methodKey => {
const metadata = Reflect.getMetadata(metadataKey, component, methodKey);
if (!metadata) {
return;
}
return { methodKey, metadata };
});
paramsMetadata.forEach(({ methodKey, metadata }) => {
const params = Object.values(metadata);
params
.map(item => item.pipes)
.flat(1)
.forEach(injectable =>
this.insertInjectable(
injectable,
token,
component,
'pipe',
methodKey,
),
);
});
} }
public reflectKeyMetadata( public reflectKeyMetadata(
component: Type<Injectable>, component: Type<Injectable>,
key: string, key: string,
method: string, methodKey: string,
) { ) {
let prototype = component.prototype; let prototype = component.prototype;
do { do {
const descriptor = Reflect.getOwnPropertyDescriptor(prototype, method); const descriptor = Reflect.getOwnPropertyDescriptor(prototype, methodKey);
if (!descriptor) { if (!descriptor) {
continue; continue;
} }
return Reflect.getMetadata(key, descriptor.value); const metadata = Reflect.getMetadata(key, descriptor.value);
if (!metadata) {
return;
}
return { methodKey, metadata };
} while ( } while (
(prototype = Reflect.getPrototypeOf(prototype)) && (prototype = Reflect.getPrototypeOf(prototype)) &&
prototype !== Object.prototype && prototype !== Object.prototype &&
@@ -298,7 +348,7 @@ export class DependenciesScanner {
return undefined; return undefined;
} }
public async calculateModulesDistance() { public calculateModulesDistance() {
const modulesGenerator = this.container.getModules().values(); const modulesGenerator = this.container.getModules().values();
// Skip "InternalCoreModule" from calculating distance // Skip "InternalCoreModule" from calculating distance
@@ -306,7 +356,7 @@ export class DependenciesScanner {
const modulesStack = []; const modulesStack = [];
const calculateDistance = (moduleRef: Module, distance = 1) => { const calculateDistance = (moduleRef: Module, distance = 1) => {
if (modulesStack.includes(moduleRef)) { if (!moduleRef || modulesStack.includes(moduleRef)) {
return; return;
} }
modulesStack.push(moduleRef); modulesStack.push(moduleRef);
@@ -383,21 +433,56 @@ export class DependenciesScanner {
scope, scope,
} as Provider; } as Provider;
const enhancerSubtype =
ENHANCER_TOKEN_TO_SUBTYPE_MAP[
type as
| typeof APP_GUARD
| typeof APP_PIPE
| typeof APP_FILTER
| typeof APP_INTERCEPTOR
];
const factoryOrClassProvider = newProvider as const factoryOrClassProvider = newProvider as
| FactoryProvider | FactoryProvider
| ClassProvider; | ClassProvider;
if (this.isRequestOrTransient(factoryOrClassProvider.scope)) { if (this.isRequestOrTransient(factoryOrClassProvider.scope)) {
return this.container.addInjectable(newProvider, token); return this.container.addInjectable(newProvider, token, enhancerSubtype);
} }
this.container.addProvider(newProvider, token); this.container.addProvider(newProvider, token, enhancerSubtype);
} }
public insertInjectable( public insertInjectable(
injectable: Type<Injectable>, injectable: Type<Injectable> | object,
token: string, token: string,
host: Type<Injectable>, host: Type<Injectable>,
subtype: EnhancerSubtype,
methodKey?: string,
) { ) {
this.container.addInjectable(injectable, token, host); if (isFunction(injectable)) {
const instanceWrapper = this.container.addInjectable(
injectable as Type,
token,
subtype,
host,
) as InstanceWrapper;
this.graphInspector.insertEnhancerMetadataCache({
moduleToken: token,
classRef: host,
enhancerInstanceWrapper: instanceWrapper,
targetNodeId: instanceWrapper.id,
subtype,
methodKey,
});
return instanceWrapper;
} else {
this.graphInspector.insertEnhancerMetadataCache({
moduleToken: token,
classRef: host,
enhancerRef: injectable,
methodKey,
subtype,
});
}
} }
public insertExportedProvider( public insertExportedProvider(
@@ -411,7 +496,10 @@ export class DependenciesScanner {
this.container.addController(controller, token); this.container.addController(controller, token);
} }
public reflectMetadata(metadataKey: string, metatype: Type<any>) { public reflectMetadata<T = any>(
metadataKey: string,
metatype: Type<any>,
): T[] {
return Reflect.getMetadata(metadataKey, metatype) || []; return Reflect.getMetadata(metadataKey, metatype) || [];
} }
@@ -421,6 +509,7 @@ export class DependenciesScanner {
this, this,
this.container.getModuleCompiler(), this.container.getModuleCompiler(),
this.container.getHttpAdapterHostRef(), this.container.getHttpAdapterHostRef(),
this.graphInspector,
); );
const [instance] = await this.scanForModules(moduleDefinition); const [instance] = await this.scanForModules(moduleDefinition);
this.container.registerCoreModuleRef(instance); this.container.registerCoreModuleRef(instance);
@@ -471,6 +560,8 @@ export class DependenciesScanner {
providerKey, providerKey,
'injectables', 'injectables',
); );
this.graphInspector.insertAttachedEnhancer(instanceWrapper);
return applyRequestProvidersMap[type as string](instanceWrapper); return applyRequestProvidersMap[type as string](instanceWrapper);
} }
instanceWrapper = getInstanceWrapper( instanceWrapper = getInstanceWrapper(
@@ -478,6 +569,7 @@ export class DependenciesScanner {
providerKey, providerKey,
'providers', 'providers',
); );
this.graphInspector.insertAttachedEnhancer(instanceWrapper);
applyProvidersMap[type as string](instanceWrapper.instance); applyProvidersMap[type as string](instanceWrapper.instance);
}, },
); );
@@ -545,10 +637,6 @@ export class DependenciesScanner {
return module && !!(module as ForwardReference).forwardRef; return module && !!(module as ForwardReference).forwardRef;
} }
private flatten<T = any>(arr: T[][]): T[] {
return arr.flat(1);
}
private isRequestOrTransient(scope: Scope): boolean { private isRequestOrTransient(scope: Scope): boolean {
return scope === Scope.REQUEST || scope === Scope.TRANSIENT; return scope === Scope.REQUEST || scope === Scope.TRANSIENT;
} }

View File

@@ -6,51 +6,64 @@ import { NestContainer } from '../../injector/container';
import { Injector } from '../../injector/injector'; import { Injector } from '../../injector/injector';
import { InstanceLoader } from '../../injector/instance-loader'; import { InstanceLoader } from '../../injector/instance-loader';
import { InstanceWrapper } from '../../injector/instance-wrapper'; import { InstanceWrapper } from '../../injector/instance-wrapper';
import { GraphInspector } from '../../inspector/graph-inspector';
describe('InstanceLoader', () => { describe('InstanceLoader', () => {
let loader: InstanceLoader;
let container: NestContainer;
let mockContainer: sinon.SinonMock;
@Controller('') @Controller('')
class TestRoute {} class TestCtrl {}
@Injectable() @Injectable()
class TestProvider {} class TestProvider {}
let loader: InstanceLoader;
let injector: Injector;
let container: NestContainer;
let graphInspector: GraphInspector;
let inspectInstanceWrapperStub: sinon.SinonStub;
let mockContainer: sinon.SinonMock;
let moduleMock: Record<string, any>;
beforeEach(() => { beforeEach(() => {
container = new NestContainer(); container = new NestContainer();
loader = new InstanceLoader(container); graphInspector = new GraphInspector(container);
mockContainer = sinon.mock(container);
});
it('should call "loadPrototype" for each provider and route in each module', async () => { inspectInstanceWrapperStub = sinon.stub(
const injector = new Injector(); graphInspector,
'inspectInstanceWrapper',
);
loader = new InstanceLoader(container, graphInspector);
injector = new Injector();
(loader as any).injector = injector; (loader as any).injector = injector;
const module = { mockContainer = sinon.mock(container);
moduleMock = {
providers: new Map(), providers: new Map(),
controllers: new Map(), controllers: new Map(),
injectables: new Map(), injectables: new Map(),
metatype: { name: 'test' }, metatype: { name: 'test' },
}; };
const modules = new Map();
modules.set('Test', moduleMock);
mockContainer.expects('getModules').returns(modules);
});
it('should call "loadPrototype" for every provider and controller in every module', async () => {
const providerWrapper: InstanceWrapper = { const providerWrapper: InstanceWrapper = {
instance: null, instance: null,
metatype: TestProvider, metatype: TestProvider,
token: 'TestProvider', token: 'TestProvider',
} as any; } as any;
const routeWrapper: InstanceWrapper = { const ctrlWrapper: InstanceWrapper = {
instance: null, instance: null,
metatype: TestRoute, metatype: TestCtrl,
token: 'TestRoute', token: 'TestRoute',
} as any; } as any;
module.providers.set('TestProvider', providerWrapper); moduleMock.providers.set('TestProvider', providerWrapper);
module.controllers.set('TestRoute', routeWrapper); moduleMock.controllers.set('TestRoute', ctrlWrapper);
const modules = new Map();
modules.set('Test', module);
mockContainer.expects('getModules').returns(modules);
const loadProviderPrototypeStub = sinon.stub(injector, 'loadPrototype'); const loadProviderPrototypeStub = sinon.stub(injector, 'loadPrototype');
@@ -58,113 +71,125 @@ describe('InstanceLoader', () => {
sinon.stub(injector, 'loadProvider'); sinon.stub(injector, 'loadProvider');
await loader.createInstancesOfDependencies(); await loader.createInstancesOfDependencies();
expect( expect(
loadProviderPrototypeStub.calledWith(providerWrapper, module.providers), loadProviderPrototypeStub.calledWith(
providerWrapper,
moduleMock.providers,
),
).to.be.true; ).to.be.true;
expect( expect(
loadProviderPrototypeStub.calledWith(routeWrapper, module.controllers), loadProviderPrototypeStub.calledWith(ctrlWrapper, moduleMock.controllers),
).to.be.true; ).to.be.true;
}); });
it('should call "loadProvider" for each provider in each module', async () => { describe('for every provider in every module', () => {
const injector = new Injector(); const testProviderToken = 'TestProvider';
(loader as any).injector = injector;
const module = { let loadProviderStub: sinon.SinonStub;
providers: new Map(),
controllers: new Map(), beforeEach(async () => {
injectables: new Map(), const testProviderWrapper = new InstanceWrapper({
metatype: { name: 'test' }, instance: null,
}; metatype: TestProvider,
const testComp = new InstanceWrapper({ name: testProviderToken,
instance: null, token: testProviderToken,
metatype: TestProvider, });
name: 'TestProvider', moduleMock.providers.set(testProviderToken, testProviderWrapper);
token: 'TestProvider',
loadProviderStub = sinon.stub(injector, 'loadProvider');
sinon.stub(injector, 'loadController');
await loader.createInstancesOfDependencies();
}); });
module.providers.set('TestProvider', testComp);
const modules = new Map(); it('should call "loadProvider"', async () => {
modules.set('Test', module); expect(
mockContainer.expects('getModules').returns(modules); loadProviderStub.calledWith(
moduleMock.providers.get(testProviderToken),
moduleMock as any,
),
).to.be.true;
});
const loadProviderStub = sinon.stub(injector, 'loadProvider'); it('should call "inspectInstanceWrapper"', async () => {
sinon.stub(injector, 'loadController'); expect(
inspectInstanceWrapperStub.calledWith(
await loader.createInstancesOfDependencies(); moduleMock.providers.get(testProviderToken),
expect( moduleMock as any,
loadProviderStub.calledWith( ),
module.providers.get('TestProvider'), ).to.be.true;
module as any, });
),
).to.be.true;
}); });
it('should call "loadController" for each route in each module', async () => { describe('for every controller in every module', () => {
const injector = new Injector(); let loadControllerStub: sinon.SinonStub;
(loader as any).injector = injector;
const module = { beforeEach(async () => {
providers: new Map(), const wrapper = new InstanceWrapper({
controllers: new Map(), name: 'TestRoute',
injectables: new Map(), token: 'TestRoute',
metatype: { name: 'test' }, instance: null,
}; metatype: TestCtrl,
const wrapper = new InstanceWrapper({ });
name: 'TestRoute', moduleMock.controllers.set('TestRoute', wrapper);
token: 'TestRoute',
instance: null, sinon.stub(injector, 'loadProvider');
metatype: TestRoute, loadControllerStub = sinon.stub(injector, 'loadController');
await loader.createInstancesOfDependencies();
});
it('should call "loadController"', async () => {
expect(
loadControllerStub.calledWith(
moduleMock.controllers.get('TestRoute'),
moduleMock as any,
),
).to.be.true;
});
it('should call "inspectInstanceWrapper"', async () => {
expect(
inspectInstanceWrapperStub.calledWith(
moduleMock.controllers.get('TestRoute'),
moduleMock as any,
),
).to.be.true;
}); });
module.controllers.set('TestRoute', wrapper);
const modules = new Map();
modules.set('Test', module);
mockContainer.expects('getModules').returns(modules);
sinon.stub(injector, 'loadProvider');
const loadRoutesStub = sinon.stub(injector, 'loadController');
await loader.createInstancesOfDependencies();
expect(
loadRoutesStub.calledWith(
module.controllers.get('TestRoute'),
module as any,
),
).to.be.true;
}); });
it('should call "loadInjectable" for each injectable in each module', async () => { describe('for every injectable in every module', () => {
const injector = new Injector(); let loadInjectableStub: sinon.SinonStub;
(loader as any).injector = injector;
const module = { beforeEach(async () => {
providers: new Map(), const testInjectable = new InstanceWrapper({
controllers: new Map(), instance: null,
injectables: new Map(), metatype: TestProvider,
metatype: { name: 'test' }, name: 'TestProvider',
}; token: 'TestProvider',
const testComp = new InstanceWrapper({ });
instance: null, moduleMock.injectables.set('TestProvider', testInjectable);
metatype: TestProvider,
name: 'TestProvider', loadInjectableStub = sinon.stub(injector, 'loadInjectable');
token: 'TestProvider', sinon.stub(injector, 'loadController');
await loader.createInstancesOfDependencies();
}); });
module.injectables.set('TestProvider', testComp);
const modules = new Map(); it('should call "loadInjectable"', async () => {
modules.set('Test', module); expect(
mockContainer.expects('getModules').returns(modules); loadInjectableStub.calledWith(
moduleMock.injectables.get('TestProvider'),
const loadInjectableStub = sinon.stub(injector, 'loadInjectable'); moduleMock as any,
sinon.stub(injector, 'loadController'); ),
).to.be.true;
await loader.createInstancesOfDependencies(); });
expect( it('should call "inspectInstanceWrapper"', async () => {
loadInjectableStub.calledWith( expect(
module.injectables.get('TestProvider'), inspectInstanceWrapperStub.calledWith(
module as any, moduleMock.injectables.get('TestProvider'),
), moduleMock as any,
).to.be.true; ),
).to.be.true;
});
}); });
}); });

View File

@@ -14,6 +14,7 @@ describe('InternalCoreModuleFactory', () => {
null, null,
null, null,
null, null,
null,
); );
expect(moduleDefinition.module).to.equal(InternalCoreModule); expect(moduleDefinition.module).to.equal(InternalCoreModule);

View File

@@ -7,6 +7,7 @@ import {
NestContainer, NestContainer,
} from '../../../injector'; } from '../../../injector';
import { InstanceLoader } from '../../../injector/instance-loader'; import { InstanceLoader } from '../../../injector/instance-loader';
import { GraphInspector } from '../../../inspector/graph-inspector';
import { MetadataScanner } from '../../../metadata-scanner'; import { MetadataScanner } from '../../../metadata-scanner';
import { DependenciesScanner } from '../../../scanner'; import { DependenciesScanner } from '../../../scanner';
@@ -18,15 +19,23 @@ describe('LazyModuleLoader', () => {
class NoopLogger { class NoopLogger {
log() {} log() {}
error() {}
warn() {}
} }
beforeEach(() => { beforeEach(() => {
const nestContainer = new NestContainer(); const nestContainer = new NestContainer();
const graphInspector = new GraphInspector(nestContainer);
dependenciesScanner = new DependenciesScanner( dependenciesScanner = new DependenciesScanner(
nestContainer, nestContainer,
new MetadataScanner(), new MetadataScanner(),
graphInspector,
);
instanceLoader = new InstanceLoader(
nestContainer,
graphInspector,
new NoopLogger(),
); );
instanceLoader = new InstanceLoader(nestContainer, new NoopLogger() as any);
modulesContainer = nestContainer.getModules(); modulesContainer = nestContainer.getModules();
lazyModuleLoader = new LazyModuleLoader( lazyModuleLoader = new LazyModuleLoader(
dependenciesScanner, dependenciesScanner,

View File

@@ -56,7 +56,7 @@ describe('Module', () => {
const setSpy = sinon.spy(collection, 'set'); const setSpy = sinon.spy(collection, 'set');
(module as any)._injectables = collection; (module as any)._injectables = collection;
module.addInjectable(TestProvider, TestModule); module.addInjectable(TestProvider, 'interceptor', TestModule);
expect( expect(
setSpy.calledWith( setSpy.calledWith(
TestProvider, TestProvider,
@@ -69,6 +69,7 @@ describe('Module', () => {
instance: null, instance: null,
durable: undefined, durable: undefined,
isResolved: false, isResolved: false,
subtype: 'interceptor',
}), }),
), ),
).to.be.true; ).to.be.true;
@@ -78,7 +79,7 @@ describe('Module', () => {
it('should call `addCustomProvider`', () => { it('should call `addCustomProvider`', () => {
const addCustomProviderSpy = sinon.spy(module, 'addCustomProvider'); const addCustomProviderSpy = sinon.spy(module, 'addCustomProvider');
module.addInjectable({ provide: 'test' } as any); module.addInjectable({ provide: 'test' } as any, 'guard');
expect(addCustomProviderSpy.called).to.be.true; expect(addCustomProviderSpy.called).to.be.true;
}); });
}); });
@@ -180,6 +181,7 @@ describe('Module', () => {
durable: true, durable: true,
instance: null, instance: null,
isResolved: false, isResolved: false,
subtype: undefined,
}), }),
), ),
).to.be.true; ).to.be.true;
@@ -211,6 +213,7 @@ describe('Module', () => {
instance: value, instance: value,
isResolved: true, isResolved: true,
async: false, async: false,
subtype: undefined,
}), }),
), ),
).to.be.true; ).to.be.true;
@@ -244,6 +247,7 @@ describe('Module', () => {
instance: null, instance: null,
isResolved: false, isResolved: false,
inject: inject as any, inject: inject as any,
subtype: undefined,
}), }),
), ),
).to.be.true; ).to.be.true;
@@ -279,6 +283,7 @@ describe('Module', () => {
inject: [provider.useExisting as any], inject: [provider.useExisting as any],
isResolved: false, isResolved: false,
isAlias: true, isAlias: true,
subtype: undefined,
}), }),
), ),
).to.be.true; ).to.be.true;

View File

@@ -0,0 +1,217 @@
import { Scope } from '@nestjs/common';
import { expect } from 'chai';
import * as sinon from 'sinon';
import { NestContainer } from '../../injector/container';
import { InstanceWrapper } from '../../injector/instance-wrapper';
import { Module } from '../../injector/module';
import { GraphInspector } from '../../inspector/graph-inspector';
import { EnhancerMetadataCacheEntry } from '../../inspector/interfaces/enhancer-metadata-cache-entry.interface';
import { SerializedGraph } from '../../inspector/serialized-graph';
describe('GraphInspector', () => {
let graphInspector: GraphInspector;
let enhancersMetadataCache: Array<EnhancerMetadataCacheEntry>;
let graph: SerializedGraph;
let container: NestContainer;
beforeEach(() => {
container = new NestContainer();
graphInspector = new GraphInspector(container);
enhancersMetadataCache = graphInspector['enhancersMetadataCache'];
graph = graphInspector['graph'];
});
describe('insertEnhancerMetadataCache', () => {
it('should insert an enhancer metadata cache entry', () => {
const entry = {
moduleToken: 'moduleToken',
classRef: class AppService {},
methodKey: undefined,
subtype: 'guard' as const,
};
graphInspector.insertEnhancerMetadataCache(entry);
expect(enhancersMetadataCache).includes(entry);
});
});
describe('inspectInstanceWrapper', () => {
class AppService {}
it('should inspect given instance wrapper and insert appropriate edges', () => {
const moduleRef = new Module(class TestModule {}, container);
const instanceWrapper = new InstanceWrapper({
token: AppService,
name: AppService.name,
metatype: AppService,
});
const param1 = new InstanceWrapper({
token: 'PARAM_1',
metatype: class A {},
host: new Module(class AModule {}, container),
});
const param2 = new InstanceWrapper({
token: 'PARAM_2',
metatype: class B {},
host: new Module(class BModule {}, container),
});
const dependency = new InstanceWrapper({
name: 'PROPERTY',
token: 'PROPERTY',
metatype: class C {},
host: new Module(class CModule {}, container),
});
instanceWrapper.addCtorMetadata(0, param1);
instanceWrapper.addCtorMetadata(1, param2);
instanceWrapper.addCtorMetadata(2, dependency);
graphInspector.inspectInstanceWrapper(instanceWrapper, moduleRef);
const edgesArr = [...graph['edges'].values()];
expect(edgesArr).to.deep.equal([
{
id: edgesArr[0].id,
metadata: {
injectionType: 'constructor',
keyOrIndex: 0,
sourceClassName: instanceWrapper.metatype.name,
sourceClassToken: instanceWrapper.token,
sourceModuleName: 'TestModule',
targetClassName: param1.name,
targetClassToken: 'PARAM_1',
targetModuleName: 'AModule',
type: 'class-to-class',
},
source: instanceWrapper.id,
target: param1.id,
},
{
id: edgesArr[1].id,
metadata: {
injectionType: 'constructor',
keyOrIndex: 1,
sourceClassName: instanceWrapper.metatype.name,
sourceClassToken: instanceWrapper.token,
sourceModuleName: 'TestModule',
targetClassName: param2.name,
targetClassToken: 'PARAM_2',
targetModuleName: 'BModule',
type: 'class-to-class',
},
source: instanceWrapper.id,
target: param2.id,
},
{
id: edgesArr[2].id,
metadata: {
injectionType: 'constructor',
keyOrIndex: 2,
sourceClassName: 'AppService',
sourceClassToken: AppService,
sourceModuleName: 'TestModule',
targetClassName: dependency.name,
targetClassToken: 'PROPERTY',
targetModuleName: 'CModule',
type: 'class-to-class',
},
source: instanceWrapper.id,
target: dependency.id,
},
]);
});
});
describe('inspectModules', () => {
class TestModule {}
class AController {}
class RandomPipe {}
it('should inspect all modules', async () => {
const moduleRef = await container.addModule(TestModule, []);
moduleRef.addController(AController);
const subtype = 'interceptor';
const enhancerInstanceWrapper = moduleRef.addInjectable(
class Enhancer {},
subtype,
) as InstanceWrapper;
const methodKey = 'findOne';
enhancersMetadataCache.push(
{
moduleToken: moduleRef.token,
classRef: AController,
enhancerRef: new RandomPipe(),
methodKey,
subtype,
},
{
moduleToken: moduleRef.token,
classRef: AController,
enhancerRef: function test() {},
methodKey,
subtype,
},
{
moduleToken: moduleRef.token,
classRef: AController,
enhancerInstanceWrapper,
methodKey: undefined,
subtype,
},
);
const serializedNode = { metadata: {} };
sinon.stub(graph, 'getNodeById').callsFake(() => serializedNode as any);
graphInspector.inspectModules();
expect(serializedNode).to.deep.equal({
metadata: {
enhancers: [
{ methodKey, name: RandomPipe.name },
{ methodKey, name: 'Function' },
{ methodKey: undefined, id: enhancerInstanceWrapper.id },
],
},
});
});
});
describe('insertAttachedEnhancer', () => {
it('should upsert existing node (update metadata) and add node to "attachedEnhancers" array', () => {
const instanceWrapper = new InstanceWrapper({
metatype: class A {},
token: 'A',
});
const nodeDefinition = {
id: instanceWrapper.id,
label: 'A',
parent: '2c989d11-2731-4828-a2eb-c86d10c73621',
metadata: {
type: 'provider' as const,
sourceModuleName: 'AppModule',
durable: false,
static: true,
scope: Scope.DEFAULT,
transient: false,
token: class A {},
},
};
const insertedNode = graph.insertNode(nodeDefinition);
graphInspector.insertAttachedEnhancer(instanceWrapper);
expect(insertedNode.metadata).to.deep.equal({
...nodeDefinition.metadata,
global: true,
});
expect(graph['extras'].attachedEnhancers).to.deep.contain({
nodeId: insertedNode.id,
});
});
});
});

View File

@@ -0,0 +1,154 @@
import { Scope } from '@nestjs/common';
import { expect } from 'chai';
import { ApplicationConfig } from '../../application-config';
import { Edge } from '../../inspector/interfaces/edge.interface';
import { Node } from '../../inspector/interfaces/node.interface';
import { SerializedGraph } from '../../inspector/serialized-graph';
describe('SerializedGraph', () => {
let serializedGraph: SerializedGraph;
let nodesCollection: Map<string, Node>;
let edgesCollection: Map<string, Edge>;
beforeEach(() => {
serializedGraph = new SerializedGraph();
nodesCollection = serializedGraph['nodes'];
edgesCollection = serializedGraph['edges'];
});
describe('insertNode', () => {
describe('when node definition represents an internal provider', () => {
it('should insert a node with the expected schema (internal: true)', () => {
const nodeDefinition = {
id: '11430093-e992-4ae6-8ba4-c7db80419de8',
label: 'ApplicationConfig',
parent: '2c989d11-2731-4828-a2eb-c86d10c73621',
metadata: {
type: 'provider' as const,
sourceModuleName: 'AppModule',
durable: false,
static: true,
transient: false,
token: ApplicationConfig,
scope: Scope.DEFAULT,
},
};
serializedGraph.insertNode(nodeDefinition);
expect(nodesCollection.get(nodeDefinition.id)).to.deep.equal({
...nodeDefinition,
metadata: {
...nodeDefinition.metadata,
internal: true,
},
});
});
});
describe('otherwise', () => {
it('should insert a node with the expected schema', () => {
class AppService {}
const nodeDefinition = {
id: '11430093-e992-4ae6-8ba4-c7db80419de8',
label: 'AppService',
parent: '2c989d11-2731-4828-a2eb-c86d10c73621',
metadata: {
type: 'provider' as const,
sourceModuleName: 'AppModule',
durable: false,
static: true,
transient: false,
token: AppService,
scope: Scope.DEFAULT,
},
};
serializedGraph.insertNode(nodeDefinition);
expect(nodesCollection.get(nodeDefinition.id)).to.equal(nodeDefinition);
});
});
});
describe('insertEdge', () => {
describe('when edge definition represents internal providers connection', () => {
it('should insert an edge with the expected schema (internal: true)', () => {
const edgeDefinition = {
source: '8920252f-4e7d-4f9e-9eeb-71da467a35cc',
target: 'c97bc04d-cfcf-41b1-96ec-db729f33676e',
metadata: {
type: 'class-to-class' as const,
sourceModuleName: 'UtilsExceptionsModule',
sourceClassName: 'AllExceptionsFilter',
targetClassName: 'HttpAdapterHost',
sourceClassToken:
'APP_FILTER (UUID: 4187828c-5c76-4aed-a29f-a6eb40054b9d)',
targetClassToken: 'HttpAdapterHost',
targetModuleName: 'InternalCoreModule',
keyOrIndex: 0,
injectionType: 'constructor' as const,
},
};
const edge = serializedGraph.insertEdge(edgeDefinition);
expect(edgesCollection.get(edge.id)).to.deep.equal({
...edgeDefinition,
metadata: {
...edgeDefinition.metadata,
internal: true,
},
id: edge.id,
});
});
});
describe('otherwise', () => {
it('should insert an edge with the expected schema', () => {
const edgeDefinition = {
source: '8920252f-4e7d-4f9e-9eeb-71da467a35cc',
target: 'c97bc04d-cfcf-41b1-96ec-db729f33676e',
metadata: {
type: 'class-to-class' as const,
sourceModuleName: 'UtilsExceptionsModule',
sourceClassName: 'AllExceptionsFilter',
targetClassName: 'AppService',
sourceClassToken:
'APP_FILTER (UUID: 4187828c-5c76-4aed-a29f-a6eb40054b9d)',
targetClassToken: 'AppService',
targetModuleName: 'InternalCoreModule',
keyOrIndex: 0,
injectionType: 'constructor' as const,
},
};
const edge = serializedGraph.insertEdge(edgeDefinition);
expect(edgesCollection.get(edge.id)).to.deep.equal({
...edgeDefinition,
id: edge.id,
});
});
});
});
describe('getNodeById', () => {
it('should return a given node', () => {
const nodeDefinition = {
id: '11430093-e992-4ae6-8ba4-c7db80419de8',
label: 'AppService',
parent: '2c989d11-2731-4828-a2eb-c86d10c73621',
metadata: {
type: 'provider' as const,
sourceModuleName: 'AppModule',
durable: false,
static: true,
transient: false,
scope: Scope.DEFAULT,
token: 'AppService',
},
};
nodesCollection.set(nodeDefinition.id, nodeDefinition);
expect(serializedGraph.getNodeById(nodeDefinition.id)).to.eq(
nodeDefinition,
);
});
});
});

View File

@@ -1,6 +1,8 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { RoutePathFactory } from '@nestjs/core/router/route-path-factory'; import { RoutePathFactory } from '@nestjs/core/router/route-path-factory';
import * as chai from 'chai';
import { expect } from 'chai'; import { expect } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import { Controller } from '../../../common/decorators/core/controller.decorator'; import { Controller } from '../../../common/decorators/core/controller.decorator';
import { RequestMapping } from '../../../common/decorators/http/request-mapping.decorator'; import { RequestMapping } from '../../../common/decorators/http/request-mapping.decorator';
@@ -12,14 +14,18 @@ import { RuntimeException } from '../../errors/exceptions/runtime.exception';
import { NestContainer } from '../../injector/container'; import { NestContainer } from '../../injector/container';
import { InstanceWrapper } from '../../injector/instance-wrapper'; import { InstanceWrapper } from '../../injector/instance-wrapper';
import { Module } from '../../injector/module'; import { Module } from '../../injector/module';
import { GraphInspector } from '../../inspector/graph-inspector';
import { MiddlewareBuilder } from '../../middleware/builder'; import { MiddlewareBuilder } from '../../middleware/builder';
import { MiddlewareContainer } from '../../middleware/container'; import { MiddlewareContainer } from '../../middleware/container';
import { MiddlewareModule } from '../../middleware/middleware-module'; import { MiddlewareModule } from '../../middleware/middleware-module';
import { RouterExceptionFilters } from '../../router/router-exception-filters'; import { RouterExceptionFilters } from '../../router/router-exception-filters';
import { NoopHttpAdapter } from '../utils/noop-adapter.spec'; import { NoopHttpAdapter } from '../utils/noop-adapter.spec';
chai.use(chaiAsPromised);
describe('MiddlewareModule', () => { describe('MiddlewareModule', () => {
let middlewareModule: MiddlewareModule; let middlewareModule: MiddlewareModule;
let graphInspector: GraphInspector;
@Controller('test') @Controller('test')
class BasicController {} class BasicController {}
@@ -39,15 +45,18 @@ describe('MiddlewareModule', () => {
} }
beforeEach(() => { beforeEach(() => {
const container = new NestContainer();
const appConfig = new ApplicationConfig(); const appConfig = new ApplicationConfig();
graphInspector = new GraphInspector(container);
middlewareModule = new MiddlewareModule(new RoutePathFactory(appConfig)); middlewareModule = new MiddlewareModule(new RoutePathFactory(appConfig));
(middlewareModule as any).routerExceptionFilter = middlewareModule['routerExceptionFilter'] = new RouterExceptionFilters(
new RouterExceptionFilters( container,
new NestContainer(), appConfig,
appConfig, new NoopHttpAdapter({}),
new NoopHttpAdapter({}), );
); middlewareModule['config'] = appConfig;
(middlewareModule as any).config = appConfig; middlewareModule['graphInspector'] = graphInspector;
}); });
describe('loadConfiguration', () => { describe('loadConfiguration', () => {
@@ -195,5 +204,68 @@ describe('MiddlewareModule', () => {
); );
expect(createMiddlewareFactoryStub.calledOnce).to.be.true; expect(createMiddlewareFactoryStub.calledOnce).to.be.true;
}); });
it('should insert the expected middleware definition', async () => {
const route = 'testPath';
const configuration = {
middleware: [TestMiddleware],
forRoutes: ['test', BasicController, BaseController],
};
const instance = new TestMiddleware();
const instanceWrapper = new InstanceWrapper({
metatype: TestMiddleware,
instance,
name: TestMiddleware.name,
});
const createMiddlewareFactoryStub = sinon
.stub()
.callsFake(() => () => null);
const app = {
createMiddlewareFactory: createMiddlewareFactoryStub,
};
const stubContainer = new NestContainer();
stubContainer
.getModules()
.set('Test', new Module(TestModule, stubContainer));
const container = new MiddlewareContainer(stubContainer);
const moduleKey = 'Test';
container.insertConfig([configuration], moduleKey);
container
.getMiddlewareCollection(moduleKey)
.set(TestMiddleware, instanceWrapper);
sinon
.stub(stubContainer, 'getModuleByKey')
.callsFake(() => new Module(class {}, stubContainer));
middlewareModule['container'] = stubContainer;
const insertEntrypointDefinitionSpy = sinon.spy(
graphInspector,
'insertEntrypointDefinition',
);
await middlewareModule.registerRouteMiddleware(
container,
{ path: route, method: RequestMethod.ALL },
configuration,
moduleKey,
app,
);
expect(createMiddlewareFactoryStub.calledOnce).to.be.true;
expect(
insertEntrypointDefinitionSpy.calledWith({
type: 'middleware',
methodName: 'use',
className: instanceWrapper.name,
classNodeId: instanceWrapper.id,
metadata: {
path: route,
requestMethod: 'ALL',
version: undefined,
},
}),
).to.be.true;
});
}); });
}); });

View File

@@ -1,8 +1,9 @@
import { InjectionToken, Scope } from '@nestjs/common';
import { expect } from 'chai'; import { expect } from 'chai';
import { InjectionToken, Logger, Scope } from '@nestjs/common';
import { ContextIdFactory } from '../helpers/context-id-factory'; import { ContextIdFactory } from '../helpers/context-id-factory';
import { InstanceLoader } from '../injector/instance-loader';
import { NestContainer } from '../injector/container'; import { NestContainer } from '../injector/container';
import { InstanceLoader } from '../injector/instance-loader';
import { GraphInspector } from '../inspector/graph-inspector';
import { NestApplicationContext } from '../nest-application-context'; import { NestApplicationContext } from '../nest-application-context';
describe('NestApplicationContext', () => { describe('NestApplicationContext', () => {
@@ -13,7 +14,10 @@ describe('NestApplicationContext', () => {
scope: Scope, scope: Scope,
): Promise<NestApplicationContext> { ): Promise<NestApplicationContext> {
const nestContainer = new NestContainer(); const nestContainer = new NestContainer();
const instanceLoader = new InstanceLoader(nestContainer); const instanceLoader = new InstanceLoader(
nestContainer,
new GraphInspector(nestContainer),
);
const module = await nestContainer.addModule(class T {}, []); const module = await nestContainer.addModule(class T {}, []);
nestContainer.addProvider( nestContainer.addProvider(
@@ -32,6 +36,7 @@ describe('NestApplicationContext', () => {
scope, scope,
}, },
module.token, module.token,
'interceptor',
); );
const modules = nestContainer.getModules(); const modules = nestContainer.getModules();

View File

@@ -1,6 +1,7 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { ApplicationConfig } from '../application-config'; import { ApplicationConfig } from '../application-config';
import { NestContainer } from '../injector/container'; import { NestContainer } from '../injector/container';
import { GraphInspector } from '../inspector/graph-inspector';
import { NestApplication } from '../nest-application'; import { NestApplication } from '../nest-application';
import { NoopHttpAdapter } from './utils/noop-adapter.spec'; import { NoopHttpAdapter } from './utils/noop-adapter.spec';
@@ -18,6 +19,7 @@ describe('NestApplication', () => {
container, container,
new NoopHttpAdapter({}), new NoopHttpAdapter({}),
applicationConfig, applicationConfig,
new GraphInspector(container),
{}, {},
); );
instance.useGlobalInterceptors(new Interceptor()); instance.useGlobalInterceptors(new Interceptor());
@@ -36,6 +38,7 @@ describe('NestApplication', () => {
container, container,
new NoopHttpAdapter({}), new NoopHttpAdapter({}),
applicationConfig, applicationConfig,
new GraphInspector(container),
{}, {},
); );
instance.useGlobalInterceptors(new Interceptor()); instance.useGlobalInterceptors(new Interceptor());

View File

@@ -0,0 +1,218 @@
import { expect } from 'chai';
import { Controller } from '../../../common/decorators/core/controller.decorator';
import {
All,
Get,
Post,
} from '../../../common/decorators/http/request-mapping.decorator';
import { RequestMethod } from '../../../common/enums/request-method.enum';
import { MetadataScanner } from '../../metadata-scanner';
import { PathsExplorer } from '../../router/paths-explorer';
describe('PathsExplorer', () => {
@Controller('global')
class TestRoute {
@Get('test')
public getTest() {}
@Post('test')
public postTest() {}
@All('another-test')
public anotherTest() {}
@Get(['foo', 'bar'])
public getTestUsingArray() {}
}
@Controller(['global', 'global-alias'])
class TestRouteAlias {
@Get('test')
public getTest() {}
@Post('test')
public postTest() {}
@All('another-test')
public anotherTest() {}
@Get(['foo', 'bar'])
public getTestUsingArray() {}
}
let pathsExplorer: PathsExplorer;
beforeEach(() => {
pathsExplorer = new PathsExplorer(new MetadataScanner());
});
describe('scanForPaths', () => {
it('should method return expected list of route paths', () => {
const paths = pathsExplorer.scanForPaths(new TestRoute());
expect(paths).to.have.length(4);
expect(paths[0].path).to.eql(['/test']);
expect(paths[1].path).to.eql(['/test']);
expect(paths[2].path).to.eql(['/another-test']);
expect(paths[3].path).to.eql(['/foo', '/bar']);
expect(paths[0].requestMethod).to.eql(RequestMethod.GET);
expect(paths[1].requestMethod).to.eql(RequestMethod.POST);
expect(paths[2].requestMethod).to.eql(RequestMethod.ALL);
expect(paths[3].requestMethod).to.eql(RequestMethod.GET);
});
it('should method return expected list of route paths alias', () => {
const paths = pathsExplorer.scanForPaths(new TestRouteAlias());
expect(paths).to.have.length(4);
expect(paths[0].path).to.eql(['/test']);
expect(paths[1].path).to.eql(['/test']);
expect(paths[2].path).to.eql(['/another-test']);
expect(paths[3].path).to.eql(['/foo', '/bar']);
expect(paths[0].requestMethod).to.eql(RequestMethod.GET);
expect(paths[1].requestMethod).to.eql(RequestMethod.POST);
expect(paths[2].requestMethod).to.eql(RequestMethod.ALL);
expect(paths[3].requestMethod).to.eql(RequestMethod.GET);
});
});
describe('exploreMethodMetadata', () => {
it('should method return expected object which represent single route', () => {
const instance = new TestRoute();
const instanceProto = Object.getPrototypeOf(instance);
const route = pathsExplorer.exploreMethodMetadata(
instance,
instanceProto,
'getTest',
);
expect(route.path).to.eql(['/test']);
expect(route.requestMethod).to.eql(RequestMethod.GET);
expect(route.targetCallback).to.eq(instance.getTest);
});
it('should method return expected object which represent single route with alias', () => {
const instance = new TestRouteAlias();
const instanceProto = Object.getPrototypeOf(instance);
const route = pathsExplorer.exploreMethodMetadata(
instance,
instanceProto,
'getTest',
);
expect(route.path).to.eql(['/test']);
expect(route.requestMethod).to.eql(RequestMethod.GET);
expect(route.targetCallback).to.eq(instance.getTest);
});
it('should method return expected object which represent multiple routes', () => {
const instance = new TestRoute();
const instanceProto = Object.getPrototypeOf(instance);
const route = pathsExplorer.exploreMethodMetadata(
instance,
instanceProto,
'getTestUsingArray',
);
expect(route.path).to.eql(['/foo', '/bar']);
expect(route.requestMethod).to.eql(RequestMethod.GET);
expect(route.targetCallback).to.eq(instance.getTestUsingArray);
});
it('should method return expected object which represent multiple routes with alias', () => {
const instance = new TestRouteAlias();
const instanceProto = Object.getPrototypeOf(instance);
const route = pathsExplorer.exploreMethodMetadata(
instance,
instanceProto,
'getTestUsingArray',
);
expect(route.path).to.eql(['/foo', '/bar']);
expect(route.requestMethod).to.eql(RequestMethod.GET);
expect(route.targetCallback).to.eq(instance.getTestUsingArray);
});
describe('when new implementation is injected into router', () => {
it('should method return changed impl of single route', () => {
const instance = new TestRoute();
const instanceProto = Object.getPrototypeOf(instance);
const newImpl = function () {};
instance.getTest = newImpl;
const route = pathsExplorer.exploreMethodMetadata(
instance,
instanceProto,
'getTest',
);
expect(route.targetCallback).to.eq(newImpl);
expect(route.path).to.eql(['/test']);
expect(route.requestMethod).to.eql(RequestMethod.GET);
});
it('should method return changed impl of single route which alias applied', () => {
const instance = new TestRouteAlias();
const instanceProto = Object.getPrototypeOf(instance);
const newImpl = function () {};
instance.getTest = newImpl;
const route = pathsExplorer.exploreMethodMetadata(
instance,
instanceProto,
'getTest',
);
expect(route.targetCallback).to.eq(newImpl);
expect(route.path).to.eql(['/test']);
expect(route.requestMethod).to.eql(RequestMethod.GET);
});
it('should method return changed impl of multiple routes', () => {
const instance = new TestRoute();
const instanceProto = Object.getPrototypeOf(instance);
const newImpl = function () {};
instance.getTestUsingArray = newImpl;
const route = pathsExplorer.exploreMethodMetadata(
instance,
instanceProto,
'getTestUsingArray',
);
expect(route.targetCallback).to.eq(newImpl);
expect(route.path).to.eql(['/foo', '/bar']);
expect(route.requestMethod).to.eql(RequestMethod.GET);
});
it('should method return changed impl of multiple routes which alias applied', () => {
const instance = new TestRouteAlias();
const instanceProto = Object.getPrototypeOf(instance);
const newImpl = function () {};
instance.getTestUsingArray = newImpl;
const route = pathsExplorer.exploreMethodMetadata(
instance,
instanceProto,
'getTestUsingArray',
);
expect(route.targetCallback).to.eq(newImpl);
expect(route.path).to.eql(['/foo', '/bar']);
expect(route.requestMethod).to.eql(RequestMethod.GET);
});
});
});
});

View File

@@ -10,16 +10,17 @@ import { RequestMethod } from '../../../common/enums/request-method.enum';
import { VersioningType } from '../../../common/enums/version-type.enum'; import { VersioningType } from '../../../common/enums/version-type.enum';
import { Injector } from '../../../core/injector/injector'; import { Injector } from '../../../core/injector/injector';
import { ApplicationConfig } from '../../application-config'; import { ApplicationConfig } from '../../application-config';
import { UnknownRequestMappingException } from '../../errors/exceptions/unknown-request-mapping.exception';
import { ExecutionContextHost } from '../../helpers/execution-context-host'; import { ExecutionContextHost } from '../../helpers/execution-context-host';
import { NestContainer } from '../../injector/container'; import { NestContainer } from '../../injector/container';
import { InstanceWrapper } from '../../injector/instance-wrapper'; import { InstanceWrapper } from '../../injector/instance-wrapper';
import { GraphInspector } from '../../inspector/graph-inspector';
import { MetadataScanner } from '../../metadata-scanner'; import { MetadataScanner } from '../../metadata-scanner';
import { RoutePathMetadata } from '../../router/interfaces/route-path-metadata.interface'; import { RoutePathMetadata } from '../../router/interfaces/route-path-metadata.interface';
import { RoutePathFactory } from '../../router/route-path-factory'; import { RoutePathFactory } from '../../router/route-path-factory';
import { RouterExceptionFilters } from '../../router/router-exception-filters'; import { RouterExceptionFilters } from '../../router/router-exception-filters';
import { RouterExplorer } from '../../router/router-explorer'; import { RouterExplorer } from '../../router/router-explorer';
import { NoopHttpAdapter } from '../utils/noop-adapter.spec'; import { NoopHttpAdapter } from '../utils/noop-adapter.spec';
import { UnknownRequestMappingException } from '../../errors/exceptions/unknown-request-mapping.exception';
describe('RouterExplorer', () => { describe('RouterExplorer', () => {
@Controller('global') @Controller('global')
@@ -59,6 +60,7 @@ describe('RouterExplorer', () => {
let exceptionsFilter: RouterExceptionFilters; let exceptionsFilter: RouterExceptionFilters;
let applicationConfig: ApplicationConfig; let applicationConfig: ApplicationConfig;
let routePathFactory: RoutePathFactory; let routePathFactory: RoutePathFactory;
let graphInspector: GraphInspector;
beforeEach(() => { beforeEach(() => {
const container = new NestContainer(); const container = new NestContainer();
@@ -66,6 +68,7 @@ describe('RouterExplorer', () => {
applicationConfig = new ApplicationConfig(); applicationConfig = new ApplicationConfig();
injector = new Injector(); injector = new Injector();
routePathFactory = new RoutePathFactory(applicationConfig); routePathFactory = new RoutePathFactory(applicationConfig);
graphInspector = new GraphInspector(container);
exceptionsFilter = new RouterExceptionFilters( exceptionsFilter = new RouterExceptionFilters(
container, container,
applicationConfig, applicationConfig,
@@ -79,179 +82,10 @@ describe('RouterExplorer', () => {
exceptionsFilter, exceptionsFilter,
applicationConfig, applicationConfig,
routePathFactory, routePathFactory,
graphInspector,
); );
}); });
describe('scanForPaths', () => {
it('should method return expected list of route paths', () => {
const paths = routerBuilder.scanForPaths(new TestRoute());
expect(paths).to.have.length(4);
expect(paths[0].path).to.eql(['/test']);
expect(paths[1].path).to.eql(['/test']);
expect(paths[2].path).to.eql(['/another-test']);
expect(paths[3].path).to.eql(['/foo', '/bar']);
expect(paths[0].requestMethod).to.eql(RequestMethod.GET);
expect(paths[1].requestMethod).to.eql(RequestMethod.POST);
expect(paths[2].requestMethod).to.eql(RequestMethod.ALL);
expect(paths[3].requestMethod).to.eql(RequestMethod.GET);
});
it('should method return expected list of route paths alias', () => {
const paths = routerBuilder.scanForPaths(new TestRouteAlias());
expect(paths).to.have.length(4);
expect(paths[0].path).to.eql(['/test']);
expect(paths[1].path).to.eql(['/test']);
expect(paths[2].path).to.eql(['/another-test']);
expect(paths[3].path).to.eql(['/foo', '/bar']);
expect(paths[0].requestMethod).to.eql(RequestMethod.GET);
expect(paths[1].requestMethod).to.eql(RequestMethod.POST);
expect(paths[2].requestMethod).to.eql(RequestMethod.ALL);
expect(paths[3].requestMethod).to.eql(RequestMethod.GET);
});
});
describe('exploreMethodMetadata', () => {
it('should method return expected object which represent single route', () => {
const instance = new TestRoute();
const instanceProto = Object.getPrototypeOf(instance);
const route = routerBuilder.exploreMethodMetadata(
instance,
instanceProto,
'getTest',
);
expect(route.path).to.eql(['/test']);
expect(route.requestMethod).to.eql(RequestMethod.GET);
expect(route.targetCallback).to.eq(instance.getTest);
});
it('should method return expected object which represent single route with alias', () => {
const instance = new TestRouteAlias();
const instanceProto = Object.getPrototypeOf(instance);
const route = routerBuilder.exploreMethodMetadata(
instance,
instanceProto,
'getTest',
);
expect(route.path).to.eql(['/test']);
expect(route.requestMethod).to.eql(RequestMethod.GET);
expect(route.targetCallback).to.eq(instance.getTest);
});
it('should method return expected object which represent multiple routes', () => {
const instance = new TestRoute();
const instanceProto = Object.getPrototypeOf(instance);
const route = routerBuilder.exploreMethodMetadata(
instance,
instanceProto,
'getTestUsingArray',
);
expect(route.path).to.eql(['/foo', '/bar']);
expect(route.requestMethod).to.eql(RequestMethod.GET);
expect(route.targetCallback).to.eq(instance.getTestUsingArray);
});
it('should method return expected object which represent multiple routes with alias', () => {
const instance = new TestRouteAlias();
const instanceProto = Object.getPrototypeOf(instance);
const route = routerBuilder.exploreMethodMetadata(
instance,
instanceProto,
'getTestUsingArray',
);
expect(route.path).to.eql(['/foo', '/bar']);
expect(route.requestMethod).to.eql(RequestMethod.GET);
expect(route.targetCallback).to.eq(instance.getTestUsingArray);
});
describe('when new implementation is injected into router', () => {
it('should method return changed impl of single route', () => {
const instance = new TestRoute();
const instanceProto = Object.getPrototypeOf(instance);
const newImpl = function () {};
instance.getTest = newImpl;
const route = routerBuilder.exploreMethodMetadata(
instance,
instanceProto,
'getTest',
);
expect(route.targetCallback).to.eq(newImpl);
expect(route.path).to.eql(['/test']);
expect(route.requestMethod).to.eql(RequestMethod.GET);
});
it('should method return changed impl of single route which alias applied', () => {
const instance = new TestRouteAlias();
const instanceProto = Object.getPrototypeOf(instance);
const newImpl = function () {};
instance.getTest = newImpl;
const route = routerBuilder.exploreMethodMetadata(
instance,
instanceProto,
'getTest',
);
expect(route.targetCallback).to.eq(newImpl);
expect(route.path).to.eql(['/test']);
expect(route.requestMethod).to.eql(RequestMethod.GET);
});
it('should method return changed impl of multiple routes', () => {
const instance = new TestRoute();
const instanceProto = Object.getPrototypeOf(instance);
const newImpl = function () {};
instance.getTestUsingArray = newImpl;
const route = routerBuilder.exploreMethodMetadata(
instance,
instanceProto,
'getTestUsingArray',
);
expect(route.targetCallback).to.eq(newImpl);
expect(route.path).to.eql(['/foo', '/bar']);
expect(route.requestMethod).to.eql(RequestMethod.GET);
});
it('should method return changed impl of multiple routes which alias applied', () => {
const instance = new TestRouteAlias();
const instanceProto = Object.getPrototypeOf(instance);
const newImpl = function () {};
instance.getTestUsingArray = newImpl;
const route = routerBuilder.exploreMethodMetadata(
instance,
instanceProto,
'getTestUsingArray',
);
expect(route.targetCallback).to.eq(newImpl);
expect(route.path).to.eql(['/foo', '/bar']);
expect(route.requestMethod).to.eql(RequestMethod.GET);
});
});
});
describe('applyPathsToRouterProxy', () => { describe('applyPathsToRouterProxy', () => {
it('should method return expected object which represent single route', () => { it('should method return expected object which represent single route', () => {
const bindStub = sinon.stub( const bindStub = sinon.stub(

View File

@@ -13,6 +13,8 @@ import { ApplicationConfig } from '../../application-config';
import { NestContainer } from '../../injector'; import { NestContainer } from '../../injector';
import { Injector } from '../../injector/injector'; import { Injector } from '../../injector/injector';
import { InstanceWrapper } from '../../injector/instance-wrapper'; import { InstanceWrapper } from '../../injector/instance-wrapper';
import { GraphInspector } from '../../inspector/graph-inspector';
import { SerializedGraph } from '../../inspector/serialized-graph';
import { RoutesResolver } from '../../router/routes-resolver'; import { RoutesResolver } from '../../router/routes-resolver';
import { NoopHttpAdapter } from '../utils/noop-adapter.spec'; import { NoopHttpAdapter } from '../utils/noop-adapter.spec';
@@ -65,6 +67,7 @@ describe('RoutesResolver', () => {
getModules: () => modules, getModules: () => modules,
getModuleByKey: (key: string) => modules.get(key), getModuleByKey: (key: string) => modules.get(key),
getHttpAdapterRef: () => applicationRef, getHttpAdapterRef: () => applicationRef,
serializedGraph: new SerializedGraph(),
} as any; } as any;
router = { router = {
get() {}, get() {},
@@ -77,6 +80,7 @@ describe('RoutesResolver', () => {
container, container,
new ApplicationConfig(), new ApplicationConfig(),
new Injector(), new Injector(),
new GraphInspector(container),
); );
}); });
@@ -175,6 +179,7 @@ describe('RoutesResolver', () => {
container, container,
applicationConfig, applicationConfig,
new Injector(), new Injector(),
new GraphInspector(container),
); );
const routes = new Map(); const routes = new Map();

View File

@@ -1,4 +1,4 @@
import { Catch, Injectable, Logger } from '@nestjs/common'; import { Catch, Injectable } from '@nestjs/common';
import { expect } from 'chai'; import { expect } from 'chai';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import { GUARDS_METADATA } from '../../common/constants'; import { GUARDS_METADATA } from '../../common/constants';
@@ -8,13 +8,15 @@ import { Module } from '../../common/decorators/modules/module.decorator';
import { Scope } from '../../common/interfaces'; import { Scope } from '../../common/interfaces';
import { ApplicationConfig } from '../application-config'; import { ApplicationConfig } from '../application-config';
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '../constants'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '../constants';
import { InvalidModuleException } from '../errors/exceptions/invalid-module.exception';
import { InvalidClassModuleException } from '../errors/exceptions/invalid-class-module.exception'; import { InvalidClassModuleException } from '../errors/exceptions/invalid-class-module.exception';
import { InvalidModuleException } from '../errors/exceptions/invalid-module.exception';
import { UndefinedModuleException } from '../errors/exceptions/undefined-module.exception'; import { UndefinedModuleException } from '../errors/exceptions/undefined-module.exception';
import { NestContainer } from '../injector/container'; import { NestContainer } from '../injector/container';
import { InstanceWrapper } from '../injector/instance-wrapper'; import { InstanceWrapper } from '../injector/instance-wrapper';
import { GraphInspector } from '../inspector/graph-inspector';
import { MetadataScanner } from '../metadata-scanner'; import { MetadataScanner } from '../metadata-scanner';
import { DependenciesScanner } from '../scanner'; import { DependenciesScanner } from '../scanner';
import Sinon = require('sinon');
describe('DependenciesScanner', () => { describe('DependenciesScanner', () => {
class Guard {} class Guard {}
@@ -55,14 +57,17 @@ describe('DependenciesScanner', () => {
let scanner: DependenciesScanner; let scanner: DependenciesScanner;
let mockContainer: sinon.SinonMock; let mockContainer: sinon.SinonMock;
let container: NestContainer; let container: NestContainer;
let graphInspector: GraphInspector;
beforeEach(() => { beforeEach(() => {
container = new NestContainer(); container = new NestContainer();
mockContainer = sinon.mock(container); mockContainer = sinon.mock(container);
graphInspector = new GraphInspector(container);
scanner = new DependenciesScanner( scanner = new DependenciesScanner(
container, container,
new MetadataScanner(), new MetadataScanner(),
graphInspector,
new ApplicationConfig(), new ApplicationConfig(),
); );
sinon.stub(scanner, 'registerCoreModule').callsFake(async () => {}); sinon.stub(scanner, 'registerCoreModule').callsFake(async () => {});
@@ -75,7 +80,7 @@ describe('DependenciesScanner', () => {
it('should "insertModule" call twice (2 modules) container method "addModule"', async () => { it('should "insertModule" call twice (2 modules) container method "addModule"', async () => {
const expectation = mockContainer.expects('addModule').twice(); const expectation = mockContainer.expects('addModule').twice();
await scanner.scan(TestModule as any); await scanner.scan(TestModule);
expectation.verify(); expectation.verify();
}); });
@@ -134,34 +139,115 @@ describe('DependenciesScanner', () => {
}); });
describe('insertInjectable', () => { describe('insertInjectable', () => {
it('should call "addInjectable"', () => { class InjectableCls {}
const addInjectable = sinon class HostCls {}
.stub((scanner as any).container, 'addInjectable')
.callsFake(() => undefined);
const comp = {};
const token = 'token';
scanner.insertInjectable(comp as any, token, null); const instanceWrapper = { id: 'random_id' };
expect(addInjectable.calledWith(comp, token)).to.be.true; const token = 'token';
const methodKey = 'methodKey';
let addInjectableStub: Sinon.SinonStub;
let insertEnhancerMetadataCacheStub: Sinon.SinonStub;
beforeEach(() => {
addInjectableStub = sinon
.stub((scanner as any).container, 'addInjectable')
.callsFake(() => instanceWrapper);
insertEnhancerMetadataCacheStub = sinon
.stub(graphInspector, 'insertEnhancerMetadataCache')
.callsFake(() => undefined);
});
describe('when injectable is of type function', () => {
const subtype = 'filter';
beforeEach(() => {
scanner.insertInjectable(
InjectableCls,
token,
HostCls,
subtype,
methodKey,
);
});
it('should call "addInjectable"', () => {
expect(addInjectableStub.calledWith(InjectableCls, token)).to.be.true;
});
it('should call "insertEnhancerMetadataCache"', () => {
expect(
insertEnhancerMetadataCacheStub.calledWith({
moduleToken: token,
classRef: HostCls,
enhancerInstanceWrapper: instanceWrapper,
targetNodeId: instanceWrapper.id,
methodKey,
subtype,
}),
).to.be.true;
});
});
describe('when injectable is not of type function', () => {
const injectableRef = new InjectableCls();
const subtype = 'interceptor';
beforeEach(() => {
scanner.insertInjectable(
injectableRef,
token,
HostCls,
subtype,
methodKey,
);
});
it('should not call "addInjectable"', () => {
expect(addInjectableStub.notCalled).to.be.true;
});
it('should call "insertEnhancerMetadataCache"', () => {
expect(
insertEnhancerMetadataCacheStub.calledWith({
moduleToken: token,
classRef: HostCls,
enhancerRef: injectableRef,
methodKey,
subtype,
}),
).to.be.true;
});
}); });
}); });
class CompMethod { class CompMethod {
@UseGuards(Guard) @UseGuards(Guard)
public method() {} public method() {}
@UseGuards(Guard, Guard)
public method2() {}
} }
describe('reflectKeyMetadata', () => { describe('reflectKeyMetadata', () => {
it('should return undefined', () => { it('should return undefined', () => {
const result = scanner.reflectKeyMetadata(TestComponent, 'key', 'method'); const result = scanner.reflectKeyMetadata(TestComponent, 'key', 'method');
expect(result).to.be.undefined; expect(result).to.be.undefined;
}); });
it('should return array', () => { it('should return an array that consists of 1 element', () => {
const methodKey = 'method';
const result = scanner.reflectKeyMetadata( const result = scanner.reflectKeyMetadata(
CompMethod, CompMethod,
GUARDS_METADATA, GUARDS_METADATA,
'method', methodKey,
); );
expect(result).to.be.eql([Guard]); expect(result).to.be.deep.equal({ methodKey, metadata: [Guard] });
});
it('should return an array that consists of 2 elements', () => {
const methodKey = 'method2';
const result = scanner.reflectKeyMetadata(
CompMethod,
GUARDS_METADATA,
methodKey,
);
expect(result).to.be.deep.equal({ methodKey, metadata: [Guard, Guard] });
}); });
}); });
@@ -312,18 +398,30 @@ describe('DependenciesScanner', () => {
(scanner as any).applicationProvidersApplyMap = [provider]; (scanner as any).applicationProvidersApplyMap = [provider];
const expectedInstance = {}; const expectedInstance = {};
const instanceWrapper = {
instance: expectedInstance,
} as unknown as InstanceWrapper;
mockContainer.expects('getModules').callsFake(() => ({ mockContainer.expects('getModules').callsFake(() => ({
get: () => ({ get: () => ({
providers: { get: () => ({ instance: expectedInstance }) }, providers: { get: () => instanceWrapper },
}), }),
})); }));
const applySpy = sinon.spy(); const applySpy = sinon.spy();
sinon.stub(scanner, 'getApplyProvidersMap').callsFake(() => ({ sinon.stub(scanner, 'getApplyProvidersMap').callsFake(() => ({
[provider.type]: applySpy, [provider.type]: applySpy,
})); }));
const insertAttachedEnhancerStub = sinon.stub(
graphInspector,
'insertAttachedEnhancer',
);
scanner.applyApplicationProviders(); scanner.applyApplicationProviders();
expect(applySpy.called).to.be.true; expect(applySpy.called).to.be.true;
expect(applySpy.calledWith(expectedInstance)).to.be.true; expect(applySpy.calledWith(expectedInstance)).to.be.true;
expect(insertAttachedEnhancerStub.calledWith(instanceWrapper)).to.be.true;
}); });
it('should apply each globally scoped provider', () => { it('should apply each globally scoped provider', () => {
const provider = { const provider = {
@@ -340,13 +438,23 @@ describe('DependenciesScanner', () => {
injectables: { get: () => expectedInstanceWrapper }, injectables: { get: () => expectedInstanceWrapper },
}), }),
})); }));
const applySpy = sinon.spy(); const applySpy = sinon.spy();
sinon.stub(scanner, 'getApplyRequestProvidersMap').callsFake(() => ({ sinon.stub(scanner, 'getApplyRequestProvidersMap').callsFake(() => ({
[provider.type]: applySpy, [provider.type]: applySpy,
})); }));
const insertAttachedEnhancerStub = sinon.stub(
graphInspector,
'insertAttachedEnhancer',
);
scanner.applyApplicationProviders(); scanner.applyApplicationProviders();
expect(applySpy.called).to.be.true; expect(applySpy.called).to.be.true;
expect(applySpy.calledWith(expectedInstanceWrapper)).to.be.true; expect(applySpy.calledWith(expectedInstanceWrapper)).to.be.true;
expect(insertAttachedEnhancerStub.calledWith(expectedInstanceWrapper)).to
.be.true;
}); });
}); });

View File

@@ -0,0 +1,9 @@
import { Transport } from '../enums';
import { PatternMetadata } from './pattern-metadata.interface';
export type MicroserviceEntrypointMetadata = {
transportId: keyof typeof Transport | symbol;
patterns: PatternMetadata[];
isEventHandler: boolean;
extras?: Record<string, any>;
};

View File

@@ -11,6 +11,7 @@ import {
InstanceWrapper, InstanceWrapper,
} from '@nestjs/core/injector/instance-wrapper'; } from '@nestjs/core/injector/instance-wrapper';
import { Module } from '@nestjs/core/injector/module'; import { Module } from '@nestjs/core/injector/module';
import { GraphInspector } from '@nestjs/core/inspector/graph-inspector';
import { MetadataScanner } from '@nestjs/core/metadata-scanner'; import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { REQUEST_CONTEXT_ID } from '@nestjs/core/router/request/request-constants'; import { REQUEST_CONTEXT_ID } from '@nestjs/core/router/request/request-constants';
import { connectable, Observable, Subject } from 'rxjs'; import { connectable, Observable, Subject } from 'rxjs';
@@ -24,13 +25,18 @@ import {
DEFAULT_GRPC_CALLBACK_METADATA, DEFAULT_GRPC_CALLBACK_METADATA,
} from './context/rpc-metadata-constants'; } from './context/rpc-metadata-constants';
import { BaseRpcContext } from './ctx-host/base-rpc.context'; import { BaseRpcContext } from './ctx-host/base-rpc.context';
import { Transport } from './enums';
import { import {
CustomTransportStrategy, CustomTransportStrategy,
MessageHandler, MessageHandler,
PatternMetadata, PatternMetadata,
RequestContext, RequestContext,
} from './interfaces'; } from './interfaces';
import { ListenerMetadataExplorer } from './listener-metadata-explorer'; import { MicroserviceEntrypointMetadata } from './interfaces/microservice-entrypoint-metadata.interface';
import {
EventOrMessageListenerDefinition,
ListenerMetadataExplorer,
} from './listener-metadata-explorer';
import { ServerGrpc } from './server'; import { ServerGrpc } from './server';
import { Server } from './server/server'; import { Server } from './server/server';
@@ -47,6 +53,7 @@ export class ListenersController {
private readonly injector: Injector, private readonly injector: Injector,
private readonly clientFactory: IClientProxyFactory, private readonly clientFactory: IClientProxyFactory,
private readonly exceptionFiltersContext: ExceptionFiltersContext, private readonly exceptionFiltersContext: ExceptionFiltersContext,
private readonly graphInspector: GraphInspector,
) {} ) {}
public registerPatternHandlers( public registerPatternHandlers(
@@ -78,61 +85,91 @@ export class ListenersController {
); );
return acc; return acc;
}, []) }, [])
.forEach( .forEach((definition: EventOrMessageListenerDefinition) => {
({ const {
patterns: [pattern], patterns: [pattern],
targetCallback, targetCallback,
methodKey, methodKey,
extras, extras,
isEventHandler, isEventHandler,
}) => { } = definition;
if (isStatic) {
const proxy = this.contextCreator.create( this.insertEntrypointDefinition(
instance as object, instanceWrapper,
targetCallback, definition,
moduleKey, server.transportId,
methodKey, );
STATIC_CONTEXT,
undefined, if (isStatic) {
defaultCallMetadata, const proxy = this.contextCreator.create(
); instance as object,
if (isEventHandler) { targetCallback,
const eventHandler: MessageHandler = (...args: unknown[]) => {
const originalArgs = args;
const [dataOrContextHost] = originalArgs;
if (dataOrContextHost instanceof RequestContextHost) {
args = args.slice(1, args.length);
}
const originalReturnValue = proxy(...args);
const returnedValueWrapper = eventHandler.next?.(
...(originalArgs as Parameters<MessageHandler>),
);
returnedValueWrapper?.then(returnedValue =>
this.connectIfStream(returnedValue as Observable<unknown>),
);
return originalReturnValue;
};
return server.addHandler(
pattern,
eventHandler,
isEventHandler,
extras,
);
} else {
return server.addHandler(pattern, proxy, isEventHandler, extras);
}
}
const asyncHandler = this.createRequestScopedHandler(
instanceWrapper,
pattern,
moduleRef,
moduleKey, moduleKey,
methodKey, methodKey,
STATIC_CONTEXT,
undefined,
defaultCallMetadata, defaultCallMetadata,
); );
server.addHandler(pattern, asyncHandler, isEventHandler, extras); if (isEventHandler) {
const eventHandler: MessageHandler = (...args: unknown[]) => {
const originalArgs = args;
const [dataOrContextHost] = originalArgs;
if (dataOrContextHost instanceof RequestContextHost) {
args = args.slice(1, args.length);
}
const originalReturnValue = proxy(...args);
const returnedValueWrapper = eventHandler.next?.(
...(originalArgs as Parameters<MessageHandler>),
);
returnedValueWrapper?.then(returnedValue =>
this.connectIfStream(returnedValue as Observable<unknown>),
);
return originalReturnValue;
};
return server.addHandler(
pattern,
eventHandler,
isEventHandler,
extras,
);
} else {
return server.addHandler(pattern, proxy, isEventHandler, extras);
}
}
const asyncHandler = this.createRequestScopedHandler(
instanceWrapper,
pattern,
moduleRef,
moduleKey,
methodKey,
defaultCallMetadata,
);
server.addHandler(pattern, asyncHandler, isEventHandler, extras);
});
}
public insertEntrypointDefinition(
instanceWrapper: InstanceWrapper,
definition: EventOrMessageListenerDefinition,
transportId: Transport | symbol,
) {
this.graphInspector.insertEntrypointDefinition<MicroserviceEntrypointMetadata>(
{
type: 'microservice',
methodName: definition.methodKey,
className: instanceWrapper.metatype?.name,
classNodeId: instanceWrapper.id,
metadata: {
transportId:
typeof transportId === 'number'
? (Transport[transportId] as keyof typeof Transport)
: transportId,
patterns: definition.patterns,
isEventHandler: definition.isEventHandler,
extras: definition.extras,
}, },
); },
);
} }
public assignClientsToProperties(instance: Controller | Injectable) { public assignClientsToProperties(instance: Controller | Injectable) {

View File

@@ -6,6 +6,7 @@ import { GuardsContextCreator } from '@nestjs/core/guards/guards-context-creator
import { NestContainer } from '@nestjs/core/injector/container'; import { NestContainer } from '@nestjs/core/injector/container';
import { Injector } from '@nestjs/core/injector/injector'; import { Injector } from '@nestjs/core/injector/injector';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { GraphInspector } from '@nestjs/core/inspector/graph-inspector';
import { InterceptorsConsumer } from '@nestjs/core/interceptors/interceptors-consumer'; import { InterceptorsConsumer } from '@nestjs/core/interceptors/interceptors-consumer';
import { InterceptorsContextCreator } from '@nestjs/core/interceptors/interceptors-context-creator'; import { InterceptorsContextCreator } from '@nestjs/core/interceptors/interceptors-context-creator';
import { PipesConsumer } from '@nestjs/core/pipes/pipes-consumer'; import { PipesConsumer } from '@nestjs/core/pipes/pipes-consumer';
@@ -23,7 +24,11 @@ export class MicroservicesModule {
private readonly clientsContainer = new ClientsContainer(); private readonly clientsContainer = new ClientsContainer();
private listenersController: ListenersController; private listenersController: ListenersController;
public register(container: NestContainer, config: ApplicationConfig) { public register(
container: NestContainer,
graphInspector: GraphInspector,
config: ApplicationConfig,
) {
const exceptionFiltersContext = new ExceptionFiltersContext( const exceptionFiltersContext = new ExceptionFiltersContext(
container, container,
config, config,
@@ -47,6 +52,7 @@ export class MicroservicesModule {
injector, injector,
ClientProxyFactory, ClientProxyFactory,
exceptionFiltersContext, exceptionFiltersContext,
graphInspector,
); );
} }

View File

@@ -12,6 +12,7 @@ import { ApplicationConfig } from '@nestjs/core/application-config';
import { MESSAGES } from '@nestjs/core/constants'; import { MESSAGES } from '@nestjs/core/constants';
import { optionalRequire } from '@nestjs/core/helpers/optional-require'; import { optionalRequire } from '@nestjs/core/helpers/optional-require';
import { NestContainer } from '@nestjs/core/injector/container'; import { NestContainer } from '@nestjs/core/injector/container';
import { GraphInspector } from '@nestjs/core/inspector/graph-inspector';
import { NestApplicationContext } from '@nestjs/core/nest-application-context'; import { NestApplicationContext } from '@nestjs/core/nest-application-context';
import { Transport } from './enums/transport.enum'; import { Transport } from './enums/transport.enum';
import { CustomTransportStrategy } from './interfaces/custom-transport-strategy.interface'; import { CustomTransportStrategy } from './interfaces/custom-transport-strategy.interface';
@@ -42,11 +43,16 @@ export class NestMicroservice
constructor( constructor(
container: NestContainer, container: NestContainer,
config: NestMicroserviceOptions & MicroserviceOptions = {}, config: NestMicroserviceOptions & MicroserviceOptions = {},
private readonly graphInspector: GraphInspector,
private readonly applicationConfig: ApplicationConfig, private readonly applicationConfig: ApplicationConfig,
) { ) {
super(container); super(container);
this.microservicesModule.register(container, this.applicationConfig); this.microservicesModule.register(
container,
this.graphInspector,
this.applicationConfig,
);
this.createServer(config); this.createServer(config);
this.selectContextModule(); this.selectContextModule();
} }
@@ -92,21 +98,45 @@ export class NestMicroservice
public useGlobalFilters(...filters: ExceptionFilter[]): this { public useGlobalFilters(...filters: ExceptionFilter[]): this {
this.applicationConfig.useGlobalFilters(...filters); this.applicationConfig.useGlobalFilters(...filters);
filters.forEach(item =>
this.graphInspector.insertStaticEnhancer({
subtype: 'filter',
ref: item,
}),
);
return this; return this;
} }
public useGlobalPipes(...pipes: PipeTransform<any>[]): this { public useGlobalPipes(...pipes: PipeTransform<any>[]): this {
this.applicationConfig.useGlobalPipes(...pipes); this.applicationConfig.useGlobalPipes(...pipes);
pipes.forEach(item =>
this.graphInspector.insertStaticEnhancer({
subtype: 'pipe',
ref: item,
}),
);
return this; return this;
} }
public useGlobalInterceptors(...interceptors: NestInterceptor[]): this { public useGlobalInterceptors(...interceptors: NestInterceptor[]): this {
this.applicationConfig.useGlobalInterceptors(...interceptors); this.applicationConfig.useGlobalInterceptors(...interceptors);
interceptors.forEach(item =>
this.graphInspector.insertStaticEnhancer({
subtype: 'interceptor',
ref: item,
}),
);
return this; return this;
} }
public useGlobalGuards(...guards: CanActivate[]): this { public useGlobalGuards(...guards: CanActivate[]): this {
this.applicationConfig.useGlobalGuards(...guards); this.applicationConfig.useGlobalGuards(...guards);
guards.forEach(item =>
this.graphInspector.insertStaticEnhancer({
subtype: 'guard',
ref: item,
}),
);
return this; return this;
} }

View File

@@ -6,13 +6,17 @@ import { Injector } from '@nestjs/core/injector/injector';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { expect } from 'chai'; import { expect } from 'chai';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import { GraphInspector } from '../../core/inspector/graph-inspector';
import { MetadataScanner } from '../../core/metadata-scanner'; import { MetadataScanner } from '../../core/metadata-scanner';
import { ClientProxyFactory } from '../client'; import { ClientProxyFactory } from '../client';
import { ClientsContainer } from '../container'; import { ClientsContainer } from '../container';
import { ExceptionFiltersContext } from '../context/exception-filters-context'; import { ExceptionFiltersContext } from '../context/exception-filters-context';
import { RpcContextCreator } from '../context/rpc-context-creator'; import { RpcContextCreator } from '../context/rpc-context-creator';
import { Transport } from '../enums/transport.enum'; import { Transport } from '../enums/transport.enum';
import { ListenerMetadataExplorer } from '../listener-metadata-explorer'; import {
EventOrMessageListenerDefinition,
ListenerMetadataExplorer,
} from '../listener-metadata-explorer';
import { ListenersController } from '../listeners-controller'; import { ListenersController } from '../listeners-controller';
describe('ListenersController', () => { describe('ListenersController', () => {
@@ -28,6 +32,7 @@ describe('ListenersController', () => {
addSpyCustom: sinon.SinonSpy, addSpyCustom: sinon.SinonSpy,
proxySpy: sinon.SinonSpy, proxySpy: sinon.SinonSpy,
container: NestContainer, container: NestContainer,
graphInspector: GraphInspector,
injector: Injector, injector: Injector,
rpcContextCreator: RpcContextCreator, rpcContextCreator: RpcContextCreator,
exceptionFiltersContext: ExceptionFiltersContext; exceptionFiltersContext: ExceptionFiltersContext;
@@ -38,6 +43,7 @@ describe('ListenersController', () => {
}); });
beforeEach(() => { beforeEach(() => {
container = new NestContainer(); container = new NestContainer();
graphInspector = new GraphInspector(container);
injector = new Injector(); injector = new Injector();
exceptionFiltersContext = new ExceptionFiltersContext( exceptionFiltersContext = new ExceptionFiltersContext(
container, container,
@@ -46,6 +52,7 @@ describe('ListenersController', () => {
rpcContextCreator = sinon.createStubInstance(RpcContextCreator) as any; rpcContextCreator = sinon.createStubInstance(RpcContextCreator) as any;
proxySpy = sinon.spy(); proxySpy = sinon.spy();
(rpcContextCreator as any).create.callsFake(() => proxySpy); (rpcContextCreator as any).create.callsFake(() => proxySpy);
instance = new ListenersController( instance = new ListenersController(
new ClientsContainer(), new ClientsContainer(),
rpcContextCreator, rpcContextCreator,
@@ -53,6 +60,7 @@ describe('ListenersController', () => {
injector, injector,
ClientProxyFactory, ClientProxyFactory,
exceptionFiltersContext, exceptionFiltersContext,
graphInspector,
); );
(instance as any).metadataExplorer = metadataExplorer; (instance as any).metadataExplorer = metadataExplorer;
addSpy = sinon.spy(); addSpy = sinon.spy();
@@ -262,6 +270,48 @@ describe('ListenersController', () => {
}); });
}); });
describe('insertEntrypointDefinition', () => {
it('should inspect & insert corresponding entrypoint definitions', () => {
class TestCtrl {}
const instanceWrapper = new InstanceWrapper({
metatype: TestCtrl,
name: TestCtrl.name,
});
const definition: EventOrMessageListenerDefinition = {
patterns: ['findOne'],
methodKey: 'find',
isEventHandler: false,
targetCallback: null,
extras: { qos: 2 },
};
const transportId = Transport.MQTT;
const insertEntrypointDefinitionSpy = sinon.spy(
graphInspector,
'insertEntrypointDefinition',
);
instance.insertEntrypointDefinition(
instanceWrapper,
definition,
transportId,
);
expect(
insertEntrypointDefinitionSpy.calledWith({
type: 'microservice',
methodName: definition.methodKey,
className: 'TestCtrl',
classNodeId: instanceWrapper.id,
metadata: {
transportId: 'MQTT',
patterns: definition.patterns,
isEventHandler: definition.isEventHandler,
extras: definition.extras,
},
}),
).to.be.true;
});
});
describe('assignClientToInstance', () => { describe('assignClientToInstance', () => {
it('should assign client to instance', () => { it('should assign client to instance', () => {
const propertyKey = 'key'; const propertyKey = 'key';

View File

@@ -2,6 +2,7 @@ import { Logger, LoggerService, Module } from '@nestjs/common';
import { ModuleMetadata } from '@nestjs/common/interfaces'; import { ModuleMetadata } from '@nestjs/common/interfaces';
import { ApplicationConfig } from '@nestjs/core/application-config'; import { ApplicationConfig } from '@nestjs/core/application-config';
import { NestContainer } from '@nestjs/core/injector/container'; import { NestContainer } from '@nestjs/core/injector/container';
import { GraphInspector } from '@nestjs/core/inspector/graph-inspector';
import { MetadataScanner } from '@nestjs/core/metadata-scanner'; import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { DependenciesScanner } from '@nestjs/core/scanner'; import { DependenciesScanner } from '@nestjs/core/scanner';
import { import {
@@ -16,9 +17,13 @@ import { TestingModule } from './testing-module';
export class TestingModuleBuilder { export class TestingModuleBuilder {
private readonly applicationConfig = new ApplicationConfig(); private readonly applicationConfig = new ApplicationConfig();
private readonly container = new NestContainer(this.applicationConfig); private readonly container = new NestContainer(this.applicationConfig);
private readonly graphInspector = new GraphInspector(this.container);
private readonly overloadsMap = new Map(); private readonly overloadsMap = new Map();
private readonly instanceLoader = new TestingInstanceLoader(
this.container,
this.graphInspector,
);
private readonly scanner: DependenciesScanner; private readonly scanner: DependenciesScanner;
private readonly instanceLoader = new TestingInstanceLoader(this.container);
private readonly module: any; private readonly module: any;
private testingLogger: LoggerService; private testingLogger: LoggerService;
private mocker?: MockFactory; private mocker?: MockFactory;
@@ -27,6 +32,7 @@ export class TestingModuleBuilder {
this.scanner = new DependenciesScanner( this.scanner = new DependenciesScanner(
this.container, this.container,
metadataScanner, metadataScanner,
this.graphInspector,
this.applicationConfig, this.applicationConfig,
); );
this.module = this.createModule(metadata); this.module = this.createModule(metadata);

View File

@@ -18,8 +18,11 @@ import {
import { ApplicationConfig } from '@nestjs/core/application-config'; import { ApplicationConfig } from '@nestjs/core/application-config';
import { NestContainer } from '@nestjs/core/injector/container'; import { NestContainer } from '@nestjs/core/injector/container';
import { Module } from '@nestjs/core/injector/module'; import { Module } from '@nestjs/core/injector/module';
import { GraphInspector } from '@nestjs/core/inspector/graph-inspector';
export class TestingModule extends NestApplicationContext { export class TestingModule extends NestApplicationContext {
protected readonly graphInspector = new GraphInspector(this.container);
constructor( constructor(
container: NestContainer, container: NestContainer,
scope: Type<any>[], scope: Type<any>[],
@@ -65,6 +68,7 @@ export class TestingModule extends NestApplicationContext {
this.container, this.container,
httpAdapter, httpAdapter,
this.applicationConfig, this.applicationConfig,
this.graphInspector,
appOptions, appOptions,
); );
return this.createAdapterProxy<T>(instance, httpAdapter); return this.createAdapterProxy<T>(instance, httpAdapter);
@@ -82,6 +86,7 @@ export class TestingModule extends NestApplicationContext {
return new NestMicroservice( return new NestMicroservice(
this.container, this.container,
options, options,
this.graphInspector,
this.applicationConfig, this.applicationConfig,
); );
} }

View File

@@ -0,0 +1,4 @@
export type WebsocketEntrypointMetadata = {
port: number;
message: unknown;
};

View File

@@ -6,6 +6,7 @@ import { loadAdapter } from '@nestjs/core/helpers/load-adapter';
import { NestContainer } from '@nestjs/core/injector/container'; import { NestContainer } from '@nestjs/core/injector/container';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { InstanceToken } from '@nestjs/core/injector/module'; import { InstanceToken } from '@nestjs/core/injector/module';
import { GraphInspector } from '@nestjs/core/inspector/graph-inspector';
import { InterceptorsConsumer } from '@nestjs/core/interceptors/interceptors-consumer'; import { InterceptorsConsumer } from '@nestjs/core/interceptors/interceptors-consumer';
import { InterceptorsContextCreator } from '@nestjs/core/interceptors/interceptors-context-creator'; import { InterceptorsContextCreator } from '@nestjs/core/interceptors/interceptors-context-creator';
import { PipesConsumer } from '@nestjs/core/pipes/pipes-consumer'; import { PipesConsumer } from '@nestjs/core/pipes/pipes-consumer';
@@ -30,21 +31,23 @@ export class SocketModule<HttpServer = any> {
public register( public register(
container: NestContainer, container: NestContainer,
config: ApplicationConfig, applicationConfig: ApplicationConfig,
graphInspector: GraphInspector,
httpServer?: HttpServer, httpServer?: HttpServer,
) { ) {
this.applicationConfig = config; this.applicationConfig = applicationConfig;
this.httpServer = httpServer; this.httpServer = httpServer;
const contextCreator = this.getContextCreator(container); const contextCreator = this.getContextCreator(container);
const serverProvider = new SocketServerProvider( const serverProvider = new SocketServerProvider(
this.socketsContainer, this.socketsContainer,
config, applicationConfig,
); );
this.webSocketsController = new WebSocketsController( this.webSocketsController = new WebSocketsController(
serverProvider, serverProvider,
config, applicationConfig,
contextCreator, contextCreator,
graphInspector,
); );
const modules = container.getModules(); const modules = container.getModules();
modules.forEach(({ providers }, moduleName: string) => modules.forEach(({ providers }, moduleName: string) =>
@@ -77,6 +80,7 @@ export class SocketModule<HttpServer = any> {
instance as NestGateway, instance as NestGateway,
metatype, metatype,
moduleName, moduleName,
wrapper.id,
); );
} }

View File

@@ -1,14 +1,19 @@
import { NestContainer } from '@nestjs/core';
import { ApplicationConfig } from '@nestjs/core/application-config'; import { ApplicationConfig } from '@nestjs/core/application-config';
import { expect } from 'chai'; import { expect } from 'chai';
import { fromEvent, lastValueFrom, Observable, of } from 'rxjs'; import { fromEvent, lastValueFrom, Observable, of } from 'rxjs';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import { GraphInspector } from '../../core/inspector/graph-inspector';
import { MetadataScanner } from '../../core/metadata-scanner'; import { MetadataScanner } from '../../core/metadata-scanner';
import { AbstractWsAdapter } from '../adapters/ws-adapter'; import { AbstractWsAdapter } from '../adapters/ws-adapter';
import { PORT_METADATA } from '../constants'; import { PORT_METADATA } from '../constants';
import { WsContextCreator } from '../context/ws-context-creator'; import { WsContextCreator } from '../context/ws-context-creator';
import { WebSocketGateway } from '../decorators/socket-gateway.decorator'; import { WebSocketGateway } from '../decorators/socket-gateway.decorator';
import { InvalidSocketPortException } from '../errors/invalid-socket-port.exception'; import { InvalidSocketPortException } from '../errors/invalid-socket-port.exception';
import { GatewayMetadataExplorer } from '../gateway-metadata-explorer'; import {
GatewayMetadataExplorer,
MessageMappingProperties,
} from '../gateway-metadata-explorer';
import { SocketServerProvider } from '../socket-server-provider'; import { SocketServerProvider } from '../socket-server-provider';
import { WebSocketsController } from '../web-sockets-controller'; import { WebSocketsController } from '../web-sockets-controller';
@@ -29,8 +34,10 @@ class NoopAdapter extends AbstractWsAdapter {
describe('WebSocketsController', () => { describe('WebSocketsController', () => {
let instance: WebSocketsController; let instance: WebSocketsController;
let provider: SocketServerProvider, let provider: SocketServerProvider,
graphInspector: GraphInspector,
config: ApplicationConfig, config: ApplicationConfig,
mockProvider: sinon.SinonMock; mockProvider: sinon.SinonMock;
const messageHandlerCallback = () => Promise.resolve(); const messageHandlerCallback = () => Promise.resolve();
const port = 90, const port = 90,
namespace = '/'; namespace = '/';
@@ -40,6 +47,7 @@ describe('WebSocketsController', () => {
beforeEach(() => { beforeEach(() => {
config = new ApplicationConfig(new NoopAdapter()); config = new ApplicationConfig(new NoopAdapter());
provider = new SocketServerProvider(null, config); provider = new SocketServerProvider(null, config);
graphInspector = new GraphInspector(new NestContainer());
mockProvider = sinon.mock(provider); mockProvider = sinon.mock(provider);
const contextCreator = sinon.createStubInstance(WsContextCreator); const contextCreator = sinon.createStubInstance(WsContextCreator);
@@ -48,6 +56,7 @@ describe('WebSocketsController', () => {
provider, provider,
config, config,
contextCreator as any, contextCreator as any,
graphInspector,
); );
}); });
describe('connectGatewayToServer', () => { describe('connectGatewayToServer', () => {
@@ -69,20 +78,37 @@ describe('WebSocketsController', () => {
instance.connectGatewayToServer( instance.connectGatewayToServer(
new InvalidGateway(), new InvalidGateway(),
InvalidGateway, InvalidGateway,
'', 'moduleKey',
'instanceWrapperId',
), ),
).throws(InvalidSocketPortException); ).throws(InvalidSocketPortException);
}); });
it('should call "subscribeToServerEvents" with default values when metadata is empty', () => { it('should call "subscribeToServerEvents" with default values when metadata is empty', () => {
const gateway = new DefaultGateway(); const gateway = new DefaultGateway();
instance.connectGatewayToServer(gateway, DefaultGateway, ''); instance.connectGatewayToServer(
expect(subscribeToServerEvents.calledWith(gateway, {}, 0, '')).to.be.true; gateway,
DefaultGateway,
'moduleKey',
'instanceWrapperId',
);
expect(subscribeToServerEvents.calledWith(gateway, {}, 0, 'moduleKey')).to
.be.true;
}); });
it('should call "subscribeToServerEvents" when metadata is valid', () => { it('should call "subscribeToServerEvents" when metadata is valid', () => {
const gateway = new Test(); const gateway = new Test();
instance.connectGatewayToServer(gateway, Test, ''); instance.connectGatewayToServer(
gateway,
Test,
'moduleKey',
'instanceWrapperId',
);
expect( expect(
subscribeToServerEvents.calledWith(gateway, { namespace }, port, ''), subscribeToServerEvents.calledWith(
gateway,
{ namespace },
port,
'moduleKey',
),
).to.be.true; ).to.be.true;
}); });
}); });
@@ -116,16 +142,28 @@ describe('WebSocketsController', () => {
assignServerToProperties = sinon.spy(); assignServerToProperties = sinon.spy();
subscribeEvents = sinon.spy(); subscribeEvents = sinon.spy();
(instance as any).assignServerToProperties = assignServerToProperties; instance['assignServerToProperties'] = assignServerToProperties;
(instance as any).subscribeEvents = subscribeEvents; instance['subscribeEvents'] = subscribeEvents;
}); });
it('should call "assignServerToProperties" with expected arguments', () => { it('should call "assignServerToProperties" with expected arguments', () => {
instance.subscribeToServerEvents(gateway, { namespace }, port, ''); instance.subscribeToServerEvents(
gateway,
{ namespace },
port,
'moduleKey',
'instanceWrapperId',
);
expect(assignServerToProperties.calledWith(gateway, server.server)).to.be expect(assignServerToProperties.calledWith(gateway, server.server)).to.be
.true; .true;
}); });
it('should call "subscribeEvents" with expected arguments', () => { it('should call "subscribeEvents" with expected arguments', () => {
instance.subscribeToServerEvents(gateway, { namespace }, port, ''); instance.subscribeToServerEvents(
gateway,
{ namespace },
port,
'moduleKey',
'instanceWrapperId',
);
expect(subscribeEvents.firstCall.args[0]).to.be.equal(gateway); expect(subscribeEvents.firstCall.args[0]).to.be.equal(gateway);
expect(subscribeEvents.firstCall.args[2]).to.be.equal(server); expect(subscribeEvents.firstCall.args[2]).to.be.equal(server);
expect(subscribeEvents.firstCall.args[1]).to.be.eql([ expect(subscribeEvents.firstCall.args[1]).to.be.eql([
@@ -137,11 +175,67 @@ describe('WebSocketsController', () => {
]); ]);
}); });
}); });
describe('inspectEntrypointDefinitions', () => {
it('should inspect & insert corresponding entrypoint definitions', () => {
class GatewayHostCls {}
const port = 80;
const instanceWrapperId = '1234';
const messageHandlers: MessageMappingProperties[] = [
{
methodName: 'findOne',
message: 'find',
callback: null,
},
{
methodName: 'create',
message: 'insert',
callback: null,
},
];
const insertEntrypointDefinitionSpy = sinon.spy(
graphInspector,
'insertEntrypointDefinition',
);
instance.inspectEntrypointDefinitions(
new GatewayHostCls(),
port,
messageHandlers,
instanceWrapperId,
);
expect(insertEntrypointDefinitionSpy.calledTwice).to.be.true;
expect(
insertEntrypointDefinitionSpy.calledWith({
type: 'websocket',
methodName: messageHandlers[0].methodName,
className: GatewayHostCls.name,
classNodeId: instanceWrapperId,
metadata: {
port,
message: messageHandlers[0].message,
},
}),
).to.be.true;
expect(
insertEntrypointDefinitionSpy.calledWith({
type: 'websocket',
methodName: messageHandlers[1].methodName,
className: GatewayHostCls.name,
classNodeId: instanceWrapperId,
metadata: {
port,
message: messageHandlers[1].message,
},
}),
).to.be.true;
});
});
describe('subscribeEvents', () => { describe('subscribeEvents', () => {
const gateway = new Test(); const gateway = new Test();
let handlers; let handlers: any;
let server, let server: any,
subscribeConnectionEvent: sinon.SinonSpy, subscribeConnectionEvent: sinon.SinonSpy,
subscribeDisconnectEvent: sinon.SinonSpy, subscribeDisconnectEvent: sinon.SinonSpy,
nextSpy: sinon.SinonSpy, nextSpy: sinon.SinonSpy,

View File

@@ -1,11 +1,12 @@
import { Type } from '@nestjs/common/interfaces/type.interface'; import { Type } from '@nestjs/common/interfaces/type.interface';
import { Logger } from '@nestjs/common/services/logger.service'; import { Logger } from '@nestjs/common/services/logger.service';
import { ApplicationConfig } from '@nestjs/core/application-config'; import { ApplicationConfig } from '@nestjs/core/application-config';
import { GraphInspector } from '@nestjs/core/inspector/graph-inspector';
import { MetadataScanner } from '@nestjs/core/metadata-scanner'; import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { import {
from as fromPromise, from as fromPromise,
Observable,
isObservable, isObservable,
Observable,
of, of,
Subject, Subject,
} from 'rxjs'; } from 'rxjs';
@@ -20,6 +21,7 @@ import {
import { GatewayMetadata } from './interfaces/gateway-metadata.interface'; import { GatewayMetadata } from './interfaces/gateway-metadata.interface';
import { NestGateway } from './interfaces/nest-gateway.interface'; import { NestGateway } from './interfaces/nest-gateway.interface';
import { ServerAndEventStreamsHost } from './interfaces/server-and-event-streams-host.interface'; import { ServerAndEventStreamsHost } from './interfaces/server-and-event-streams-host.interface';
import { WebsocketEntrypointMetadata } from './interfaces/websockets-entrypoint-metadata.interface';
import { SocketServerProvider } from './socket-server-provider'; import { SocketServerProvider } from './socket-server-provider';
import { compareElementAt } from './utils/compare-element.util'; import { compareElementAt } from './utils/compare-element.util';
@@ -35,12 +37,14 @@ export class WebSocketsController {
private readonly socketServerProvider: SocketServerProvider, private readonly socketServerProvider: SocketServerProvider,
private readonly config: ApplicationConfig, private readonly config: ApplicationConfig,
private readonly contextCreator: WsContextCreator, private readonly contextCreator: WsContextCreator,
private readonly graphInspector: GraphInspector,
) {} ) {}
public connectGatewayToServer( public connectGatewayToServer(
instance: NestGateway, instance: NestGateway,
metatype: Type<unknown> | Function, metatype: Type<unknown> | Function,
moduleKey: string, moduleKey: string,
instanceWrapperId: string,
) { ) {
const options = Reflect.getMetadata(GATEWAY_OPTIONS, metatype) || {}; const options = Reflect.getMetadata(GATEWAY_OPTIONS, metatype) || {};
const port = Reflect.getMetadata(PORT_METADATA, metatype) || 0; const port = Reflect.getMetadata(PORT_METADATA, metatype) || 0;
@@ -48,7 +52,13 @@ export class WebSocketsController {
if (!Number.isInteger(port)) { if (!Number.isInteger(port)) {
throw new InvalidSocketPortException(port, metatype); throw new InvalidSocketPortException(port, metatype);
} }
this.subscribeToServerEvents(instance, options, port, moduleKey); this.subscribeToServerEvents(
instance,
options,
port,
moduleKey,
instanceWrapperId,
);
} }
public subscribeToServerEvents<T extends GatewayMetadata>( public subscribeToServerEvents<T extends GatewayMetadata>(
@@ -56,6 +66,7 @@ export class WebSocketsController {
options: T, options: T,
port: number, port: number,
moduleKey: string, moduleKey: string,
instanceWrapperId: string,
) { ) {
const nativeMessageHandlers = this.metadataExplorer.explore(instance); const nativeMessageHandlers = this.metadataExplorer.explore(instance);
const messageHandlers = nativeMessageHandlers.map( const messageHandlers = nativeMessageHandlers.map(
@@ -76,6 +87,13 @@ export class WebSocketsController {
); );
this.assignServerToProperties(instance, observableServer.server); this.assignServerToProperties(instance, observableServer.server);
this.subscribeEvents(instance, messageHandlers, observableServer); this.subscribeEvents(instance, messageHandlers, observableServer);
this.inspectEntrypointDefinitions(
instance,
port,
messageHandlers,
instanceWrapperId,
);
} }
public subscribeEvents( public subscribeEvents(
@@ -172,6 +190,28 @@ export class WebSocketsController {
return of(result); return of(result);
} }
public inspectEntrypointDefinitions(
instance: NestGateway,
port: number,
messageHandlers: MessageMappingProperties[],
instanceWrapperId: string,
) {
messageHandlers.forEach(handler => {
this.graphInspector.insertEntrypointDefinition<WebsocketEntrypointMetadata>(
{
type: 'websocket',
methodName: handler.methodName,
className: instance.constructor?.name,
classNodeId: instanceWrapperId,
metadata: {
port,
message: handler.message,
},
},
);
});
}
private assignServerToProperties<T = any>( private assignServerToProperties<T = any>(
instance: NestGateway, instance: NestGateway,
server: object, server: object,