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`,
|
remoteEntry: `http://localhost:${port}/remoteEntry.js`,
|
||||||
exposedModule: './App',
|
exposedModule: './App',
|
||||||
roles: ['admin'],
|
roles: ['admin'],
|
||||||
|
database: 'self',
|
||||||
}, null, 2) + '\n',
|
}, null, 2) + '\n',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -105,12 +106,15 @@ spec:
|
||||||
'@nestjs/core': '^10.0.0',
|
'@nestjs/core': '^10.0.0',
|
||||||
'@nestjs/microservices': '^10.0.0',
|
'@nestjs/microservices': '^10.0.0',
|
||||||
'@nestjs/platform-express': '^10.0.0',
|
'@nestjs/platform-express': '^10.0.0',
|
||||||
|
'@nestjs/typeorm': '^10.0.2',
|
||||||
'@nestjs/serve-static': '^4.0.2',
|
'@nestjs/serve-static': '^4.0.2',
|
||||||
'amqp-connection-manager': '^4.1.14',
|
'amqp-connection-manager': '^4.1.14',
|
||||||
amqplib: '^0.10.3',
|
amqplib: '^0.10.3',
|
||||||
'erp-plugin-sdk': 'git+https://gitea.ss.3w.com.ua/erp/erp-plugin-sdk.git',
|
'erp-plugin-sdk': 'git+https://gitea.ss.3w.com.ua/erp/erp-plugin-sdk.git',
|
||||||
|
pg: '^8.12.0',
|
||||||
'reflect-metadata': '^0.1.13',
|
'reflect-metadata': '^0.1.13',
|
||||||
rxjs: '^7.8.1',
|
rxjs: '^7.8.1',
|
||||||
|
typeorm: '^0.3.20',
|
||||||
uuid: '^9.0.1',
|
uuid: '^9.0.1',
|
||||||
},
|
},
|
||||||
devDependencies: {
|
devDependencies: {
|
||||||
|
|
@ -181,14 +185,36 @@ CMD ["npm", "run", "start:prod"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
relativePath: 'backend/src/main.ts',
|
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 { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const logger = new Logger('PluginBootstrap');
|
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);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
app.useGlobalInterceptors(new RequestContextInterceptor());
|
app.useGlobalInterceptors(new RequestContextInterceptor());
|
||||||
|
|
@ -214,6 +240,7 @@ async function bootstrap() {
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { getRabbitMQOptions } = await import('erp-plugin-sdk');
|
||||||
app.connectMicroservice(getRabbitMQOptions('plugin_${snake}_queue'));
|
app.connectMicroservice(getRabbitMQOptions('plugin_${snake}_queue'));
|
||||||
|
|
||||||
app.enableCors();
|
app.enableCors();
|
||||||
|
|
@ -227,7 +254,11 @@ async function bootstrap() {
|
||||||
|
|
||||||
logger.log(\`Plugin Service is running on: http://localhost:\${port}\`);
|
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';
|
content: `import { CoreService, getRabbitMQClientOptions } from 'erp-plugin-sdk';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ClientsModule } from '@nestjs/microservices';
|
import { ClientsModule } from '@nestjs/microservices';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { PingController } from './ping.controller';
|
import { PingController } from './ping.controller';
|
||||||
|
import { ItemController } from './item.controller';
|
||||||
|
import { ItemService } from './item.service';
|
||||||
|
import { Item } from './entities/item.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
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([
|
ClientsModule.register([
|
||||||
{
|
{
|
||||||
name: 'RABBITMQ_CLIENT',
|
name: 'RABBITMQ_CLIENT',
|
||||||
|
|
@ -247,8 +293,8 @@ import { PingController } from './ping.controller';
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
controllers: [AppController, PingController],
|
controllers: [AppController, PingController, ItemController],
|
||||||
providers: [CoreService],
|
providers: [CoreService, ItemService],
|
||||||
exports: [CoreService],
|
exports: [CoreService],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|
@ -290,6 +336,107 @@ export class PingController {
|
||||||
return { status: 'ok', timestamp: Date.now() };
|
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',
|
relativePath: 'frontend/tsconfig.node.json',
|
||||||
content: JSON.stringify({
|
content: JSON.stringify({
|
||||||
compilerOptions: {
|
|
||||||
composite: true,
|
composite: true,
|
||||||
module: 'ESNext',
|
module: 'ESNext',
|
||||||
moduleResolution: 'Node',
|
moduleResolution: 'Node',
|
||||||
allowSyntheticDefaultImports: true,
|
allowSyntheticDefaultImports: true,
|
||||||
},
|
|
||||||
include: ['vite.config.ts'],
|
|
||||||
}, null, 2) + '\n',
|
}, null, 2) + '\n',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -459,6 +603,10 @@ Arguments:
|
||||||
plugin-name Plugin name in kebab-case (e.g., "my-custom-plugin")
|
plugin-name Plugin name in kebab-case (e.g., "my-custom-plugin")
|
||||||
--port, -p Port number for the plugin server (default: 3002)
|
--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:
|
Example:
|
||||||
create-erp-plugin my-plugin --port 3007
|
create-erp-plugin my-plugin --port 3007
|
||||||
`);
|
`);
|
||||||
|
|
@ -494,12 +642,16 @@ Example:
|
||||||
fs.writeFileSync(filePath, tpl.content, 'utf-8');
|
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();
|
||||||
console.log('Next steps:');
|
console.log('Next steps:');
|
||||||
console.log(` cd ${name}/backend && npm install`);
|
console.log(` cd ${name}/backend && npm install`);
|
||||||
console.log(` cd ${name}/frontend && npm install`);
|
console.log(` cd ${name}/frontend && npm install`);
|
||||||
console.log(` cd ${name}/backend && npm run start:dev`);
|
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) => {
|
main().catch((err) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue