generate server side

This commit is contained in:
Roland Schneider 2025-11-18 18:32:52 +01:00
parent e352f161bc
commit c5addf58d3
6 changed files with 237 additions and 7 deletions

98
package-lock.json generated
View File

@ -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",

View File

@ -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": {

View File

@ -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 {}

View File

@ -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<void> {
@ -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);

View File

@ -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<void> {
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 {

View File

@ -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<void> {
// ... (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<void> {
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.');
}
}