nest
nest copied to clipboard
Ability To Cherry Pick/Exclude Controllers in Module Imports
Is there an existing issue that is already proposing this?
- [x] I have searched the existing issues
Is your feature request related to a problem? Please describe it
I needed to use the same codebase to handle Core App and Admin App, The Codebase started with the core app, and i wasn't ready to refactor things to meet the recommend mono repo structure.
instead i created two application entry file, with respective root modules /apps/core and /apps/admin, configured the nestjs cli to point to them
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"projects": {
"core-app": {
"type": "application",
"entryFile": "./apps/core/main",
"compilerOptions": {
"tsConfigPath": "src/apps/core/tsconfig.app.json"
}
},
"admin-app": {
"type": "application",
"entryFile": "./apps/admin/main",
"compilerOptions": {
"tsConfigPath": "src/apps/admin/tsconfig.app.json"
}
}
},
"compilerOptions": {
"deleteOutDir": true,
"plugins": ["@nestjs/swagger"],
"assets": ["mail/templates/**/*", "app-utils/data/**/*"],
"watchAssets": true
}
}
The first problem i encountered was importing modules used in Core App in the Admin App, will make the controllers spill over, this controllers would appear in the admin swagger documentation, i had a lot of issue trying to hide them from swagger.
Then i came up with a helper method to clone modules (with subclasses), so as not to override the original module class, while excluding the controllers in those modules.
import {
DynamicModule,
Type,
ForwardReference,
forwardRef,
} from '@nestjs/common';
const excludedModuleCache = new WeakMap<Type<any>, Type<any>>();
function createExcludedModule(module: Type<any>): Type<any> {
if (excludedModuleCache.has(module)) {
return excludedModuleCache.get(module)!;
}
// Create a subclass to avoid modifying the original module
const ExcludedModule = class extends module {};
// Copy existing metadata from the original module
const metadataKeys = Reflect.getMetadataKeys(module);
metadataKeys.forEach((key) => {
const value = Reflect.getMetadata(key, module);
Reflect.defineMetadata(key, value, ExcludedModule);
});
// Override controllers to be empty
Reflect.defineMetadata('controllers', [], ExcludedModule);
// Process imports recursively to replace with excluded modules
const originalImports = Reflect.getMetadata('imports', ExcludedModule) || [];
const processedImports = originalImports.map((importItem) => {
if (typeof importItem === 'function') {
return createExcludedModule(importItem);
} else if ((importItem as ForwardReference).forwardRef) {
const originalRef = (importItem as ForwardReference).forwardRef();
const excludedRef = createExcludedModule(originalRef);
return forwardRef(() => excludedRef);
} else if ((importItem as DynamicModule).module) {
const dynamicModule = importItem as DynamicModule;
return {
...dynamicModule,
module: createExcludedModule(dynamicModule.module),
};
}
return importItem;
});
Reflect.defineMetadata('imports', processedImports, ExcludedModule);
excludedModuleCache.set(module, ExcludedModule);
return ExcludedModule;
}
export function excludeControllers(modules: Type<any>[]): DynamicModule[] {
return modules.map((module) => {
const ExcludedModule = createExcludedModule(module);
return {
module: ExcludedModule,
imports: Reflect.getMetadata('imports', ExcludedModule) || [],
providers: Reflect.getMetadata('providers', ExcludedModule) || [],
exports: Reflect.getMetadata('exports', ExcludedModule) || [],
controllers: [], // Explicit exclusion
};
});
}
//USAGE
@Module({
imports: [
...excludeControllers([
WorkspaceModule,
WorkspaceMemberModule,
MediaModule,
SubscriptionPlanModule,
]),
OfflinePaymentOptionsModule,
],
providers: [AdminManagementService],
exports: [],
})
This was working great until i had a circular dependency issue in the core app and i decided to use forwardRef, which broke the utility function on the admin side of things, refactored to use shared module, and i got the below error which i am still trying to resolve.
ERROR [ExceptionHandler] UnknownExportException [Error]: Nest cannot export a provider/module that is not a part of the currently processed module (ExcludedModule). Please verify whether the exported MongooseModule is available in this particular context.
Possible Solutions:
- Is MongooseModule part of the relevant providers/imports within ExcludedModule?
Shared Module
@Module({
imports: [
MongooseModule.forFeature([
{
name: CohortParticipant.name,
schema: CohortParticipantSchema,
},
]),
],
exports: [MongooseModule],
})
export class SharedSchemaModule {}
Describe the solution you'd like
It would have been great if such a method exists internally in the nestjs core modules
Teachability, documentation, adoption, migration strategy
//USAGE
@Module({
imports: [
...excludeControllers([
WorkspaceModule,
WorkspaceMemberModule,
MediaModule,
SubscriptionPlanModule,
]),
OfflinePaymentOptionsModule,
],
providers: [AdminManagementService],
exports: [],
})
What is the motivation / use case for changing the behavior?
Dynamically Exclude Controllers when importing modules into other modules