From 788acab14ec562ce25d948316de8624a14e74df5 Mon Sep 17 00:00:00 2001 From: Kyryll O Date: Tue, 26 May 2026 23:44:55 +0300 Subject: [PATCH] feat(cli): generate plugins with dual DB mode (self/provide) - manifest.json includes "database": "self" field - main.ts reads manifest and conditionally calls CoreService.setupDatabase() - TypeORM with PLUGIN_DB_* -> POSTGRES_* fallback for both modes - Example Item entity, controller, service with CRUD - @nestjs/typeorm, typeorm, pg added to generated package.json - Dynamic import for AppModule to support provide mode --- src/cli/index.ts | 178 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 165 insertions(+), 13 deletions(-) diff --git a/src/cli/index.ts b/src/cli/index.ts index fe78bc0..79aae52 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -43,6 +43,7 @@ function buildTemplates(name: string, port: number): FileTemplate[] { remoteEntry: `http://localhost:${port}/remoteEntry.js`, exposedModule: './App', roles: ['admin'], + database: 'self', }, null, 2) + '\n', }, { @@ -105,12 +106,15 @@ spec: '@nestjs/core': '^10.0.0', '@nestjs/microservices': '^10.0.0', '@nestjs/platform-express': '^10.0.0', + '@nestjs/typeorm': '^10.0.2', '@nestjs/serve-static': '^4.0.2', 'amqp-connection-manager': '^4.1.14', amqplib: '^0.10.3', 'erp-plugin-sdk': 'git+https://gitea.ss.3w.com.ua/erp/erp-plugin-sdk.git', + pg: '^8.12.0', 'reflect-metadata': '^0.1.13', rxjs: '^7.8.1', + typeorm: '^0.3.20', uuid: '^9.0.1', }, devDependencies: { @@ -181,14 +185,36 @@ CMD ["npm", "run", "start:prod"] }, { relativePath: 'backend/src/main.ts', - content: `import { getRabbitMQOptions, CoreService, RequestContextInterceptor } from 'erp-plugin-sdk'; + content: `import { CoreService, RequestContextInterceptor } from 'erp-plugin-sdk'; import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; import { Logger } from '@nestjs/common'; import * as path from 'path'; +import * as fs from 'fs'; async function bootstrap() { const logger = new Logger('PluginBootstrap'); + + // Read manifest to determine database mode + const manifestPath = path.join(process.cwd(), '..', 'manifest.json'); + if (fs.existsSync(manifestPath)) { + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); + if (manifest.database === 'provide') { + logger.log('Core-provisioned database mode detected. Setting up...'); + for (let i = 0; i < 10; i++) { + try { + await CoreService.setupDatabase({ pluginId: '${name}' }); + logger.log('Database provisioned successfully'); + break; + } catch (err: any) { + logger.warn(\`Database setup attempt \${i + 1} failed: \${err.message}\`); + if (i < 9) await new Promise(r => setTimeout(r, 3000)); + else throw err; + } + } + } + } + + const { AppModule } = await import('./app.module'); const app = await NestFactory.create(AppModule); app.useGlobalInterceptors(new RequestContextInterceptor()); @@ -214,6 +240,7 @@ async function bootstrap() { next(); }); + const { getRabbitMQOptions } = await import('erp-plugin-sdk'); app.connectMicroservice(getRabbitMQOptions('plugin_${snake}_queue')); app.enableCors(); @@ -227,7 +254,11 @@ async function bootstrap() { logger.log(\`Plugin Service is running on: http://localhost:\${port}\`); } -bootstrap(); + +bootstrap().catch((err) => { + console.error('Failed to bootstrap plugin:', err); + process.exit(1); +}); `, }, { @@ -235,11 +266,26 @@ bootstrap(); content: `import { CoreService, getRabbitMQClientOptions } from 'erp-plugin-sdk'; import { Module } from '@nestjs/common'; import { ClientsModule } from '@nestjs/microservices'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { PingController } from './ping.controller'; +import { ItemController } from './item.controller'; +import { ItemService } from './item.service'; +import { Item } from './entities/item.entity'; @Module({ imports: [ + TypeOrmModule.forRoot({ + type: 'postgres', + host: process.env.PLUGIN_DB_HOST || process.env.POSTGRES_HOST || 'localhost', + port: parseInt(process.env.PLUGIN_DB_PORT || process.env.POSTGRES_PORT || '5432', 10), + username: process.env.PLUGIN_DB_USER || process.env.POSTGRES_USER || 'postgres', + password: process.env.PLUGIN_DB_PASSWORD || process.env.POSTGRES_PASSWORD || 'postgres', + database: process.env.PLUGIN_DB_NAME || process.env.POSTGRES_DB || 'crm', + autoLoadEntities: true, + synchronize: true, + }), + TypeOrmModule.forFeature([Item]), ClientsModule.register([ { name: 'RABBITMQ_CLIENT', @@ -247,8 +293,8 @@ import { PingController } from './ping.controller'; }, ]), ], - controllers: [AppController, PingController], - providers: [CoreService], + controllers: [AppController, PingController, ItemController], + providers: [CoreService, ItemService], exports: [CoreService], }) export class AppModule {} @@ -290,6 +336,107 @@ export class PingController { return { status: 'ok', timestamp: Date.now() }; } } +`, + }, + { + relativePath: 'backend/src/item.controller.ts', + content: `import { Controller, Get, Post, Put, Delete, Body, Param } from '@nestjs/common'; +import { ItemService } from './item.service'; +import { Item } from './entities/item.entity'; + +@Controller('items') +export class ItemController { + constructor(private readonly service: ItemService) {} + + @Get() + async findAll(): Promise { + return this.service.findAll(); + } + + @Get(':id') + async findOne(@Param('id') id: string): Promise { + return this.service.findOne(id); + } + + @Post() + async create(@Body() data: Partial): Promise { + return this.service.create(data); + } + + @Put(':id') + async update(@Param('id') id: string, @Body() data: Partial): Promise { + return this.service.update(id, data); + } + + @Delete(':id') + async remove(@Param('id') id: string): Promise { + return this.service.remove(id); + } +} +`, + }, + { + relativePath: 'backend/src/item.service.ts', + content: `import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Item } from './entities/item.entity'; + +@Injectable() +export class ItemService { + constructor( + @InjectRepository(Item) + private readonly repo: Repository, + ) {} + + async findAll(): Promise { + return this.repo.find(); + } + + async findOne(id: string): Promise { + const item = await this.repo.findOne({ where: { id } }); + if (!item) throw new NotFoundException(\`Item \${id} not found\`); + return item; + } + + async create(data: Partial): Promise { + const item = this.repo.create(data); + return this.repo.save(item); + } + + async update(id: string, data: Partial): Promise { + await this.repo.update(id, data); + return this.findOne(id); + } + + async remove(id: string): Promise { + const result = await this.repo.delete(id); + if (!result.affected) throw new NotFoundException(\`Item \${id} not found\`); + } +} +`, + }, + { + relativePath: 'backend/src/entities/item.entity.ts', + content: `import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity() +export class Item { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + title: string; + + @Column({ type: 'text', nullable: true }) + description: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} `, }, { @@ -348,13 +495,10 @@ export class PingController { { relativePath: 'frontend/tsconfig.node.json', content: JSON.stringify({ - compilerOptions: { - composite: true, - module: 'ESNext', - moduleResolution: 'Node', - allowSyntheticDefaultImports: true, - }, - include: ['vite.config.ts'], + composite: true, + module: 'ESNext', + moduleResolution: 'Node', + allowSyntheticDefaultImports: true, }, null, 2) + '\n', }, { @@ -459,6 +603,10 @@ Arguments: plugin-name Plugin name in kebab-case (e.g., "my-custom-plugin") --port, -p Port number for the plugin server (default: 3002) +Database modes (set in manifest.json): + "self" (default) Plugin connects using shared POSTGRES_* env vars + "provide" Core provisions a dedicated database; change manifest + Example: create-erp-plugin my-plugin --port 3007 `); @@ -494,12 +642,16 @@ Example: fs.writeFileSync(filePath, tpl.content, 'utf-8'); } - console.log(`✅ Plugin "${name}" created successfully in ./${name}/`); + console.log(`\u2705 Plugin "${name}" created successfully in ./${name}/`); console.log(); console.log('Next steps:'); console.log(` cd ${name}/backend && npm install`); console.log(` cd ${name}/frontend && npm install`); console.log(` cd ${name}/backend && npm run start:dev`); + console.log(); + console.log('Database:'); + console.log(' Current mode: "self" (shared DB via POSTGRES_* env vars)'); + console.log(' To switch to a dedicated DB, set manifest.json "database": "provide"'); } main().catch((err) => {