initial commit
This commit is contained in:
commit
e352f161bc
56
.gitignore
vendored
Normal file
56
.gitignore
vendored
Normal 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
4
.prettierrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
98
README.md
Normal file
98
README.md
Normal 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>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](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
19
dvbooking-cli.json
Normal 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
35
eslint.config.mjs
Normal 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
8
nest-cli.json
Normal 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
11074
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
76
package.json
Normal file
76
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
22
src/app.controller.spec.ts
Normal file
22
src/app.controller.spec.ts
Normal 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
12
src/app.controller.ts
Normal 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
13
src/app.module.ts
Normal 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
8
src/app.service.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
||||
52
src/commands/generate.command.ts
Normal file
52
src/commands/generate.command.ts
Normal 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
9
src/main.ts
Normal 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();
|
||||
48
src/services/config.service.ts
Normal file
48
src/services/config.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
305
src/services/crud-generator.service.ts
Normal file
305
src/services/crud-generator.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
177
src/services/entity-generator.service.ts
Normal file
177
src/services/entity-generator.service.ts
Normal 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
25
test/app.e2e-spec.ts
Normal 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
9
test/jest-e2e.json
Normal 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
4
tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user