erp-plugin-sdk/README.md

627 lines
18 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 (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` | Порт (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, ...)
├── 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 компонентів
```