erp

@erp/plugin-sdk (1.7.0)

Published 2026-05-28 08:06:26 +00:00 by mrbbfst in erp/erp-plugin-sdk

Installation

@erp:registry=
npm install @erp/plugin-sdk@1.7.0
"@erp/plugin-sdk": "1.7.0"

About this package

@erp/plugin-sdk

SDK для створення плагінів ERP-платформи. Містить бекенд (NestJS) та фронтенд (React) інструменти для інтеграції з ядром.

Зміст


Встановлення

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, TypeORM
  • frontend/ — 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  │
└───────────────────┘                       └──────────────┘

Комунікація:

  1. HTTP (REST)CoreService.fetchCore() для запитів до ядра
  2. RabbitMQ (асинхронно)CoreService.emit() для подій, @EventPattern() для отримання
  3. Plugin→Plugin RPC (v1.3.0+) — CoreService.callPluginApi() для виклику API іншого плагіна через core-proxy
  4. 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 плагіна для додаткового binding plugin.<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)

Система атрибутивного контролю доступу дозволяє плагінам визначати правила доступу на основі атрибутів користувачів і ресурсів, не прив'язуючи ядро до конкретних атрибутів.

Як це працює

  1. Плагін визначає атрибути — наприклад team, region, clearance
  2. Ядро зберігає атрибути — key-value для юзерів і ресурсів
  3. Плагін задає правила — наприклад user.team == resource.team
  4. Ядро перевіряє — порівнює атрибути згідно з правилами

Приклад: арбітражний плагін

// 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, який:

  1. Перевіряє JWT (як і будь-який захищений ендпоінт)
  2. Визначає backendUrl цільового плагіна з реєстру
  3. Проксує 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 RPC
  • events: { 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
Details
npm
2026-05-28 08:06:26 +00:00
16
ISC
24 KiB
Assets (1)
Versions (27) View all
1.11.3 2026-06-19
1.11.2 2026-06-18
1.11.1 2026-06-18
1.11.0 2026-06-12
1.10.3 2026-06-12