feat: add ReportCapabilityProvider factory for report data sources — v1.9.0
This commit is contained in:
parent
f92adbd683
commit
3136f0b5f4
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@erp/plugin-sdk",
|
||||
"version": "1.5.0",
|
||||
"version": "1.9.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@erp/plugin-sdk",
|
||||
"version": "1.5.0",
|
||||
"version": "1.9.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"uuid": "^9.0.1"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@erp/plugin-sdk",
|
||||
"version": "1.7.1",
|
||||
"version": "1.9.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
export interface CapabilityField {
|
||||
name: string;
|
||||
type: 'string' | 'number' | 'currency' | 'date' | 'percent';
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface CapabilityFilter {
|
||||
name: string;
|
||||
type: 'date' | 'dateRange' | 'select' | 'text' | 'number';
|
||||
label: string;
|
||||
options?: { label: string; value: string }[];
|
||||
}
|
||||
|
||||
export interface ReportEndpointDef {
|
||||
endpoint: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
outputFields: CapabilityField[];
|
||||
filters: CapabilityFilter[];
|
||||
}
|
||||
|
||||
export interface ReportCapabilitiesResponse {
|
||||
endpoints: ReportEndpointDef[];
|
||||
}
|
||||
|
||||
export interface ReportControllerOptions {
|
||||
pluginId: string;
|
||||
endpoints: ReportEndpointDef[];
|
||||
handlerToken: any;
|
||||
methodPrefix?: string;
|
||||
}
|
||||
|
|
@ -18,9 +18,6 @@ export class CoreService {
|
|||
@Inject('RABBITMQ_CLIENT') private readonly client: ClientProxy,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Спеціальний метод для виконання запитів до Ядра з автоматичним прокиданням авторизації
|
||||
*/
|
||||
async fetchCore(endpoint: string, options: RequestInit = {}) {
|
||||
const coreUrl = process.env.CORE_BACKEND_URL || 'http://localhost:3001';
|
||||
const store = requestContext.getStore();
|
||||
|
|
@ -46,8 +43,6 @@ export class CoreService {
|
|||
return response.json();
|
||||
}
|
||||
|
||||
// ... (решта методів register, emit, request залишаються без змін)
|
||||
|
||||
async register(): Promise<void> {
|
||||
const coreUrl = process.env.CORE_BACKEND_URL || 'http://localhost:3001';
|
||||
const possiblePaths = [
|
||||
|
|
@ -65,12 +60,13 @@ export class CoreService {
|
|||
|
||||
if (!manifestPath) return;
|
||||
|
||||
// Dynamically calculate the backendUrl
|
||||
const port = process.env.PORT || '3002';
|
||||
const serviceName = process.env.SERVICE_NAME || 'plugin';
|
||||
const isDocker = coreUrl.includes('core-backend') || (!coreUrl.includes('localhost') && !coreUrl.includes('127.0.0.1'));
|
||||
const backendUrl = process.env.PLUGIN_BACKEND_URL || (isDocker ? `http://${serviceName}:${port}` : `http://localhost:${port}`);
|
||||
|
||||
const registrationKey = process.env.REGISTRATION_KEY || '';
|
||||
|
||||
let registered = false;
|
||||
while (!registered) {
|
||||
try {
|
||||
|
|
@ -96,18 +92,38 @@ export class CoreService {
|
|||
|
||||
const response = await fetch(`${coreUrl}/api/plugins/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(registrationKey ? { 'x-registration-key': registrationKey } : {}),
|
||||
},
|
||||
body: JSON.stringify(manifest),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const resBody = await response.json();
|
||||
const data = resBody && resBody.data !== undefined ? resBody.data : resBody;
|
||||
|
||||
this.logger.log(`Plugin registered successfully!`);
|
||||
|
||||
if (data && data.dbCredentials) {
|
||||
const creds = data.dbCredentials;
|
||||
process.env.PLUGIN_DB_NAME = creds.dbName;
|
||||
if (creds.host && !process.env.DB_HOST) process.env.PLUGIN_DB_HOST = creds.host;
|
||||
if (creds.port && !process.env.DB_PORT) process.env.PLUGIN_DB_PORT = String(creds.port);
|
||||
process.env.PLUGIN_DB_USER = creds.username;
|
||||
process.env.PLUGIN_DB_PASSWORD = creds.password;
|
||||
this.logger.log(`Database credentials received for plugin ${manifest.id}`);
|
||||
}
|
||||
|
||||
registered = true;
|
||||
this.startHeartbeatLoop(manifest.id, backendUrl);
|
||||
} else {
|
||||
const errText = await response.text();
|
||||
this.logger.error(`Registration failed (${response.status}): ${errText}`);
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Registration error: ${error}`);
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
|
|
@ -142,13 +158,9 @@ export class CoreService {
|
|||
} catch (error: any) {
|
||||
this.logger.error(`[Heartbeat] Error sending heartbeat:`, error.message || error);
|
||||
}
|
||||
}, 30 * 1000); // 30 seconds
|
||||
}, 30 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Викликати API іншого плагіна через core RPC-проксі.
|
||||
* Запит проксується ядром на backendUrl цільового плагіна методом POST.
|
||||
*/
|
||||
async callPluginApi<T = any>(pluginId: string, path: string, body?: any): Promise<T> {
|
||||
return this.fetchCore(`/api/plugins/rpc/${pluginId}/${path.replace(/^\//, '')}`, {
|
||||
method: 'POST',
|
||||
|
|
@ -156,18 +168,11 @@ export class CoreService {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Отримати список усіх зареєстрованих плагінів з їхніми events, objectTypes, backendUrl тощо.
|
||||
* Корисно для discovery — дізнатись, які плагіни доступні та що вони вміють.
|
||||
*/
|
||||
async discoverPlugins(): Promise<any[]> {
|
||||
const response = await this.fetchCore('/api/plugins/discovery');
|
||||
return response.data || response || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Знайти плагіни, які оголосили певну подію у своєму manifest.json (поле events).
|
||||
*/
|
||||
async findPluginsByEvent(eventName: string): Promise<any[]> {
|
||||
const plugins = await this.discoverPlugins();
|
||||
return plugins.filter((p) =>
|
||||
|
|
@ -175,18 +180,6 @@ export class CoreService {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Надіслати подію конкретному плагіну через RabbitMQ.
|
||||
* Публікує в system_events з routingKey: plugin.<pluginId>.<pattern>.
|
||||
* Цільовий плагін отримує подію через @EventPattern('plugin.<pluginId>.<pattern>')
|
||||
* або більш загальний @EventPattern('plugin.*').
|
||||
*
|
||||
* Інші плагіни теж отримують подію (через # binding), але за конвенцією
|
||||
* мають ігнорувати події з префіксом plugin.<чужойId>.
|
||||
*
|
||||
* IMPORTANT: Uses raw amqplib to publish to system_events exchange (not ClientProxy),
|
||||
* because NestJS 10 ClientRMQ ignores the exchange option and sends directly to a queue.
|
||||
*/
|
||||
async emitToPlugin(pluginId: string, pattern: string, payload: any): Promise<void> {
|
||||
const routingKey = `plugin.${pluginId}.${pattern}`;
|
||||
const event: SystemEvent = {
|
||||
|
|
@ -211,18 +204,6 @@ export class CoreService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC-виклик до іншого плагіна з очікуванням відповіді.
|
||||
* Використовує NestJS RabbitMQ RPC (send / @MessagePattern).
|
||||
* Публікує в system_events exchange (raw amqplib, not ClientProxy).
|
||||
* Цільовий плагін має обробити запит через @MessagePattern('plugin.<pluginId>.<pattern>').
|
||||
*
|
||||
* @param pluginId — ID цільового плагіна
|
||||
* @param pattern — назва команди
|
||||
* @param payload — тіло запиту
|
||||
* @param timeoutMs — таймаут очікування (за замовчуванням 5000 мс)
|
||||
* @returns відповідь від цільового плагіна або null по таймауту
|
||||
*/
|
||||
async requestPlugin<T = any>(
|
||||
pluginId: string,
|
||||
pattern: string,
|
||||
|
|
@ -374,34 +355,34 @@ export class CoreService {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Отримати назву БД, наданої ядром (має сенс після виклику setupDatabase)
|
||||
*/
|
||||
getDbName(): string {
|
||||
return process.env.PLUGIN_DB_NAME || process.env.DB_DATABASE || 'ultimate_crm';
|
||||
}
|
||||
|
||||
/**
|
||||
* Зареєструвати БД плагіна в ядрі (створює БД p_<id>, якщо ще не існує).
|
||||
* Викликати ДО створення NestJS app, щоб TypeORM міг підхопити PLUGIN_DB_NAME.
|
||||
*/
|
||||
static async setupDatabase(options: { pluginId: string }): Promise<string> {
|
||||
const coreUrl = process.env.CORE_BACKEND_URL || 'http://localhost:3001';
|
||||
const registrationKey = process.env.REGISTRATION_KEY || '';
|
||||
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (registrationKey) {
|
||||
headers['x-registration-key'] = registrationKey;
|
||||
}
|
||||
|
||||
const response = await fetch(`${coreUrl}/api/db/setup`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers,
|
||||
body: JSON.stringify({ pluginId: options.pluginId }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Database setup failed: ${response.statusText}`);
|
||||
throw new Error(`Database setup failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
const data = result.data || result;
|
||||
const dbName = data.dbName;
|
||||
if (!dbName) throw new Error('Database name not returned from core');
|
||||
process.env.PLUGIN_DB_NAME = dbName;
|
||||
if (data.host) process.env.PLUGIN_DB_HOST = data.host;
|
||||
if (data.port) process.env.PLUGIN_DB_PORT = String(data.port);
|
||||
if (data.host && !process.env.DB_HOST) process.env.PLUGIN_DB_HOST = data.host;
|
||||
if (data.port && !process.env.DB_PORT) process.env.PLUGIN_DB_PORT = String(data.port);
|
||||
if (data.username) process.env.PLUGIN_DB_USER = data.username;
|
||||
if (data.password) process.env.PLUGIN_DB_PASSWORD = data.password;
|
||||
return dbName;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import { Controller, Inject, Type } from '@nestjs/common';
|
||||
import { MessagePattern } from '@nestjs/microservices';
|
||||
import { ReportControllerOptions } from '../interfaces/report-capability.types';
|
||||
|
||||
export function createReportController(options: ReportControllerOptions): Type<any> {
|
||||
const { pluginId, endpoints, handlerToken, methodPrefix = 'handle' } = options;
|
||||
|
||||
@Controller()
|
||||
class ReportCapabilityController {
|
||||
constructor(@Inject(handlerToken) private readonly handler: any) {}
|
||||
|
||||
@MessagePattern(`plugin.${pluginId}.report.capabilities`)
|
||||
async getCapabilities() {
|
||||
return {
|
||||
endpoints: endpoints.map(e => ({
|
||||
endpoint: e.endpoint,
|
||||
label: e.label,
|
||||
description: e.description,
|
||||
outputFields: e.outputFields,
|
||||
filters: e.filters,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const ep of endpoints) {
|
||||
const pattern = `plugin.${pluginId}.report.${ep.endpoint}`;
|
||||
const safeName = ep.endpoint.replace(/[^a-zA-Z0-9_]/g, '_');
|
||||
const methodName = `handle_${safeName}`;
|
||||
const handlerMethod = `${methodPrefix}${ep.endpoint.charAt(0).toUpperCase()}${ep.endpoint.slice(1)}`;
|
||||
|
||||
Object.defineProperty(ReportCapabilityController.prototype, methodName, {
|
||||
value: async function (this: any, payload: any) {
|
||||
return this.handler[handlerMethod](payload);
|
||||
},
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const descriptor = Object.getOwnPropertyDescriptor(
|
||||
ReportCapabilityController.prototype,
|
||||
methodName,
|
||||
);
|
||||
MessagePattern(pattern)(ReportCapabilityController.prototype, methodName, descriptor!);
|
||||
}
|
||||
|
||||
return ReportCapabilityController;
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
export * from './backend/services/core.service';
|
||||
export * from './backend/services/core-client.service';
|
||||
export * from './backend/interfaces/communication';
|
||||
export * from './backend/interfaces/report-capability.types';
|
||||
export * from './backend/services/report-capability.service';
|
||||
export * from './backend/filters/exception.filter';
|
||||
export * from './backend/interceptors/request-context.interceptor';
|
||||
export * from './backend/interceptors/transform.interceptor';
|
||||
|
|
|
|||
Loading…
Reference in New Issue