initial commit

This commit is contained in:
Roland Schneider 2025-11-18 18:06:54 +01:00
commit e352f161bc
21 changed files with 12079 additions and 0 deletions

56
.gitignore vendored Normal file
View File

@ -0,0 +1,56 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

98
README.md Normal file
View File

@ -0,0 +1,98 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ npm install -g @nestjs/mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

19
dvbooking-cli.json Normal file
View File

@ -0,0 +1,19 @@
{
"server": {
"path": "../dvbooking/server",
"entitiesPath": "src/entity/"
},
"admin": {
"path": "../dvbooking/admin",
"modelsPath": "src/app/models",
"componentsPath": "src/app/components"
},
"database": {
"host": "localhost",
"port": 4301,
"username": "postgres",
"password": "test",
"database": "dvbooking",
"type": "postgres"
}
}

35
eslint.config.mjs Normal file
View File

@ -0,0 +1,35 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'commonjs',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
"prettier/prettier": ["error", { endOfLine: "auto" }],
},
},
);

8
nest-cli.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

11074
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

76
package.json Normal file
View File

@ -0,0 +1,76 @@
{
"name": "dvbooking-cli",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"cli": "ts-node src/main.ts"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"commander": "^11.1.0",
"nest-commander": "^3.20.1",
"pg": "^8.16.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.27"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

12
src/app.controller.ts Normal file
View File

@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

13
src/app.module.ts Normal file
View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
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';
@Module({
imports: [],
providers: [AppService,GenerateCommand,ConfigService,EntityGeneratorService,CrudGeneratorService],
})
export class AppModule {}

8
src/app.service.ts Normal file
View File

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@ -0,0 +1,52 @@
// src/commands/generate.command.ts
import { Command, CommandRunner } from 'nest-commander';
import { EntityGeneratorService } from '../services/entity-generator.service';
import { CrudGeneratorService } from '../services/crud-generator.service'; // <-- 1. Import
@Command({
name: 'generate',
description: 'Generate code for the dvbooking project',
aliases: ['g'],
arguments: '<type> <name>',
})
export class GenerateCommand extends CommandRunner {
constructor(
private readonly entityGeneratorService: EntityGeneratorService,
private readonly crudGeneratorService: CrudGeneratorService, // <-- 2. Inject
) {
super();
}
async run(passedParams: string[]): Promise<void> {
const [type, name] = passedParams;
if (!type || !name) {
console.error('Error: Missing required arguments: <type> and <name>.');
console.log('Example: npm run cli -- generate <type> <name>');
console.log('Types: entity, crud, all');
return;
}
switch (type) {
case 'entity':
await this.entityGeneratorService.generate(name);
break;
case 'crud':
await this.crudGeneratorService.generate(name);
break;
case 'all':
console.log('--- Generating Entity ---');
await this.entityGeneratorService.generate(name);
console.log('\n--- Generating CRUD Module ---');
await this.crudGeneratorService.generate(name);
console.log('\n✨ All backend files generated successfully! ✨');
break;
default:
console.log(`Generator for type "${type}" is not implemented yet.`);
break;
}
}
}

9
src/main.ts Normal file
View File

@ -0,0 +1,9 @@
import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module';
async function bootstrap() {
await CommandFactory.run(AppModule, ['log', 'error', 'warn']);
}
bootstrap();

View File

@ -0,0 +1,48 @@
// src/services/config.service.ts
import { Injectable } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';
// Define a type for our configuration structure for better autocompletion
export type CliConfig = {
server: {
path: string;
entitiesPath: string;
};
admin: {
path: string;
modelsPath: string;
componentsPath: string;
};
database: {
host: string;
port: number;
username: string;
password: string;
database: string;
type: 'postgres' | 'mysql' | 'mariadb'; // Add other supported types if needed
};
};
@Injectable()
export class ConfigService {
private readonly config: CliConfig;
constructor() {
// We assume the CLI is run from the root of the main project (dvbooking)
const configPath = path.join(process.cwd(), 'dvbooking-cli.json');
try {
const configFile = fs.readFileSync(configPath, 'utf8');
this.config = JSON.parse(configFile);
} catch (error) {
console.error('Error: Could not read or parse dvbooking-cli.json.');
console.error('Please ensure the file exists in the project root and is valid JSON.');
process.exit(1); // Exit the process if config is not found
}
}
// A getter method to safely access the config
public get(): CliConfig {
return this.config;
}
}

View File

@ -0,0 +1,305 @@
// src/services/crud-generator.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from './config.service';
import * as path from 'path';
import * as fs from 'fs';
import { promises as fsPromises } from 'fs';
interface NamingConvention {
singular: string;
plural: string;
pascal: string;
camel: string;
}
@Injectable()
export class CrudGeneratorService {
constructor(private readonly configService: ConfigService) {
}
public async generate(tableName: string): Promise<void> {
console.log(`Generating CRUD module for table: ${tableName}...`);
const names = this.getNamingConvention(tableName);
const config = this.configService.get();
const serverRoot = path.resolve(process.cwd(), config.server.path);
const moduleDir = path.join(serverRoot, 'src', names.singular);
try {
await this.generateModuleFile(names, moduleDir);
await this.generateControllerFile(names, moduleDir);
await this.generateServiceFile(names, moduleDir);
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!`);
} catch (error) {
console.error(`❌ An error occurred during CRUD generation:`, error.message);
}
}
// --- MISSING METHODS NOW INCLUDED ---
private async generateModuleFile(names: NamingConvention, moduleDir: string) {
const template = this.getModuleTemplate(names);
const filePath = path.join(moduleDir, `${names.plural}.module.ts`);
await fsPromises.mkdir(moduleDir, { recursive: true });
fs.writeFileSync(filePath, template);
}
private async generateControllerFile(names: NamingConvention, moduleDir: string) {
const template = this.getControllerTemplate(names);
const filePath = path.join(moduleDir, `${names.plural}.controller.ts`);
await fsPromises.mkdir(moduleDir, { recursive: true });
fs.writeFileSync(filePath, template);
}
private async generateServiceFile(names: NamingConvention, moduleDir: string) {
const template = this.getServiceTemplate(names);
const filePath = path.join(moduleDir, `${names.plural}.service.ts`);
await fsPromises.mkdir(moduleDir, { recursive: true });
fs.writeFileSync(filePath, template);
}
// --- END OF MISSING METHODS ---
private async generateDtoFiles(names: NamingConvention, moduleDir: string) {
const dtoDir = path.join(moduleDir, 'dto');
await fsPromises.mkdir(dtoDir, { recursive: true });
fs.writeFileSync(path.join(dtoDir, `create-${names.singular}.dto.ts`), this.getCreateDtoTemplate(names));
fs.writeFileSync(path.join(dtoDir, `update-${names.singular}.dto.ts`), this.getUpdateDtoTemplate(names));
fs.writeFileSync(path.join(dtoDir, `query-${names.singular}.dto.ts`), this.getQueryDtoTemplate(names));
}
// --- TEMPLATES ---
// (All the template methods from the previous response go here, unchanged)
private getQueryDtoTemplate(names: NamingConvention): string {
return `import { IsOptional, IsString, IsNumber, IsIn, IsBoolean } from 'class-validator';
import { Type } from 'class-transformer';
export class Query${names.pascal}Dto {
@IsOptional()
@Type(() => Number)
@IsNumber()
page?: number;
@IsOptional()
@Type(() => Number)
@IsNumber()
limit?: number;
@IsOptional()
@IsString()
sortBy?: string; // Should be a property of the ${names.pascal} entity
@IsOptional()
@IsIn(['ASC', 'DESC'])
order?: 'ASC' | 'DESC';
// --- Add other filterable properties below ---
// @IsOptional()
// @IsString()
// name?: string;
// @IsOptional()
// @Type(() => Boolean)
// @IsBoolean()
// is_available?: boolean;
}
`;
}
private getControllerTemplate(names: NamingConvention): string {
return `import { Controller, Get, Post, Body, Patch, Param, Delete, Query, ParseIntPipe } from '@nestjs/common';
import { ${names.pascal}sService } from './${names.plural}.service';
import { Create${names.pascal}Dto } from './dto/create-${names.singular}.dto';
import { Update${names.pascal}Dto } from './dto/update-${names.singular}.dto';
import { Query${names.pascal}Dto } from './dto/query-${names.singular}.dto';
@Controller('${names.plural}')
export class ${names.pascal}sController {
constructor(private readonly ${names.camel}sService: ${names.pascal}sService) {}
@Post()
create(@Body() create${names.pascal}Dto: Create${names.pascal}Dto) {
return this.${names.camel}sService.create(create${names.pascal}Dto);
}
@Get()
findAll(@Query() queryParams: Query${names.pascal}Dto) {
return this.${names.camel}sService.findAll(queryParams);
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.${names.camel}sService.findOne(id);
}
@Patch(':id')
update(@Param('id', ParseIntPipe) id: number, @Body() update${names.pascal}Dto: Update${names.pascal}Dto) {
return this.${names.camel}sService.update(id, update${names.pascal}Dto);
}
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.${names.camel}sService.remove(id);
}
}
`;
}
// In src/services/crud-generator.service.ts
private getServiceTemplate(names: NamingConvention): string {
const entityPath = path.relative(path.join('src', names.singular), path.join(this.configService.get().server.entitiesPath, `${names.singular}.entity`)).replace(/\\/g, '/');
return `import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindManyOptions, FindOptionsWhere, ILike } from 'typeorm';
import { Create${names.pascal}Dto } from './dto/create-${names.singular}.dto';
import { Update${names.pascal}Dto } from './dto/update-${names.singular}.dto';
import { Query${names.pascal}Dto } from './dto/query-${names.singular}.dto';
import { ${names.pascal} } from '${entityPath}';
type QueryConfigItem = {
param: keyof Omit<Query${names.pascal}Dto, 'page' | 'limit' | 'sortBy' | 'order'>;
dbField: keyof ${names.pascal};
operator: 'equals' | 'like';
};
@Injectable()
export class ${names.pascal}sService {
constructor(
@InjectRepository(${names.pascal})
private readonly ${names.camel}Repository: Repository<${names.pascal}>,
) {}
create(create${names.pascal}Dto: Create${names.pascal}Dto) {
const newRecord = this.${names.camel}Repository.create(create${names.pascal}Dto);
return this.${names.camel}Repository.save(newRecord);
}
async findAll(queryParams: Query${names.pascal}Dto) {
const { page = 1, limit = 0, sortBy, order, ...filters } = queryParams;
const queryConfig: QueryConfigItem[] = [
// Example: { param: 'name', dbField: 'name', operator: 'like' },
// Example: { param: 'is_available', dbField: 'is_available', operator: 'equals' },
];
// --- START OF THE FIX ---
// 1. Create a loosely typed object to build the where clause.
const whereClause: { [key: string]: any } = {};
// 2. Populate it dynamically. This avoids the TypeScript error.
for (const config of queryConfig) {
if (filters[config.param] !== undefined) {
if (config.operator === 'like') {
whereClause[config.dbField] = ILike(\`%\${filters[config.param]}%\`);
} else {
whereClause[config.dbField] = filters[config.param];
}
}
}
// 3. Assign the complete, built clause to the strongly-typed options.
const findOptions: FindManyOptions<${names.pascal}> = {
where: whereClause as FindOptionsWhere<${names.pascal}>,
};
// --- END OF THE FIX ---
const paginated = limit > 0;
if (paginated) {
findOptions.skip = (page - 1) * limit;
findOptions.take = limit;
}
if (sortBy && order) {
findOptions.order = { [sortBy]: order };
}
const [data, totalItems] = await this.${names.camel}Repository.findAndCount(findOptions);
if (!paginated) {
return { data, total: data.length };
}
return {
data,
meta: {
totalItems,
itemCount: data.length,
itemsPerPage: limit,
totalPages: Math.ceil(totalItems / limit),
currentPage: page,
},
};
}
async findOne(id: number) {
const record = await this.${names.camel}Repository.findOneBy({ id: id as any });
if (!record) {
throw new NotFoundException(\`${names.pascal} with ID \${id} not found\`);
}
return record;
}
async update(id: number, update${names.pascal}Dto: Update${names.pascal}Dto) {
const record = await this.findOne(id);
Object.assign(record, update${names.pascal}Dto);
return this.${names.camel}Repository.save(record);
}
async remove(id: number) {
const result = await this.${names.camel}Repository.delete(id);
if (result.affected === 0) {
throw new NotFoundException(\`${names.pascal} with ID \${id} not found\`);
}
return { deleted: true, id };
}
}
`;
}
private getModuleTemplate(names: NamingConvention): string {
const entityPath = path.relative(path.join('src', names.singular), path.join(this.configService.get().server.entitiesPath, `${names.singular}.entity`)).replace(/\\/g, '/');
return `import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { ${names.pascal}sService } from './${names.plural}.service';\nimport { ${names.pascal}sController } from './${names.plural}.controller';\nimport { ${names.pascal} } from '${entityPath}';\n\n@Module({\n imports: [TypeOrmModule.forFeature([${names.pascal}])],\n controllers: [${names.pascal}sController],\n providers: [${names.pascal}sService],\n})\nexport class ${names.pascal}sModule {}\n`;
}
private getCreateDtoTemplate(names: NamingConvention): string {
const entityPath = path.relative(path.join('src', names.singular, 'dto'), path.join(this.configService.get().server.entitiesPath, `${names.singular}.entity`)).replace(/\\/g, '/');
return `import { OmitType } from '@nestjs/mapped-types';\nimport { ${names.pascal} } from '${entityPath}';\n\n// NOTE: Use class-validator decorators here for production-grade validation.\nexport class Create${names.pascal}Dto extends OmitType(${names.pascal}, ['id']) {}\n`;
}
private getUpdateDtoTemplate(names: NamingConvention): string {
return `import { PartialType } from '@nestjs/mapped-types';\nimport { Create${names.pascal}Dto } from './create-${names.singular}.dto';\n\nexport class Update${names.pascal}Dto extends PartialType(Create${names.pascal}Dto) {}\n`;
}
private getNamingConvention(tableName: string): NamingConvention {
const singular = this.toSingular(tableName);
return {
singular: singular,
plural: tableName,
pascal: this.toPascalCase(singular),
camel: this.toCamelCase(singular),
};
}
private toSingular(name: string): string {
if (name.endsWith('ies')) return name.slice(0, -3) + 'y';
return name.endsWith('s') ? name.slice(0, -1) : name;
}
private toPascalCase(text: string): string {
return text.replace(/(^\w|-\w|_w)/g, (c) => c.replace(/[-_]/, '').toUpperCase());
}
private toCamelCase(text: string): string {
const pascal = this.toPascalCase(text);
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
}
}

View File

@ -0,0 +1,177 @@
// src/services/entity-generator.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from './config.service';
import { DataSource, DataSourceOptions, TableColumn } from 'typeorm';
import * as path from 'path';
import * as fs from 'fs';
import { promises as fsPromises } from 'fs';
@Injectable()
export class EntityGeneratorService {
constructor(private readonly configService: ConfigService) {}
public async generate(tableName: string): Promise<void> {
console.log(`Generating entity for table: ${tableName}...`);
const config = this.configService.get();
const dbConfig = config.database;
const dataSource = new DataSource(dbConfig as DataSourceOptions);
try {
await dataSource.initialize();
console.log('Database connection successful.');
const queryRunner = dataSource.createQueryRunner();
const table = await queryRunner.getTable(tableName);
if (!table) {
throw new Error(`Table "${tableName}" not found in database.`);
}
const columns = table.columns;
console.log(`Found ${columns.length} columns in table "${tableName}".`);
const entityContent = this.createEntityTemplate(tableName, columns);
const singularName = this.toSingular(tableName);
const entityFileName = `${singularName}.entity.ts`;
const serverRoot = path.resolve(process.cwd(), config.server.path);
const entitiesDir = path.join(serverRoot, config.server.entitiesPath);
const outputPath = path.join(entitiesDir, entityFileName);
await fsPromises.mkdir(entitiesDir, { recursive: true });
fs.writeFileSync(outputPath, entityContent);
console.log(`✅ Entity created successfully at: ${outputPath}`);
} catch (error) {
console.error('❌ An error occurred:', error.message);
} finally {
if (dataSource.isInitialized) {
await dataSource.destroy();
console.log('Database connection closed.');
}
}
}
private createEntityTemplate(tableName: string, columns: TableColumn[]): string {
const className = this.toPascalCase(this.toSingular(tableName));
const props = columns
.map((col) => {
const typeormDecorator = this.getDecorator(col);
const validationDecorators = this.getValidationDecorators(col);
const tsType = this.mapDbToTsType(col.type);
const nullable = col.isNullable ? ' | null' : '';
const defaultValue = col.default ? ` = ${this.formatDefaultValue(col)}` : '';
const allDecorators = [typeormDecorator, validationDecorators].filter(Boolean).join('\n ');
return `\n ${allDecorators}\n ${col.name}: ${tsType}${nullable}${defaultValue};\n`;
})
.join('');
return `import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
import { IsString, IsNumber, IsBoolean, IsDate, IsOptional } from 'class-validator';
@Entity({ name: '${tableName}' })
export class ${className} {${props}
}
`;
}
private getValidationDecorators(column: TableColumn): string {
if (column.isPrimary && column.isGenerated) {
return '';
}
const decorators: string[] = [];
if (column.isNullable) {
decorators.push('@IsOptional()');
}
const tsType = this.mapDbToTsType(column.type);
switch (tsType) {
case 'string':
decorators.push('@IsString()');
break;
case 'number':
decorators.push('@IsNumber()');
break;
case 'boolean':
decorators.push('@IsBoolean()');
break;
case 'Date':
decorators.push('@IsDate()');
break;
}
return decorators.join('\n ');
}
// --- UPDATED METHOD ---
private getDecorator(column: TableColumn): string {
if (column.isPrimary && column.isGenerated) {
return `@PrimaryGeneratedColumn()`;
}
const columnOptions: string[] = [];
const isNumeric = ['numeric', 'decimal', 'float', 'double', 'real'].includes(column.type);
// Add the explicit type if the column is nullable OR if it's a specific numeric type.
// This is more robust for TypeORM.
if (column.isNullable || isNumeric) {
columnOptions.push(`type: '${column.type}'`);
}
if (column.isNullable) {
columnOptions.push('nullable: true');
}
if (column.default) {
columnOptions.push(`default: ${this.formatDefaultValue(column)}`);
}
// Return an empty decorator if no options are needed, for cleaner output.
if (columnOptions.length === 0) {
return `@Column()`;
}
return `@Column({ ${columnOptions.join(', ')} })`;
}
// --- NEW HELPER METHOD ---
private formatDefaultValue(column: TableColumn): string {
if (column.default === null || column.default === undefined) {
return column.default;
}
const isStringLike = ['varchar', 'text', 'char', 'character varying'].some(t => column.type.includes(t));
if (isStringLike) {
// Properly escape single quotes in the default value itself
const escapedDefault = column.default.replace(/'/g, "''");
return `'${escapedDefault}'`;
}
return column.default;
}
private mapDbToTsType(dbType: string): string {
if (dbType.includes('int') || dbType.includes('serial')) return 'number';
if (['float', 'double', 'decimal', 'numeric', 'real'].includes(dbType)) return 'number';
if (dbType.includes('char') || dbType.includes('text') || dbType === 'uuid') return 'string';
if (dbType === 'boolean' || dbType === 'bool') return 'boolean';
if (dbType.includes('date') || dbType.includes('time')) return 'Date';
return 'any';
}
private toSingular(name: string): string {
if (name.endsWith('ies')) {
return name.slice(0, -3) + 'y';
}
return name.endsWith('s') ? name.slice(0, -1) : name;
}
private toPascalCase(text: string): string {
return text.replace(/(^\w|-\w|_w)/g, (c) => c.replace(/[-_]/, '').toUpperCase());
}
}

25
test/app.e2e-spec.ts Normal file
View File

@ -0,0 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

9
test/jest-e2e.json Normal file
View File

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

4
tsconfig.build.json Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

25
tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"resolvePackageJsonExports": true,
"esModuleInterop": true,
"isolatedModules": true,
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false
}
}