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
This commit is contained in:
parent
107af7b0f1
commit
788acab14e
170
src/cli/index.ts
170
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<Item[]> {
|
||||
return this.service.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@Param('id') id: string): Promise<Item> {
|
||||
return this.service.findOne(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(@Body() data: Partial<Item>): Promise<Item> {
|
||||
return this.service.create(data);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
async update(@Param('id') id: string, @Body() data: Partial<Item>): Promise<Item> {
|
||||
return this.service.update(id, data);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async remove(@Param('id') id: string): Promise<void> {
|
||||
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<Item>,
|
||||
) {}
|
||||
|
||||
async findAll(): Promise<Item[]> {
|
||||
return this.repo.find();
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<Item> {
|
||||
const item = await this.repo.findOne({ where: { id } });
|
||||
if (!item) throw new NotFoundException(\`Item \${id} not found\`);
|
||||
return item;
|
||||
}
|
||||
|
||||
async create(data: Partial<Item>): Promise<Item> {
|
||||
const item = this.repo.create(data);
|
||||
return this.repo.save(item);
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<Item>): Promise<Item> {
|
||||
await this.repo.update(id, data);
|
||||
return this.findOne(id);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
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'],
|
||||
}, 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) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue