Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add recursiveModuleScan option to scanner #3186

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

hee-c
Copy link

@hee-c hee-c commented Nov 24, 2024

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Other... Please describe:

What is the current behavior?

The Swagger scanner only scans up to 1-depth for modules included via the include option, even when deepScanRoutes is set to true.

Issue Number: #3102

What is the new behavior?

스크린샷 2024-11-24 오후 2 01 00

When deepScanRoutes is set to true and the newly added recursiveModuleScan flag is also set to true, the modules included via include will be scanned recursively up to the depth defined by maxScanDepth.

  • deepScanRoutes: false: Only scans root modules imported by include (current behavior).
  • deepScanRoutes: true: Scans 1-depth modules imported by include (current behavior).
  • deepScanRoutes: true, recursiveModuleScan: true: Scans all modules recursively, with no depth limit.
  • deepScanRoutes: true, recursiveModuleScan: true, maxScanDepth: 5: Scans modules recursively up to a depth of 5.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information


@ApiTags('depth1-dogs')
@Controller('depth1-dogs')
export class Depth1DogsController {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The files under e2e/src/dogs are modules used for testing purposes.

@@ -215,3 +216,115 @@ describe('Validate OpenAPI schema', () => {
});
});
});

describe('Nested module scanning', () => {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added the tests here for now, but if you prefer to separate them into a dedicated file, let me know and I will update accordingly.

*
* @default Infinity
*/
maxScanDepth?: number;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value for maxScanDepth is currently set to Infinity. If you have any suggestions, let me know.

@hee-c hee-c changed the title feat: Enhance included module scanning depth control feat: Add recursiveModuleScan to module scanner Nov 24, 2024
@hee-c hee-c changed the title feat: Add recursiveModuleScan to module scanner feat: Add recursiveModuleScan option to module scanner Nov 24, 2024
@hee-c hee-c changed the title feat: Add recursiveModuleScan option to module scanner feat: Add recursiveModuleScan option to scanner Nov 24, 2024
The module scanning now correctly respects:
- Default scan depth (1 level) with `deepScanRoutes`
- Full recursive scanning with `recursiveModuleScan`
- Custom depth limits with `maxScanDepth`
@hee-c hee-c force-pushed the hee/allow-swagger-scanner-deepScanRoute branch from 9ece4d5 to 4ca7799 Compare November 24, 2024 04:59
}

private getModuleId(metatype: Type<any>): string {
return metatype.name || Math.random().toString(36).substring(2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I understand what's the purpose of having Math.random().toString(36).substring(2) if it isn't calculated based on the input argument value (metatype)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right about the random fallback being unnecessary. Let me explain the context better. The getModuleId is used with processedModules set to prevent duplicate scanning and circular dependencies while recursively scanning module imports. However, I realized that using just metatype.name could be problematic - if we have two different modules with the same name in different locations (like auth.module.ts in different directories), only one would be scanned. To properly handle such cases while still preventing infinite recursion, I think we should use metatype.toString(). While this generates longer strings, it ensures we don't miss any modules during the scanning process. What do you think about this approach?

Suggested change
return metatype.name || Math.random().toString(36).substring(2);
return metatype.toString();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds reasonable. I'm afraid the size of the array might become notably large for bigger projects though

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at ModuleTokenFactory.create, I see that id uses uid(21) and token adds metatype.name and dynamicModuleMetadata(optional) to that. Since NestContainer.addModule seems to use token as the identifier, rather than using metatype, what if we destructure token along with the existing properties in the imports.values() loop within the scanModuleImportsRecursively method and use that as the moduleId?

Comment on lines +217 to +220
for (const { metatype, controllers, imports: subImports } of imports.values()) {
// Skip if module has already been processed
const moduleId = this.getModuleId(metatype);
if (processedModules.has(moduleId) || container.isGlobalModule(metatype) || (maxDepth !== undefined && currentDepth > maxDepth)) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (const { metatype, controllers, imports: subImports } of imports.values()) {
// Skip if module has already been processed
const moduleId = this.getModuleId(metatype);
if (processedModules.has(moduleId) || container.isGlobalModule(metatype) || (maxDepth !== undefined && currentDepth > maxDepth)) {
for (const { token, metatype, controllers, imports: subImports } of imports.values()) {
// Skip if module has already been processed
if (processedModules.has(token) || container.isGlobalModule(metatype) || (maxDepth !== undefined && currentDepth > maxDepth)) {

Here's an example implementation of the approach I suggested

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants