From c5addf58d369d9728f0e3f414d01a98ddcb3183c Mon Sep 17 00:00:00 2001 From: Roland Schneider Date: Tue, 18 Nov 2025 18:32:52 +0100 Subject: [PATCH] generate server side --- package-lock.json | 98 ++++++++++++++++++- package.json | 1 + src/app.module.ts | 3 +- src/services/crud-generator.service.ts | 13 ++- src/services/entity-generator.service.ts | 14 ++- src/services/module-updater.service.ts | 115 +++++++++++++++++++++++ 6 files changed, 237 insertions(+), 7 deletions(-) create mode 100644 src/services/module-updater.service.ts diff --git a/package-lock.json b/package-lock.json index 178dbb9..a79fac2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "pg": "^8.16.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "ts-morph": "^27.0.2", "typeorm": "^0.3.27" }, "devDependencies": { @@ -1369,7 +1370,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, "license": "MIT", "engines": { "node": "20 || >=22" @@ -1379,7 +1379,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, "license": "MIT", "dependencies": { "@isaacs/balanced-match": "^4.0.1" @@ -2703,6 +2702,32 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, + "node_modules/@ts-morph/common": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.28.1.tgz", + "integrity": "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==", + "license": "MIT", + "dependencies": { + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1", + "tinyglobby": "^0.2.14" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", @@ -4652,6 +4677,12 @@ "node": ">= 0.12.0" } }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "license": "MIT" + }, "node_modules/collect-v8-coverage": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", @@ -5621,6 +5652,23 @@ "bser": "2.1.1" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -8285,6 +8333,12 @@ "node": ">= 0.8" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8459,7 +8513,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -9827,6 +9881,34 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -9998,6 +10080,16 @@ "webpack": "^5.0.0" } }, + "node_modules/ts-morph": { + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-27.0.2.tgz", + "integrity": "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==", + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.28.1", + "code-block-writer": "^13.0.3" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", diff --git a/package.json b/package.json index 3446165..e8e1d77 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "pg": "^8.16.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "ts-morph": "^27.0.2", "typeorm": "^0.3.27" }, "devDependencies": { diff --git a/src/app.module.ts b/src/app.module.ts index cc2ae6d..674f423 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,9 +5,10 @@ import { GenerateCommand } from './commands/generate.command'; import { ConfigService } from './services/config.service'; import { EntityGeneratorService } from './services/entity-generator.service'; import { CrudGeneratorService } from './services/crud-generator.service'; +import { ModuleUpdaterService } from './services/module-updater.service'; @Module({ imports: [], - providers: [AppService,GenerateCommand,ConfigService,EntityGeneratorService,CrudGeneratorService], + providers: [AppService,GenerateCommand,ConfigService,EntityGeneratorService,CrudGeneratorService,ModuleUpdaterService], }) export class AppModule {} diff --git a/src/services/crud-generator.service.ts b/src/services/crud-generator.service.ts index a294df4..fce24ec 100644 --- a/src/services/crud-generator.service.ts +++ b/src/services/crud-generator.service.ts @@ -4,6 +4,7 @@ import { ConfigService } from './config.service'; import * as path from 'path'; import * as fs from 'fs'; import { promises as fsPromises } from 'fs'; +import { ModuleUpdaterService } from './module-updater.service'; interface NamingConvention { singular: string; @@ -14,7 +15,7 @@ interface NamingConvention { @Injectable() export class CrudGeneratorService { - constructor(private readonly configService: ConfigService) { + constructor(private readonly configService: ConfigService, private readonly moduleUpdaterService: ModuleUpdaterService,) { } public async generate(tableName: string): Promise { @@ -31,7 +32,15 @@ export class CrudGeneratorService { await this.generateDtoFiles(names, moduleDir); console.log(`āœ… CRUD module for "${tableName}" created successfully in: ${moduleDir}`); - console.warn(`\nšŸ”” Action Required: Remember to import ${names.pascal}sModule into your main app.module.ts!`); + + // --- 3. CALL THE UPDATER --- + const moduleFileName = `${names.plural}.module.ts`; + const fullModulePath = path.join(moduleDir, moduleFileName); + await this.moduleUpdaterService.addImportToAppModule(`${names.pascal}sModule`, fullModulePath); + // --- END OF CALL --- + + // We can now remove the old warning! + console.log('\n✨ AppModule has been updated automatically! ✨'); } catch (error) { console.error(`āŒ An error occurred during CRUD generation:`, error.message); diff --git a/src/services/entity-generator.service.ts b/src/services/entity-generator.service.ts index 1a8fbd8..fd5d014 100644 --- a/src/services/entity-generator.service.ts +++ b/src/services/entity-generator.service.ts @@ -5,10 +5,13 @@ import { DataSource, DataSourceOptions, TableColumn } from 'typeorm'; import * as path from 'path'; import * as fs from 'fs'; import { promises as fsPromises } from 'fs'; +import { ModuleUpdaterService } from './module-updater.service'; @Injectable() export class EntityGeneratorService { - constructor(private readonly configService: ConfigService) {} + constructor(private readonly configService: ConfigService, + private readonly moduleUpdaterService: ModuleUpdaterService) {} + public async generate(tableName: string): Promise { console.log(`Generating entity for table: ${tableName}...`); @@ -35,6 +38,12 @@ export class EntityGeneratorService { const entityContent = this.createEntityTemplate(tableName, columns); const singularName = this.toSingular(tableName); + + // --- THIS IS THE FIX --- + // Define className here so it can be used below. + const className = this.toPascalCase(singularName); + // --- END OF FIX --- + const entityFileName = `${singularName}.entity.ts`; const serverRoot = path.resolve(process.cwd(), config.server.path); const entitiesDir = path.join(serverRoot, config.server.entitiesPath); @@ -44,6 +53,9 @@ export class EntityGeneratorService { fs.writeFileSync(outputPath, entityContent); console.log(`āœ… Entity created successfully at: ${outputPath}`); + // Now the call will work correctly + await this.moduleUpdaterService.addEntityToTypeOrm(className, outputPath); + } catch (error) { console.error('āŒ An error occurred:', error.message); } finally { diff --git a/src/services/module-updater.service.ts b/src/services/module-updater.service.ts new file mode 100644 index 0000000..f9b1e3e --- /dev/null +++ b/src/services/module-updater.service.ts @@ -0,0 +1,115 @@ +// src/services/module-updater.service.ts +import { Injectable } from '@nestjs/common'; +import { ConfigService } from './config.service'; +import { Project, PropertyAssignment, SyntaxKind, ObjectLiteralExpression, ArrayLiteralExpression, ArrowFunction } from 'ts-morph'; +import * as path from 'path'; + +@Injectable() +export class ModuleUpdaterService { + constructor(private readonly configService: ConfigService) {} + + public async addImportToAppModule(moduleNameToAdd: string, modulePath: string): Promise { + // ... (this method remains unchanged) + const config = this.configService.get(); + const serverRoot = path.resolve(process.cwd(), config.server.path); + const appModulePath = path.join(serverRoot, 'src', 'app.module.ts'); + + console.log(`Attempting to update ${appModulePath} for module import...`); + + const project = new Project(); + const sourceFile = project.addSourceFileAtPath(appModulePath); + + const relativeModulePath = path.relative(path.dirname(appModulePath), modulePath).replace(/\\/g, '/').replace('.ts', ''); + + const existingImport = sourceFile.getImportDeclaration(d => d.getModuleSpecifierValue() === `./${relativeModulePath}`); + if (!existingImport) { + sourceFile.addImportDeclaration({ + namedImports: [moduleNameToAdd], + moduleSpecifier: `./${relativeModulePath}`, + }); + console.log(`Added import for ${moduleNameToAdd}.`); + } else { + console.log(`Import for ${moduleNameToAdd} already exists.`); + } + + const appModuleClass = sourceFile.getClassOrThrow('AppModule'); + const moduleDecorator = appModuleClass.getDecoratorOrThrow('Module'); + const decoratorArg = moduleDecorator.getArguments()[0] as ObjectLiteralExpression; + + const importsProperty = decoratorArg.getProperty('imports') as PropertyAssignment; + if (!importsProperty) { + throw new Error('Could not find "imports" array in AppModule decorator.'); + } + + const importsArray = importsProperty.getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression); + + const moduleAlreadyExists = importsArray.getElements().some(elem => elem.getText() === moduleNameToAdd); + + if (!moduleAlreadyExists) { + importsArray.addElement(moduleNameToAdd); + console.log(`Added ${moduleNameToAdd} to the AppModule imports array.`); + } else { + console.log(`${moduleNameToAdd} is already present in the AppModule imports array.`); + } + + await sourceFile.save(); + console.log('AppModule saved successfully.'); + } + + // --- NEW METHOD --- + public async addEntityToTypeOrm(entityNameToAdd: string, entityPath: string): Promise { + const config = this.configService.get(); + const serverRoot = path.resolve(process.cwd(), config.server.path); + const appModulePath = path.join(serverRoot, 'src', 'app.module.ts'); + + console.log(`Attempting to update ${appModulePath} for entity registration...`); + + const project = new Project(); + const sourceFile = project.addSourceFileAtPath(appModulePath); + + // 1. Add the import for the entity + const relativeEntityPath = path.relative(path.dirname(appModulePath), entityPath).replace(/\\/g, '/').replace('.ts', ''); + + const existingImport = sourceFile.getImportDeclaration(d => d.getModuleSpecifierValue() === `./${relativeEntityPath}`); + if (!existingImport) { + sourceFile.addImportDeclaration({ + namedImports: [entityNameToAdd], + moduleSpecifier: `./${relativeEntityPath}`, + }); + console.log(`Added import for entity ${entityNameToAdd}.`); + } else { + console.log(`Import for entity ${entityNameToAdd} already exists.`); + } + + // 2. Find the 'moduleTypeOrm' variable declaration + const variableDeclaration = sourceFile.getVariableDeclarationOrThrow('moduleTypeOrm'); + + // 3. Navigate through the AST to find the 'entities' array + const useFactory = variableDeclaration + .getInitializerIfKindOrThrow(SyntaxKind.CallExpression) + .getArguments()[0] + .asKindOrThrow(SyntaxKind.ObjectLiteralExpression) + .getPropertyOrThrow('useFactory') + .asKindOrThrow(SyntaxKind.PropertyAssignment); + + const factoryFunction = useFactory.getInitializerIfKindOrThrow(SyntaxKind.ArrowFunction); + const returnStatement = factoryFunction.getFirstDescendantByKindOrThrow(SyntaxKind.ReturnStatement); + + const returnedObject = returnStatement.getExpressionIfKindOrThrow(SyntaxKind.ObjectLiteralExpression); + const entitiesProperty = returnedObject.getProperty('entities')!.asKindOrThrow(SyntaxKind.PropertyAssignment); + const entitiesArray = entitiesProperty.getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression); + + // 4. Check if the entity is already in the array and add it if not + const entityAlreadyExists = entitiesArray.getElements().some(elem => elem.getText() === entityNameToAdd); + if (!entityAlreadyExists) { + entitiesArray.addElement(entityNameToAdd); + console.log(`Added ${entityNameToAdd} to the TypeORM entities array.`); + } else { + console.log(`${entityNameToAdd} is already in the TypeORM entities array.`); + } + + // 5. Save the file + await sourceFile.save(); + console.log('AppModule saved successfully after entity registration.'); + } +} \ No newline at end of file