erp-plugin-sdk/README.md

880 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# @erp/plugin-sdk
SDK для створення плагінів ERP-платформи. Містить бекенд (NestJS) та фронтенд (React) інструменти для інтеграції з ядром.
## Зміст
- [Встановлення](#встановлення)
- [Швидкий старт](#швидкий-старт)
- [Архітектура](#архітектура)
- [API Reference](#api-reference)
- [CoreService](#coreservice)
- [AttributeAccessService](#attributeaccessservice)
- [CoreClientService](#coreclientservice)
- [getRabbitMQOptions](#getrabbitmqoptions)
- [getRabbitMQClientOptions](#getrabbitmqclientoptions)
- [TransformInterceptor](#transforminterceptor)
- [RequestContextInterceptor](#requestcontextinterceptor)
- [AllExceptionsFilter](#allexceptionsfilter)
- [Інтерфейси](#інтерфейси)
- [Attribute Access Control (ABAC)](#attribute-access-control-abac)
- [Події та RabbitMQ](#події-та-rabbitmq)
- [Міжплагінна комунікація](#міжплагінна-комунікація)
- [CLI — create-erp-plugin](#cli--create-erp-plugin)
- [Експорт SDK](#експорт-sdk)
---
## Встановлення
```bash
npm install @erp/plugin-sdk
```
### `.npmrc`
SDK опубліковано в Gitea Packages. Додайте в `backend/.npmrc`:
```
@erp:registry=https://gitea.ss.3w.com.ua/api/packages/erp/npm/
```
### Dockerfile
```dockerfile
COPY backend/.npmrc ./backend/
RUN cd backend && npm install
```
---
## Швидкий старт
```bash
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
Головний сервіс для комунікації плагіна з ядром.
```typescript
import { CoreService } from '@erp/plugin-sdk';
```
#### `fetchCore(endpoint, options?)`
HTTP-запит до ядра. Автоматично прокидyє JWT токен (з RequestContext).
```typescript
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`. Повторює з ретраями поки не зареєструється.
```typescript
await coreService.register();
```
#### `emit<T>(pattern, payload)`
Асинхронна подія через RabbitMQ (fire-and-forget).
```typescript
await coreService.emit('myplugin.data.synced', { recordId: 42 });
```
#### `auditLog(action, resource, details?)`
Запис в аудит-лог ядра.
```typescript
await coreService.auditLog('deals.create', 'deal:42', { dealName: 'Test' });
```
#### `getGlobalData<T>(key)` / `setGlobalData(key, value)` / `deleteGlobalData(key)`
Глобальне сховище (key-value) в ядрі.
```typescript
await coreService.setGlobalData('myplugin_config', { theme: 'dark' });
const config = await coreService.getGlobalData('myplugin_config');
```
#### `sendNotification(payload)`
Відправка сповіщення користувачу.
```typescript
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ється.
```typescript
// Викликати ендпоінт плагіна notifications
const result = await coreService.callPluginApi('notifications', 'stats', { period: 'week' });
// Викликати з вкладеним шляхом
const user = await coreService.callPluginApi('user-management', 'users/42');
```
#### `findPluginsByEvent(eventName)`
Знайти плагіни, які оголосили певну подію у своєму `manifest.json`.
```typescript
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()`.
```typescript
// Надіслати подію тільки плагіну notifications
await coreService.emitToPlugin('notifications', 'send', {
userId: 1,
title: 'New deal',
message: 'Deal #42 created',
});
```
Цільовий плагін обробляє через стандартний `@EventPattern`:
```typescript
@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`).
```typescript
const stats = await coreService.requestPlugin('notifications', 'getStats', {
period: 'week',
});
// stats = { sent: 42, failed: 1 }
```
Цільовий плагін обробляє через `@MessagePattern`:
```typescript
@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`.
```typescript
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`).
```typescript
const dbName = coreService.getDbName();
```
#### `static setupDatabase({ pluginId })`
Запитує у ядра виділену БД для плагіна. Викликати ДО ініціалізації NestJS.
```typescript
// В main.ts перед створенням app
const dbName = await CoreService.setupDatabase({ pluginId: 'my-plugin' });
```
---
### AttributeAccessService
Сервіс для Attribute-Based Access Control (ABAC). Дозволяє керувати атрибутами користувачів і ресурсів та перевіряти доступ на основі правил.
```typescript
import { AttributeAccessService } from '@erp/plugin-sdk';
```
#### Типи
```typescript
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;
}
```
#### Робота з атрибутами користувачів
```typescript
// Встановити атрибут
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');
```
#### Робота з атрибутами ресурсів
```typescript
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');
```
#### Перевірка доступу
```typescript
// 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' });
```
#### Фільтрація списку
Корисно коли вже маєте список об'єктів і треба відфільтрувати доступні.
```typescript
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.
```typescript
import { CoreClientService } from '@erp/plugin-sdk';
```
```typescript
// Запитати дані користувача (синхронно через RabbitMQ RPC)
const userInfo = await coreClient.getUserInfo(1);
// Сповістити ядро про подію
await coreClient.notifyCore('myplugin.event', { data: 'value' });
```
---
### getRabbitMQOptions
Повертає `MicroserviceOptions` для NestJS. Використовується в `main.ts` для підключення до RabbitMQ.
```typescript
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()`.
```typescript
import { getRabbitMQClientOptions } from '@erp/plugin-sdk';
@Module({
imports: [
ClientsModule.register([
{ name: 'RABBITMQ_CLIENT', ...getRabbitMQClientOptions() },
]),
],
})
```
---
### TransformInterceptor
NestJS interceptor — обгортає всі відповіді в `ApiResponse` формат.
```typescript
import { TransformInterceptor } from '@erp/plugin-sdk';
app.useGlobalInterceptors(new TransformInterceptor());
```
Формат відповіді:
```json
{
"success": true,
"data": { ... },
"meta": {
"timestamp": 1716800000000,
"traceId": "trace-xxx",
"service": "my-plugin"
}
}
```
---
### RequestContextInterceptor
NestJS interceptor — зберігає `authorization` токен та IP в `AsyncLocalStorage`. Використовується `CoreService.fetchCore()` для автоматичного прокидвання JWT.
```typescript
import { RequestContextInterceptor } from '@erp/plugin-sdk';
app.useGlobalInterceptors(new RequestContextInterceptor());
```
---
### AllExceptionsFilter
NestJS exception filter — перехоплює всі необроблені помилки і повертає стандартизований `ApiResponse` з помилкою.
```typescript
import { AllExceptionsFilter } from '@erp/plugin-sdk';
app.useGlobalFilters(new AllExceptionsFilter());
```
Формат помилки:
```json
{
"success": false,
"error": {
"code": "ERR_404",
"message": "Item not found",
"details": null
},
"meta": {
"timestamp": 1716800000000,
"traceId": "uuid",
"service": "my-plugin"
}
}
```
---
### Інтерфейси
#### `ApiResponse<T>`
Стандартна відповідь API.
```typescript
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.
```typescript
interface SystemEvent<T = any> {
eventId: string;
pattern: string;
payload: T;
source: string;
timestamp: number;
priority?: 'low' | 'medium' | 'high';
}
```
#### `host/UIKit` (фронтенд)
Типи для компонентів інтерфейсу, що надаються host-застосунком:
```typescript
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. **Ядро перевіряє** — порівнює атрибути згідно з правилами
### Приклад: арбітражний плагін
```typescript
// 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
```typescript
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)
```typescript
// З плагіна в ядро (або інші плагіни)
await coreService.emit('myplugin.data.updated', { id: 42 });
// Конверт автоматично обгортається в SystemEvent:
// { eventId, pattern: 'myplugin.data.updated', payload: { id: 42 }, source: 'my-plugin', timestamp }
```
### Отримання подій (@EventPattern)
```typescript
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`:
```typescript
import { getRabbitMQOptions } from '@erp/plugin-sdk';
app.connectMicroservice(getRabbitMQOptions('plugin_my_queue'));
await app.startAllMicroservices();
```
### Шаблон "Запит-Відповідь" (Request/Response)
Для двосторонньої комунікації через події:
```typescript
// 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
```typescript
// Всі зареєстровані плагіни
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`).
```typescript
// Відправник
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`):
```typescript
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()`.
```typescript
// Відправник
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
```typescript
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
### Типовий сценарій
```typescript
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` | Порт (102465535, за замовчуванням 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 компонентів
```