mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 23:11:44 +00:00
feat(core): discover by decorator, explorer pattern
This commit is contained in:
35
integration/discovery/e2e/discover-by-meta.spec.ts
Normal file
35
integration/discovery/e2e/discover-by-meta.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { expect } from 'chai';
|
||||
import { AppModule } from '../src/app.module';
|
||||
import { WebhooksExplorer } from '../src/webhooks.explorer';
|
||||
|
||||
describe('DiscoveryModule', () => {
|
||||
it('should discover all providers & handlers with corresponding annotations', async () => {
|
||||
const builder = Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
});
|
||||
const testingModule = await builder.compile();
|
||||
const webhooksExplorer = testingModule.get(WebhooksExplorer);
|
||||
|
||||
expect(webhooksExplorer.getWebhooks()).to.be.eql([
|
||||
{
|
||||
handlers: [
|
||||
{
|
||||
event: 'start',
|
||||
methodName: 'onStart',
|
||||
},
|
||||
],
|
||||
name: 'cleanup',
|
||||
},
|
||||
{
|
||||
handlers: [
|
||||
{
|
||||
event: 'start',
|
||||
methodName: 'onStart',
|
||||
},
|
||||
],
|
||||
name: 'flush',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
10
integration/discovery/src/app.module.ts
Normal file
10
integration/discovery/src/app.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DiscoveryModule } from '@nestjs/core';
|
||||
import { MyWebhookModule } from './my-webhook/my-webhook.module';
|
||||
import { WebhooksExplorer } from './webhooks.explorer';
|
||||
|
||||
@Module({
|
||||
imports: [MyWebhookModule, DiscoveryModule],
|
||||
providers: [WebhooksExplorer],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { DiscoveryService } from '@nestjs/core';
|
||||
|
||||
export const Webhook = DiscoveryService.createDecorator<{ name: string }>();
|
||||
export const WebhookHandler = DiscoveryService.createDecorator<{
|
||||
event: string;
|
||||
}>();
|
||||
9
integration/discovery/src/my-webhook/cleanup.webhook.ts
Normal file
9
integration/discovery/src/my-webhook/cleanup.webhook.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Webhook, WebhookHandler } from '../decorators/webhook.decorators';
|
||||
|
||||
@Webhook({ name: 'cleanup' })
|
||||
export class CleanupWebhook {
|
||||
@WebhookHandler({ event: 'start' })
|
||||
onStart() {
|
||||
console.log('cleanup started');
|
||||
}
|
||||
}
|
||||
9
integration/discovery/src/my-webhook/flush.webhook.ts
Normal file
9
integration/discovery/src/my-webhook/flush.webhook.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Webhook, WebhookHandler } from '../decorators/webhook.decorators';
|
||||
|
||||
@Webhook({ name: 'flush' })
|
||||
export class FlushWebhook {
|
||||
@WebhookHandler({ event: 'start' })
|
||||
onStart() {
|
||||
console.log('flush started');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CleanupWebhook } from './cleanup.webhook';
|
||||
import { FlushWebhook } from './flush.webhook';
|
||||
|
||||
@Module({ providers: [CleanupWebhook, FlushWebhook] })
|
||||
export class MyWebhookModule {}
|
||||
39
integration/discovery/src/webhooks.explorer.ts
Normal file
39
integration/discovery/src/webhooks.explorer.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DiscoveryService, MetadataScanner } from '@nestjs/core';
|
||||
import { Webhook, WebhookHandler } from './decorators/webhook.decorators';
|
||||
|
||||
@Injectable()
|
||||
export class WebhooksExplorer {
|
||||
constructor(
|
||||
private readonly discoveryService: DiscoveryService,
|
||||
private readonly metadataScanner: MetadataScanner,
|
||||
) {}
|
||||
|
||||
getWebhooks() {
|
||||
const webhooks = this.discoveryService.getProviders({
|
||||
metadataKey: Webhook.KEY,
|
||||
});
|
||||
return webhooks.map(wrapper => {
|
||||
const { name } = this.discoveryService.getMetadataByDecorator(
|
||||
Webhook,
|
||||
wrapper,
|
||||
);
|
||||
return {
|
||||
name,
|
||||
handlers: this.metadataScanner
|
||||
.getAllMethodNames(wrapper.metatype.prototype)
|
||||
.map(methodName => {
|
||||
const { event } = this.discoveryService.getMetadataByDecorator(
|
||||
WebhookHandler,
|
||||
wrapper,
|
||||
methodName,
|
||||
);
|
||||
return {
|
||||
methodName,
|
||||
event,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
40
integration/discovery/tsconfig.json
Normal file
40
integration/discovery/tsconfig.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": false,
|
||||
"noImplicitAny": false,
|
||||
"removeComments": true,
|
||||
"noLib": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
"outDir": "./dist",
|
||||
"paths": {
|
||||
"@nestjs/common": ["../../packages/common"],
|
||||
"@nestjs/common/*": ["../../packages/common/*"],
|
||||
"@nestjs/core": ["../../packages/core"],
|
||||
"@nestjs/core/*": ["../../packages/core/*"],
|
||||
"@nestjs/microservices": ["../../packages/microservices"],
|
||||
"@nestjs/microservices/*": ["../../packages/microservices/*"],
|
||||
"@nestjs/websockets": ["../../packages/websockets"],
|
||||
"@nestjs/websockets/*": ["../../packages/websockets/*"],
|
||||
"@nestjs/testing": ["../../packages/testing"],
|
||||
"@nestjs/testing/*": ["../../packages/testing/*"],
|
||||
"@nestjs/platform-express": ["../../packages/platform-express"],
|
||||
"@nestjs/platform-express/*": ["../../packages/platform-express/*"],
|
||||
"@nestjs/platform-socket.io": ["../../packages/platform-socket.io"],
|
||||
"@nestjs/platform-socket.io/*": ["../../packages/platform-socket.io/*"],
|
||||
"@nestjs/platform-ws": ["../../packages/platform-ws"],
|
||||
"@nestjs/platform-ws/*": ["../../packages/platform-ws/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"e2e/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
]
|
||||
}
|
||||
153
packages/core/discovery/discoverable-meta-host-collection.ts
Normal file
153
packages/core/discovery/discoverable-meta-host-collection.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Type } from '@nestjs/common';
|
||||
import { InstanceWrapper } from '../injector/instance-wrapper';
|
||||
import { ModulesContainer } from '../injector/modules-container';
|
||||
|
||||
export class DiscoverableMetaHostCollection {
|
||||
/**
|
||||
* A map of class references to metadata keys.
|
||||
*/
|
||||
public static readonly metaHostLinks = new Map<Type | Function, string>();
|
||||
|
||||
/**
|
||||
* A map of metadata keys to instance wrappers (providers) with the corresponding metadata key.
|
||||
* The map is weakly referenced by the modules container (unique per application).
|
||||
*/
|
||||
private static readonly providersByMetaKey = new WeakMap<
|
||||
ModulesContainer,
|
||||
Map<string, Set<InstanceWrapper>>
|
||||
>();
|
||||
|
||||
/**
|
||||
* A map of metadata keys to instance wrappers (controllers) with the corresponding metadata key.
|
||||
* The map is weakly referenced by the modules container (unique per application).
|
||||
*/
|
||||
private static readonly controllersByMetaKey = new WeakMap<
|
||||
ModulesContainer,
|
||||
Map<string, Set<InstanceWrapper>>
|
||||
>();
|
||||
|
||||
/**
|
||||
* Adds a link between a class reference and a metadata key.
|
||||
* @param target The class reference.
|
||||
* @param metadataKey The metadata key.
|
||||
*/
|
||||
public static addClassMetaHostLink(
|
||||
target: Type | Function,
|
||||
metadataKey: string,
|
||||
) {
|
||||
this.metaHostLinks.set(target, metadataKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspects a provider instance wrapper and adds it to the collection of providers
|
||||
* if it has a metadata key.
|
||||
* @param hostContainerRef A reference to the modules container.
|
||||
* @param instanceWrapper A provider instance wrapper.
|
||||
* @returns void
|
||||
*/
|
||||
public static inspectProvider(
|
||||
hostContainerRef: ModulesContainer,
|
||||
instanceWrapper: InstanceWrapper,
|
||||
) {
|
||||
return this.inspectInstanceWrapper(
|
||||
hostContainerRef,
|
||||
instanceWrapper,
|
||||
this.providersByMetaKey,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspects a controller instance wrapper and adds it to the collection of controllers
|
||||
* if it has a metadata key.
|
||||
* @param hostContainerRef A reference to the modules container.
|
||||
* @param instanceWrapper A controller's instance wrapper.
|
||||
* @returns void
|
||||
*/
|
||||
public static inspectController(
|
||||
hostContainerRef: ModulesContainer,
|
||||
instanceWrapper: InstanceWrapper,
|
||||
) {
|
||||
return this.inspectInstanceWrapper(
|
||||
hostContainerRef,
|
||||
instanceWrapper,
|
||||
this.controllersByMetaKey,
|
||||
);
|
||||
}
|
||||
|
||||
public static insertByMetaKey(
|
||||
metaKey: string,
|
||||
instanceWrapper: InstanceWrapper,
|
||||
collection: Map<string, Set<InstanceWrapper>>,
|
||||
) {
|
||||
if (collection.has(metaKey)) {
|
||||
const wrappers = collection.get(metaKey);
|
||||
wrappers.add(instanceWrapper);
|
||||
} else {
|
||||
const wrappers = new Set<InstanceWrapper>();
|
||||
wrappers.add(instanceWrapper);
|
||||
collection.set(metaKey, wrappers);
|
||||
}
|
||||
}
|
||||
|
||||
public static getProvidersByMetaKey(
|
||||
hostContainerRef: ModulesContainer,
|
||||
metaKey: string,
|
||||
): Set<InstanceWrapper> {
|
||||
const wrappersByMetaKey = this.providersByMetaKey.get(hostContainerRef);
|
||||
return wrappersByMetaKey.get(metaKey);
|
||||
}
|
||||
|
||||
public static getControllersByMetaKey(
|
||||
hostContainerRef: ModulesContainer,
|
||||
metaKey: string,
|
||||
): Set<InstanceWrapper> {
|
||||
const wrappersByMetaKey = this.controllersByMetaKey.get(hostContainerRef);
|
||||
return wrappersByMetaKey.get(metaKey);
|
||||
}
|
||||
|
||||
private static inspectInstanceWrapper(
|
||||
hostContainerRef: ModulesContainer,
|
||||
instanceWrapper: InstanceWrapper,
|
||||
wrapperByMetaKeyMap: WeakMap<
|
||||
ModulesContainer,
|
||||
Map<string, Set<InstanceWrapper>>
|
||||
>,
|
||||
) {
|
||||
const metaKey =
|
||||
DiscoverableMetaHostCollection.getMetaKeyByInstanceWrapper(
|
||||
instanceWrapper,
|
||||
);
|
||||
if (!metaKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
let collection: Map<string, Set<InstanceWrapper>>;
|
||||
if (wrapperByMetaKeyMap.has(hostContainerRef)) {
|
||||
collection = wrapperByMetaKeyMap.get(hostContainerRef);
|
||||
} else {
|
||||
collection = new Map<string, Set<InstanceWrapper>>();
|
||||
wrapperByMetaKeyMap.set(hostContainerRef, collection);
|
||||
}
|
||||
this.insertByMetaKey(metaKey, instanceWrapper, collection);
|
||||
}
|
||||
|
||||
private static getMetaKeyByInstanceWrapper(
|
||||
instanceWrapper: InstanceWrapper<any>,
|
||||
) {
|
||||
return this.metaHostLinks.get(
|
||||
// NOTE: Regarding the ternary statement below,
|
||||
// - The condition `!wrapper.metatype` is needed because when we use `useValue`
|
||||
// the value of `wrapper.metatype` will be `null`.
|
||||
// - The condition `wrapper.inject` is needed here because when we use
|
||||
// `useFactory`, the value of `wrapper.metatype` will be the supplied
|
||||
// factory function.
|
||||
// For both cases, we should use `wrapper.instance.constructor` instead
|
||||
// of `wrapper.metatype` to resolve processor's class properly.
|
||||
// But since calling `wrapper.instance` could degrade overall performance
|
||||
// we must defer it as much we can.
|
||||
instanceWrapper.metatype || instanceWrapper.inject
|
||||
? instanceWrapper.instance?.constructor ?? instanceWrapper.metatype
|
||||
: instanceWrapper.metatype,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,48 @@
|
||||
import { flatten, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
CustomDecorator,
|
||||
flatten,
|
||||
Injectable,
|
||||
SetMetadata,
|
||||
} from '@nestjs/common';
|
||||
import { uid } from 'uid';
|
||||
import { InstanceWrapper } from '../injector/instance-wrapper';
|
||||
import { Module } from '../injector/module';
|
||||
import { ModulesContainer } from '../injector/modules-container';
|
||||
import { DiscoverableMetaHostCollection } from './discoverable-meta-host-collection';
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export interface DiscoveryOptions {
|
||||
export interface FilterByInclude {
|
||||
/**
|
||||
* List of modules to include (whitelist) into the discovery process.
|
||||
*/
|
||||
include?: Function[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export interface FilterByMetadataKey {
|
||||
/**
|
||||
* A key to filter controllers and providers by.
|
||||
* Only instance wrappers with the specified metadata key will be returned.
|
||||
*/
|
||||
metadataKey?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export type DiscoveryOptions = FilterByInclude | FilterByMetadataKey;
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export type DiscoverableDecorator<T> = ((opts?: T) => CustomDecorator) & {
|
||||
KEY: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
@@ -17,24 +50,108 @@ export interface DiscoveryOptions {
|
||||
export class DiscoveryService {
|
||||
constructor(private readonly modulesContainer: ModulesContainer) {}
|
||||
|
||||
getProviders(
|
||||
/**
|
||||
* Creates a decorator that can be used to decorate classes and methods with metadata.
|
||||
* The decorator will also add the class to the collection of discoverable classes (by metadata key).
|
||||
* Decorated classes can be discovered using the `getProviders` and `getControllers` methods.
|
||||
* @param metadataKey The metadata key to use.
|
||||
* @returns A decorator function.
|
||||
*/
|
||||
static createDecorator<T>(): DiscoverableDecorator<T> {
|
||||
const metadataKey = uid(21);
|
||||
const decoratorFn =
|
||||
(opts: T) =>
|
||||
(target: object | Function, key?: string | symbol, descriptor?: any) => {
|
||||
if (!descriptor) {
|
||||
DiscoverableMetaHostCollection.addClassMetaHostLink(
|
||||
target as Function,
|
||||
metadataKey,
|
||||
);
|
||||
}
|
||||
SetMetadata(metadataKey, opts ?? {})(target, key, descriptor);
|
||||
};
|
||||
|
||||
decoratorFn.KEY = metadataKey;
|
||||
return decoratorFn as DiscoverableDecorator<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of instance wrappers (providers).
|
||||
* Depending on the options, the array will contain either all providers or only providers with the specified metadata key.
|
||||
* @param options Discovery options.
|
||||
* @param modules A list of modules to filter by.
|
||||
* @returns An array of instance wrappers (providers).
|
||||
*/
|
||||
public getProviders(
|
||||
options: DiscoveryOptions = {},
|
||||
modules: Module[] = this.getModules(options),
|
||||
): InstanceWrapper[] {
|
||||
if ('metadataKey' in options) {
|
||||
const providers = DiscoverableMetaHostCollection.getProvidersByMetaKey(
|
||||
this.modulesContainer,
|
||||
options.metadataKey,
|
||||
);
|
||||
return Array.from(providers);
|
||||
}
|
||||
|
||||
const providers = modules.map(item => [...item.providers.values()]);
|
||||
return flatten(providers);
|
||||
}
|
||||
|
||||
getControllers(
|
||||
/**
|
||||
* Returns an array of instance wrappers (controllers).
|
||||
* Depending on the options, the array will contain either all controllers or only controllers with the specified metadata key.
|
||||
* @param options Discovery options.
|
||||
* @param modules A list of modules to filter by.
|
||||
* @returns An array of instance wrappers (controllers).
|
||||
*/
|
||||
public getControllers(
|
||||
options: DiscoveryOptions = {},
|
||||
modules: Module[] = this.getModules(options),
|
||||
): InstanceWrapper[] {
|
||||
if ('metadataKey' in options) {
|
||||
const controllers =
|
||||
DiscoverableMetaHostCollection.getControllersByMetaKey(
|
||||
this.modulesContainer,
|
||||
options.metadataKey,
|
||||
);
|
||||
return Array.from(controllers);
|
||||
}
|
||||
|
||||
const controllers = modules.map(item => [...item.controllers.values()]);
|
||||
return flatten(controllers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves metadata from the specified instance wrapper.
|
||||
* @param decorator The decorator to retrieve metadata of.
|
||||
* @param instanceWrapper Reference to the instance wrapper.
|
||||
* @param methodKey An optional method key to retrieve metadata from.
|
||||
* @returns Discovered metadata.
|
||||
*/
|
||||
public getMetadataByDecorator<T extends DiscoverableDecorator<any>>(
|
||||
decorator: T,
|
||||
instanceWrapper: InstanceWrapper,
|
||||
methodKey?: string,
|
||||
): T extends DiscoverableDecorator<infer R> ? R | undefined : T | undefined {
|
||||
if (methodKey) {
|
||||
return Reflect.getMetadata(
|
||||
decorator.KEY,
|
||||
instanceWrapper.instance[methodKey],
|
||||
);
|
||||
}
|
||||
|
||||
const clsRef =
|
||||
instanceWrapper.instance?.constructor ?? instanceWrapper.metatype;
|
||||
return Reflect.getMetadata(decorator.KEY, clsRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of modules to be used for discovery.
|
||||
*/
|
||||
protected getModules(options: DiscoveryOptions = {}): Module[] {
|
||||
if (!options.include) {
|
||||
const includeInOpts = 'include' in options;
|
||||
if (!includeInOpts) {
|
||||
const moduleRefs = [...this.modulesContainer.values()];
|
||||
return moduleRefs;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from '@nestjs/common/constants';
|
||||
import { Injectable, Type } from '@nestjs/common/interfaces';
|
||||
import { ApplicationConfig } from '../application-config';
|
||||
import { DiscoverableMetaHostCollection } from '../discovery/discoverable-meta-host-collection';
|
||||
import {
|
||||
CircularDependencyException,
|
||||
UndefinedForwardRefException,
|
||||
@@ -223,7 +224,7 @@ export class NestContainer {
|
||||
relatedModule,
|
||||
);
|
||||
const related = this.modules.get(relatedModuleToken);
|
||||
moduleRef.addRelatedModule(related);
|
||||
moduleRef.addImport(related);
|
||||
}
|
||||
|
||||
public addProvider(
|
||||
@@ -238,7 +239,12 @@ export class NestContainer {
|
||||
if (!moduleRef) {
|
||||
throw new UnknownModuleException();
|
||||
}
|
||||
return moduleRef.addProvider(provider, enhancerSubtype) as Function;
|
||||
const providerKey = moduleRef.addProvider(provider, enhancerSubtype);
|
||||
const providerRef = moduleRef.getProviderByKey(providerKey);
|
||||
|
||||
DiscoverableMetaHostCollection.inspectProvider(this.modules, providerRef);
|
||||
|
||||
return providerKey as Function;
|
||||
}
|
||||
|
||||
public addInjectable(
|
||||
@@ -268,6 +274,12 @@ export class NestContainer {
|
||||
}
|
||||
const moduleRef = this.modules.get(token);
|
||||
moduleRef.addController(controller);
|
||||
|
||||
const controllerRef = moduleRef.controllers.get(controller);
|
||||
DiscoverableMetaHostCollection.inspectController(
|
||||
this.modules,
|
||||
controllerRef,
|
||||
);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
@@ -292,7 +304,7 @@ export class NestContainer {
|
||||
if (target === globalModule || target === this.internalCoreModule) {
|
||||
return;
|
||||
}
|
||||
target.addRelatedModule(globalModule);
|
||||
target.addImport(globalModule);
|
||||
}
|
||||
|
||||
public getDynamicMetadataByToken(token: string): Partial<DynamicModule>;
|
||||
|
||||
@@ -240,11 +240,11 @@ export class Module {
|
||||
return instanceWrapper;
|
||||
}
|
||||
|
||||
public addProvider(provider: Provider): Provider | InjectionToken;
|
||||
public addProvider(provider: Provider): InjectionToken;
|
||||
public addProvider(
|
||||
provider: Provider,
|
||||
enhancerSubtype: EnhancerSubtype,
|
||||
): Provider | InjectionToken;
|
||||
): InjectionToken;
|
||||
public addProvider(provider: Provider, enhancerSubtype?: EnhancerSubtype) {
|
||||
if (this.isCustomProvider(provider)) {
|
||||
if (this.isEntryProvider(provider.provide)) {
|
||||
@@ -517,6 +517,13 @@ export class Module {
|
||||
});
|
||||
}
|
||||
|
||||
public addImport(moduleRef: Module) {
|
||||
this._imports.add(moduleRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public addRelatedModule(module: Module) {
|
||||
this._imports.add(module);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user