@erp/plugin-sdk (1.2.4)
Installation
@erp:registry=npm install @erp/plugin-sdk@1.2.4"@erp/plugin-sdk": "1.2.4"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 (BE) │ ◄──────────────────► │ Core (BE) │
│ CoreService │ RabbitMQ │ Port 3001 │
│ AttributeAccess │ ◄──────────────────► │ │
└────────┬──────────┘ └──────────────┘
│
Module Federation
│
┌────────▼──────────┐
│ Plugin (FE) │
│ React + Vite │
│ host/UIKit │
└───────────────────┘
Комунікація:
- HTTP (REST) —
CoreService.fetchCore()для запитів до ядра - RabbitMQ (асинхронно) —
CoreService.emit()для подій,@EventPattern()для отримання - 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',
});
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'));
Конфігурація:
- Exchange:
system_events(topic) - Routing key:
#(отримує всі події) - 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' });
}
CLI — create-erp-plugin
Генерація нового плагіна:
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, ...)
├── 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/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 |
| reflect-metadata | ^0.1.12 || ^0.2.0 |
| rxjs | ^7.8.2 |