627 lines
18 KiB
Markdown
627 lines
18 KiB
Markdown
# @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 (BE) │ ◄──────────────────► │ Core (BE) │
|
||
│ CoreService │ RabbitMQ │ Port 3001 │
|
||
│ AttributeAccess │ ◄──────────────────► │ │
|
||
└────────┬──────────┘ └──────────────┘
|
||
│
|
||
Module Federation
|
||
│
|
||
┌────────▼──────────┐
|
||
│ Plugin (FE) │
|
||
│ React + Vite │
|
||
│ host/UIKit │
|
||
└───────────────────┘
|
||
```
|
||
|
||
**Комунікація:**
|
||
1. **HTTP (REST)** — `CoreService.fetchCore()` для запитів до ядра
|
||
2. **RabbitMQ (асинхронно)** — `CoreService.emit()` для подій, `@EventPattern()` для отримання
|
||
3. **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',
|
||
});
|
||
```
|
||
|
||
#### `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'));
|
||
```
|
||
|
||
Конфігурація:
|
||
- Exchange: `system_events` (topic)
|
||
- Routing key: `#` (отримує всі події)
|
||
- 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' });
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 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, ...)
|
||
├── 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 компонентів
|
||
```
|