Skip to content

Commit

Permalink
Merge pull request #12107 from nestjs/feat/discover-by-decorator
Browse files Browse the repository at this point in the history
feat(core): discover by decorator, explorer pattern
  • Loading branch information
kamilmysliwiec authored Aug 18, 2023
2 parents 380b0c0 + f99f6f1 commit b8c2c29
Show file tree
Hide file tree
Showing 13 changed files with 459 additions and 17 deletions.
35 changes: 35 additions & 0 deletions integration/discovery/e2e/discover-by-meta.spec.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 10 additions & 0 deletions integration/discovery/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
6 changes: 6 additions & 0 deletions integration/discovery/src/decorators/webhook.decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { DiscoveryService } from '@nestjs/core';

export const Webhook = DiscoveryService.createDecorator<{ name: string }>();
export const WebhookHandler = DiscoveryService.createDecorator<{
event: string;
}>();
9 changes: 9 additions & 0 deletions integration/discovery/src/my-webhook/cleanup.webhook.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 9 additions & 0 deletions integration/discovery/src/my-webhook/flush.webhook.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
6 changes: 6 additions & 0 deletions integration/discovery/src/my-webhook/my-webhook.module.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 39 additions & 0 deletions integration/discovery/src/webhooks.explorer.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 40 additions & 0 deletions integration/discovery/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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 changes: 153 additions & 0 deletions packages/core/discovery/discoverable-meta-host-collection.ts
Original file line number Diff line number Diff line change
@@ -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,
);
}
}
Loading

0 comments on commit b8c2c29

Please sign in to comment.