@erp/plugin-sdk (1.10.2)
Installation
@erp:registry=npm install @erp/plugin-sdk@1.10.2"@erp/plugin-sdk": "1.10.2"About this package
@erp/plugin-sdk
SDK для створення плагінів ERP-платформи. Містить бекенд (NestJS) та фронтенд (React) інструменти для інтеграції з ядром.
Зміст
- Встановлення
- Швидкий старт
- Архітектура
- API Reference
- Attribute Access Control (ABAC)
- Події та RabbitMQ
- Міжплагінна комунікація
- CLI — create-erp-plugin
- Експорт SDK
Встановлення
npm install @erp/plugin-sdk
.npmrc
SDK опубліковано в Gitea Packages. Додайте в backend/.npmrc:
@erp:registry=https://gitea.ss.3w.com.ua/api/packages/erp/npm/
Dockerfile
COPY backend/.npmrc ./backend/
RUN cd backend && npm install
Швидкий старт
npx create-erp-plugin my-plugin --port 3007
Створює scaffold плагіна з:
backend/— NestJS бекенд з CRUD, RabbitMQ, TypeORMfrontend/— React (Module Federation) фронтендmanifest.json— реєстрація плагіна в ядрі
Архітектура
┌───────────────────┐ HTTP/REST ┌──────────────┐
│ Plugin A (BE) │ ◄──────────────────► │ Core (BE) │
│ CoreService │ RabbitMQ │ Port 3001 │
│ AttributeAccess │ ◄──────────────────► │ │
└────────┬──────────┘ └──────────┬───┘
│ ┌───────────────────────▲ │
│ Plugin→Plugin│ HTTP RPC Proxy │ │
│ v1.3.0 │ │ │
│ ┌──────┴───────┐ │ │
│ │ Plugin B (BE)│ │ │
│ │ callPlugin │───────────────┘ │
│ └──────────────┘ │
│ │
Module Federation Module Fed.
│ │
┌────────▼──────────┐ ┌──────────▼───┐
│ Plugin A (FE) │ │ Plugin B(FE)│
│ React + Vite │ │ React + Vite│
│ host/UIKit │ │ host/UIKit │
└───────────────────┘ └──────────────┘
Комунікація:
- HTTP (REST) —
CoreService.fetchCore()для запитів до ядра - RabbitMQ (асинхронно) —
CoreService.emit()для подій,@EventPattern()для отримання - Plugin→Plugin RPC (v1.3.0+) —
CoreService.callPluginApi()для виклику API іншого плагіна через core-proxy - Module Federation — фронтенд плагіна монтується як remote module всередині host-застосунку
API Reference
CoreService
Головний сервіс для комунікації плагіна з ядром.
import { CoreService } from '@erp/plugin-sdk';
fetchCore(endpoint, options?)
HTTP-запит до ядра. Автоматично прокидyє JWT токен (з RequestContext).
const users = await coreService.fetchCore('/api/admin/users');
const created = await coreService.fetchCore('/api/admin/users', {
method: 'POST',
body: JSON.stringify({ username: 'newuser', password: '123' }),
});
register()
Реєструє плагін в ядрі. Читає manifest.json, відправляє POST на /api/plugins/register. Повторює з ретраями поки не зареєструється.
await coreService.register();
emit<T>(pattern, payload)
Асинхронна подія через RabbitMQ (fire-and-forget).
await coreService.emit('myplugin.data.synced', { recordId: 42 });
auditLog(action, resource, details?)
Запис в аудит-лог ядра.
await coreService.auditLog('deals.create', 'deal:42', { dealName: 'Test' });
getGlobalData<T>(key) / setGlobalData(key, value) / deleteGlobalData(key)
Глобальне сховище (key-value) в ядрі.
await coreService.setGlobalData('myplugin_config', { theme: 'dark' });
const config = await coreService.getGlobalData('myplugin_config');
sendNotification(payload)
Відправка сповіщення користувачу.
await coreService.sendNotification({
userId: 1,
title: 'Deal updated',
message: 'Deal #42 has been modified',
});
callPluginApi<T>(pluginId, path, body?)
Викликати API іншого плагіна через core RPC-проксі. Запит проксується ядром на backendUrl цільового плагіна методом POST. JWT-токен автоматично прокидyється.
// Викликати ендпоінт плагіна notifications
const result = await coreService.callPluginApi('notifications', 'stats', { period: 'week' });
// Викликати з вкладеним шляхом
const user = await coreService.callPluginApi('user-management', 'users/42');
findPluginsByEvent(eventName)
Знайти плагіни, які оголосили певну подію у своєму manifest.json.
const listeners = await coreService.findPluginsByEvent('core.user.created');
// listeners = [ { id: 'audit-logs', name: 'Audit Logs', ... } ]
emitToPlugin(pluginId, pattern, payload)
Надіслати подію конкретному плагіну через RabbitMQ. Публікує в system_events з routing key plugin.<pluginId>.<pattern>. Цільовий плагін отримує подію через @EventPattern().
// Надіслати подію тільки плагіну notifications
await coreService.emitToPlugin('notifications', 'send', {
userId: 1,
title: 'New deal',
message: 'Deal #42 created',
});
Цільовий плагін обробляє через стандартний @EventPattern:
@EventPattern('plugin.notifications.send')
async handle(@Payload() data: any) {
const event = data.data || data;
// event.payload = { userId: 1, title: 'New deal', ... }
}
requestPlugin<T>(pluginId, pattern, payload, timeoutMs?)
RPC-виклик до іншого плагіна з очікуванням відповіді. Використовує NestJS RabbitMQ RPC (send / @MessagePattern).
const stats = await coreService.requestPlugin('notifications', 'getStats', {
period: 'week',
});
// stats = { sent: 42, failed: 1 }
Цільовий плагін обробляє через @MessagePattern:
@MessagePattern('plugin.notifications.getStats')
async getStats(@Payload() data: { period: string }) {
// логіка підрахунку...
return { sent: 42, failed: 1 };
}
Параметри:
pluginId— ID цільового плагінаpattern— назва команди (без префіксаplugin.<id>., він додається автоматично)payload— тіло запитуtimeoutMs— таймаут (за замовчуванням 5000 мс). Повертаєnullпри перевищенні.
discoverPlugins()
Отримати список усіх зареєстрованих плагінів з їхніми можливостями. Повертає масив маніфестів з полями: id, name, label, icon, routePath, backendUrl, events, objectTypes, permissions, roles, database, menu, headerActions.
const plugins = await coreService.discoverPlugins();
// Знайти плагін за id
const notifPlugin = plugins.find(p => p.id === 'notifications');
console.log(notifPlugin.backendUrl); // http://notifications:3008
// Знайти всі плагіни, що оголосили певну подію
const listeners = plugins.filter(p =>
p.events?.some(e => e.name === 'core.user.created')
);
// Знайти плагіни, що працюють з певним типом об'єктів
const dealPlugins = plugins.filter(p =>
p.objectTypes?.includes('deal')
);
getDbName()
Отримати назву БД (після setupDatabase).
const dbName = coreService.getDbName();
static setupDatabase({ pluginId })
Запитує у ядра виділену БД для плагіна. Викликати ДО ініціалізації NestJS.
// В main.ts перед створенням app
const dbName = await CoreService.setupDatabase({ pluginId: 'my-plugin' });
AttributeAccessService
Сервіс для Attribute-Based Access Control (ABAC). Дозволяє керувати атрибутами користувачів і ресурсів та перевіряти доступ на основі правил.
import { AttributeAccessService } from '@erp/plugin-sdk';
Типи
type AttributeMatch = 'eq' | 'neq' | 'in' | 'contains' | 'exists' | 'gte' | 'lte';
interface AttributeRule {
userAttr: string; // ключ атрибута на користувачеві
match: AttributeMatch; // тип порівняння
resourceAttr?: string; // ключ атрибута на ресурсі
value?: string; // або фіксоване значення
}
interface ResourceRef {
type: string; // наприклад 'deal', 'server'
id: string;
}
Робота з атрибутами користувачів
// Встановити атрибут
await attrService.setUserAttribute(1, 'team', 'alpha', 'arbitration');
// Встановити кілька атрибутів
await attrService.setUserAttributes(1, [
{ key: 'team', value: 'alpha' },
{ key: 'region', value: 'EU' },
{ key: 'clearance', value: '3' },
], 'arbitration');
// Отримати атрибути
const attrs = await attrService.getUserAttributes(1);
// Видалити
await attrService.deleteUserAttribute(1, 'team');
Робота з атрибутами ресурсів
await attrService.setResourceAttribute('deal', '42', 'team', 'alpha', 'arbitration');
await attrService.setResourceAttributes('deal', '42', [
{ key: 'team', value: 'alpha' },
{ key: 'region', value: 'EU' },
], 'arbitration');
const attrs = await attrService.getResourceAttributes('deal', '42');
await attrService.deleteResourceAttribute('deal', '42', 'team');
Перевірка доступу
// 1. Check: чи user в тій самій команді що й deal?
const allowed = await attrService.evaluate(1, [
{ userAttr: 'team', match: 'eq', resourceAttr: 'team' }
], { type: 'deal', id: '42' });
// 2. Check: чи має user рівень доступу >= 3?
const allowed = await attrService.evaluate(1, [
{ userAttr: 'clearance', match: 'gte', value: '3' }
]);
// 3. Check: чи є user менеджером власника ресурсу?
const allowed = await attrService.evaluate(1, [
{ userAttr: 'subordinates', match: 'contains', resourceAttr: 'owner_id' }
], { type: 'deal', id: '42' });
Фільтрація списку
Корисно коли вже маєте список об'єктів і треба відфільтрувати доступні.
const visibleDeals = await attrService.filter(
userId,
[{ userAttr: 'team', match: 'eq', resourceAttr: 'team' }],
allDeals,
(deal) => ({ team: deal.team }) // екстрактор атрибутів з об'єкта
);
Типи порівняння
match |
Опис | Приклад |
|---|---|---|
eq |
Дорівнює | user.team == resource.team |
neq |
Не дорівнює | user.region != 'blocked' |
in |
Значення user в списку resource | user.region in resource.allowed_regions |
contains |
Список user містить resource | user.roles contains 'admin' |
exists |
Атрибут існує | user.clearance exists |
gte |
>= | user.clearance >= 3 |
lte |
<= | user.priority <= 2 |
CoreClientService
Альтернативний спосіб комунікації з ядром через RabbitMQ RPC.
import { CoreClientService } from '@erp/plugin-sdk';
// Запитати дані користувача (синхронно через RabbitMQ RPC)
const userInfo = await coreClient.getUserInfo(1);
// Сповістити ядро про подію
await coreClient.notifyCore('myplugin.event', { data: 'value' });
getRabbitMQOptions
Повертає MicroserviceOptions для NestJS. Використовується в main.ts для підключення до RabbitMQ.
import { getRabbitMQOptions } from '@erp/plugin-sdk';
// Звичайне підключення — отримує всі події
app.connectMicroservice(getRabbitMQOptions('plugin_my_queue'));
// З підтримкою targeted events — також байндить plugin.<id>.*
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
app.connectMicroservice(getRabbitMQOptions('plugin_my_queue', undefined, manifest.id));
Параметри:
queueName— назва чергиurl?— RabbitMQ URL (за замовчуванням зRABBITMQ_URLабоamqp://localhost:5672)pluginId?— (v1.4.0+) ID плагіна для додаткового bindingplugin.<pluginId>.*(targeted events)
Конфігурація:
- Exchange:
system_events(topic) - Routing keys:
#(всі події) +plugin.<id>.*(якщо переданоpluginId) - Queue: вказана назва (durable)
- Повідомлення: persistent
getRabbitMQClientOptions
Повертає конфігурацію для ClientsModule.register().
import { getRabbitMQClientOptions } from '@erp/plugin-sdk';
@Module({
imports: [
ClientsModule.register([
{ name: 'RABBITMQ_CLIENT', ...getRabbitMQClientOptions() },
]),
],
})
TransformInterceptor
NestJS interceptor — обгортає всі відповіді в ApiResponse формат.
import { TransformInterceptor } from '@erp/plugin-sdk';
app.useGlobalInterceptors(new TransformInterceptor());
Формат відповіді:
{
"success": true,
"data": { ... },
"meta": {
"timestamp": 1716800000000,
"traceId": "trace-xxx",
"service": "my-plugin"
}
}
RequestContextInterceptor
NestJS interceptor — зберігає authorization токен та IP в AsyncLocalStorage. Використовується CoreService.fetchCore() для автоматичного прокидвання JWT.
import { RequestContextInterceptor } from '@erp/plugin-sdk';
app.useGlobalInterceptors(new RequestContextInterceptor());
AllExceptionsFilter
NestJS exception filter — перехоплює всі необроблені помилки і повертає стандартизований ApiResponse з помилкою.
import { AllExceptionsFilter } from '@erp/plugin-sdk';
app.useGlobalFilters(new AllExceptionsFilter());
Формат помилки:
{
"success": false,
"error": {
"code": "ERR_404",
"message": "Item not found",
"details": null
},
"meta": {
"timestamp": 1716800000000,
"traceId": "uuid",
"service": "my-plugin"
}
}
Інтерфейси
ApiResponse<T>
Стандартна відповідь API.
interface ApiResponse<T = any> {
success: boolean;
data?: T;
error?: { code: string; message: string; details?: any };
meta: { timestamp: number; traceId: string; service: string };
}
SystemEvent<T>
Конверт для асинхронних подій RabbitMQ.
interface SystemEvent<T = any> {
eventId: string;
pattern: string;
payload: T;
source: string;
timestamp: number;
priority?: 'low' | 'medium' | 'high';
}
host/UIKit (фронтенд)
Типи для компонентів інтерфейсу, що надаються host-застосунком:
import { Button, Input, Card, Typography } from 'host/UIKit';
Attribute Access Control (ABAC)
Система атрибутивного контролю доступу дозволяє плагінам визначати правила доступу на основі атрибутів користувачів і ресурсів, не прив'язуючи ядро до конкретних атрибутів.
Як це працює
- Плагін визначає атрибути — наприклад
team,region,clearance - Ядро зберігає атрибути — key-value для юзерів і ресурсів
- Плагін задає правила — наприклад
user.team == resource.team - Ядро перевіряє — порівнює атрибути згідно з правилами
Приклад: арбітражний плагін
// 1. Адмін призначає атрибути
await attrService.setUserAttributes(userId, [
{ key: 'team', value: 'alpha' },
{ key: 'region', value: 'EU' },
], 'arbitration');
// 2. При створенні deal, зберігаємо атрибути ресурсу
await attrService.setResourceAttributes('deal', dealId, [
{ key: 'team', value: deal.team },
{ key: 'region', value: deal.region },
], 'arbitration');
// 3. При перегляді — перевіряємо
const canView = await attrService.evaluate(userId, [
{ userAttr: 'team', match: 'eq', resourceAttr: 'team' },
{ userAttr: 'region', match: 'eq', resourceAttr: 'region' },
], { type: 'deal', id: dealId });
if (!canView) throw new ForbiddenException();
Раунд-тріп vs локальна фільтрація
evaluate() робить HTTP-запит до ядра (перевіряє атрибути ресурсу в БД).
filter() робить один запит для отримання атрибутів юзера, потім фільтрує локально.
Інтеграція з RBAC
async viewDeal(userId: number, dealId: string) {
// 1. RBAC — чи має дозвіл
const hasPerm = await this.usersService.hasPermission(userId, 'deals.view');
if (!hasPerm) throw new ForbiddenException();
// 2. ABAC — чи збігаються атрибути
const access = await this.attributeService.evaluate(userId, [
{ userAttr: 'team', match: 'eq', resourceAttr: 'team' }
], { type: 'deal', id: dealId });
if (!access) throw new ForbiddenException();
return this.dealRepo.findOne(dealId);
}
Події та RabbitMQ
Відправка подій (emit)
// З плагіна в ядро (або інші плагіни)
await coreService.emit('myplugin.data.updated', { id: 42 });
// Конверт автоматично обгортається в SystemEvent:
// { eventId, pattern: 'myplugin.data.updated', payload: { id: 42 }, source: 'my-plugin', timestamp }
Отримання подій (@EventPattern)
import { EventPattern, Payload } from '@nestjs/microservices';
@Injectable()
export class MyHandler {
@EventPattern('core.search.request')
async handle(@Payload() data: any) {
const payload = data.payload || data;
// обробка...
}
}
Підключення мікросервісу в main.ts:
import { getRabbitMQOptions } from '@erp/plugin-sdk';
app.connectMicroservice(getRabbitMQOptions('plugin_my_queue'));
await app.startAllMicroservices();
Шаблон "Запит-Відповідь" (Request/Response)
Для двосторонньої комунікації через події:
// Core відправляє запит
coreService.emit('plugin.some.request', { requestId, ... });
// Плагін відповідає
@EventPattern('plugin.some.request')
async handle(@Payload() data: any) {
const { requestId } = data.payload || data;
this.client.emit('core.some.response', { requestId, result: 'ok' });
}
Міжплагінна комунікація
До версії 1.3.0 плагіни могли спілкуватись тільки через ядро (HTTP до core + RabbitMQ broadcast). Починаючи з v1.3.0 додано підтримку прямого плагін→плагін зв'язку через HTTP RPC проксі.
Архітектура
Plugin A Core Backend Plugin B
| | |
|-- discoverPlugins() ----------->| |
|<-- [{ id:'notifications', ... }]| |
| | |
|-- callPluginApi('notifications', | |
| 'stats', { period:'week' })-->| |
| |-- POST /stats -------------->|
| |<-- { sent: 42 } ------------|
|<-- { sent: 42 } ----------------| |
Всі запити проходять через Core, який:
- Перевіряє JWT (як і будь-який захищений ендпоінт)
- Визначає
backendUrlцільового плагіна з реєстру - Проксує POST-запит з автентифікацією та тілом
Усі способи комунікації
| Метод | Тип | Канал | Адресат |
|---|---|---|---|
callPluginApi() |
Sync HTTP RPC | HTTP через core-proxy | Конкретний плагін |
emitToPlugin() |
Async event | RabbitMQ plugin.<id>.<pattern> |
Конкретний плагін |
requestPlugin() |
Sync RPC | RabbitMQ send/@MessagePattern |
Конкретний плагін |
emit() |
Async event | RabbitMQ broadcast | Всі плагіни |
1. Discovery
// Всі зареєстровані плагіни
const plugins = await coreService.discoverPlugins();
// Знайти плагіни за подією
const listeners = await coreService.findPluginsByEvent('core.user.created');
Кожен плагін має:
id,name,label,backendUrl— адреса для HTTP RPCevents: { name, description }[]— які події оголошуєobjectTypes: string[]— з якими об'єктами працюєpermissions,roles— права доступу
2. Targeted Events (RabbitMQ)
Надіслати подію конкретному плагіну через RabbitMQ. Подія публікується в system_events з routing key plugin.<pluginId>.<pattern>. Цільовий плагін має додатковий binding plugin.<id>.* (якщо передано pluginId у getRabbitMQOptions).
// Відправник
await coreService.emitToPlugin('notifications', 'send', {
userId: 1,
title: 'New deal',
message: 'Deal #42 created',
});
// Отримувач (notifications plugin)
@EventPattern('plugin.notifications.send')
async handle(@Payload() data: any) {
const event = data.data || data;
console.log(event.payload.title); // 'New deal'
}
Підключення отримувача (в main.ts):
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
app.connectMicroservice(
getRabbitMQOptions('plugin_notifications_queue', undefined, manifest.id)
);
// Черга байндиться з routing keys: # (всі події) + plugin.notifications.* (targeted)
3. RPC Request/Response (RabbitMQ)
Синхронний виклик до іншого плагіна з очікуванням відповіді. Використовує NestJS send() / @MessagePattern().
// Відправник
const stats = await coreService.requestPlugin('notifications', 'getStats', {
period: 'week',
});
console.log(stats); // { sent: 42, failed: 1 }
// Отримувач (notifications plugin)
@MessagePattern('plugin.notifications.getStats')
async getStats(@Payload() data: { period: string }) {
const stats = await this.calcStats(data.period);
return stats; // повертається відправнику
}
Параметри requestPlugin:
pluginId— ID цільового плагінаpattern— назва команди (без префікса)payload— тіло запитуtimeoutMs— таймаут у мс (за замовчуванням 5000).nullпри перевищенні.
4. HTTP RPC через core-proxy
const stats = await coreService.callPluginApi('notifications', 'stats');
const user = await coreService.callPluginApi('user-management', 'users/42');
const result = await coreService.callPluginApi('global-store', 'store/set', {
key: 'my-key',
value: { foo: 'bar' },
});
Обмеження:
- Тільки POST-метод
- Потребує JWT-токена (автоматично прокидyється з контексту)
- Якщо плагін не зареєстрований або без
backendUrl— 404
Типовий сценарій
async function notifyAndLog() {
// 1. Discovery
const [notifPlugin] = await this.coreService.findPluginsByEvent('notification.send');
// 2. Targeted event
if (notifPlugin) {
await this.coreService.emitToPlugin('notifications', 'send', {
userId: 1,
title: 'Event occurred',
message: 'Plugin A triggered a notification',
});
}
// 3. RPC запит до audit-logs
const status = await this.coreService.requestPlugin('audit-logs', 'dailyStats', {});
}
---
## CLI — create-erp-plugin
Генерація нового плагіна:
```bash
npx create-erp-plugin <plugin-name> [--port <port>]
Параметри
| Аргумент | Опис |
|---|---|
plugin-name |
Назва в kebab-case (наприклад my-plugin) |
--port, -p |
Порт (1024–65535, за замовчуванням 3002) |
Режими БД
У manifest.json:
database |
Опис |
|---|---|
"self" (default) |
Плагін використовує спільну БД через POSTGRES_* змінні |
"provide" |
Ядро створює виділену БД p_<pluginId> |
Структура згенерованого плагіна
my-plugin/
├── manifest.json # Реєстраційний маніфест
├── backend/
│ ├── .npmrc # @erp:registry=...
│ ├── package.json
│ ├── tsconfig.json
│ ├── nest-cli.json
│ ├── Dockerfile
│ └── src/
│ ├── main.ts
│ ├── app.module.ts
│ ├── app.controller.ts
│ ├── ping.controller.ts
│ ├── item.controller.ts
│ ├── item.service.ts
│ └── entities/
│ └── item.entity.ts
├── frontend/
│ ├── package.json
│ ├── tsconfig.json
│ ├── vite.config.ts
│ ├── index.html
│ └── src/
│ └── App.tsx
└── k8s-plugin.yaml
Експорт SDK
@erp/plugin-sdk
├── CoreService # Головний сервіс (fetchCore, emit, register, callPluginApi, discoverPlugins, findPluginsByEvent, emitToPlugin, requestPlugin, ...)
├── CoreClientService # RabbitMQ RPC клієнт
├── AttributeAccessService # ABAC — керування атрибутами та перевірка доступу
├── getRabbitMQOptions() # NestJS Microservice options
├── getRabbitMQClientOptions() # NestJS Client options
├── TransformInterceptor # Обгортка відповідей в ApiResponse
├── RequestContextInterceptor # AsyncLocalStorage для JWT
├── AllExceptionsFilter # Глобальний фільтр помилок
├── ApiResponse # Інтерфейс відповіді
├── SystemEvent # Інтерфейс події
└── host/UIKit (types) # Типи UI компонентів
Dependencies
Dependencies
| ID | Version |
|---|---|
| uuid | ^9.0.1 |
Development Dependencies
| ID | Version |
|---|---|
| @nestjs/common | ^11.1.23 |
| @nestjs/core | ^11.1.23 |
| @nestjs/microservices | ^11.1.23 |
| @types/amqplib | ^0.10.8 |
| @types/node | ^25.9.1 |
| @types/react | ^19.2.15 |
| @types/uuid | ^9.0.8 |
| reflect-metadata | ^0.2.2 |
| rxjs | ^7.8.2 |
| typescript | ^6.0.3 |
Peer Dependencies
| ID | Version |
|---|---|
| @nestjs/common | ^10.0.0 || ^11.0.0 |
| @nestjs/core | ^10.0.0 || ^11.0.0 |
| @nestjs/microservices | ^10.0.0 || ^11.0.0 |
| amqplib | ^0.10.3 |
| reflect-metadata | ^0.1.12 || ^0.2.0 |
| rxjs | ^7.8.2 |