-
-
Notifications
You must be signed in to change notification settings - Fork 7.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #12107 from nestjs/feat/discover-by-decorator
feat(core): discover by decorator, explorer pattern
- Loading branch information
Showing
13 changed files
with
459 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}>(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
}), | ||
}; | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
153
packages/core/discovery/discoverable-meta-host-collection.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
); | ||
} | ||
} |
Oops, something went wrong.