UNPKG

nestjs-resource-gen

Version:

A CLI tool to generate NestJS resources (controllers, services, DTOs, modules) from entity files

1,272 lines (1,237 loc) â€ĸ 58.1 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const fs = __importStar(require("fs")); const path = __importStar(require("path")); function parseEntityFile(filePath) { const content = fs.readFileSync(filePath, "utf-8"); const lines = content.split("\n"); // Extract class name - look for "export class" followed by the class name const classMatch = content.match(/export\s+class\s+(\w+)/); if (!classMatch) { throw new Error("Could not find entity class name"); } const className = classMatch[1]; // Extract table name from @Entity decorator const entityMatch = content.match(/@Entity\(['"`]([^'"`]+)['"`]\)/); const tableName = entityMatch ? entityMatch[1] : className.toLowerCase(); // Extract fields - look for property definitions with decorators const fields = []; // Split content into lines and process each line const contentLines = content.split("\n"); for (let i = 0; i < contentLines.length; i++) { const line = contentLines[i].trim(); // Look for property definitions (field name followed by colon and type) const propertyMatch = line.match(/^(\w+)(\?)?\s*:\s*([^;]+);/); if (!propertyMatch) continue; const fieldName = propertyMatch[1]; const isOptional = !!propertyMatch[2]; let fieldType = propertyMatch[3].trim(); // Skip if this looks like a decorator or import if (fieldName.startsWith("@") || fieldName === "import" || fieldName === "export") continue; // Extract decorators for this field by looking backwards const decorators = []; for (let j = i - 1; j >= 0; j--) { const prevLine = contentLines[j].trim(); if (prevLine.startsWith("@")) { decorators.unshift(prevLine); } else if (prevLine && !prevLine.startsWith("//") && !prevLine.startsWith("import") && !prevLine.startsWith("export")) { break; } } // Clean up field type - remove any decorator properties const decoratorProperties = [ "cascade", "eager", "nullable", "onDelete", "onUpdate", "primary", "unique", "default", "length", "precision", "scale", "enum", "array", "transformer", "comment", ]; // Remove any decorator properties from the field type decoratorProperties.forEach((prop) => { const propRegex = new RegExp(`\\s*,\\s*${prop}\\s*:\\s*[^,}]+`, "g"); fieldType = fieldType.replace(propRegex, ""); }); // Clean up any remaining decorator syntax fieldType = fieldType .replace(/\s*,\s*}\s*$/, "}") // Remove trailing comma before closing brace .replace(/\s*{\s*}\s*$/, "") // Remove empty object braces .replace(/\s*,\s*$/, "") // Remove trailing comma .trim(); fields.push({ name: fieldName, type: fieldType, isOptional, decorators, }); } return { name: className.toLowerCase(), className, tableName, fields, }; } function generateDTO(entityInfo) { const { className, fields } = entityInfo; // Collect relationship DTOs for imports const relationshipDTOs = []; const exposedFields = fields .filter((field) => !field.decorators.some((d) => d.includes("@Exclude"))) .map((field) => { const decorators = []; // Check if this is a relationship field const isRelationship = field.decorators.some((d) => d.includes("@OneToOne") || d.includes("@OneToMany") || d.includes("@ManyToOne") || d.includes("@ManyToMany")); if (isRelationship) { // Extract the related entity name from the relationship decorator let relatedEntityName = ""; for (const decorator of field.decorators) { const match = decorator.match(/@(?:OneToOne|OneToMany|ManyToOne|ManyToMany)\s*\(\s*\(\)\s*=>\s*(\w+)/); if (match) { relatedEntityName = match[1]; break; } } if (relatedEntityName) { // Add @Type decorator for the relationship decorators.push(`@Type(() => ${relatedEntityName}DTO)`); relationshipDTOs.push(relatedEntityName); } } // Add @Expose decorator decorators.push("@Expose()"); // Add type-specific validators (only for non-relationship fields) if (!isRelationship) { if (field.type.includes("string")) { decorators.push("@IsString()"); if (!field.isOptional) { decorators.push("@IsNotEmpty()"); } } else if (field.type.includes("number")) { decorators.push("@IsNumber()"); if (!field.isOptional) { decorators.push("@IsNotEmpty()"); } } else if (field.type.includes("boolean")) { decorators.push("@IsBoolean()"); } else if (field.type.includes("Date")) { // Check if it's a date or timestamp column const hasDateColumn = field.decorators.some((d) => d.includes("@Column") && d.includes("type: 'date'")); const hasTimestampColumn = field.decorators.some((d) => d.includes("@Column") && d.includes("type: 'timestamp'")); if (hasTimestampColumn) { decorators.push("@IsDateString()"); } else { decorators.push("@IsDate()"); } } if (field.isOptional) { decorators.push("@IsOptional()"); } } // For relationships, use the DTO type instead of the entity type let fieldType = field.type; if (isRelationship && relationshipDTOs.length > 0) { const relatedEntityName = relationshipDTOs[relationshipDTOs.length - 1]; fieldType = `${relatedEntityName}DTO`; } return ` ${decorators.join("\n ")} ${field.name}${field.isOptional ? "?" : ""}: ${fieldType};`; }) .join("\n\n"); // Generate imports for relationship DTOs const relationshipImports = relationshipDTOs.length > 0 ? `import { ${relationshipDTOs.map((name) => `${name}DTO`).join(", ")} } from '../../../shared/DTO';` : ""; return `import { Expose, Type } from 'class-transformer'; import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString, IsDate, IsDateString, } from 'class-validator'; import { OmitType, PartialType } from '@nestjs/mapped-types'; ${relationshipImports} export class ${className}DTO { ${exposedFields} } export class ${className}CreateDTO extends OmitType(${className}DTO, [ 'id', ]) {} export class ${className}PatchDTO extends PartialType(${className}DTO) {} `; } function generateController(entityInfo, entityFilePath) { const { className, name } = entityInfo; // Calculate relative paths based on nesting level const nestingLevel = getEntityNestingLevel(entityFilePath); const basePath = "../".repeat(nestingLevel + 1); const authPath = `${basePath}auth/guard`; const authzPath = `${basePath}authz`; const interceptorPath = `${basePath}interceptor/serialize.interceptor`; const sharedPath = `${basePath}shared/Id.params`; const entitiesPath = `${basePath}database/entities`; return `import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards, } from '@nestjs/common'; import { JwtAuthGuard } from '${authPath}'; import { PermissionsGuard } from '${authzPath}/guard'; import { ${className}Service } from './${name}.service'; import { Serialize } from '${interceptorPath}'; import { ${className}CreateDTO, ${className}DTO, ${className}PatchDTO } from './dtos'; import { CheckPermissions } from '${authzPath}/decorator'; import { PermissionAction } from '${authzPath}/casl-ability.factory'; import { IdParams } from '${sharedPath}'; import { PERMISSION_ITEM_NAME } from '${entitiesPath}'; @Controller() @UseGuards(JwtAuthGuard, PermissionsGuard) export class ${className}Controller { constructor(private _${name}Service: ${className}Service) {} @Get('/') @Serialize(${className}DTO) @CheckPermissions([PermissionAction.READ, PERMISSION_ITEM_NAME.${className.toUpperCase()}S]) async getAll${className}s() { return await this._${name}Service.getAll${className}s(); } @Get('/:id') @Serialize(${className}DTO) @CheckPermissions([PermissionAction.READ, PERMISSION_ITEM_NAME.${className.toUpperCase()}S]) async get${className}(@Param() { id }: IdParams) { return await this._${name}Service.get${className}ById(id); } @Delete('/:id') @CheckPermissions([PermissionAction.DELETE, PERMISSION_ITEM_NAME.${className.toUpperCase()}S]) async delete${className}(@Param() { id }: IdParams) { return await this._${name}Service.delete${className}(id); } @Post('/') @Serialize(${className}DTO) @CheckPermissions([PermissionAction.CREATE, PERMISSION_ITEM_NAME.${className.toUpperCase()}S]) async create${className}(@Body() ${name}: ${className}CreateDTO) { return await this._${name}Service.create${className}(${name}); } @Patch('/:id') @Serialize(${className}PatchDTO) @CheckPermissions([PermissionAction.UPDATE, PERMISSION_ITEM_NAME.${className.toUpperCase()}S]) async patch${className}( @Param() { id }: IdParams, @Body() ${name}: ${className}PatchDTO, ) { return await this._${name}Service.patch${className}(id, ${name}); } }`; } function generateService(entityInfo, entityFilePath) { const { className, name } = entityInfo; // Calculate relative paths based on nesting level const nestingLevel = getEntityNestingLevel(entityFilePath); const basePath = "../".repeat(nestingLevel + 1); const entitiesPath = `${basePath}database/entities`; const sharedPath = `${basePath}shared/CustomQuery.exceptionfilter`; const authzPath = `${basePath}authz`; return `import { BadRequestException, Injectable, NotFoundException, Scope, } from '@nestjs/common'; import { FindOptionsRelations, Not, Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { ${className}, PERMISSION_ITEM_NAME } from '${entitiesPath}'; import { CustomQueryException } from '${sharedPath}'; import { ${className}CreateDTO, ${className}PatchDTO } from './dtos/${name}.dto'; import { AuthzService } from '${authzPath}/authz.service'; import { PermissionAction } from '${authzPath}/casl-ability.factory'; @Injectable({ scope: Scope.REQUEST }) export class ${className}Service { constructor( @InjectRepository(${className}) private ${name}Repository: Repository<${className}>, private authzService: AuthzService, ) {} /** * Get all ${name}s * @returns All ${name}s */ async getAll${className}s() { const { user, ability } = await this.authzService.getDetails(); return await this.${name}Repository.find({ where: { // locationId: ability.can( // PermissionAction.MANAGE, // PERMISSION_ITEM_NAME.CAN_VIEW_ALL_LOCATION_DATA, // ) // ? undefined // View all if can // : user.locationId ?? -1, // View none if can't and no locationId }, order: { id: 'ASC', }, }); } /** * Get a ${name} by id * @param id ${className} id * @returns ${className} found with specific id */ async get${className}ById(id: number) { const ${name} = await this.find${className}OrThrow(id); // await this.authzService.canAccessOrThrow( // ${name}.locationId, // 'locationId', // PermissionAction.READ, // PERMISSION_ITEM_NAME.CAN_VIEW_ALL_LOCATION_DATA, // ); return ${name}; } /** * Create a new ${name} * @param ${name} ${className} to create * @returns Created ${name} */ async create${className}(${name}: ${className}CreateDTO) { // await this.authzService.canAccessOrThrow( // ${name}.locationId, // 'locationId', // PermissionAction.CREATE, // PERMISSION_ITEM_NAME.CAN_VIEW_ALL_LOCATION_DATA, // ); try { return await this.${name}Repository.save(${name}); } catch (error) { throw new CustomQueryException(error.message, '${className}'); } } /** * Update a ${name} * @param ${name} ${className} to update * @returns Updated ${name} */ async patch${className}(id: number, ${name}: ${className}PatchDTO) { const entity = await this.find${className}OrThrow(id); // await this.authzService.canAccessOrThrow( // entity.locationId, // 'locationId', // PermissionAction.UPDATE, // PERMISSION_ITEM_NAME.CAN_VIEW_ALL_LOCATION_DATA, // ); const updatedEntity = this.${name}Repository.merge(entity, ${name}); try { return await this.${name}Repository.save(updatedEntity); } catch (error) { throw new CustomQueryException(error.message, '${className}'); } } /** * Delete a ${name} * @param id ${className} id to delete */ async delete${className}(id: number) { const ${name} = await this.find${className}OrThrow(id); // await this.authzService.canAccessOrThrow( // ${name}.locationId, // 'locationId', // PermissionAction.DELETE, // PERMISSION_ITEM_NAME.CAN_VIEW_ALL_LOCATION_DATA, // ); try { await this.${name}Repository.remove(${name}); } catch (error) { throw new CustomQueryException(error.message, '${className}'); } } /** * Find ${name} by id or throw * @param id ${className} id * @param relations Relations to load * @returns ${className} found */ async find${className}OrThrow( id: number, relations?: FindOptionsRelations<${className}>, ): Promise<${className}> { const ${name} = await this.${name}Repository.findOne({ where: { id }, relations, }); if (!${name}) { throw new NotFoundException(\`${className} with id \${id} not found\`); } return ${name}; } }`; } function generateModule(entityInfo, entityFilePath, resourceDir) { const { className, name } = entityInfo; // Calculate relative path from resource directory to entity file const relativePath = path.relative(resourceDir, entityFilePath); const entityImportPath = relativePath.replace(".entity.ts", ".entity"); // Calculate relative path to authz module based on nesting level const nestingLevel = getEntityNestingLevel(entityFilePath); const authzRelativePath = "../".repeat(nestingLevel + 1) + "authz/authz.module"; return `import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AuthzModule } from '${authzRelativePath}'; import { ${className}Service } from './${name}.service'; import { ${className}Controller } from './${name}.controller'; import { ${className} } from '${entityImportPath}'; @Module({ imports: [ TypeOrmModule.forFeature([${className}]), AuthzModule, ], controllers: [${className}Controller], providers: [${className}Service], exports: [${className}Service], }) export class ${className}Module {}`; } function generateRoutes(entityInfo, entityFilePath) { const { className, name } = entityInfo; // Calculate relative path from routes to the module const entityFullSubPath = getEntityFullSubPath(entityFilePath); const highestParent = getHighestParentDirectory(entityFilePath); // Routes file is at: src/routes/{highestParent}/ or src/routes/ // Module file is at: src/{entityFullSubPath}/{name}/ // We need to go up 2 levels from routes/{highestParent}/ to reach src, then down to the module const moduleRelativePath = entityFullSubPath ? `../../${entityFullSubPath}/${name}/${name}.module` : `../${name}/${name}.module`; // Generate nested route structure const routeStructure = generateNestedRouteStructure(entityInfo, entityFilePath); const routesVariableName = highestParent ? highestParent.toLowerCase() : name; console.log(`🔍 Debug - Entity: ${className}, Highest Parent: '${highestParent}', Routes Variable: '${routesVariableName}ControllerRoutes'`); return `import { RouteTree, RouterModule } from '@nestjs/core'; import { Module } from '@nestjs/common'; import { ${className}Module } from '${moduleRelativePath}'; const ${routesVariableName}ControllerRoutes: RouteTree[] = [ ${routeStructure} ]; @Module({ imports: [RouterModule.register(${routesVariableName}ControllerRoutes)] }) export class ${className}RoutesModule {}`; } function generateIndexFile(entityInfo) { const { className } = entityInfo; return `export * from './${className.toLowerCase()}.dto';`; } function addRoutesToIndex(entityInfo, routesDir, entityFilePath) { const indexFilePath = path.join(routesDir, "index.ts"); if (!fs.existsSync(indexFilePath)) { console.log("âš ī¸ Warning: routes/index.ts file not found"); return; } const indexContent = fs.readFileSync(indexFilePath, "utf-8"); const highestParent = getHighestParentDirectory(entityFilePath); const routeFileName = highestParent ? highestParent.toLowerCase() : entityInfo.name; const relativePath = highestParent ? `./${highestParent}/${routeFileName}.routes` : `./${routeFileName}.routes`; const exportStatement = `export * from '${relativePath}';`; // Check if the export already exists if (indexContent.includes(exportStatement)) { console.log("â„šī¸ Routes export already exists in routes/index.ts"); return; } // Add the export statement at the end of the file const updatedContent = indexContent.trim() + "\n" + exportStatement + "\n"; fs.writeFileSync(indexFilePath, updatedContent); console.log(`✅ Added routes export to ${indexFilePath}`); } function addRoutesToModule(entityInfo, routesDir) { const moduleFilePath = path.join(routesDir, "routes.module.ts"); if (!fs.existsSync(moduleFilePath)) { console.log("âš ī¸ Warning: routes.module.ts file not found"); return; } const moduleContent = fs.readFileSync(moduleFilePath, "utf-8"); const routesModuleName = `${entityInfo.className}RoutesModule`; const importLine = ` ${routesModuleName},`; const lines = moduleContent.split("\n"); // Find destructured import block let importStart = -1; let importEnd = -1; for (let i = 0; i < lines.length; i++) { if (lines[i].trim().startsWith("import {")) importStart = i; if (importStart !== -1 && lines[i].includes("} from '.';")) { importEnd = i; break; } } if (importStart === -1 || importEnd === -1) { console.log("âš ī¸ Warning: Could not find destructured import block in routes.module.ts"); return; } // Check if already in destructured import block const importBlock = lines.slice(importStart + 1, importEnd); if (!importBlock.some((line) => line.trim() === importLine.trim())) { // Insert in alphabetical order importBlock.push(importLine); importBlock.sort((a, b) => a.trim().localeCompare(b.trim())); const newImportBlock = [ lines[importStart], ...importBlock, lines[importEnd], ]; // Replace old import block lines.splice(importStart, importEnd - importStart + 1, ...newImportBlock); } // Add to imports array in @Module let importsInsertIndex = -1; for (let i = 0; i < lines.length; i++) { if (lines[i].includes("ParentPortalRoutesModule,")) { importsInsertIndex = i + 1; break; } } if (importsInsertIndex === -1) { console.log("âš ī¸ Warning: Could not find imports array in routes.module.ts"); return; } // Only add if not already present const importsArrayBlock = lines .slice(Math.max(0, importsInsertIndex - 30), importsInsertIndex + 30) .join("\n"); if (!importsArrayBlock.includes(importLine.trim())) { lines.splice(importsInsertIndex, 0, importLine); } const updatedContent = lines.join("\n"); fs.writeFileSync(moduleFilePath, updatedContent); console.log(`✅ Added routes module to routes.module.ts`); } function addPermissionItemToEnum(entityInfo, entitiesDir) { const permissionItemPath = path.join(entitiesDir, "PermissionItem.entity.ts"); if (!fs.existsSync(permissionItemPath)) { console.log("âš ī¸ Warning: PermissionItem.entity.ts file not found"); return; } const content = fs.readFileSync(permissionItemPath, "utf-8"); const permissionName = entityInfo.className.toUpperCase() + "S"; const permissionValue = entityInfo.className + "s Page"; // Check if the permission already exists (check for both the name and value) if (content.includes(`${permissionName} =`) || content.includes(`'${permissionValue}'`)) { console.log(`â„šī¸ Permission item ${permissionName} already exists in PERMISSION_ITEM_NAME enum`); return; } // Find the last permission item in the enum (before the closing brace) const lines = content.split("\n"); let insertIndex = -1; // Look for the last permission item before the closing brace for (let i = lines.length - 1; i >= 0; i--) { if (lines[i].includes("CAN_ASSIGN_CLASS = 'Can Assign Class'")) { insertIndex = i + 1; // Insert after this line break; } } if (insertIndex === -1) { // Fallback: look for the closing brace of the enum for (let i = 0; i < lines.length; i++) { if (lines[i].trim() === "}" && i > 0 && lines[i - 1].includes("CAN_ASSIGN_CLASS")) { insertIndex = i; break; } } } if (insertIndex === -1) { console.log("âš ī¸ Warning: Could not find insertion point in PERMISSION_ITEM_NAME enum"); return; } // Insert the new permission item before the closing brace const newPermissionLine = ` ${permissionName} = '${permissionValue}',`; lines.splice(insertIndex, 0, newPermissionLine); const updatedContent = lines.join("\n"); fs.writeFileSync(permissionItemPath, updatedContent); console.log(`✅ Added permission item ${permissionName} to PERMISSION_ITEM_NAME enum`); } function addEntityToIndex(entityInfo, entitiesDir, originalFileName, entityFilePath) { const indexFilePath = path.join(entitiesDir, "index.ts"); if (!fs.existsSync(indexFilePath)) { console.log("âš ī¸ Warning: index.ts file not found in entities directory"); return; } // Calculate relative path from entities index.ts to the entity file const entityDir = path.dirname(entityFilePath); const relativePath = path.relative(entitiesDir, entityDir); const exportPath = relativePath === "." ? `./${originalFileName.replace(".entity.ts", ".entity")}` : `./${relativePath}/${originalFileName.replace(".entity.ts", ".entity")}`; const indexContent = fs.readFileSync(indexFilePath, "utf-8"); const exportStatement = `export * from '${exportPath}';`; // Check if the export already exists if (indexContent.includes(exportStatement)) { console.log("â„šī¸ Entity export already exists in index.ts"); return; } // Add the export statement at the end of the file const updatedContent = indexContent.trim() + "\n" + exportStatement + "\n"; fs.writeFileSync(indexFilePath, updatedContent); console.log(`✅ Added entity export to ${indexFilePath}`); } function createDirectoryIfNotExists(dirPath) { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } function generateFrontendInterface(entityInfo) { const { className, fields } = entityInfo; // Collect relationship types for imports const relationshipTypes = new Set(); const interfaceFields = fields .filter((field) => { // Exclude fields with @Exclude decorator if (field.decorators.some((d) => d.includes("@Exclude"))) { return false; } // Include all fields, including relational ones return true; }) .map((field) => { // Convert TypeORM types to TypeScript types let tsType = field.type; let isOptional = field.isOptional; // Handle common type conversions if (tsType.includes("number")) { if (tsType.includes("[]")) { tsType = "number[]"; } else { tsType = "number"; } } else if (tsType.includes("string")) { tsType = "string"; } else if (tsType.includes("boolean")) { tsType = "boolean"; } else if (tsType.includes("Date")) { tsType = "Date"; } else { // Handle nullable types first if (tsType.includes("| null")) { isOptional = true; tsType = tsType.replace("| null", ""); } // Handle relationship types - add 'I' prefix and handle arrays if (tsType.includes("[]")) { // Array type const baseType = tsType.replace("[]", "").trim(); tsType = `I${baseType}[]`; relationshipTypes.add(baseType.trim()); } else { // Single relationship type tsType = `I${tsType}`; relationshipTypes.add(tsType.replace("I", "").trim()); } } return ` ${field.name}${isOptional ? "?" : ""}: ${tsType.trim()};`; }) .join("\n"); // Generate imports for relationship types const imports = Array.from(relationshipTypes) .map((type) => `import { I${type} } from '../I${type}';`) .join("\n"); const interfaceContent = imports ? `${imports}\n\n` : ""; return `${interfaceContent}export interface I${className} { ${interfaceFields} } `; } function findFrontendDirectory(entityFilePath) { const resolved = path.resolve(entityFilePath); const parts = resolved.split(path.sep); const srcIdx = parts.lastIndexOf("src"); if (srcIdx === -1) throw new Error("No src dir in path"); // Go up from src to find the project root (where Backend and Frontend are) const projectRoot = parts.slice(0, srcIdx - 1).join(path.sep); const frontendDir = path.join(projectRoot, "Frontend"); return frontendDir; } function getEntitySubPath(entityFilePath) { const resolved = path.resolve(entityFilePath); const parts = resolved.split(path.sep); const entitiesIdx = parts.lastIndexOf("entities"); if (entitiesIdx === -1) return ""; // Get the path from entities to the entity file (excluding the file itself) const entitySubPath = parts.slice(entitiesIdx + 1, -1).join(path.sep); return entitySubPath; } function getEntityNestingLevel(entityFilePath) { const resolved = path.resolve(entityFilePath); const parts = resolved.split(path.sep); const entitiesIdx = parts.lastIndexOf("entities"); if (entitiesIdx === -1) return 0; // Count the number of directories from entities to the entity file return parts.slice(entitiesIdx + 1, -1).length; } function getEntityFullSubPath(entityFilePath) { const resolved = path.resolve(entityFilePath); const parts = resolved.split(path.sep); const entitiesIdx = parts.lastIndexOf("entities"); if (entitiesIdx === -1) return ""; // Get the full path from entities to the entity file (excluding the file itself) const entitySubPath = parts.slice(entitiesIdx + 1, -1).join(path.sep); return entitySubPath; } function getEntityParentDirectories(entityFilePath) { const resolved = path.resolve(entityFilePath); const parts = resolved.split(path.sep); const entitiesIdx = parts.lastIndexOf("entities"); if (entitiesIdx === -1) return []; // Get all parent directories from entities to the entity file (excluding the file itself) return parts.slice(entitiesIdx + 1, -1); } function getHighestParentDirectory(entityFilePath) { const parentDirs = getEntityParentDirectories(entityFilePath); return parentDirs.length > 0 ? parentDirs[0] : ""; } function generateNestedRouteStructure(entityInfo, entityFilePath) { const parentDirs = getEntityParentDirectories(entityFilePath); const { className, name } = entityInfo; if (parentDirs.length === 0) { // No parent directories, simple route return `{ path: '${name}s', module: ${className}Module, }`; } // Build nested structure from bottom up let currentLevel = `{ path: '${name}s', module: ${className}Module, }`; // Add parent directories from bottom to top for (let i = parentDirs.length - 1; i >= 0; i--) { const parentDir = parentDirs[i].toLowerCase(); currentLevel = `{ path: '${parentDir}', children: [ ${currentLevel} ], }`; } return currentLevel; } function createFrontendInterface(entityInfo, entityFilePath) { // Calculate path to Frontend directory using the same logic as Backend const frontendDir = findFrontendDirectory(entityFilePath); const entitySubPath = getEntitySubPath(entityFilePath); const frontendInterfacesDir = path.join(frontendDir, "src", "app", "shared", "interfaces", entitySubPath); // Create interfaces directory if it doesn't exist if (!fs.existsSync(frontendInterfacesDir)) { fs.mkdirSync(frontendInterfacesDir, { recursive: true }); } const interfaceContent = generateFrontendInterface(entityInfo); const interfaceFilePath = path.join(frontendInterfacesDir, `I${entityInfo.className}.interface.ts`); fs.writeFileSync(interfaceFilePath, interfaceContent); console.log(`✅ Created Frontend interface: ${interfaceFilePath}`); } function addFrontendInterfaceToIndex(entityInfo, entityFilePath) { const frontendDir = findFrontendDirectory(entityFilePath); const entitySubPath = getEntitySubPath(entityFilePath); const rootInterfacesDir = path.join(frontendDir, "src", "app", "shared", "interfaces"); const indexFilePath = path.join(rootInterfacesDir, "index.ts"); // Calculate relative path from root interfaces to the nested interface file const relativePath = entitySubPath ? `./${entitySubPath}/I${entityInfo.className}.interface` : `./I${entityInfo.className}.interface`; const exportStatement = `export * from '${relativePath}';`; if (!fs.existsSync(indexFilePath)) { fs.writeFileSync(indexFilePath, exportStatement + "\n"); console.log(`✅ Created index.ts and added export for ${entityInfo.name}.interface.ts`); return; } const indexContent = fs.readFileSync(indexFilePath, "utf-8"); if (indexContent.includes(exportStatement)) { console.log(`â„šī¸ Interface export already exists in interfaces/index.ts`); return; } fs.appendFileSync(indexFilePath, exportStatement + "\n"); console.log(`✅ Added interface export to interfaces/index.ts`); } function generateFrontendService(entityInfo, entityFilePath) { const { className, name } = entityInfo; const interfaceName = `I${className}`; // Calculate relative path from service to interface based on nesting level const entitySubPath = getEntitySubPath(entityFilePath); const nestingLevel = getEntityNestingLevel(entityFilePath); // Calculate the number of "../" needed to go up from the service directory to the shared directory // Service is at: Frontend/src/app/shared/services/{entitySubPath}/ // Interface is at: Frontend/src/app/shared/interfaces/{entitySubPath}/ // We need to go up (nestingLevel + 1) levels to reach shared, then down to interfaces const upLevels = nestingLevel + 1; const upPath = "../".repeat(upLevels); const interfacePath = entitySubPath ? `${upPath}interfaces/${entitySubPath}/I${className}.interface` : `${upPath}interfaces/I${className}.interface`; const interfaceImport = `import { ${interfaceName} } from '${interfacePath}';`; // Build the API URL path based on entity subdirectory structure // Convert directories to lowercase for API URL const apiUrlPath = entitySubPath ? `${entitySubPath.toLowerCase()}/${name}` : name; // Calculate the path to environments directory // Service is at: Frontend/src/app/shared/services/{entitySubPath}/ // Environments is at: Frontend/src/environments/ // We need to go up (nestingLevel + 4) levels to reach src, then down to environments const envUpLevels = nestingLevel + 3; const envPath = "../".repeat(envUpLevels); console.log(`🔍 Debug - Entity: ${entityInfo.className}, Nesting Level: ${nestingLevel}, Env Up Levels: ${envUpLevels}, Env Path: '${envPath}'`); return `import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { environment } from '${envPath}environments/environment'; ${interfaceImport} @Injectable({ providedIn: 'root' }) export class ${className}Service { private readonly apiUrl = \`\${environment.baseApiUrl}/${apiUrlPath}\`; constructor(private http: HttpClient) {} /** * Get all ${name}s * @returns Observable of ${name} array */ getAll(): Observable<${interfaceName}[]> { return this.http.get<${interfaceName}[]>(this.apiUrl); } /** * Get a ${name} by id * @param id ${className} id * @returns Observable of ${name} */ getById(id: number): Observable<${interfaceName}> { return this.http.get<${interfaceName}>(this.apiUrl + '/' + id); } /** * Create a new ${name} * @param data ${className} data to create * @returns Observable of created ${name} */ create(data: Partial<${interfaceName}>): Observable<${interfaceName}> { return this.http.post<${interfaceName}>(this.apiUrl, data); } /** * Update a ${name} * @param id ${className} id to update * @param data ${className} data to update * @returns Observable of updated ${name} */ patch(id: number, data: Partial<${interfaceName}>): Observable<${interfaceName}> { return this.http.patch<${interfaceName}>(this.apiUrl + '/' + id, data); } /** * Delete a ${name} * @param id ${className} id to delete * @returns Observable of void */ delete(id: number): Observable<void> { return this.http.delete<void>(this.apiUrl + '/' + id); } } `; } function addFrontendServiceToIndex(entityInfo, entityFilePath) { const frontendDir = findFrontendDirectory(entityFilePath); const entitySubPath = getEntitySubPath(entityFilePath); const rootServicesDir = path.join(frontendDir, "src", "app", "shared", "services"); const indexFilePath = path.join(rootServicesDir, "index.ts"); // Calculate relative path from root services to the nested service file const relativePath = entitySubPath ? `./${entitySubPath}/${entityInfo.name}.service` : `./${entityInfo.name}.service`; const exportStatement = `export * from '${relativePath}';`; if (!fs.existsSync(indexFilePath)) { fs.writeFileSync(indexFilePath, exportStatement + "\n"); console.log(`✅ Created index.ts and added export for ${entityInfo.name}.service.ts`); return; } const indexContent = fs.readFileSync(indexFilePath, "utf-8"); if (indexContent.includes(exportStatement)) { console.log(`â„šī¸ Service export already exists in services/index.ts`); return; } fs.appendFileSync(indexFilePath, exportStatement + "\n"); console.log(`✅ Added service export to services/index.ts`); } function createFrontendService(entityInfo, entityFilePath) { const frontendDir = findFrontendDirectory(entityFilePath); const entitySubPath = getEntitySubPath(entityFilePath); const frontendServicesDir = path.join(frontendDir, "src", "app", "shared", "services", entitySubPath); if (!fs.existsSync(frontendServicesDir)) { fs.mkdirSync(frontendServicesDir, { recursive: true }); } const serviceFilePath = path.join(frontendServicesDir, `${entityInfo.name}.service.ts`); if (fs.existsSync(serviceFilePath)) { console.log(`â„šī¸ Service file already exists: ${serviceFilePath}`); return; } const serviceContent = generateFrontendService(entityInfo, entityFilePath); fs.writeFileSync(serviceFilePath, serviceContent); console.log(`✅ Created Frontend service: ${serviceFilePath}`); // Add service to index file addFrontendServiceToIndex(entityInfo, entityFilePath); } function runFormatCommands(entityFilePath) { try { // Calculate paths to Backend and Frontend directories const backendDir = path.join(path.dirname(entityFilePath), "..", "..", ".."); const frontendDir = path.join(backendDir, "..", "Frontend"); // Check if Backend directory exists and has package.json const backendPackageJson = path.join(backendDir, "package.json"); if (fs.existsSync(backendPackageJson)) { console.log("🔄 Running format command on Backend package.json..."); const { execSync } = require("child_process"); try { execSync("npm run format", { cwd: backendDir, stdio: "inherit", }); console.log("✅ Backend package.json formatted successfully"); } catch (error) { console.log("âš ī¸ Warning: Could not run format command on Backend package.json"); } } else { console.log("âš ī¸ Warning: Backend package.json not found"); } // Check if Frontend directory exists and has package.json const frontendPackageJson = path.join(frontendDir, "package.json"); if (fs.existsSync(frontendPackageJson)) { console.log("🔄 Running format command on Frontend package.json..."); const { execSync } = require("child_process"); try { execSync("npm run format", { cwd: frontendDir, stdio: "inherit", }); console.log("✅ Frontend package.json formatted successfully"); } catch (error) { console.log("âš ī¸ Warning: Could not run format command on Frontend package.json"); } } else { console.log("âš ī¸ Warning: Frontend package.json not found"); } } catch (error) { console.log("âš ī¸ Warning: Error running format commands:", error instanceof Error ? error.message : String(error)); } } function findPermissionItemFile(entitiesDir) { const permissionItemPath = path.join(entitiesDir, "PermissionItem.entity.ts"); if (fs.existsSync(permissionItemPath)) { return permissionItemPath; } // Search recursively in subdirectories const searchPermissionItem = (dir) => { const items = fs.readdirSync(dir); for (const item of items) { const fullPath = path.join(dir, item); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { const found = searchPermissionItem(fullPath); if (found) return found; } else if (item === "PermissionItem.entity.ts") { return fullPath; } } return null; }; return searchPermissionItem(entitiesDir); } function findEntitiesIndexFile(entitiesDir) { const indexPath = path.join(entitiesDir, "index.ts"); if (fs.existsSync(indexPath)) { return indexPath; } // Search recursively in subdirectories const searchIndex = (dir) => { const items = fs.readdirSync(dir); for (const item of items) { const fullPath = path.join(dir, item); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { const found = searchIndex(fullPath); if (found) return found; } else if (item === "index.ts") { return fullPath; } } return null; }; return searchIndex(entitiesDir); } function findAppModuleFile(srcDir) { const appModulePath = path.join(srcDir, "app.module.ts"); if (fs.existsSync(appModulePath)) { return appModulePath; } // Search recursively in subdirectories const searchAppModule = (dir) => { const items = fs.readdirSync(dir); for (const item of items) { const fullPath = path.join(dir, item); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { const found = searchAppModule(fullPath); if (found) return found; } else if (item === "app.module.ts") { return fullPath; } } return null; }; return searchAppModule(srcDir); } function addModuleToAppModule(entityInfo, srcDir, resourceDir) { const appModulePath = findAppModuleFile(srcDir); if (!appModulePath) { console.log("âš ī¸ Warning: app.module.ts file not found"); return; } const appModuleContent = fs.readFileSync(appModulePath, "utf-8"); const { className, name } = entityInfo; // Calculate relative path from app.module.ts to the new module const relativePath = path.relative(path.dirname(appModulePath), resourceDir); const moduleImportPath = `./${relativePath}/${name}.module`; const moduleClassName = `${className}Module`; // Check if the import already exists const importStatement = `import { ${moduleClassName} } from '${moduleImportPath}';`; if (appModuleContent.includes(importStatement)) { console.log(`â„šī¸ Module import already exists in app.module.ts`); return; } // Add the import statement const lines = appModuleContent.split("\n"); let importInsertIndex = -1; // Find the last import statement for (let i = lines.length - 1; i >= 0; i--) { if (lines[i].trim().startsWith("import ")) { importInsertIndex = i + 1; break; } } if (importInsertIndex === -1) { console.log("âš ī¸ Warning: Could not find import statements in app.module.ts"); return; } // Insert the new import lines.splice(importInsertIndex, 0, importStatement); // Find the main @Module decorator's imports array let moduleDecoratorFound = false; let importsArrayStart = -1; let importsArrayEnd = -1; let bracketCount = 0; let inImportsArray = false; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Find the start of @Module decorator if (line.startsWith("@Module(")) { moduleDecoratorFound = true; bracketCount = 1; continue; } // Only look for imports after we've found @Module if (moduleDecoratorFound) { // Count brackets to track nesting level for (const char of line) { if (char === "{") bracketCount++; if (char === "}") bracketCount--; } // Look for the first imports: [ (not nested) if (line.includes("imports:") && line.includes("[") && bracketCount === 1) { importsArrayStart = i; inImportsArray = true; if (line.includes("]")) { importsArrayEnd = i; break; } } else if (inImportsArray && line.includes("]") && bracketCount === 1) { importsArrayEnd = i; break; } // If we've closed the @Module decorator, stop searching if (bracketCount === 0) break; } } if (importsArrayStart === -1 || importsArrayEnd === -1) { console.log("âš ī¸ Warning: Could not find imports array in main @Module decorator"); return; } // Check if module is already in imports array const importsArrayContent = lines .slice(importsArrayStart, importsArrayEnd + 1) .join("\n"); if (importsArrayContent.includes(moduleClassName)) { console.log(`â„šī¸ Module already exists in imports array`); return; } // Add the module to the imports array (as the last element) // Find the actual last element in the imports array let lastElementIndex = importsArrayEnd; let lastElementLine = lines[importsArrayEnd]; // Look backwards to find the last non-empty element for (let i = importsArrayEnd - 1; i >= importsArrayStart; i--) { const line = lines[i].trim(); if (line && !line.startsWith("//") && !line.startsWith("/*")) { lastElementIndex = i; lastElementLine = lines[i]; break; } } // Check if the last element already ends with a comma const trimmedLastElement = lastElementLine.trim(); const needsComma = !trimmedLastElement.endsWith(",") && !trimmedLastElement.endsWith("["); let newImportLine; if (needsComma) { newImportLine = lastElementLine.replace(/,$/, "").replace(/$/, ","); lines[lastElementIndex] = newImportLine; } // Add the new module on a new line before the closing bracket const newModuleLine = ` ${moduleClassName},`; lines.splice(importsArrayEnd, 0, newModuleLine); const updatedContent = lines.join("\n"); fs.writeFileSync(appModulePath, updatedContent); console.log(`✅ Added ${moduleClassName} to app.module.ts`); } function addRouteToExistingFile(entityInfo, entityFilePath, routeFileDir, routeFileName, newRoutesContent) { const routeFilePath = path.join(routeFileDir, routeFileName); if (!fs.existsSync(routeFilePath)) { // File doesn't exist, create it with the new content fs.writeFileSync(routeFilePath, newRoutesContent); console.log(`✅ Created new routes file: ${routeFilePath}`); return; } // File exists, merge the new route with existing routes let content = fs.readFileSync(routeFilePath, "utf-8"); // Add new import if it doesn't exist const newImport = `import { ${entityInfo.className}Module } from '../../${getEntityFullSubPath(entityFilePath)}/${entityInfo.name}/${entityInfo.name}.module';`; if (!content.includes(entityInfo.className + "Module")) { // Find the last import and add the new one const lines = content.split("\n"); let lastImportIndex = -1; for (let i = 0; i < lines.length; i++) { if (lines[i].trim().startsWith("import ")) { lastImportIndex = i; } } if (lastImportIndex !== -1) { lines.splice(lastImportIndex + 1, 0, newImport); content = lines.join("\n"); } } // Generate the new route object const newRouteObject = ` { path: '${entityInfo.name}s', module: ${entityInfo.className}Module, }`; // Find the children array and add the new route const lines = content.split("\n"); let childrenStartIndex = -1; let childrenEndIndex = -1; let braceCount = 0; let inChildrenArray = false; // Find the children array boundaries for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.includes("children: [")) { inChildrenArray = true; childrenStartIndex = i; braceCount = 0; } else if (inChildrenArray) { if (line.includes("{")) braceCount++; if (