# CSP 3 для разработчитка

# Фреймворк Morphcluster



# MorphCluster Core

MorphCluster Core — это модульный фреймворк для построения микросервисных приложений на Node.js. Он предоставляет базовые абстракции для сервисов, событий, логирования, кэширования и обмена данными между локальными и удалёнными компонентами.

## Основные концепции

Фреймворк строится вокруг следующих сущностей:

- **Service** – минимальная единица бизнес-логики. Сервис описывает свою схему (`ServiceSchema`), может принимать запросы и генерировать события.
- **ServiceHost** – контейнер, управляющий жизненным циклом группы сервисов (запуск, остановка, перезапуск).
- **GlobalService** – прокси-объект, который объединяет локальные и удалённые реализации одного и того же сервиса, скрывая разницу между ними.
- **GlobalServices** – реестр всех глобальных сервисов в системе, отвечает за их связывание.
- **ServiceClient** – декларативное описание потребителя сервиса. Позволяет сервису подключаться к другому сервису по имени.
- **Logger** – иерархическая система логирования с поддержкой разных бэкендов (консоль, воркер, файл).
- **Event / Trigger / Timer** – механизмы для реактивного взаимодействия, планирования задач и обработки событий.
- **ChannelSender / ChannelAggregator** – средства публикации и сбора данных между сервисами без прямых вызовов.

---

## Ядро (Core)

### Config

`Config` управляет конфигурацией приложения.

```js
const config = new Config(defaultConfig);
await config.load();
```

- `defaultConfig` – объект с параметрами по умолчанию.
- `load()` загружает глобальный конфиг из файла (`MCL_CONFIG` или `global-config.json`), объединяет через `utils.overlay` и создаёт директорию данных.
- Свойства:
  - `values` – итоговая конфигурация.
  - `packageRoot` – корень пакета.
  - `dataDir` – директория для данных.

### Logger и бэкенды

`Logger` обеспечивает иерархическую запись логов с возможностью создания дочерних "писателей" для отслеживания цепочек вызовов.

```js
const logger = new Logger({ host: 'main', service: 'MyService' });
logger.backends = [new LoggerBackendConsole()];

const logId = logger.write('Старт', { someData: 1 });
const subLogger = logger.createWriter('Вложенная операция', payload);
// ...
subLogger.close();
```

Методы:
- `write(message, payload, options)` – записать простое сообщение.
- `writeRaw(message, payload, options)` – записать "сырое" сообщение (не закрытое).
- `createWriter(message, payload, options)` – создать дочерний Logger, связанный с текущей записью.
- `attachWriter(parentId, options)` – прикрепиться к существующему родительскому логу.
- `writeException(error, options)` – записать ошибку и закрыть логгер.
- `writeExceptionOnly(error, options)` – записать ошибку без закрытия.
- `wrap(callback)` – выполнить функцию, перехватить исключения и закрыть логгер.
- `createWrapped(message, callback, payload, options)` – выполнить функцию внутри нового логгера.

**Бэкенды логов:**
- `LoggerBackend` – абстрактный класс.
- `LoggerBackendConsole` – вывод в консоль (info / error в зависимости от уровня).
- `LoggerBackendWorker` – отправка логов в родительский поток через `parentPort`.

### ComplexError

Расширенный класс ошибки с дополнительной информацией.

```js
throw new ComplexError('Сообщение', 'КодОшибки', { detail: '...' }, { showUser: true, httpStatus: 422 });
```

Поля: `name`, `message`, `payload`, `options` (может содержать `showUser`, `httpStatus`, `logId`).

### Event

Паттерн "наблюдатель" с поддержкой вложенных событий и подсчёта подписчиков.

```js
const event = new Event();

event.on((workspace, ...args) => { ... });
event.off(callback);
event.emit('workspace', data);

// Вложенные события (subEvents)
const sub = new Event();
event.addSubEvent(sub);   // подписчики sub учитываются в event.isSubscribed()
event.removeSubEvent(sub);
```

Свойства:
- `onSubscribe` / `onUnsubscribe` – колбэки при появлении/исчезновении подписчиков.
- `isSubscribed()` – есть ли активные слушатели (свои или subEvents).

### Timer

Периодическое выполнение задач с обработкой ошибок и логированием.

```js
const timer = new Timer('MyTimer', 5000, async (log) => {
  // логика
});
timer.logger = serviceLogger;
timer.serviceName = 'MyService';
timer.start();
timer.stop();
```

- При возникновении ошибок вызывается `onTickError`.
- `ignoreErrorCount` – число допустимых ошибок подряд; при превышении таймер останавливается.
- `maxRunningTime` – максимальное время выполнения одного тика (для детекта зависаний).

### Trigger

Связывает события с выполнением callback-функции.

```js
const trigger = new Trigger('Обработка события', async (log, ...args) => { ... });
trigger.logger = serviceLogger;
trigger.serviceName = 'MyService';
trigger.connect(someEvent);   // подписаться на событие
trigger.disconnect();         // отписаться от всех
```

При срабатывании события автоматически создаётся лог-писатель.

---

## Сервисная архитектура

### ServiceSchema

Описывает контракт сервиса: запросы и события.

```js
const schema = new ServiceSchema();
schema.name = 'MyService';
schema.addRequest({ name: 'doSomething', ... });
schema.addEvent({ name: 'onChanged', ... });

// Сериализация
const json = schema.getJson();
schema.setJson(json);
```

`RequestSchema` – описание запроса, включает `name`, `request` (JSON-схема параметров), `response`, флаги `anonymous`, `needAdmin`, `noLogs` и привязку `http`.
`EventSchema` – описание события, содержит `name`, `description`, `structure`.

### ServiceClient

Клиент для подключения к глобальному сервису. Используется сервисами для декларативного указания зависимостей.

```js
class MyService extends Service {
  constructor() {
    super();
    this.clients.push(new ServiceClient({ name: 'OtherService', requests: [...], events: [...] }));
  }
}
```

- `connected` – флаг подключения.
- `waitConnect()` – ожидание подключения к сервису.
- `requestHandlers` и `eventHandlers` – заполняются при подключении к реальному сервису.

### Service

Базовый класс для любого сервиса.

```js
class MyService extends Service {
  constructor() {
    super();
    this.name = 'MyService';
    this.addRequest({ name: 'ping', ... });
  }

  async ping(params, workspace, log) {
    return { pong: true };
  }

  async start(log) {
    await super.start(log);
    // инициализация, запуск таймеров
  }
  async stop() { ... }
}
```

Основные свойства и методы:
- `schema` (`ServiceSchema`)
- `name`, `local` (приватный), `remote` (виртуальный)
- `clients`, `timers`, `triggers`, `senders`, `aggregators`
- `start(log)`, `stop()`
- `onStarted`, `onFailed`, `onRestored` – события жизненного цикла.

### ServiceHost

Управляет набором сервисов.

```js
const host = new ServiceHost('main');
host.Config = config;
host.Logger = logger;

host.addService(new MyService());
await host.start(log);
```

- `addService(service, name?)` – добавляет сервис, создаёт для него Logger и Config.
- `startService(service)`, `stopService(service)`, `restartService(service)` – управление.
- `requireService(name, workspace?)` – получить запущенный сервис по имени.
- `waitService(name)` – ожидать появления сервиса.
- `services` – массив всех сервисов.
- События: `onServiceAdded`, `onServiceStarted`.

### ServiceRequire

Сервис, который ожидает доступности других сервисов перед собственным запуском.

```js
class MyConsumer extends ServiceRequire {
  constructor(host) {
    super(host);
    this.requirements = ['DataService'];      // обязательные зависимости
    this.optionalServices = ['LoggerService']; // необязательные
  }

  async start(log) {
    await super.start(log);
    // this.DataService уже доступен
    const result = await this.DataService.query(...);
  }
}
```

После выполнения `super.start()` в свойствах сервиса появятся ссылки на требуемые сервисы (имена из `requirements`). Необязательные сервисы подтягиваются асинхронно.

---

## Глобальные сервисы и взаимодействие

### GlobalServiceInterface

Абстрактный интерфейс для удалённого взаимодействия с сервисом.

```js
class RemoteInterface extends GlobalServiceInterface {
  async request(name, params, parentLog) { ... }
  async subscribeEvent(name, callback) { ... }
  async unsubscribeEvent(name, callback) { ... }
}
```

### GlobalService

Прокси-объект, скрывающий разницу между локальной и удалённой реализацией сервиса.

```js
const gsvc = new GlobalService(schema);
gsvc.localService = localInstance;   // связать с локальным сервисом
// или
gsvc.interface = remoteInterface;    // связать с удалённым интерфейсом

// Отправка запроса
const result = await gsvc.sendRequest('method', params, log);

// Подписка на событие
gsvc.getEvent('onChanged').on(callback);

// Подключение клиента
gsvc.connectClient(client);
```

`GlobalService` управляет маршрутизацией запросов и событий через `requestHandlers` и `eventHandlers`. При изменении источника (локальный/удалённый) он корректно переключает связи.

### GlobalServices

Центральный реестр, объединяющий все глобальные сервисы в системе.

```js
class MyGlobalServices extends GlobalServices {
  async start(log) {
    await super.start(log);
    // Автоматически подписывается на host.onServiceStarted для добавления локальных сервисов
  }
}
```

Методы:
- `addLocalService(service)` – зарегистрировать локальный сервис, создать для него GlobalService.
- `addRemoteService(schema, iface, hostName)` – добавить удалённый сервис.
- `getServiceBySchema(schema)` – найти или создать GlobalService по схеме.
- Событие `onServiceChanged` – оповещает об изменениях.

### ServiceFactory

Упрощает создание сервисов из конфигурации.

```js
const factory = new ServiceFactory();
factory.addServiceClass(MyService);
factory.addMicroservice(pack); // объект со свойством services
factory.run(host, config);
```

`config.services` – массив объектов вида `{ name: '...', className: '...', config: {...} }`.

### ServiceBridge

Сервис, предоставляющий HTTP-доступ к любому зарегистрированному сервису.

```js
const bridge = new ServiceBridge(host, config);
bridge.GlobalServices = globalServices;
```

Запросы:
- `list` – список всех сервисов с их схемами.
- `getSchema` – схема конкретного сервиса.
- `request` – выполнить произвольный запрос: `{ service, request, payload }`. Автоматически логируется, ошибки оборачиваются в `ComplexError`.

---

## Встроенные сервисы

### LocalHealth

Предоставляет информацию о здоровье хоста.

```js
const health = new LocalHealth(host, config);
```

Запрос `list` возвращает объект:
```json
{
  "result": [{
    "hostName": "main",
    "allStarted": true,
    "noRequirements": [...],
    "fullServices": [...]
  }]
}
```

### Sessions

Хранилище сессий в памяти (неперсистентное).

```js
const sessions = new MemorySessions(host, config);
```

Основные запросы:
- `create({ userId, login, isAdmin })` → `{ token }`
- `delete({ token })`
- `get({ token })` → объект сессии (или ошибка `InvalidToken`)
- `list`, `deleteMany` – работа с фильтрами.
- `validateHttp(request, reply, log, needAdmin)` – извлечение сессии из заголовка `Authorization`.

### Auth

Базовая аутентификация администратора.

```js
const auth = new Auth(host, { admin: { login: 'admin', password: '...' } });
```

Запросы:
- `admin({ login, password })` – возвращает токен сессии.
- `logout` – удаляет текущую сессию.

Требует `Sessions` в зависимостях.

---

## Каналы данных

### ChannelSender

Позволяет сервису публиковать данные в именованный канал.

```js
class Producer extends Service {
  constructor() {
    super();
    this.senders.push(new ChannelSender('updates'));
  }

  async onNewData(data) {
    this.senders[0].data = data;  // автоматически вызовет onDataChanged
  }
}
```

### ChannelAggregator

Собирает данные от нескольких сервисов в один канал.

```js
class Consumer extends Service {
  constructor() {
    super();
    const agg = new ChannelAggregator('updates');
    this.aggregators.push(agg);
    agg.onDataChanged.on((ws, allData) => {
      // allData: [{ serviceName, data }, ...]
    });
  }
}
```

Для связи Sender и Aggregator используется `GlobalService`, который при обнаружении `ChannelSender` начинает передавать данные в соответствующий `ChannelAggregator` (детали реализации не входят в данную документацию, но являются частью внутренней логики).

---

## Утилиты

### Кэширование

#### MemoryCache

In-memory кэш с TTL, LRU-подобным вытеснением и событиями.

```js
const cache = new MemoryCache({ stdTTL: 60, checkperiod: 30, maxKeys: 1000 });
cache.set('key', value, ttl);
const val = cache.get('key');
cache.del('key');
cache.flushAll();
```

#### MemoryCacheAsync

Расширяет `MemoryCache` для работы с асинхронной загрузкой данных, автоматически объединяя одновременные запросы одного ключа.

```js
const cache = new MemoryCacheAsync({
  stdTTL: 120,
  callback: async (id, params) => {
    return await fetchFromDb(id);
  }
});

const data = await cache.fetch('someId', optionalParams);
```

#### AsyncCache (устаревший)

Аналогичный механизм без наследования от MemoryCache. Рекомендуется использовать `MemoryCacheAsync`.

#### AsyncCacheList

Кэш для списка элементов с индивидуальной асинхронной загрузкой.

```js
const listCache = new AsyncCacheList();
// необходимо переопределить load(id)
const item = await listCache.get('id');
```

#### PromiseSingleton

Гарантирует однократное создание экземпляра.

```js
const singleton = new PromiseSingleton(async (params) => await createResource(params));
const res = await singleton.getInstance(params);
singleton.reset(); // сброс для повторного создания
```

### Работа с данными

#### JsonSchema

Утилиты для работы с JSON-схемами:

- `overlay(target, overlay)` – рекурсивное наложение схем.
- `generateJson(schema)` – создаёт пустой объект по схеме.
- `sanitize(schema, data)` – очищает данные согласно схеме, подставляя значения по умолчанию.

#### JsonDirList

Хранение списка объектов в виде JSON-файлов в директории.

```js
const list = new JsonDirList('./data/myList');
await list.reload();
await list.set({ id: '1', ... });
await list.remove('1');
list.changedEvent = () => { /* обновить UI или кэш */ };
```

#### ExpiringList

Список с автоматическим удалением устаревших элементов по TTL.

```js
const list = new ExpiringList(3600, 60); // ttl=1 час, очистка раз в минуту
list.push({ id: 'token1', ... });
const item = list.get('token1');
list.remove('token1');
```

### Дата и время

Функции для работы с dayjs:

- `removeTimezone(dateWithTz)` – приводит к МСК и возвращает строку `YYYY-MM-DDTHH:mm:ss`.
- `strToDayjs(str)` – парсит дату с поддержкой нескольких форматов.
- `dayjsToStr(djs)` – обратное преобразование.
- `msTime()` – high-resolution время в миллисекундах (process.hrtime).

### Прочее

#### array-utils

- `groupBy(array, key)` – группировка массива объектов.
- `unique(array)` – удаление дубликатов.
- `compare(arr1, arr2)` – поэлементное сравнение.
- `complement(arr1, arr2)` – разность множеств.

#### object-utils

- `clone(obj)` – глубокое клонирование.
- `compareRecursive(a, b)` – глубокое сравнение.
- `overlay(a, b)` – рекурсивное слияние объектов (shallow для необъектов).
- `arrayLimit(item, limit)` – обрезает массивы в структуре данных до указанной длины (полезно для логирования).

#### sleep

- `sleep(ms)` – пауза.
- `sleepM(minutes, abortSignal)` – пауза в минутах с возможностью отмены.

#### stream-utils

- `stream2buffer(stream)` – чтение ReadableStream в Buffer.

---

## Экспорт

Библиотека экспортирует все основные классы и утилиты через `src/index.js`. Импорт может быть как деструктурированный, так и через объект `utils`:

```js
import {
  Config, Logger, LoggerBackendConsole,
  ComplexError, Event, Timer, Trigger,
  ChannelSender, ChannelAggregator,
  ServiceSchema, ServiceClient, Service, ServiceHost, ServiceRequire,
  GlobalService, GlobalServices, GlobalServiceInterface,
  ServiceFactory, ServiceBridge,
  LocalHealth, Sessions, Auth,
  JsonDirList, Profiler, ExpiringList,
  MemoryCache, MemoryCacheAsync, PromiseSingleton, JsonSchema,
  utils
} from '@morphcluster/core';
```

Вспомогательные функции доступны через `utils`:
```js
utils.sleep(1000);
utils.clone(data);
utils.groupBy(arr, 'category');
// и т.д.
```

# Структура сервисов Morphcluster

`Service` — это основа для всех сервисов. Каждый сервис обладает:

## Свойства сервиса

| Свойство      | Тип          | Описание |
|---------------|--------------|----------|
| `name`        | `string`     | Имя сервиса (по умолчанию имя класса) |
| `schema` | `Schema` | Публичный контракт сервиса |
| `started`     | `boolean`    | Запущен ли сервис в данный момент |
| `starting`    | `boolean`    | Находится ли в процессе запуска |
| `private` / `local` | `boolean` | Сервис не публикуется в глобальной системе |
| `remote`      | `boolean`    | Виртуальный сервис, не добавляется в глобальную систему |
| `Logger` | `Logger` | Экземпляр для журнала |
| `Config` | `Config` | Глобальной конфигурация приложения |
| `clients`     | `ServiceClient[]` | Клиенты, через которые сервис обращается к другим сервисам |
| `timers`      | `Timer[]`    | Периодические задачи |
| `triggers`    | `Trigger[]`  | Обработчики событий других сервисов |
| `senders`     | `ChannelSender[]` | Каналы для публикации данных |
| `aggregators` | `ChannelAggregator[]` | Агрегаторы данных из каналов |

## Схема сервиса

Схема задаётся через объект `ServiceSchema` и определяет публичный контракт сервиса.

Определяется через методы:
- this.addRequest(requestSchema)
- this.addEvent(requestSchema)
- this.setSvcSchema(jsonSchema)

Структуры описаны ниже

## Жизненный цикл

```
[создание] → host.addService(service) → host.startService(service)
                                            → service.start(log)
                                            → service.started = true
                                            → service.stop()
                                            → service.started = false
```

При возникновении ошибки во время старта хост автоматически попытается перезапустить сервис через 5 секунд.

## Запросы

Каждый сервис может предоставлять один или несколько **запросов** — методов, доступных для вызова извне (другими сервисами, через HTTP-мост, клиентами).  
Запрос описывается схемой `RequestSchema` и реализуется методом сервиса с определённой сигнатурой.

### Схема запроса (`RequestSchema`)

При добавлении запроса через `this.addRequest({...})` создаётся объект `RequestSchema` со следующими полями:

| Поле | Тип | Обязательное | Описание |
|------|-----|--------------|----------|
| `name` | `string` | да | Уникальное имя запроса в рамках сервиса. **Должно совпадать с именем метода-обработчика**. |
| `description` | `string` | нет | Человекочитаемое описание. |
| `request` | `object` | нет | JSON-схема (стандарт JSON Schema) для валидации входящих параметров. |
| `response` | `object` | нет | JSON-схема, описывающая структуру ответа. |
| `anonymous` | `boolean` | нет | Если `true`, запрос доступен без аутентификации. |
| `needAdmin` | `boolean` | нет | Если `true`, требует прав администратора. |
| `noLogs` | `boolean` | нет | Отключает автоматическое создание лога. |
| `http` | `string` | нет | HTTP-метод (`GET`, `POST` и т.д.) при экспорте через `ServiceBridge`. |

Пример добавления запроса:

```javascript
this.addRequest({
    name: 'getReport',
    description: 'Возвращает отчёт за период',
    request: {
        type: 'object',
        properties: {
            from: { type: 'string', format: 'date' },
            to:   { type: 'string', format: 'date' }
        },
        required: ['from', 'to']
    },
    response: {
        type: 'object',
        properties: {
            rows: { type: 'array' }
        }
    }
});
```

### Метод-обработчик запроса

Для каждого запроса, объявленного в схеме, в классе сервиса должен быть реализован **асинхронный метод** с точно таким же именем.

```javascript
async methodName(params, ws, log) {
    // params – объект с параметрами запроса
    // ws – Устаревший параметр, всегда null
    // log – экземпляр Logger для данного вызова
}
```

- Параметры **не валидируются автоматически** на уровне ядра. Рекомендуется проверять входные данные внутри метода.
- Если запрос помечен как `noLogs: true`, всё равно можно писать в `log` — это безопасно, так как логгер проигнорирует запись.
- Метод может возвращать любое сериализуемое значение (объект, массив, число, строку и т.д.). Этот результат будет передан обратно вызывающей стороне.
- Все обработчики должны быть `async`, даже если внутри нет асинхронных операций.
- Не забывайте закрывать дочерние логи, созданные внутри метода, или используйте `log.wrap()` для автоматического закрытия.
 
## Обработка ошибок

**`ComplexError`** – специальный класс ошибки, позволяющий передать дополнительную информацию:
  - `name` – тип ошибки (строка),
  - `payload` – любые данные,
  - `options.showUser` – показывать ли сообщение пользователю,
  - `options.httpStatus` – HTTP-статус (при использовании `ServiceBridge`),

  Если метод выбрасывает `ComplexError`, он будет проброшен до вызывающего кода с сохранением всех свойств. `ServiceBridge` автоматически добавляет в ошибку `logId` текущего лога.

Любое другое исключение (`Error`) будет преобразовано в `ComplexError` с именем класса ошибки.

```javascript
throw new ComplexError('Отчёт не найден', 'ReportNotFound', { from, to }, { showUser: true });
```

## Объявление зависимостей

```javascript
class MyService extends ServiceRequire {
    constructor(host) {
      super(host);
      /** @type {Database} */
      this.Database = null //Будет автоматически заполнен после start
      /** @type {Cache} */
      this.Cache = null //Будет автоматически заполнен после start
      this.requirements = ['Database', 'Cache'];   // жёсткие зависимости
    }

    async start(log) {
      await super.start(log)
      //Здесь доступен this.Database и this.Cache
    }

}
```

1. Запускается `start(log)` родительского класса.
2. Для каждого имени в `optionalServices` вызывается `host.waitService(optName)`, но **без ожидания**.
3. Для `requirements` формируются Promise на `waitRequire(svcName, log)`, которые резолвятся после получения сервиса.
4. Все требования загружаются параллельно (`Promise.all`).
5. Загруженные сервисы сохраняются в свойства `this[svcName]`.

Состояние загрузки можно отследить через `requirementsStatus()`.

## Логирование

Каждый сервис получает экземпляр `Logger` с предустановленными `service` и `host`.

**Основные методы:**

| Метод | Описание |
|-------|----------|
| `write(message, payload?, options?)` | Записать простое сообщение |
| `createWriter(message, payload?, options?)` | Создать дочерний логгер, привязанный к текущей записи как к родительской. Новая запись остаётся открытой (`closed: false`). |
| `close()` | Закрыть текущую запись (помечает `closed: true`, закрывать только те логи, которые создал сам). |
| `writeException(error, options?)` | Записать исключение и **закрыть запись**. |
| `writeExceptionOnly(error, options?)` | Записать только данные исключения без закрытия записи. |
| `wrap(callback)` | Обернуть асинхронную функцию: автоматически закрыть лог при успехе или записать исключение при ошибке. |
| `createWrapped(message, callback, payload?, options?)` | Создать логи и обернуть им асинхронную функцию. |

### Логирование метода сервиса (запроса)

Каждый метод сервиса, объявленный через `addRequest`, принимает аргумент `log`. Это - **дочерний логгер**, созданный вызывающей стороной. Внутри метода рекомендуется использовать именно этот `log` для всех записей, чтобы сохранить иерархию.

**Пример:**
```javascript
async generateReport(params, ws, log) {
  log.write('Начало генерации отчёта', { from: params.from, to: params.to });
  //При вызове другого сервиса обязательно передаем log
  const data = await this.DataService.fetchData(params, null, log);
  log.write('Данные получены', { recordCount: data.length });
  return { report: data };
}
```

### Дочерние доги

Если код вызывается асинхронно (без await), необходимо создать ему дочерний лог

```javascript
async complexTask(params, workspace, log) {
    // log уже передан извне
    const ChildLog = log.createWriter('Шаг 1', { details: '...' });
    //`wrap` автоматически оборачивает функцию, закрывая лог при успехе и записывая ошибку при неудаче.
    await ChildLog.wrap(async () => {
      await this.step1();
    });
}
```

## События

### Схема события (`EventSchema`)

| Поле          | Тип       | Описание |
|---------------|-----------|----------|
| `name`        | `string`  | Имя события |
| `description` | `string`  | Описание |
| `structure`   | `object`  | Структура передаваемых данных |

Реализация событий с поддержкой:
- подписки/отписки (`on`, `off`),
- вложенных событий (`addSubEvent`, `removeSubEvent`),
- автоматического вызова `onSubscribe`/`onUnsubscribe` при появлении первого/последнего слушателя.

## Таймеры и триггеры

**Таймер** (`Timer`) — периодическая асинхронрная задача. Таймаут начнется только после завершения асихронного вызова.

```javascript
this.timers.push(new Timer('cleanup', 60000, async () => {
    // очистка каждые 60 секунд
}));
```

**Триггер** (`Trigger`) — реакция на событие другого сервиса. Автоматически создает лог.

```javascript
this.onDataChanged = new Event();
this.trgDataChanged = new Trigger('onDataChanged', async (log, data) => {
    // обработать событие
});
this.triggers.push(trgDataChanged);
// подключить триггер к событию
trigger.connect(this.onDataChanged);
```

Оба компонента автоматически инициализируются при старте сервиса (им присваиваются `serviceName` и `logger`).

## Каналы (ChannelSender / ChannelAggregator)

- **ChannelSender** — позволяет сервису публиковать данные в именованный канал.
- **ChannelAggregator** — собирает данные от нескольких сервисов в одном канале, вызывая событие при изменении.

Пример использования:

```javascript
this.senders.push(new ChannelSender('metrics'));
// где-то в коде
this.senders[0].data = { cpu: 50 };

this.aggregators.push(new ChannelAggregator('metrics'));
this.aggregators[0].onDataChanged.on((ws, allData) => {
    // allData — массив { serviceName, data }
});
```

## Config

Глобальная конфигурация, загружаемая при старте приложения. Сервис получает её через `this.Config`.

## ServiceClient — клиент к сервису

Клиент позволяет сервису обращаться к другому сервису, не заботясь о его физическом расположении (локальный или удалённый).

На данный момент `ServiceClient` - это экспериментальная функциональность

# MorphCluster Fastify

Данный пакет предоставляет готовые сервисы для обработки HTTP-запросов, организации единой точки входа (API Gateway), реверс-проксирования и раздачи статических файлов. Все компоненты наследуются от `ServiceRequire` и могут быть интегрированы в `ServiceHost` как обычные сервисы MorphCluster.

## Fastify

`Fastify` — базовый класс для создания HTTP‑сервисов на основе [Fastify](https://www.fastify.io/). Интегрируется с MorphCluster, автоматически регистрирует маршруты, обрабатывает ошибки и поддерживает загрузку файлов, CORS и Swagger‑документацию.

### Подключение

```js
import { ServiceHost } from '@morphcluster/core';
import { Fastify } from '@morphcluster/web';

const host = new ServiceHost('main');
const web = new Fastify(host, {
  host: '0.0.0.0',
  port: 8080,
  title: 'My API',
  prefixUrl: '/api',
  maxFileSize: 2 * 1024 * 1024,
  disableProxy: false
});
host.addService(web);
```

### Конфигурация

| Параметр      | Тип     | По умолчанию          | Описание |
|---------------|---------|-----------------------|----------|
| `host`        | string  | `127.0.0.1`           | IP‑адрес для прослушивания |
| `port`        | number  | `0` (случайный)       | Порт (если `0`, назначается ОС) |
| `title`       | string  | `Fastify Service`     | Заголовок для Swagger |
| `prefixUrl`   | string  | `''`                  | Общий префикс всех маршрутов |
| `maxFileSize` | number  | `1000000` (1 МБ)      | Максимальный размер тела запроса и загружаемых файлов |
| `disableProxy`| boolean | `false`               | Отключить автоматическую регистрацию в `HttpProxy` |
| `useProxy`    | boolean | `true`                | Устаревший синоним `disableProxy` (инвертирован) |

### Основные методы

#### `addRoute(route)`

Добавляет маршрут Fastify. Принимает объект со свойствами `method`, `url` и `handler`. Если маршрут с таким методом и URL уже существует, он заменяется.

```js
web.addRoute({
  method: 'GET',
  url: '/hello',
  handler: (req, reply) => reply.send({ hello: 'world' })
});
```

- URL автоматически дополняется префиксом `prefixUrl`.

#### `restart()`

Пересоздаёт экземпляр Fastify и заново регистрирует все накопленные маршруты. Полезно после массового добавления маршрутов.

#### Внутренние особенности

- Обработчик ошибок `setErrorHandler` оборачивает `ComplexError` в понятный JSON‑ответ с HTTP‑статусом из `options.httpStatus` и сообщением, если `showUser: true`.
- При запуске автоматически регистрируется в `HttpProxy` (если `disableProxy` не `true`), добавляя прокси‑маршрут с адресом `http://127.0.0.1:<порт>`.
- Поддерживает загрузку файлов через `@fastify/multipart`.
- Включает Swagger UI по адресу `<prefixUrl>/documentation`.

---

## FastifyGateway

`FastifyGateway` — наследник `Fastify`, реализующий **API Gateway**. Он автоматически создаёт HTTP‑маршруты для всех запросов, зарегистрированных в `GlobalServices`. При появлении нового сервиса или изменении его схемы шлюз динамически перестраивает маршруты.

### Подключение

```js
import { FastifyGateway } from '@morphcluster/web';

const gateway = new FastifyGateway(host, {
  baseUrl: '/api',          // префикс всех маршрутов
  prefixUrl: '/gateway'     // собственный префикс Fastify (Swagger будет на /gateway/documentation)
});
```

Обычно `GlobalServices` передаётся через `host` или внедряется как опциональная зависимость `GlobalServices`. Также требуется `Sessions`, если не все запросы анонимны.

### Как это работает

1. При старте `Gateway` подписывается на событие `onServiceChanged` реестра `GlobalServices`.
2. Для каждого сервиса и каждого его запроса, у которого задано свойство `http` (метод HTTP, например `'GET'`, `'POST'`), создаётся маршрут вида:
   ```
   <baseUrl>/<serviceName>/<requestName>
   ```
   Имена сервисов и запросов конвертируются из CamelCase в kebab-case (`MyService` → `my-service`, `doSomething` → `do-something`).

3. При получении HTTP‑запроса шлюз:
   - извлекает токен сессии из заголовка `Authorization: Bearer <token>`;
   - если запрос не помечен как `anonymous`, проверяет сессию через сервис `Sessions`;
   - если требуется `needAdmin`, проверяет флаг `isAdmin` в сессии;
   - подготавливает параметры запроса (`request.body` + IP + данные сессии);
   - логирует вызов с указанием сервиса и метода (если не `noLogs`);
   - вызывает `service.sendRequest()` соответствующего `GlobalService`;
   - результат возвращает клиенту как JSON, либо статус 204, если ответа нет;
   - в случае ошибки оборачивает её в `ComplexError` и возвращает клиенту (если пользователь — админ, ошибка всегда показывается).

### Тонкая настройка запросов

В схеме сервиса для каждого запроса можно указать:
- `http` — метод HTTP (`GET`, `POST`, `PUT`, `DELETE`). Если не задан, маршрут не создаётся.
- `anonymous` — разрешить доступ без токена.
- `needAdmin` — требовать права администратора.
- `noLogs` — отключить логирование вызова.
- `logLevel` — уровень логирования (по умолчанию 10).

### Генерация Swagger

Для каждого созданного маршрута автоматически формируется схема:
- `summary` берётся из `description` запроса.
- Если запрос не `anonymous`, добавляется `security: [{ token: [] }]`.
- Для методов, отличных от `GET`, тело запроса описывается JSON‑схемой `request`, из которой исключается поле `session`.

---

## ServiceHealth

`ServiceHealth` — простой HTTP‑сервис, предоставляющий информацию о состоянии всех сервисов в `ServiceHost`. Расширяет `Fastify` и добавляет один маршрут `GET /services`.

### Подключение

```js
import { ServiceHealth } from '@morphcluster/web';

const health = new ServiceHealth(host, { prefixUrl: '/health' });
```

### Ответ `/services`

```json
{
  "allStarted": false,
  "notStarted": ["SomeService"],
  "noRequirements": ["DependencyService"],
  "fullServices": [
    {
      "name": "MyService",
      "starting": false,
      "started": true,
      "requirements": []
    }
  ]
}
```

- `allStarted` — все ли сервисы запущены.
- `notStarted` — имена незапущенных сервисов.
- `noRequirements` — сервисы, чьи обязательные зависимости не загружены (но сами они не в `notStarted`).
- `fullServices` — детальная информация по каждому сервису.

---

## HttpProxy

`HttpProxy` — обратный прокси‑сервер, позволяющий объединить несколько внутренних HTTP‑серверов (и WebSocket) под одним портом. Сам является сервисом MorphCluster и может динамически добавлять/удалять маршруты.

### Подключение

```js
import { HttpProxy } from '@morphcluster/web';

const proxy = new HttpProxy(host, { port: 80, host: '0.0.0.0', proxyTimeout: 60000 });
```

### Конфигурация

| Параметр       | Тип    | По умолчанию | Описание |
|----------------|--------|--------------|----------|
| `port`         | number | обязательно   | Порт, на котором будет слушать прокси |
| `host`         | string | `'0.0.0.0'`  | IP для прослушивания |
| `proxyTimeout` | number | `120000`     | Таймаут проксирования запросов (мс) |

### Методы (доступны как запросы сервиса)

#### `addProxy({ url, address, type? })`
Добавляет новый прокси‑маршрут.
- `url` — префикс пути, по которому прокси будет принимать запросы.
- `address` — целевой URL (например, `http://localhost:8080`).
- `type` — `'proxy'` (по умолчанию) или `'websocket'`.

#### `delProxy({ url })`
Удаляет маршрут по URL-префиксу.

#### `list()`
Возвращает массив всех текущих маршрутов.

### Механизм работы

- Прокси слушает входящие HTTP‑запросы и ищет маршрут с наиболее длинным совпадающим префиксом.
- Если маршрут найден, запрос проксируется на целевой сервер с добавлением заголовка `x-real-ip`.
- При ошибке соединения (ECONNREFUSED, ETIMEDOUT и др.) клиенту возвращается соответствующий HTTP‑статус (502, 504) в формате JSON. Если ошибка `ECONNREFUSED` повторяется, прокси может автоматически удалить проблемный маршрут (опция включается принудительно в коде при 502).
- WebSocket‑соединения проксируются с использованием `http-proxy` в режиме `ws`.
- Сервис также предоставляет эндпоинт `/ip`, возвращающий информацию об IP клиента (полезно для отладки заголовков `X-Forwarded-For` и `X-Real-IP`).

### Автоматическая регистрация сервисов Fastify

Любой сервис, наследующий `Fastify`, при старте (если `disableProxy !== true`) автоматически регистрирует себя в `HttpProxy`, добавляя маршрут со своим `prefixUrl` и адресом `http://127.0.0.1:<порт>`. Это позволяет собирать все HTTP‑интерфейсы под единым входным портом.

---

## HttpStatic

`HttpStatic` — сервис для раздачи статических файлов (HTML, CSS, JS, изображений и т.д.). Поддерживает кэширование по ETag и автоматическую регистрацию в `HttpProxy` (опционально).

### Подключение

```js
import { HttpStatic } from '@morphcluster/web';

const staticServer = new HttpStatic(host, {
  prefixUrl: '/static',
  staticPath: './public',
  port: 8081,
  useProxy: true
});
```

### Конфигурация

| Параметр    | Тип     | По умолчанию | Описание |
|-------------|---------|--------------|----------|
| `prefixUrl` | string  | `''`         | URL‑префикс, по которому доступна статика |
| `staticPath`| string  | `'./static'` | Локальная директория с файлами (разрешается относительно `Config.packageRoot`) |
| `host`      | string  | `127.0.0.1`  | IP для прослушивания |
| `port`      | number  | `0`          | Порт |
| `useProxy`  | boolean | `false`      | Автоматически добавить прокси‑маршрут в `HttpProxy` |

### Поведение

- Запросы к путям, начинающимся с `prefixUrl`, транслируются в файловую систему относительно `staticPath`. Например, запрос `/static/css/style.css` ищет файл `./public/css/style.css`.
- Если путь заканчивается на `/`, добавляется `index.html`.
- Поддерживается условный запрос через заголовок `If-None-Match`: сервер отправляет ETag, основанный на времени модификации файла, и при совпадении возвращает `304 Not Modified`.
- MIME‑тип определяется по расширению файла с помощью библиотеки `mime-types`.
- Запрещены пути, содержащие `..`.

---

## Swagger UI

Встроенный модуль Swagger UI реализован как Fastify‑плагин (`fastifySwaggerUi`). Он автоматически добавляется ко всем сервисам, наследующим `Fastify`, и предоставляет интерфейс по адресу `<prefixUrl>/documentation`.

### Файлы

- **`swagger-initializer.mjs`** – Генерирует JavaScript‑код для инициализации Swagger UI с конфигурацией, полученной из OpenAPI‑схемы Fastify.
- **`index-html.mjs`** – Генерирует HTML‑страницу документации с подключением необходимых скриптов и стилей.
- **`serialize.mjs`** – Сериализует JavaScript‑значения в строковый код для вставки в инициализирующий скрипт.

### Особенности

- Swagger UI берёт спецификацию через эндпоинт `./json`, предоставляемый `@fastify/swagger`.
- Поддерживает кастомные темы через `opts.theme.css` и `opts.theme.js`.
- Все статические ресурсы Swagger UI (CSS, JS) встроены в бандл и раздаются виртуальными маршрутами Fastify (без необходимости отдельной папки).

---

## Схемы сервисов

### FastifyGateway (`src/fastify-gateway/service-schema.mjs`)

```json
{
  "name": "FastifyGateway",
  "description": "HTTP шлюз для сервисов",
  "requests": [
    {
      "name": "recreateRoutes",
      "description": "Пересоздать маршруты",
      "anonymous": false,
      "needAdmin": true,
      "noLogs": true,
      "http": null,
      "request": {},
      "response": {}
    }
  ]
}
```

Запрос `recreateRoutes` позволяет администратору принудительно перестроить все маршруты шлюза.

### HttpProxy (`src/http-proxy/service-schema.js`)

Содержит запросы:
- `list` – получить список маршрутов (только для админов).
- `addProxy` – добавить HTTP‑ или WebSocket‑прокси.
- `addWebsocket` (устаревший алиас для `addProxy` с типом `websocket`).
- `addStatic` (зарезервирован, но не реализован в коде; в схеме описан).
- `delRoute` – удалить маршрут по URL.

Все административные запросы требуют сессии с правами администратора.

---

## Экспорт

Пакет экспортирует все основные классы через `src/index.js`:

```js
import {
  Fastify,
  FastifyGateway,
  ServiceHealth,
  HttpProxy,
  HttpStatic
} from '@morphcluster/web';
```

Вспомогательные функции и схема Swagger UI доступны для внутреннего использования, но не экспортируются наружу.

---

## Интеграция с MorphCluster Core

Все описанные сервисы построены на базе `ServiceRequire`, поэтому могут использовать:
- `this.host.requireService(name)` / `this.host.waitService(name)` для доступа к другим сервисам.
- `this.Config` и `this.Logger` для конфигурации и логирования.
- Автоматическое внедрение зависимостей, указанных в `this.requirements` и `this.optionalServices`.

Для корректной работы в системе, использующей `GlobalServices`, необходимо, чтобы `HttpProxy`, `Sessions` (если нужна аутентификация) и все сервисы, к которым обращается `FastifyGateway`, были зарегистрированы в реестре.

# MorphCluster NATS

Данный модуль расширяет MorphCluster Core поддержкой распределённого взаимодействия через [NATS](https://nats.io/). Он предоставляет сервисы для публикации схем, маршрутизации запросов, событий, каналов данных, мониторинга здоровья и таймеров между узлами кластера.

---

## Обзор

Модуль реализует транспортный слой на базе NATS, позволяя локальным сервисам прозрачно вызывать удалённые сервисы и подписываться на события других узлов. Основные компоненты:

- **NatsConnection** – управление подключением, отправка запросов, подписки, сериализация.
- **Сервисы публикации** – экспортируют локальные схемы, запросы, события, таймеры, данные каналов в NATS.
- **Сервисы подписки** – получают объявления от других узлов и регистрируют их в локальном `GlobalServices`, делая удалённые сервисы доступными для локального кода.
- **Вспомогательные классы** – `ServiceNats` (устаревший), `NatsEvent` (устаревший), `NatsChannels` и т.д.

Все сервисы строятся на базе `ServiceRequire` и ожидают наличия в хосте `NatsConnection` и `GlobalServices`.

---

## NatsConnection

Базовый сервис, устанавливающий соединение с NATS-сервером и предоставляющий методы для обмена сообщениями. Наследуется от `ServiceRequire`, поэтому сам является сервисом.

```js
import { NatsConnection } from '@morphcluster/nats';
// или
const { NatsConnection } = require('@morphcluster/nats');
```

### Конструктор

```js
new NatsConnection(host, natsConfig)
```

- `host` – экземпляр `ServiceHost`.
- `natsConfig` – объект конфигурации:
  - `server` – адрес NATS-сервера (например, `"localhost:4222"`).
  - `timeout` – таймаут запросов в мс (по умолчанию из конфигурации).
  - `prefix` – префикс для всех subject'ов (по умолчанию `""`).
  - `requestMode` – режим запросов: `"auto"` (автовыбор короткого/длинного запроса), `"long"` (принудительно длинные запросы), `"simple"` (короткие, устаревший).
  - `format` – формат сериализации: `"json"` или `"msgpack"`.
  - `queue` – имя группы подписчиков для балансировки (используется в `NatsSubscriber`).

### Свойства

- `connection` – объект соединения NATS (`Nats.NatsConnection`) или `null` до подключения.
- `serializer` – экземпляр сериализатора (`SerializerJson` или `SerializerMsgpack`).
- `requestMode` – режим запросов.
- `prefix` – префикс subject'ов.

### Методы

#### `connect()`
Устанавливает соединение с сервером. Вызывается автоматически при `start()`.

#### `disconnect()`
Корректно закрывает соединение (drain).

#### `request(subject, params)`
Отправляет запрос и ожидает ответ. В зависимости от `requestMode` использует короткий или длинный протокол.
- `subject` – строка (автоматически добавляется префикс).
- `params` – объект данных.
- Возвращает Promise с ответом. Если ответ содержит поле `error`, выбрасывается `ComplexError`.

#### `subscribe(subject, callback)`
Подписывается на сообщения. Возвращает объект подписки (`Nats.Subscription`), который можно использовать для отмены.
- `callback(msg, subject)` – вызывается при получении сообщения. Данные уже десериализованы.

#### `unsubscribe(subscription)`
Отменяет подписку.

#### `subscribeEvent(subject, callback)`
Подписка на события (см. `NatsEventer`). Отличие от `subscribe` в том, что не ожидается ответов и используется отдельная коллекция подписок.

#### `unsubscribeEvent(subject, callback)`
Отписывается от события.

#### `publish(subject, params)`
Публикует сообщение без ожидания ответа.

### Внутренние классы

#### SerializerJson / SerializerMsgpack
Реализуют кодирование/декодирование данных с помощью `JSON` или `msgpackr`. Используются внутри `NatsConnection`.

#### NatsRequester
Отвечает за логику запросов. Методы:
- `simpleRequest(subject, rawRequest)` – короткий запрос-ответ.
- `longRequest(subject, params)` – отправляет запрос, при необходимости создаёт канал для больших данных, получает ответ.
- `autoRequest(subject, params)` – автоматически выбирает между коротким и длинным протоколом в зависимости от размера запроса.
- `fetchLongResponse(channel)` – собирает ответ по частям через созданный канал.

#### NatsSubscriber
Управляет подписками на входящие запросы. Использует очередь, если задан `queue`. Для каждого сообщения с полем `reply` автоматически формирует ответ (в том числе через канал, если ответ большой).

#### NatsEventer
Упрощённая подписка на события (без ответов). Агрегирует несколько колбэков на один subject.

#### RequestChannel
Временный канал для передачи больших запросов/ответов. Получает данные по частям, собирает их, затем передаёт ответ также по частям.
- Свойства: `subject`, `status` (этапы: `request`, `process`, `responseHeader`, `response`, `idle`).
- Методы: `subscribe()`, `waitRequest()`, `sendResponse(data)`, `unsubscribe()`.

---

## ServiceNats (УСТАРЕВШИЙ)

```js
import { ServiceNats } from '@morphcluster/nats';
```

**Deprecated.** Вместо него рекомендуется использовать глобальные сервисы и `CoreClient`. `ServiceNats` реализует прокси для удалённого сервиса через NATS, динамически создавая методы запросов и событий.

### Конструктор

```js
new ServiceNats(host, svcSchema, dontWait = false)
```
- `host` – `ServiceHost`.
- `svcSchema` – объект схемы (`ServiceSchema`).
- `dontWait` – если `true`, не ждать появления схемы в NATS при старте.

### Особенности

- При `start()` создаёт динамические методы на основе схемы: для каждого запроса `reqS.name` становится методом, для каждого события – свойством типа `NatsEvent`.
- Отправляет запросы через `NatsConnection.request`.
- Может ожидать появления схемы от других узлов через `waitSchema`.

---

## NatsEvent (УСТАРЕВШИЙ)

```js
import { NatsEvent } from '@morphcluster/nats';
```

**Deprecated.** Используйте стандартный `Event` из Core в связке с глобальными сервисами. `NatsEvent` расширяет `Event`, автоматически подписываясь на соответствующий subject в NATS при появлении слушателей.

### Методы

- `on(callback, workspace)` – добавляет слушателя и при необходимости инициирует NATS-подписку.
- `off(callback)` – удаляет слушателя и, если больше нет подписчиков, отписывается от NATS.
- `setNats(natsConnection)` – задаёт соединение и активирует подписку, если уже есть слушатели.

---

## Сервисы NATS-интеграции

Все перечисленные ниже сервисы должны быть добавлены в `ServiceHost` и правильно сконфигурированы. Они используют `NatsConnection` и `GlobalServices` как зависимости.

### NatsSchemaPublisher

Публикует схемы локальных сервисов в NATS, чтобы другие узлы могли их обнаружить.

```js
import { NatsSchemaPublisher } from '@morphcluster/nats';
```

#### Зависимости
- `GlobalServices`
- `NatsConnection`
- `NatsRequestListener`

#### Поведение
- При старте подписывается на события `Services.broadcast.refresh` и `Services.broadcast.refreshOne` – по ним отправляет все схемы или конкретную.
- Слушает `onServicePublished` от `NatsRequestListener` и автоматически публикует новую схему.
- Если задан `config.interval`, периодически рассылает все схемы.
- Публикует в subject `Services.broadcast.schema`.

#### Конфигурация
- `interval` – интервал в мс для повторной публикации (необязательно).

---

### NatsSchemaListener

Слушает объявления схем от других узлов и регистрирует их в `GlobalServices` как удалённые сервисы.

```js
import { NatsSchemaListener } from '@morphcluster/nats';
```

#### Зависимости
- `GlobalServices`
- `NatsConnection`

#### Поведение
- Подписывается на `Services.broadcast.schema`.
- При получении схемы создаёт `NatsServiceInterface` и вызывает `GlobalServices.addRemoteService`.
- При старте отправляет запрос `Services.broadcast.refresh`, чтобы получить схемы уже работающих узлов.

#### NatsServiceInterface (внутренний)
Реализует `RemoteServiceInterface` для NATS.
- `request(requestName, params, log)` – отправляет запрос через `NatsConnection.request`.
- `subscribeEvent(eventName, callback)` – подписывается на событие.
- `unsubscribeEvent(eventName, callback)` – отписывается.

---

### NatsRequestListener

Принимает входящие запросы из NATS и передаёт их локальным сервисам.

```js
import { NatsRequestListener } from '@morphcluster/nats';
```

#### Зависимости
- `GlobalServices`
- `NatsConnection`

#### Поведение
- Отслеживает локальные сервисы (не удалённые и не виртуальные), для каждого запроса в схеме создаёт NATS-подписку вида `<serviceName>.<requestName>`.
- При получении запроса создаёт дочерний логгер (если разрешено), вызывает метод сервиса и возвращает ответ.
- Поддерживает специальный subject для каналов (большие запросы).
- Публикует событие `onServicePublished` при добавлении схемы сервиса.

#### Важно
- Использует `host.onServiceStarted`, чтобы подписывать сервисы, запущенные позже.

---

### NatsEventPublisher

Публикует события локальных сервисов в NATS.

```js
import { NatsEventPublisher } from '@morphcluster/nats';
```

#### Зависимости
- `NatsConnection`

#### Поведение
- Для каждого локального сервиса находит события (из `schema.events`), подписывается на них и при срабатывании публикует данные в NATS с subject `<serviceName>.<eventName>`.
- Использует `host.onServiceStarted` для автоматической обработки новых сервисов.

---

### NatsServicePublishers

Сервис для публикации данных от `Publisher` (вероятно, имеются в виду `ChannelSender`) в NATS. Аналогичен `NatsChannels`, но использует другой формат subject'ов: `Channels.<channelName>`.

**Примечание:** В предоставленном коде используется свойство `svc.publishers` и метод `pub.onDataChanged`, но в Core таких сущностей нет (есть `ChannelSender`). Возможно, это устаревший компонент. Рекомендуется использовать `NatsChannels`.

```js
import { NatsServicePublishers } from '@morphcluster/nats';
```

---

### NatsChannels

Обеспечивает прозрачную синхронизацию `ChannelSender` и `ChannelAggregator` между узлами через NATS.

```js
import { NatsChannels } from '@morphcluster/nats';
```

#### Зависимости
- `NatsConnection`

#### Поведение
- При старте подписывается на `host.onServiceStarted` и обрабатывает все существующие сервисы.
- Для каждого `ChannelSender` подписывается на `onDataChanged` и публикует данные в `Channels.<channelName>`, добавляя `hostName` и `serviceName`.
- Для каждого `ChannelAggregator` подписывается на `Channels.<channelName>` и при получении данных вызывает `aggregator.setData(serviceName, data)`.
- Также подписывается на `Channels.Request.*` для ответа на запросы актуальных данных от других узлов.
- При запуске отправляет запросы данных для всех локальных агрегаторов.

---

### NatsHealthPublisher

Периодически публикует состояние здоровья локального хоста (какие сервисы запущены, есть ли невыполненные зависимости) в NATS.

```js
import { NatsHealthPublisher } from '@morphcluster/nats';
```

#### Зависимости
- `NatsConnection`

#### Конфигурация
- `publishTime` – интервал публикации в мс (по умолчанию 30000). Если 0, то автоматическая периодическая публикация отключается (только при старте и изменении состояния).

#### Публикуемые данные
```json
{
  "hostName": "main",
  "allStarted": false,
  "noRequirements": ["SomeService"],
  "fullServices": [
    {
      "name": "MyService",
      "starting": false,
      "started": true,
      "requirements": [...]
    }
  ]
}
```
Отправляется в `Health.broadcast.status`.

---

### NatsHealthSubscriber

Принимает данные о здоровье от других узлов и предоставляет их через запрос `list`.

```js
import { NatsHealthSubscriber } from '@morphcluster/nats';
```

#### Зависимости
- `NatsConnection`

#### Запросы
- `list()` – возвращает массив объектов с информацией о хостах, включая поле `updated` (секунд с последнего обновления). Требует `needAdmin: true`.

#### Поведение
- Подписывается на `Health.broadcast.status`, сохраняет данные в `this.healthData`.

---

### NatsTimersPublisher

Периодически публикует состояние всех таймеров локального хоста (запущены, ошибки) в NATS.

```js
import { NatsTimersPublisher } from '@morphcluster/nats';
```

#### Зависимости
- `NatsConnection`

#### Конфигурация
- `publishInterval` – интервал в мс (по умолчанию 5000).

#### Данные
Публикует в `Timers.broadcast.status` объект с полями `hostName` и `timers[]`, где каждый таймер содержит `name`, `serviceName`, `enabled`, `running`, `lastError`, `errorCounter`.

---

### NatsTimersAggregator

Собирает данные о таймерах со всех узлов и предоставляет их через запрос `list`.

```js
import { NatsTimersAggregator } from '@morphcluster/nats';
```

#### Зависимости
- `NatsConnection`

#### Запросы
- `list()` – возвращает агрегированный список таймеров с отметкой времени `updated`.

---

## Тестовый сервис NatsConnectionTest

```js
import { NatsConnectionTest } from '@morphcluster/nats';
```

Предназначен для проверки корректности работы `NatsConnection`, включая передачу больших сообщений.

#### Запросы (все требуют `needAdmin: true`)
- `testShort` – короткий запрос-ответ.
- `testLargeRequest` – большой запрос, короткий ответ.
- `testLargeResponse` – короткий запрос, большой ответ.
- `testLarge` – оба больших.
- `reqForTest` – внутренний, используется для эхо-тестов.

#### Конструктор
```js
new NatsConnectionTest(host, config)
```
- `config.largeSize` – размер больших данных (по умолчанию 10 МБ).

---

## Интеграция в хост

Типичная конфигурация хоста с NATS:

```js
import { ServiceHost, Config, Logger } from '@morphcluster/core';
import {
  NatsConnection,
  NatsSchemaPublisher,
  NatsSchemaListener,
  NatsRequestListener,
  NatsChannels,
  NatsHealthPublisher,
  NatsHealthSubscriber,
  // ... другие
} from '@morphcluster/nats';

const host = new ServiceHost('main');
const config = new Config({ /* ... */ });

// Добавляем базовые сервисы Core (GlobalServices, Logger и т.д.)
// ...

// NATS-соединение
host.addService(new NatsConnection(host, { server: 'localhost:4222', format: 'msgpack' }));

// Обнаружение сервисов
host.addService(new NatsSchemaPublisher(host));
host.addService(new NatsSchemaListener(host));
host.addService(new NatsRequestListener(host));

// Каналы данных
host.addService(new NatsChannels(host));

// Мониторинг
host.addService(new NatsHealthPublisher(host, { publishTime: 10000 }));
host.addService(new NatsHealthSubscriber(host));

await host.start(logger);
```

После запуска локальные сервисы становятся доступны удалённым узлам, а удалённые сервисы – локальным через `GlobalServices`.

# MorphCluster Registry

**MorphCluster Registry** — это пакет, расширяющий MorphCluster Core и предоставляющий сервисы для централизованного хранения и управления настройками (конфигурацией) системы. Он включает два сервиса:

- **CommonRegistry** — базовое персистентное хранилище настроек в виде JSON-файла с возможностью расширения схемы данных.
- **RegistryDummy** — адаптер, обеспечивающий обратную совместимость и предоставляющий старый интерфейс реестра, делегируя вызовы в CommonRegistry.

Оба сервиса строятся на основе `ServiceRequire` из ядра MorphCluster Core и могут работать как локально, так и в составе распределённой системы через глобальные сервисы.

---

## Установка и подключение

```js
import { CommonRegistry, RegistryDummy } from '@morphcluster/registry';
```

Пакет автоматически регистрирует схемы сервисов и готов к использованию в `ServiceHost`.

---

## CommonRegistry

`CommonRegistry` — это сервис, реализующий персистентное хранилище произвольных JSON-данных с возможностью валидации и расширения схемы. Данные сохраняются в файл `common-registry-data.json`, а схема — в `common-registry-schema.json` внутри директории данных приложения (`Config.dataDir`).

Наследуется от `ServiceRequire`, поэтому может ожидать запуска других сервисов перед собственным стартом (по умолчанию зависимостей нет).

### Инициализация

```js
const registry = new CommonRegistry(host);
// host — экземпляр ServiceHost
```

В конструкторе:
- инициализируется схема сервиса через `setSvcSchema(serviceSchema)` (импортируется из `./service-schema.js`);
- создаётся стартовая JSON-схема реестра (`registrySchema`) на основе `defaultSchema` — объект с единственным возможным ключом `main`, содержащим поля `name` и `description` (оба необязательные);
- создаются события `onChanged` и `onSchemaRequest`.

### Свойства

| Свойство | Тип | Описание |
|----------|-----|----------|
| `data` | `object` | Текущие данные реестра. Изначально пустой объект, заполняется при старте из файла. |
| `registrySchema` | `object` | Текущая JSON-схема, описывающая допустимую структуру данных. Может расширяться вызовом `mergeSchema`. |
| `onChanged` | `Event` | Срабатывает при любом изменении данных (`set` или `merge`). В событии передаётся рабочее пространство `"common"`. |
| `onSchemaRequest` | `Event` | Событие запроса на отправку схемы (может использоваться для синхронизации). |

### Методы запросов (Request Handlers)

Все методы требуют прав администратора (`needAdmin: true`), за исключением отсутствующих явных проверок в коде — согласно схеме, все запросы помечены `needAdmin: true`. Доступ к ним осуществляется через стандартный механизм вызова сервиса (`sendRequest`).

#### `get()`
Возвращает текущие данные реестра.

```js
const data = await registry.sendRequest('get', {}, log);
// data = { ... }
```

#### `set({ data })`
Полностью заменяет данные реестра новым объектом. Автоматически сохраняет изменения на диск и генерирует событие `onChanged`.

```js
await registry.sendRequest('set', {
  data: {
    main: { name: 'MyServer', description: 'Основной сервер' },
    features: { darkMode: true }
  }
}, log);
```

#### `merge({ data })`
Глубоко объединяет переданный объект с текущими данными (используется `deepmerge`). Изменения сразу пишутся на диск и вызывают `onChanged`.

```js
await registry.sendRequest('merge', {
  data: { features: { newUI: false } }
}, log);
```

#### `mergeSchema({ schema })`
Расширяет JSON-схему реестра путём рекурсивного наложения (`JsonSchema.overlay`). Новая схема записывается в файл.

```js
await registry.sendRequest('mergeSchema', {
  schema: {
    properties: {
      features: {
        type: 'object',
        properties: {
          darkMode: { type: 'boolean' },
          newUI: { type: 'boolean' }
        }
      }
    }
  }
}, log);
```

#### `getSchema()`
Возвращает текущую полную JSON-схему реестра.

```js
const schema = await registry.sendRequest('getSchema', {}, log);
```

#### `clearSchema()`
Сбрасывает схему реестра до значения по умолчанию (`defaultSchema`) и сохраняет её.

```js
await registry.sendRequest('clearSchema', {}, log);
```

### Жизненный цикл

При старте сервиса (`start(log)`):
1. Определяются пути `dataDir`, `registrySchemaPath`, `dataPath`.
2. Вызывается `super.start(log)` для ожидания обязательных зависимостей.
3. Выполняется попытка загрузить схему из файла `common-registry-schema.json`. Если файл отсутствует или повреждён, в лог пишется ошибка уровня 50.
4. Аналогично загружаются данные из `common-registry-data.json`. При отсутствии файла `this.data` остаётся пустым объектом, ошибка логируется.

Сервис не переопределяет `stop()` (используется стандартный из `ServiceRequire`).

---

## RegistryDummy

`RegistryDummy` — сервис, предоставляющий интерфейс реестра, совместимый с ранними версиями MorphCluster. Он оборачивает `CommonRegistry`, добавляя поддержку «рабочих пространств» и метод `delete`.

Наследуется от `ServiceRequire` и требует `CommonRegistry` в качестве обязательной зависимости.

### Инициализация

```js
const dummy = new RegistryDummy(host);
```

В конструкторе:
- регистрируется схема сервиса `Registry` (из `./service-schema.js`);
- объявляется требование `CommonRegistry`;
- создаются события `onChanged` и `onSchemaRequest`.

### Свойства

| Свойство | Тип | Описание |
|----------|-----|----------|
| `CommonRegistry` | `CommonRegistry` | Ссылка на экземпляр `CommonRegistry`, заполняется после `super.start()`. |
| `onChanged` | `Event` | Проксирует событие `onChanged` от CommonRegistry, но с workspace `"main"`. |
| `onSchemaRequest` | `Event` | Аналогично событию из CommonRegistry (может использоваться для уведомлений о необходимости отправить схему). |

### Методы запросов

#### `listWorkspaces()`
Возвращает статический список рабочих пространств. В текущей реализации — только одно пространство `"main"`.

```js
const { workspaces } = await dummy.sendRequest('listWorkspaces', {}, log);
// workspaces = ["main"]
```

#### `getAll()`
Возвращает данные всех рабочих пространств. Для пространства `"main"` берутся текущие данные из `CommonRegistry.data`, к ним добавляются поля `name: 'main'` и `dummy: true`.

```js
const { workspaces } = await dummy.sendRequest('getAll', {}, log);
// workspaces: [{ name: 'main', dummy: true, ...data }]
```

#### `delete({ name })`
В `RegistryDummy` не реализован — всегда выбрасывает ошибку `"Not available in RegistryDummy"`.

#### `set({ project })`
Заменяет данные реестра, вызывая `CommonRegistry.set({ data: project })`.

```js
await dummy.sendRequest('set', { project: { ... } }, log);
```

#### `merge({ project })`
Дополняет данные через `CommonRegistry.merge({ data: project })`.

#### `mergeSchema({ schema })`
Проксирует вызов `CommonRegistry.mergeSchema`.

#### `getSchema()`
Проксирует вызов `CommonRegistry.getSchema`.

#### `clearSchema()`
Проксирует вызов `CommonRegistry.clearSchema`.

### Жизненный цикл

- **start(log)**: вызывает `super.start(log)` для получения доступа к `CommonRegistry`, затем подписывается на событие `CommonRegistry.onChanged` и при его срабатывании генерирует собственное `onChanged` с workspace `"main"`.
- **stop()**: вызывает `super.stop()`, затем отписывается от `CommonRegistry.onChanged`.

---

## Схемы сервисов

### CommonRegistry

| Запрос | Параметры | Ответ | Описание |
|--------|-----------|-------|----------|
| `get` | — | `object` | Получить все настройки |
| `set` | `data: object` | — | Заменить настройки целиком |
| `merge` | `data: object` | — | Глубоко дополнить настройки |
| `mergeSchema` | `schema: object` | — | Расширить JSON-схему реестра |
| `getSchema` | — | `object` | Получить текущую схему реестра |
| `clearSchema` | — | — | Сбросить схему до стандартной |

События:
- `onChanged` — настройки изменились (параметр workspace = `"common"`);
- `onSchemaRequest` — запрос на отправку схем реестра.

### Registry (RegistryDummy)

| Запрос | Параметры | Ответ | Описание |
|--------|-----------|-------|----------|
| `listWorkspaces` | — | `{ workspaces: string[] }` | Список пространств (всегда `["main"]`) |
| `getAll` | — | `{ workspaces: object[] }` | Данные всех пространств |
| `delete` | `name: string` | — | **Не поддерживается** (выбрасывает ошибку) |
| `set` | `project: object` | — | Замена настроек пространства `main` |
| `merge` | `project: object` | — | Дополнение настроек |
| `mergeSchema` | `schema: object` | — | Расширение схемы реестра |
| `getSchema` | — | `object` | Текущая схема реестра |
| `clearSchema` | — | — | Сброс схемы |

События:
- `onChanged` — настройки изменились (параметр workspace = `"main"`);
- `onSchemaRequest` — запрос схем реестра.

---

## Пример использования

```js
import { ServiceHost, Config, Logger, LoggerBackendConsole } from '@morphcluster/core';
import { CommonRegistry, RegistryDummy } from '@morphcluster/registry';

const config = new Config({ /* ... */ });
await config.load();

const host = new ServiceHost('main');
host.Config = config;
host.Logger = new Logger({ host: 'main' });
host.Logger.backends = [new LoggerBackendConsole()];

// Добавляем базовый реестр
const commonReg = new CommonRegistry(host);
host.addService(commonReg, 'CommonRegistry');

// Добавляем адаптер совместимости
const dummyReg = new RegistryDummy(host);
host.addService(dummyReg, 'Registry');

await host.start(host.Logger);

// Используем адаптер для установки настроек
await dummyReg.sendRequest('set', {
  project: {
    main: { name: 'Prod', description: 'Production server' }
  }
}, host.Logger.createWriter('Initial config'));

// Читаем через CommonRegistry
const data = await commonReg.sendRequest('get', {}, host.Logger.createWriter('Read config'));
console.log(data);
```

---

## Интеграция с другими сервисами

Оба сервиса могут быть зарегистрированы в `GlobalServices` и использоваться удалённо через `ServiceBridge` или напрямую через `GlobalService`. Благодаря наследованию от `ServiceRequire`, они легко встраиваются в цепочки зависимостей. Например, сервис аутентификации может требовать `CommonRegistry` для хранения настроек политик безопасности.

```js
class MyAuthService extends ServiceRequire {
  constructor(host) {
    super(host);
    this.requirements = ['CommonRegistry'];
  }

  async start(log) {
    await super.start(log);
    const cfg = await this.CommonRegistry.sendRequest('get', {}, log);
    // использовать cfg.policies...
  }
}
```

---

## Примечания

- Данные реестра хранятся в файлах без шифрования. Для безопасности критичных данных рекомендуется ограничивать права доступа к директории `dataDir`.
- Схема реестра (`registrySchema`) используется для валидации через `JsonSchema.sanitize` или другие инструменты MorphCluster Core.
- `RegistryDummy` существует для плавного перехода со старого API. В новых проектах рекомендуется использовать `CommonRegistry` напрямую.
- При отсутствии файлов схемы и данных сервис стартует с пустыми значениями, записывая ошибки в лог (уровень 50). Это позволяет системе не падать при первом запуске.

# MorphCluster Logger

Подсистема логирования состоит из двух пакетов:

- **@morphcluster/logger** — серверная часть, предоставляющая сервисы для сбора, хранения и администрирования логов.
- **@morphcluster/logger-client** — клиентский бэкенд для `Logger` из ядра, обеспечивающий передачу логов на сервер логирования.

## Серверная часть (@morphcluster/logger)

Пакет регистрирует в `ServiceHost` четыре сервиса: `LogStoreBackend`, `LogStoreAdmin`, `LogOraList` и вспомогательный `PostgresMigrator` (из отдельного пакета). Основное хранилище — PostgreSQL, взаимодействие с которым вынесено в сервис `LogPgStorage2`.

### LogStoreBackend

Главный сервис приёма логов. Реализует два запроса: `write` и `close`. Полученные данные не пишутся в базу сразу, а накапливаются в оперативной памяти в виде дерева объектов `LogRecord` (класс `ActiveLogs`). При закрытии корневого лога вся ветка отправляется в `LogPgStorage2.insertLogTree()`.

```js
// Пример использования (происходит автоматически при подключении LoggerBackendStore)
await logStoreBackend.write({ id, parentId, message, level, ... });
await logStoreBackend.close({ id, timestamp });
```

**Жизненный цикл активного лога:**

1. Клиент вызывает `write` при создании лога и для каждого дочернего сообщения.
2. Сообщения собираются в древовидную структуру `LogRecord` внутри `ActiveLogs`.
3. Когда клиент вызывает `close` для записи (или по тайм-ауту), `ActiveLogs` проверяет, все ли дочерние записи закрыты. Если да — вся ветка помечается на архивацию и через 5 секунд (`flushLogTime`) вызывается `onArchive`, передающий корневой `LogRecord` в `LogPgStorage2`.
4. `LogPgStorage2` вставляет всё дерево одной массовой вставкой в таблицу `logs2`.

**Свойства и зависимости:**

- `LogPgStorage2` — обязательный сервис для работы с БД.
- `RegistryHelper` — для получения настроек логирования из общего реестра (`minLevel`, `maxRecords`).
- `activeLogs: ActiveLogs` — хранилище незавершённых логов.

### LogRecord

Модель одной записи лога. Образует древовидную структуру: родитель (`parent`) и массив дочерних записей (`children`).

```js
const record = new LogRecord({
  id: 'uuid',
  message: 'Запрос выполнен',
  level: 3,
  service: 'MyService',
  payload: { ... }
});
record.addChild(childRecord);
```

**Основные поля:**

- `id`, `message`, `level`, `host`, `service`, `username`, `ip`
- `payload` — произвольные данные
- `order` — порядок среди дочерних одного родителя
- `duration` — длительность (заполняется при закрытии)
- `timestamp` — временная метка
- `closed` — флаг завершения записи
- `parent` / `children` — связи дерева
- `serverTimestamp` — момент попадания на сервер (для детекта зависших логов)
- `dontArchive` — если `true`, запись не будет сохранена в БД (например, автоматически созданный родитель «Unknown Parent»).

**Методы:**

- `addChild(record)` — добавить дочернюю запись (только если текущая ещё не закрыта).
- `levelToRoot()` — протолкнуть максимальный уровень вверх по дереву до корня.
- `findRecordById(id)` — рекурсивный поиск записи по идентификатору.
- `getPath()` — путь от корня до текущей записи.
- `getRoot()` — корневая запись.
- `sortChildren()` — сортировка детей по `order` (рекурсивно).
- `getJson()` — сериализация всего поддерева в объект (для ответа API).
- `forEach(callback)` — обход всех узлов дерева в ширину.

### ActiveLogs

Хранилище всех активных (ещё не заархивированных) записей в памяти. Управляет их жизненным циклом: вставка, закрытие, автоматическое закрытие по тайм-ауту, архивация.

```js
const activeLogs = new ActiveLogs();
activeLogs.onArchive = (rootLog) => { /* сохранить в БД */ };
activeLogs.insert({ id: '...', message: 'Старт' });
activeLogs.close('id', Date.now());
```

**Основные методы и логика:**

- `insert(request)` — создаёт или обновляет `LogRecord`.  
  - Если передан `parentId`, находит или создаёт родителя. При отсутствии реального родителя создаётся запись-заглушка с `dontArchive = true` и сообщением `'Unknown Parent'`.  
  - Вызывает `levelToRoot()` для подъёма уровня.  
  - Обрабатывает ситуацию, когда `close` пришёл раньше `write` (через очередь `unknownCloses`).
- `close(id, timestamp)` — закрывает запись. Если запись ещё не известна, сохраняет «закрытие» в `unknownCloses` на 10 секунд, ожидая появления записи. После закрытия рекурсивно проверяет родительские ветки (`checkBranch`); если корень полностью закрыт, запускает таймер (5 секунд), после которого ветка удаляется из памяти и передаётся в `onArchive`.
- `checkTimeouts()` — периодическая проверка (каждую секунду):
  - Закрытые ветки, готовые к архивации, отправляются в `onArchive`.
  - Неиспользованные `unknownCloses` удаляются через 10 секунд.
  - Незакрытые логи, неактивные более 15 минут (`autoCloseTime`), автоматически закрываются с уровнем 50 и сообщением `'Log Timeout'`.
- `removeBranch(curLog)` — удаляет всё поддерево из `logsPlain`.
- `getRootLogs()` — возвращает только корневые логи (для `listActive`).

**Свойства:**

- `logsPlain` — плоский массив всех известных записей (и корневых, и дочерних).
- `unknownCloses` — очередь «закрытий», ожидающих свои записи.
- `onArchive` — колбэк, вызываемый при готовности корневого лога к сохранению.

### LogPgStorage2

Сервис для взаимодействия с PostgreSQL. Выполняет миграции, создаёт пул соединений, вставляет деревья логов, предоставляет методы для чтения и очистки.

```js
await logPgStorage2.insertLogTree(rootLogRecord);
const result = await logPgStorage2.list({ service: 'MyService', limit: 20 });
const log = await logPgStorage2.byId('uuid');
```

**Управление:**

- `minLevel: number` — логи с уровнем ниже не сохраняются (по умолчанию 0, берётся из реестра).
- `maxRecords: number` — после вставки, если счётчик корневых записей превышает лимит, вызывается `cleanup()` (полная очистка таблицы). Значение по умолчанию 500.
- `recordsCount` — текущее количество корневых записей.

**Методы:**

- `insertLogTree(rootLog)` — рекурсивно обходит дерево, собирает массив записей для вставки. Игнорирует записи с `level < minLevel`. Выполняет массовый `INSERT`. При дубликате `id` корневого лога переименовывает его, генерирует новый `id` и ставит уровень 50.
- `list(request)` — выборка корневых логов с фильтрацией (`service`, `username`, `minLevel`, `dateFrom/dateTo` и др.) и пагинацией.
- `listChildren(parentId)` / `listChildrenRecursive(parentId)` — получение дочерних записей.
- `byId(logId)` — одна запись по идентификатору.
- `cleanup()` — `TRUNCATE` таблицы.
- `tableSize()` — размер таблицы в человекочитаемом виде.
- `serviceOptions()` — список уникальных сервисов, встречающихся в логах.
- `start(log)` — инициализирует миграции, создаёт пул соединений, проверяет подключение (`PGTest`) и считает количество корневых записей.

**Схема БД** (подразумевается): таблица `logs2` с колонками `id`, `parent_id`, `message`, `level`, `duration`, `timestamp`, `order`, `host`, `service`, `username`, `ip`, `created`, `payload`, `num` (автоинкрементный номер для курсорной пагинации).

### LogStoreAdmin

Сервис для административного доступа к логам. Все запросы требуют прав администратора (`needAdmin: true`).

**Запросы:**

- `list(request)` — делегирует в `LogPgStorage2.list`.
- `listActive(req)` — возвращает текущие незавершённые корневые логи из `activeLogs`. Поддерживает фильтрацию по `minLevel` и `service`.
- `details(request)` — сначала ищет лог в активных (`activeLogs`), если не найден — загружает из базы с рекурсивными дочерними записями.
- `stats` — возвращает `recordsCount` и размер таблицы.
- `cleanup` — принудительная очистка таблицы логов.
- `serviceOptions` — делегирует в `LogPgStorage2`.

**Зависимости:** `LogStoreBackend`, `LogPgStorage2`.

### LogOraList

Специализированное представление для получения логов запросов к Oracle (тип сообщения `'OraQueries/exec'` или `'OraQueries/execFunc'`). Извлекает из древовидной структуры входные и выходные параметры, собирая их из фиксированной иерархии дочерних записей (4 уровня вложенности). Возвращает плоский список с полями `queryName`, `duration`, `in_payload`, `out_payload`.

**Запрос:**

- `list(filter)` — `filter` может содержать `username`, `payload` (поиск по тексту во входном payload), `minLevel`, `minDuration`, `dateFrom/dateTo`, плюс стандартные `limit` и `offset`.

### Схемы сервисов

- **LogStoreBackend** — два внутренних запроса `write` и `close` (не требуют аутентификации, не логируются сами).
- **LogStoreAdmin** — запросы `list`, `listActive`, `details`, `serviceOptions`, `stats`, `cleanup` (требуют прав администратора).
- **LogOraList** — запрос `list` с фильтром.

## Клиентская часть (@morphcluster/logger-client)

### LoggerBackendStore

Реализация бэкенда `LoggerBackend` из ядра, которая отправляет все логи на серверный `LogStoreBackend`. Обеспечивает прозрачную буферизацию логов до момента появления сервиса логирования в хосте.

```js
import { LoggerBackendStore } from '@morphcluster/logger-client';

const logStoreBackend = new LoggerBackendStore(host);
logger.backends.push(logStoreBackend);
```

**Принцип работы:**

1. При создании получает ссылку на `ServiceHost` и подписывается на событие `onServiceStarted`.
2. Как только в хосте появляется сервис с именем `LogStoreBackend` (или `LogStoreAcc`, если есть), он сохраняет ссылку на него.
3. До этого момента все вызовы `write` и `close` помещаются в очередь (`queue`).
4. При обнаружении сервиса очередь «сбрасывается» — все накопленные операции последовательно отправляются в реальный сервис. После этого `flushed` устанавливается в `true`, и дальнейшие вызовы выполняются напрямую, без очереди.

Таким образом, логирование не теряется даже на этапе запуска приложения, когда сервис логирования ещё не готов.

**Свойства:**

- `host: ServiceHost`
- `LogStore` — ссылка на обнаруженный сервис (например, `LogStoreBackend`).
- `queue: Array` — очередь отложенных вызовов `{ method, data, resolve, reject }`.
- `flushed: boolean` — флаг завершения «слива» очереди.

## Взаимодействие компонентов

1. Сервис приложения использует обычный `Logger` из ядра, добавив в него бэкенд `LoggerBackendStore`.
2. `LoggerBackendStore` отправляет запросы `write` и `close` в локальный или удалённый `LogStoreBackend` (через `GlobalService`).
3. `LogStoreBackend` сохраняет все записи в `ActiveLogs` в оперативной памяти.
4. Когда ветка лога полностью закрыта, `ActiveLogs` вызывает `LogPgStorage2.insertLogTree()`, который одной массовой вставкой записывает всё дерево в PostgreSQL.
5. Для чтения логов используются сервисы `LogStoreAdmin` (универсальный) и `LogOraList` (специализированный для Oracle-запросов). Они читают данные напрямую из `ActiveLogs` (активные) и из `LogPgStorage2` (архивные).

Такая архитектура минимизирует количество обращений к базе данных и позволяет гибко настраивать уровни логирования через центральный реестр.

# MorphCluster Sessions

Пакет `@morphcluster/sessions` предоставляет сервис управления сессиями пользователей в экосистеме MorphCluster. Он состоит из двух основных компонентов: `Sessions` (публичный API) и `SessionsStorage` (персистентное хранение в PostgreSQL). Сервис обеспечивает создание, проверку, удаление и листинг сессионных токенов с поддержкой административных прав.

## Импорт

```js
import { Sessions, SessionsStorage } from '@morphcluster/sessions';
```

## Sessions

**Назначение:** Централизованное управление сессиями пользователей. Предоставляет методы для создания, удаления, получения и валидации токенов. Реализует двухуровневое кэширование: in‑memory для быстрого доступа и постоянное хранение в PostgreSQL через внутренний сервис `SessionsStorage`.

**Наследует:** `ServiceRequire` (из `@morphcluster/core`)

**Зависимости (`requirements`):**

| Сервис | Тип | Описание |
|--------|-----|----------|
| `SessionsStorage` | обязательный | Сервис персистентного хранения сессий в БД |

**Конфигурация:**

Параметры передаются через объект `config`, который `Sessions` получает при создании. Основные настройки PostgreSQL извлекаются из `config.common` и передаются в `SessionsStorage`:

- `config.common.postgresUri` → `config.pg.uri` (строка подключения к PostgreSQL)
- `config.common.timeZone` → `config.pg.timezone` (часовой пояс для сессий)

Сам `Sessions` не имеет собственных параметров, кроме тех, что нужны для настройки `SessionsStorage`.

### Запросы (методы)

Все запросы описаны в `service-schema.mjs` и регистрируются через `setSvcSchema`.

| Метод | HTTP | Права | Параметры | Ответ | Описание |
|-------|------|-------|-----------|-------|----------|
| `get` | POST | `anonymous: true` | `{ token: string }` | `Session` | Получить сессию по токену. Сначала проверяется in‑memory кэш, затем PostgreSQL. Если сессия не найдена, выбрасывается `ComplexError('Invalid Token', 'InvalidToken')`. |
| `list` | POST | `needAdmin: true` | `{ filters?: { login?, isAdmin?, permanent? } }` | `Session[]` | Получить список сессий из PostgreSQL по фильтрам. Параметр `filters` обязателен, но может быть пустым объектом. |
| `create` | POST | `needAdmin: true` | `{ userId: number, login: string, isAdmin: boolean }` | `{ token: string }` | Создать новую сессию. Генерирует UUID-токен, сохраняет в памяти и асинхронно записывает в БД через `SessionsStorage.insert`. |
| `delete` | POST | `needAdmin: true` | `{ token: string }` | `void` | Удалить сессию. Удаляет из БД и из in‑memory кэша, если запись имеет тип `"storage"`. |
| `deleteMany` | POST | `needAdmin: true` | `{ filters: { login?, isAdmin?, permanent? } }` | `number` (rowCount) | Удалить несколько сессий по фильтрам. Фильтры обязательны. Также очищает in‑memory кэш полностью. |
| `setPermanent` | POST | `needAdmin: true` | `{ token: string, permanent: boolean }` | `number` (rowCount) | Установить флаг постоянной сессии (не истекающей). |

#### Вспомогательный метод `validateHttp`

Используется другими сервисами (например, `FastifyGateway`, `Eventer`) для проверки сессии из HTTP‑заголовка `Authorization: Bearer <token>`. Сигнатура:

```js
async validateHttp(request, reply, log = null, needAdmin = false)
```

- Извлекает токен из заголовка `Authorization`.
- Вызывает `this.get(token, "common", log)`.
- Если `needAdmin === true`, проверяет наличие `isAdmin` в сессии.
- В случае отсутствия токена или недостатка прав выбрасывает `ComplexError`.

### Внутреннее устройство

- **In‑memory кэш:** массив `this.sessions`, хранящий объекты сессий (тип `Session`). Используется для быстрого поиска и хранения «невалидных» записей (чтобы не обращаться в БД повторно для несуществующих токенов).
- **Генерация токенов:** метод `makeid()` возвращает UUID v4 через `crypto.randomUUID()`.
- **Поиск сессии:** метод `get()` сначала ищет в `this.sessions`. Если не найдено и токен соответствует формату UUID, выполняет запрос в `SessionsStorage.byId()`. При отрицательном результате создаёт в памяти запись `{ token, type: "invalid" }` и выбрасывает ошибку `InvalidToken`.
- **Удаление:** метод `delete()` удаляет запись из БД, затем из кэша, но только если сессия в кэше имеет тип `"storage"`. Это предотвращает повторное появление невалидной записи.

### Жизненный цикл

- `start(log)`: вызывает `super.start(log)`, что в свою очередь запускает зависимый `SessionsStorage`.
- `stop()`: наследуется от `ServiceRequire` (останавливает `SessionsStorage`).

### Примечания

- Токены создаются с использованием криптографически стойкого UUID.
- Асинхронная вставка в БД при создании сессии не ожидается (`this.SessionsStorage.insert(newSession).then()`), поэтому возможна кратковременная несогласованность между кэшем и базой.
- Метод `deleteMany` полностью очищает in‑memory кэш (`this.sessions = []`), что может привести к лишним запросам к БД для ещё действительных сессий.

---

## SessionsStorage

**Назначение:** Персистентное хранение сессий в PostgreSQL. Обеспечивает миграцию схемы БД, пул соединений и все CRUD-операции над таблицей `sessions`.

**Наследует:** `ServiceRequire`

**Зависимости (`requirements`):**

| Сервис | Тип | Описание |
|--------|-----|----------|
| `PostgresMigrator` | обязательный | Сервис миграции схемы БД (из `@morphcluster/postgres-migrator`) |

**Конфигурация:** передаётся через объект `config.pg`:

- `uri` — строка подключения к PostgreSQL (формируется из `config.common.postgresUri`).
- `schema` — имя схемы БД, в которой будет создана таблица `sessions`.
- `timezone` — часовой пояс (из `config.common.timeZone`).

### Методы (внутренние, вызываются сервисом `Sessions`)

| Метод | Параметры | Возврат | Описание |
|-------|-----------|---------|----------|
| `byId(id)` | `id: string` | `Session` или `null` | Получить сессию по идентификатору (токену). Выполняет запрос `SELECT ... WHERE id = $1`. |
| `insert(session)` | `session: Session` | `void` | Вставить новую сессию в таблицу. |
| `list(req)` | `req: { login?, isAdmin?, permanent? }` | `Session[]` | Получить список сессий с динамическими фильтрами. Поля `isAdmin` и `permanent` приводятся к `0/1`. |
| `delete(id)` | `id: string` | `number` (rowCount) | Удалить одну сессию по ID. |
| `deleteMany(req)` | `req: { login?, isAdmin?, permanent? }` | `number` (rowCount) | Удалить сессии по фильтрам. |
| `setPemanent(id, perm)` | `id: string`, `perm: boolean` | `number` (rowCount) | Установить флаг `permanent` для сессии. |

### Жизненный цикл

- **`start(log)`:**
  1. Вызывает `super.start(log)` для инициализации `PostgresMigrator`.
  2. Запускает миграцию SQL-файлов из директории `./sql` (или `./dist/sessions-sql` для Webpack-сборки) с указанием схемы и строки подключения.
  3. Создаёт пул соединений `pg.Pool` с переданной строкой подключения.
  4. Проверяет соединение запросом `SELECT 1`.
- **`stop()`:** Закрывает пул соединений, если он был создан.

### Примечания

- Имя метода `setPemanent` содержит опечатку (правильно `setPermanent`), но сохранено для совместимости с существующим кодом.
- Миграции находятся в подпапке `sql` относительно файла `storage.mjs`.
- Пул соединений создаётся один раз и используется всеми запросами.

---

## Тип Session

```js
/**
 * @typedef {Object} Session
 * @property {string} token
 * @property {string} type       // "storage" или "invalid"
 * @property {number} userId
 * @property {string} login
 * @property {boolean} isAdmin
 */
```

Поле `type` используется в `Sessions` для различения записей: `"storage"` — сессия из БД, `"invalid"` — кэшированное отсутствие сессии.

---

## Пример использования

```js
import { ServiceHost, Config, Logger } from '@morphcluster/core';
import { Sessions } from '@morphcluster/sessions';

const host = new ServiceHost('main');
const config = new Config({
  common: {
    postgresUri: 'postgresql://user:pass@localhost:5432/mydb',
    timeZone: 'UTC'
  },
  pg: {
    schema: 'public'
  }
});

const sessions = new Sessions(host, config.values);
host.addService(sessions, 'Sessions');

await host.start(logger);

// Создание сессии администратора
const { token } = await sessions.sendRequest('create', {
  userId: 1,
  login: 'admin',
  isAdmin: true
}, log);

// Проверка сессии
const session = await sessions.sendRequest('get', { token }, log);
console.log(session.isAdmin); // true
```

---

## Интеграция с другими сервисами

- **`Auth`** из `@morphcluster/core` использует `Sessions` для аутентификации администратора.
- **`FastifyGateway`** и **`HttpProxy`** применяют `Sessions.validateHttp` для проверки авторизационных заголовков.
- **`Eventer`** из Supervisor использует `Sessions` для аутентификации WebSocket-подключений.

# Создание новых хостов

В MorphCluster приложение строится из **хостов** (ServiceHost) и **сервисов** (Service), которые могут объединяться в единую систему через GlobalServices. Ниже приведены пошаговые инструкции по созданию новых хостов и сервисов на основе исходного кода и документации.

---

## 1. Основные понятия

- **ServiceHost** – контейнер, управляющий жизненным циклом сервисов: запуск, остановка, перезапуск при ошибках, ожидание зависимостей.
- **Service** – базовый класс любого сервиса. Имеет схему (ServiceSchema) с запросами, событиями, таймерами.
- **ServiceRequire** – расширение Service с механизмом ожидания других сервисов (зависимостей) перед стартом.
- **GlobalServices** – реестр, связывающий локальные и удалённые сервисы, позволяет вызывать их по имени через схему.
- **Config** – глобальная конфигурация (загружается из `global-config.json`).
- **Logger** – иерархическое логирование с привязкой к сервису и запросу.

---

## 2. Создание нового хоста

Хост – это экземпляр `ServiceHost`. Обычно он создаётся в точке входа приложения.

**Пример** (`main.js`):
```js
import { ServiceHost, Config, Logger, LoggerBackendConsole } from '@morphcluster/core';
import { Fastify, HttpProxy } from '@morphcluster/fastify';
import MyService from './my-service.js';

const config = new Config();
await config.load();

const host = new ServiceHost('main');
host.Config = config;

// Настройка логгера
const logger = new Logger({
  service: 'host',
  host: 'main',
  loggerErrorsFile: './logs/errors.log'
});
logger.backends.push(new LoggerBackendConsole());
host.Logger = logger;

// Добавление сервисов
host.addService(new Fastify(host, { port: 3000 }));
host.addService(new MyService(host));

await host.start(logger);
```

**Основные шаги:**
- Загрузить конфигурацию через `Config.load()`.
- Создать `ServiceHost` с уникальным именем.
- Присвоить хосту `Config` и `Logger`.
- Добавить сервисы через `host.addService(service)`.
- Запустить хост через `host.start(logger)`.

---

## 3. Создание простого сервиса

Наследуйте класс от `Service` или `ServiceRequire`. Определите схему и реализуйте обработчики.
Зависимости объявляются в массиве `this.requirements`.

```js
import { ServiceRequire } from '@morphcluster/core';

export default class MyDependentService extends ServiceRequire {
  constructor(host, config) {
    super(host); // обязательно передаём host для ServiceRequire
    /** @type {MySimpleService} */
    this.MySimpleService = null; // будет заполнено автоматически
    this.requirements = ['MySimpleService'];
    this.addRequest({
      name: 'echoTwice',
      description: 'Вызывает echo дважды',
      request: {
        type: 'object',
        properties: { text: { type: 'string' } },
        required: ['text']
      }
    });
  }

  async echoTwice(req, workspace, log) {
    const first = await this.MySimpleService.echo(req, workspace, log);
    const second = await this.MySimpleService.echo(req, workspace, log);
    return { first, second };
  }

  async start(log) {
    await super.start(log); // здесь произойдёт ожидание всех requirements
  }
}
```

Сервис с именем `MySimpleService` будет автоматически найден в том же хосте и присвоен в свойство с таким же именем.

---

## 4. Схема сервиса: запросы, события, таймеры

Полную схему задают через методы `addRequest`, `addEvent` или прямо через `this.setSvcSchema(json)`.

### 4.1. Запросы (requests)
Объект запроса:
```js
{
  name: 'methodName',        // имя метода
  description: '...',
  request: { ... },          // JSON-схема входных данных (может быть null)
  response: { ... },         // JSON-схема ответа
  anonymous: false,          // доступно без токена (для HTTP)
  needAdmin: false,          // требуются права админа
  noLogs: false,             // отключить логирование
  http: 'POST',              // HTTP-метод (для автоматической генерации маршрутов)
}
```
После добавления запроса обработчик должен быть методом с таким же именем в классе.

### 4.2. События (events)
```js
this.addEvent({
  name: 'onUserAdded',
  description: 'Вызывается при добавлении пользователя',
  structure: { ... }   // JSON-схема передаваемых данных
});

// В коде сервиса событие становится экземпляром Event
this.onUserAdded = new Event();
// Отправка события
this.onUserAdded.emit(workspace, data);
```

Клиенты подписываются через `svc.getEvent('onUserAdded').on(callback)`.

### 4.3. Таймеры
```js
import { Timer } from '@morphcluster/core';

const timer = new Timer('cleanupTimer', 60000, async (log) => {
  // код, выполняемый каждую минуту
});
timer.maxRunningTime = 10000; // таймаут выполнения в мс
timer.serviceName = this.name; // будет установлено при старте
timer.logger = this.Logger;    // то же
this.timers.push(timer);
```

---

## 5. Конфигурация сервиса

Параметры сервиса обычно передаются вторым аргументом конструктора:
```js
constructor(host, config) {
  super(host);
  this.someOption = config.someOption || 'default';
}
```
Конфигурация заполняется через `config.services` в `global-config.json`:
```json
{
  "common": { "baseUrl": "http://localhost:3000" },
  "services": [
    { "name": "MyService", "config": { "someOption": "value" } }
  ]
}
```
Затем `ServiceFactory` (см. раздел 6) автоматически инстанцирует сервис с нужным конфигом.

---

## 6. HTTP‑интерфейс и Fastify

Если сервису нужен свой HTTP‑эндпоинт, удобно наследоваться от `Fastify` (из `@morphcluster/fastify`).

```js
import { Fastify } from '@morphcluster/fastify';

export default class MyHttpService extends Fastify {
  constructor(host, config) {
    super(host, config);
    this.addRoute({
      method: 'GET',
      url: '/hello',
      handler: (req, reply) => reply.send('Hello World')
    });
  }
}
```

Сервис сам запустит Fastify-сервер и при необходимости зарегистрируется в `HttpProxy` (если он есть в системе).

Для создания API Gateway, объединяющего все сервисы, используется `FastifyGateway`. Он автоматически строит маршруты на основе схем сервисов с полем `http`.

## 7. Глобальные сервисы и межхостовое взаимодействие

Если приложение разбито на несколько хостов (например, отдельные процессы), для общения используется `GlobalServices`.  
- На каждом хосте запускается `GlobalServices` (обычно через `ServiceFactory`).
- При добавлении локального сервиса он автоматически публикуется в реестре.
- Для вызова удалённого сервиса используется `GlobalServiceInterface` (например, через NATS или HTTP-мост `ServiceBridge`).

Подробнее в документации к пакетам `@morphcluster/nats` и `@morphcluster/fastify`.

---

## 8. Обработка ошибок

Используйте `ComplexError` для ошибок, которые могут быть показаны пользователю:

```js
throw new ComplexError('Текст ошибки', 'MyErrorCode', { доп_данные }, {
  showUser: true,
  httpStatus: 422
});
```

В логах ошибки автоматически обрабатываются, в HTTP‑ответе от FastifyGateway они возвращаются клиенту с указанным статусом.

---

## 10. Резюме

- **Хост** управляет сервисами; создайте `ServiceHost`, назначьте `Config` и `Logger`, вызовите `start`.
- **Сервис** – класс с методами, соответствующими `addRequest`, и наследующий от `Service` / `ServiceRequire`.
- Зависимости указываются в массиве `requirements` (только для `ServiceRequire`).
- HTTP-функциональность добавляется через классы из `@morphcluster/fastify`.
- Глобальная коммуникация – через `GlobalServices` и `GlobalServiceInterface`.

Эти шаблоны покрывают большинство сценариев при разработке на MorphCluster.

# Хост Supervisor

Supervisor — это центральный управляющий хост в экосистеме MorphCluster, построенный на базе фреймворка `@morphcluster/core`. Он выполняет функции развёртывания, мониторинга и обновления микросервисных пакетов, а также предоставляет служебные сервисы (лицензирование, WebSocket-уведомления, проксирование и др.). Supervisor запускается как самостоятельный процесс Node.js и управляет жизненным циклом дочерних процессов и воркеров, в которых работают другие микросервисы.

Документация описывает внутреннее устройство Supervisor на основе предоставленного исходного кода.

## Общая архитектура

Supervisor оформлен как главный модуль, создающий экземпляр `ServiceHost` и регистрирующий в нём все необходимые сервисы.

Каждый функциональный блок реализован в виде отдельного сервиса, наследующего `ServiceRequire` (или `Service`). Сервисы общаются между собой через механизм зависимостей (`requirements`) и вызовы запросов (`sendRequest`), а также через глобальные объекты (например, `GlobalServices`, `Config`, `Logger`).

Supervisor управляет **пакетами** — директориями, содержащими микросервисы (каждый пакет — это самостоятельное приложение, которое может быть запущено как отдельный процесс или воркер). Пакеты могут быть двух типов:
- **Дистрибутивные (dist)** — поставляются вместе с Supervisor и автоматически запускаются при первом старте.
- **Установленные вручную или через обновление** — находятся в `packagesDir`.

Сам Supervisor тоже является пакетом и может обновлять сам себя.

## Точка входа и инициализация

Файл `src/start.mjs` выполняет следующие шаги:

1. Создаёт экземпляр `ServiceHost` с именем `"Supervisor"`.
2. Загружает конфигурацию (`Config`) из `config.mjs` и сохраняет в `host.Config`.
3. Настраивает корневой логгер `Logger`:
   - Читает параметры из `config.values.Logger`.
   - Добавляет бэкенды: `LoggerBackendConsole` и `LoggerBackendStore` (отправка логов в централизованное хранилище).
4. Регистрирует глобальный обработчик `uncaughtException`.
5. Последовательно добавляет все системные и пользовательские сервисы при помощи функции `addService(svcName, svcClass)`. Каждый сервис получает конфигурацию, объединяющую `config.values[svcName]` и `config.values.common`.
6. Запускает хост: `host.start(log)`.
7. После успешного старта выводит `"***All Services Started***"`.
8. Регистрирует обработчики сигналов `SIGINT` и `SIGTERM`, а также сообщения `shutdown` для воркеров, которые вызывают функцию `stop()`.

Функция `stop()` вызывает `host.stop()`, после чего завершает процесс с заданным кодом (по умолчанию 0). Таймаут принудительного завершения — 60 секунд.

Список системных сервисов, добавляемых в хост:

| Имя в хосте | Класс / Назначение |
|-------------|-------------------|
| `PostgresMigrator` | Миграция схем БД PostgreSQL |
| `LogStoreBackend`, `LogStoreAdmin`, `LogOraList` | Сервисы логирования (из `@morphcluster/logger`) |
| `NatsTimersPublisher`, `NatsTimersAggregator` | Мониторинг таймеров через NATS |
| `License` | Проверка и управление лицензией |
| `Sessions`, `Auth` | Хранение сессий и аутентификация |
| `Registry` (Dummy), `CommonRegistry`, `RegistryHelper` | Централизованное хранилище настроек |
| `GlobalServices` | Реестр глобальных сервисов |
| `HttpProxy`, `FastifyGateway`, `ServiceBridge` | HTTP-инфраструктура |
| `NatsConnection`, `NatsRequestListener`, `NatsEventPublisher`, `NatsSchemaPublisher`, `NatsSchemaListener`, `NatsHealthPublisher`, `NatsHealthSubscriber`, `NatsConnectionTest` | Подключение и маршрутизация через NATS |
| `Eventer` | WebSocket-сервер для real-time уведомлений |
| `PackageInfo`, `NodeHosts`, `Packages`, `PkgExtractor`, `PkgUpdater`, `PkgCleanup` | Управление пакетами (см. ниже) |

## Основные сервисы

### PackageInfo

**Файл:** `src/PackageInfo/index.js`

Хранит информацию об установленных пакетах в JSON-файле `package-infos.json` в директории данных (`dataDir`). Каждая запись содержит имя пакета, источник установки, признак ручного управления и дату установки.

**Запросы:**
- `list` — возвращает список всех записей.
- `set({ name, ... })` — добавляет или обновляет информацию о пакете.
- `remove({ name })` — удаляет запись.
- `reload` — перечитывает файл с диска.

Зависимости: нет (кроме базового `ServiceRequire`). Используется другими сервисами для отслеживания статуса установки.

### NodeHosts

**Файл:** `src/NodeHosts/index.js`

Управляет запущенными экземплярами пакетов — процессами и воркерами. Хранит массив объектов `ProcessMaster` или `WorkerMaster`.

**Запросы:**
- `list` — статусы всех запущенных хостов (id, тип, статус, ошибка).
- `startHost({ id, dir, dataDir, nodeExec, isWorker })` — запускает новый процесс или воркер.
- `stopHost({ id })` — останавливает процесс/воркер.
- `getStdOut({ id })` — возвращает накопленный вывод stdout/stderr.

При запуске `startHost` проверяет, запущен ли уже хост с таким ID. Если нет — создаёт экземпляр `ProcessMaster` или `WorkerMaster` в зависимости от флага `isWorker`, устанавливает переменные окружения (`MCL_CONFIG`, `MCL_DATA_DIR`, `TZ`) и запускает.

**Зависимости:** нет (базовый сервис).

### Packages

**Файл:** `src/Packages/index.js`

Центральный сервис управления жизненным циклом пакетов. Отвечает за:
- Сканирование директорий с пакетами (`packagesDir` и `packagesDistDir`).
- Ведение списка автозапуска (`packages-autostart.json`).
- Запуск и остановку пакетов.
- Удаление пакетов.
- Перезапуск самого Supervisor при обновлении.

**Запросы:**
- `list` — возвращает список всех пакетов с их статусами, информацией из PackageInfo, автостартом.
- `startPackage({ id })` — запускает пакет. Если пакет с таким именем уже запущен (но с другой версией), останавливает старый и запускает новый, обновляя автостарт. Для supervisor выполняется обновление файла запуска и перезапуск всей системы.
- `stopPackage({ id })` — останавливает пакет и удаляет из автозапуска.
- `remove({ id })` — удаляет пакет (файлы) после остановки.
- `setManual({ pkgId, manual })` — устанавливает признак ручного управления (через PackageInfo).
- `restartSupervisor` — вызывает `HostStopper.stop(85)`.

Методы жизненного цикла:
- `start()`: создаёт директории пакетов, загружает список автозапуска и запускает все пакеты из него. Если файл автозапуска отсутствует, вызывает `createAutostart()`, который автоматически запускает все дистрибутивные пакеты.
- `stop()`: вызывает `super.stop()` (базовый).

**Зависимости:** `PackageInfo`, `NodeHosts`.

### PkgExtractor

**Файл:** `src/PkgExtractor/index.js`

Распаковывает архивы `.7z` с пакетами, находящиеся в `dataDir/pkg-install`, в директорию пакетов.

**Запросы:**
- `extractAll` — извлекает все архивы. Сначала обрабатывает все пакеты, кроме supervisor, затем запускает каждый из них через `Packages.startPackage`. Supervisor извлекается и запускается последним.
- `extract({ pkgId, arcPath, source, manual })` — извлекает конкретный архив и регистрирует установку в `PackageInfo`.

Использует библиотеку `node-7z` для распаковки.

**Зависимости:** `Packages`, `PackageInfo`.

### PkgUpdater

**Файл:** `src/PkgUpdater/index.js`

Сервис автоматического обновления пакетов с удалённого сервера обновлений. Работает по таймеру, настраиваемому через `CommonRegistry`.

**Запросы:**
- `check` — обращается к API сервера обновлений (`/api/main/csp-update/check`), передаёт системное имя, получает список доступных обновлений. Сравнивает с установленными версиями, отмечает флаги `downloaded` и `installed`.
- `downloadAll` — для каждого неустановленного пакета скачивает архив `.7z` в `pkgUpdaterDir`, затем вызывает `PkgExtractor.extract` и `Packages.startPackage` (с учётом ручного режима). Старые версии пакетов удаляются через `Packages.remove`.

**Зависимости:** `Packages`, `PackageInfo`, `PkgExtractor`, `RegistryHelper`.

Конфигурация в `CommonRegistry` (схема `registry-schema.js`):
- `main.name` — системное имя (передаётся в запросы).
- `PkgUpdater.token` — токен авторизации.
- `PkgUpdater.timer` — интервал проверки обновлений в мс (по умолчанию 300000).

### PkgCleanup

**Файл:** `src/PkgCleanup/index.js`

Сервис автоматической очистки устаревших пакетов (не запущенных и не помеченных как ручные, установленных через обновление). Периодичность задаётся в `CommonRegistry`, но в текущей реализации таймер закомментирован, очистка выполняется только при ручном вызове `cleanup`.

**Запросы:**
- `cleanup` — удаляет все неиспользуемые автообновлённые пакеты.

**Зависимости:** `Packages`, `PackageInfo`, `RegistryHelper`.

### Eventer

**Файл:** `src/Eventer/index.mjs`

Реализует WebSocket-сервер для доставки real-time сообщений подключённым пользователям. Работает поверх библиотеки `ws`.

**Запросы:**
- `send({ userId, login, broadcast, channel, message })` — отправляет JSON-сообщение пользователям по фильтрам.
- `connectedUsers` — возвращает список авторизованных пользователей (уникальных).
- `requestConnections` — технический список всех соединений.

**События:**
- `onUserOnline` — генерируется при подключении первого соединения пользователя или отключении последнего.

**Аутентификация:** клиент после подключения должен отправить сообщение `{"type":"auth","token":"..."}`. Токен проверяется через сервис `Sessions`. После успешной аутентификации клиент может подписаться на каналы сообщением `{"type":"subscribe","channel":"..."}`. Внутренний класс `Connection` управляет состоянием одного WebSocket-соединения.

Сервер запускается на выделенном порту (или 0, тогда назначается случайный), регистрируется в `HttpProxy` как WebSocket-прокси по префиксу `prefixUrl` (по умолчанию `/eventer`).

Периодически (каждые `pingTimeout` мс) проверяет живучесть соединений с помощью ping/pong.

**Зависимости:** `Sessions`.

### License

**Файл:** `src/License/index.mjs`

Управляет файлом лицензии (`license.json`). Содержит поля `name`, `expire`, `key` (зашифрован). Периодически (каждые 60 секунд) проверяет изменение лицензии и генерирует событие `onLicenseChanged`.

**Запросы:**
- `getLicense` — возвращает объект с полями `name`, `expire`, `expired` (bool).
- `setLicense({ license })` — записывает новую лицензию в файл.

**Зависимости:** нет.

## Вспомогательные классы управления процессами

Находятся в `src/lib/`. Иерархия: `BaseMaster` → `ProcessMaster`, `WorkerMaster`.

### BaseMaster

Абстрактный класс, реализующий общую логику управления запущенным процессом или воркером:
- Хранение статуса (`running`, `stopping`, `restarting`, `error`, `null`).
- Буферизация stdout/stderr (ограничение по `maxLines`).
- Таймеры перезапуска (`restartMsec`) и принудительного завершения (`terminateMsec`).
- Методы `start()`, `stop()`, `restart()`, `terminate()`.
- `_sendStop()` — абстрактный метод отправки сигнала остановки.
- `_processStopped(e)` — обработчик завершения: если остановка ожидаемая, разрешает промис остановки; иначе запускает таймер перезапуска.

### ProcessMaster

Наследует `BaseMaster`. Запускает дочерний процесс через `child_process.spawn`. Передаёт переменные окружения, рабочую директорию. При `stop()` отправляет `SIGTERM`, при `terminate()` — `SIGKILL`.

### WorkerMaster

Наследует `BaseMaster`. Запускает worker thread (`worker_threads.Worker`). Поддерживает передачу сообщений через `postMessage`. При `_sendStop()` отправляет сообщение `{ type: 'shutdown' }` в воркер. При `terminate()` вызывает `worker.terminate()`.

## Взаимодействие с инфраструктурой MorphCluster

Supervisor активно использует следующие компоненты MorphCluster:

- **NATS** — для обнаружения сервисов на других узлах, публикации схем, событий, здоровья и каналов. Сервисы `NatsSchemaPublisher`, `NatsSchemaListener`, `NatsRequestListener`, `NatsEventPublisher`, `NatsHealthPublisher`, `NatsHealthSubscriber` делают локальные сервисы доступными удалённо.
- **GlobalServices** — реестр глобальных сервисов, скрывающий разницу между локальными и удалёнными реализациями.
- **HttpProxy / FastifyGateway** — единая точка HTTP-доступа ко всем сервисам. `FastifyGateway` создаёт маршруты вида `/<prefix>/<serviceName>/<requestName>`. Supervisor регистрирует `ServiceBridge` для ручного вызова сервисов.
- **Sessions / Auth** — хранение сессий и базовая аутентификация администратора.
- **CommonRegistry** — централизованные настройки (токены, интервалы).
- **Logger** — все логи Supervisor отправляются в централизованное хранилище через `LoggerBackendStore`.

## Жизненный цикл пакетов

1. **Установка** — архив `.7z` помещается в `dataDir/pkg-install`. PkgExtractor распаковывает его в `packagesDir`, создавая директорию с именем `<name>-<version>`. В PackageInfo добавляется запись об установке.
2. **Первый запуск** — Packages сканирует директории, при отсутствии автозапуска запускает все дистрибутивные пакеты.
3. **Автозапуск** — список ID пакетов сохраняется в `packages-autostart.json`. При старте Supervisor запускает все пакеты из этого списка.
4. **Обновление** — PkgUpdater периодически опрашивает сервер обновлений, скачивает новые версии, устанавливает их рядом со старыми. При запуске новой версии старая останавливается, а после успешного старта — удаляется (если не помечена как manual).
5. **Ручное управление** — если пакет помечен `manual: true`, он не будет автоматически остановлен или удалён при обновлении. Пользователь сам управляет его запуском.
6. **Очистка** — PkgCleanup удаляет неиспользуемые автоустановленные пакеты.

## Конфигурация

Файл `src/config.mjs` содержит значения по умолчанию, которые могут быть переопределены через внешний файл конфигурации (`Config.load()`). Основные параметры:

- `Logger.*` — настройки логирования.
- `Sessions.pg` — подключение к PostgreSQL для сессий.
- `NatsConnection.queue` — группа очереди NATS.
- `Packages.packagesDir` — путь к директории с установленными пакетами (по умолчанию `./packages`).
- `PkgUpdater.updateServerUrl` — URL сервера обновлений.
- `Eventer.port`, `prefixUrl`, `pingTimeout` — параметры WebSocket-сервера.

## Процесс остановки

1. При получении сигнала или вызове `HostStopper.stop()` запускается `stop()` в `start.mjs`.
2. Устанавливается 60-секундный таймаут, после которого процесс будет принудительно завершён, а список незавершённых сервисов выведен в консоль.
3. Вызывается `host.stop()`, который последовательно останавливает все сервисы (каждый сервис вызывает остановку своих процессов/воркеров через `NodeHosts`).
4. После успешной остановки процесс завершается с заданным кодом.

`HostStopper` — глобальный синглтон, позволяющий другим сервисам (например, `Packages.restartSupervisor`) инициировать остановку всего хоста с определённым кодом (85 для перезапуска supervisor).

# Слой базы данных



# Хост Postgres

**Назначение:** Обеспечивает работу с PostgreSQL через Oracle-совместимые интерфейсы. Предоставляет загрузку метаданных функций, пулы соединений, короткие и длинные транзакции, нотификации и утилиты генерации обёрток. Включает адаптеры, скрывающие различия между Oracle и PostgreSQL.

Пакет предназначен для работы в составе MorphCluster‑приложения (например, Supervisor) и использует общие сервисы: `DbServices`, `RegistryHelper`, `GlobalServices` и т.д.

## Точка входа (`src/index.mjs`)

```js
export default {
  name: "Postgres",
  config,
  clients: {},
  services: {
    DbServices, PgFunctions, OraFunctions, PgPool, OraQueries,
    PgTransactions, PgTools, PgNotify, OraLongTransactions,
    OraQueriesPool, OLTTest, OraAdpTest
  }
}
```

Все сервисы регистрируются в `ServiceHost`. Они общаются через механизм зависимостей (`requirements`) и прямые вызовы методов друг друга.

---

## Основные сервисы

### 1. PgFunctions
**Файл:** `src/pg-queries/pg-functions/index.mjs`

Загружает метаданные о всех функциях/процедурах из системных таблиц PostgreSQL и кэширует их. Используется другими сервисами для поиска функций по имени и пространству имён.

**Зависимости:** нет  
**Ключевые методы:**
- `reload()` – перечитывает функции из БД (вызывается при старте).
- `find({namespace, name})` – поиск функции по схеме и имени.
- `findByName({name})` – поиск только по имени (менее точно).

### 2. OraFunctions
**Файл:** `src/ora-adapter/ora-functions/index.mjs`

Представляет функции PostgreSQL в «оракул-подобном» виде: пакеты и процедуры. Использует `PgFunctions` для получения сырых метаданных и конвертирует их в формат, привычный для Oracle-клиентов.

**Зависимости:** `PgFunctions`  
**Ключевые методы:**
- `list()` – возвращает список всех схем (пакетов).
- `getPackage({packageName})` – список функций в схеме.
- `getFunc({packageName, funcName})` – детальное описание функции (параметры, типы, направления).
- `reload()` – обновляет данные, перезагружая `PgFunctions`.

### 3. PgPool
**Файл:** `src/pg-queries/pg-pool/index.mjs`

Пул «коротких» соединений с PostgreSQL. Каждый запрос выполняется в отдельной транзакции (BEGIN → COMMIT/ROLLBACK). Используется сервисами `OraQueries` для быстрых запросов.

**Зависимости:** `PgFunctions`, `RegistryHelper`  
**Управление через CommonRegistry:**
- `pgQueryPool.connectionCount` – размер пула (по умолчанию 0 = отключен).

**Методы:**
- `execSql({sql, values, session})` – выполнить SQL.
- `execFunc({namespace, funcName, params, session})` – выполнить функцию.
- `reconnectAll()` – пересоздать все соединения.
- `state()` – текущий статус соединений (для `OraQueriesPool`).
- `reload()` – инициализация ссылок на служебные функции (`set_user_id`, `write_error_log`).

**Особенности:**
- Устанавливает контекст пользователя через `accounts.set_user_id`.
- Логирует ошибки в таблицу `logs.write_error_log`.
- Автоматически переподключается при обрывах (таймер).

### 4. PgTransactions
**Файл:** `src/pg-queries/pg-transactions/index.mjs`

Управляет длинными транзакциями. В отличие от `PgPool`, соединение удерживается открытым между вызовами, поддерживается ручное управление (BEGIN, выполнение нескольких запросов, COMMIT/ROLLBACK).

**Зависимости:** `RegistryHelper`, `DbServices`, `PgFunctions`  
**Управление через CommonRegistry:**
- `PgTransactions.idleTimeout` – таймаут бездействия транзакции (мс).
- `PgTransactions.cleanTimeout` – через сколько очищать закрытые транзакции (мс).

Внутренний класс `PGTPool` содержит массив экземпляров `PGTransaction`. Каждая транзакция имеет жизненный цикл: `created → connecting → ready → executing → executed → fetching → closing → closed`.

**Методы (внешние через `OraLongTransactions`):**
- `create({session})` – создать новую транзакцию, возвращает `trxId`.
- `execFunc({trxId, namespace, funcName, params, session})` – выполнить функцию в транзакции.
- `execSql({trxId, sql, values, session})` – выполнить SQL.
- `getExecResult({trxId})` – получить результат после выполнения (блокирующий).
- `fetch({trxId, count})` – дочитать строки курсора.
- `commit({trxId})` / `rollback({trxId})` – завершить транзакцию.
- `poolStatus()` – состояние всех транзакций.

### 5. PgTools
**Файл:** `src/pg-queries/pg-tools/index.mjs`

Вспомогательный сервис, создающий функцию, возвращающую `TABLE(...)`, на основе существующей функции, возвращающей `REFCURSOR`. Упрощает миграцию с Oracle.

**Методы:**
- `funcTableFromRefcursor({funcFrom, funcTo})` – генерирует SQL-обёртку и создаёт её в БД.

### 6. PgNotify
**Файл:** `src/pg-queries/pg-notify/index.mjs`

Реализует механизм асинхронных уведомлений PostgreSQL (`LISTEN`/`NOTIFY`). Поддерживает подписку на каналы, переподключение и единое событие `onNotify`.

**События:**
- `onNotify` – генерируется при получении уведомления (полезная нагрузка парсится из JSON).

**Методы:**
- `subscribe({channelName})` / `unsubscribe({channelName})`.
- `isConnected()` – проверка соединения.
- `getSubscriptions()` – список активных подписок.

### 7. OraQueries
**Файл:** `src/ora-adapter/ora-queries/index.mjs`

Основной адаптер для выполнения «коротких» Oracle-подобных запросов. Принимает вызовы вида `package.func` с именованными параметрами, преобразует их через `DbServices` в реальные вызовы PostgreSQL и исполняет через `PgPool`.

**Зависимости:** `PgPool`, `DbServices`  
**Методы:**
- `exec({queryName, params, count, offset, session})` – универсальный вызов.
- `execFunc(...)` – явное указание пакета и функции.
- `execSql({sql, params, session})` – выполнение SQL с заменой именованных параметров `:param` на позиционные `$1`.
- `resetUser()` / `resetAllUsers()` – сброс сессионного контекста (не реализованы).

### 8. OraLongTransactions
**Файл:** `src/ora-adapter/ora-long-transactions/index.mjs`

Аналог `OraQueries`, но для длинных транзакций. Использует `PgTransactions` для удержания соединения. Поддерживает многошаговые сценарии: создать транзакцию, выполнить несколько запросов, получить результаты, зафиксировать.

**Зависимости:** `PgTransactions`, `DbServices`  
**Методы:**
- `create({session})` – создать транзакцию (возвращает `oltId`).
- `execFunc({oltId, package, func, params, session})` / `execSql(...)`.
- `getExecResult({oltId})` – получить результат выполненного запроса.
- `fetch({oltId, count})` – получить строки из открытого курсора.
- `commit({oltId})` / `rollback({oltId})`.
- `poolStatus()` – состояние пула длинных транзакций.

### 9. OraQueriesPool
**Файл:** `src/ora-adapter/ora-queries-pool/index.mjs`

Административный интерфейс для просмотра состояния пула `PgPool`.

**Зависимости:** `PgPool`, `DbServices`  
**Методы:**
- `state()` – возвращает детализацию по соединениям (количество, статусы).

---

## Вспомогательные классы и утилиты

### ArgumentParser
Распарсивает строку аргументов функции PostgreSQL (из `pg_get_function_arguments`) в структурированный вид: `{ name, type, isOut, hasDefault }`.

### PgFormats
Отвечает за:
- Конвертацию типов полей при чтении из БД (числа, даты).
- Формирование SQL-вызова функции с именованными параметрами (`"arg" => $1`).
- Проверку кодов ошибок PostgreSQL (отделение логических ошибок от проблем соединения).

### PgConnection
Управляет одним подключением для «коротких» запросов (`PgPool`). Выполняет SQL в транзакции (BEGIN → запрос → COMMIT/ROLLBACK). Обрабатывает refcursor, загружая данные из курсора.

### PGTransaction
Класс одной длинной транзакции. Хранит состояние, управляет жизненным циклом, поддерживает выполнение SQL и функций, фетч курсора. Используется внутри `PGTPool`.

### PGTPool
Менеджер пула `PGTransaction`. Создаёт транзакции по требованию, отслеживает таймауты, автоматически подчищает закрытые соединения, пишет ошибки в лог БД.

### PgNotifyConnection
Подключение для `LISTEN`/`NOTIFY`. Инкапсулирует логику переподключения и подписки.

---

## Конфигурация

Файл `config.mjs` содержит параметры по умолчанию:

- `NatsConnection.queue` = `"pgqueries"`, `timeout` = 60000
- `NatsPublisher.interval` = 60000

Остальные настройки берутся из `config.common` и `CommonRegistry`:

| Параметр | Описание |
|----------|----------|
| `common.postgresUri` | Строка подключения к PostgreSQL |
| `common.timeZone` | Часовой пояс для сессий БД |
| `pgQueryPool.connectionCount` | Размер пула коротких запросов (в `CommonRegistry`) |
| `PgTransactions.idleTimeout` | Таймаут простоя транзакции (мс) |
| `PgTransactions.cleanTimeout` | Интервал очистки закрытых транзакций (мс) |

Все сервисы получают конфигурацию через стандартный механизм `Config` и могут динамически обновляться через подписку на `CommonRegistry`.

---

## Схема взаимодействия

1. **Загрузка метаданных**
   `PgFunctions` при старте загружает список всех функций PostgreSQL. `OraFunctions` на его основе строит «оракул-подобное» представление.

2. **Регистрация в DbServices**
   Сервис `DbServices` (из внешнего пакета) хранит каталог всех доступных вызовов (жёсткие и мягкие). `OraQueries` и `OraLongTransactions` используют `DbServices.getOraSql()` для генерации PL/SQL‑блока с подстановкой параметров.

3. **Выполнение запросов**
   - **Короткие запросы**: `OraQueries.exec` → `DbServices.getOraSql` → формирование вызова → `PgPool.execFunc` → `PgConnection`.
   - **Длинные транзакции**: `OraLongTransactions.create` → `PgTransactions.create` (создаётся `PGTransaction`) → далее `execFunc/execSql` → по окончании `getExecResult` + `commit/rollback`.

4. **Нотификации**
   `PgNotify` слушает каналы PostgreSQL и генерирует событие `onNotify`, на которое могут подписываться другие сервисы.

5. **Администрирование**
   `OraQueriesPool` и метод `poolStatus` у длинных транзакций дают мониторинг соединений.

---

## Примечания

- Для корректной работы необходима настройка `common.postgresUri` и наличие в БД схемы `carabimeta` с процедурами `set_user_id`, `write_error_log`.

# Пакет DbServices

## Обзор

`@morphcluster/db-services` — это библиотека для работы с сервисами базы данных. Она предоставляет:

- **Клиентские прокси** для удалённого вызова сервисов `DbServices`, `OraQueries` и `OraLongTransactions` через NATS.
- **Конструкторы SQL-запросов** для программного построения сложных `SELECT`-выражений (`SqlQuery`, `WhereCondition`, `SqlModBuilder`).
- **Хелперы** для удобного выполнения запросов к БД из любого сервиса, включая управление длинными транзакциями (`QueriesHelper`, `OLTHelper`).
- **Инструменты развёртывания** для загрузки описаний сервисов из файлов и автоматической установки хранимых функций в PostgreSQL (`DbServicesLoader`, `DbFunctionsInstaller`).

Все компоненты ориентированы на миграцию с Oracle на PostgreSQL и обеспечивают совместимость с ожидаемыми интерфейсами Oracle-клиентов.

---

## Состав библиотеки

Основные экспорты из `index.mjs`:

```js
export { default as DbServices } from './clients/DbServices.mjs'
export { default as OraLongTransactions } from './clients/OraLongTransactions.mjs'
export { default as OraQueries } from './clients/OraQueries.mjs'
export { SqlEscape, SqlQuery, WhereCondition } from './SqlQuery.mjs'
export { default as SqlModBuilder } from './SqlModBuilder.mjs'
export { default as DbServicesLoader } from './DbServicesLoader.mjs'
export { default as QueriesHelper } from './QueriesHelper/index.mjs'
export { default as OLTHelper } from './OLTHelper.mjs'
export { default as DbFunctionsInstaller } from './DbFunctionsInstaller/index.mjs'
```

---

## Клиентские прокси (ServiceNats)

Клиенты `DbServices`, `OraQueries` и `OraLongTransactions` являются наследниками `ServiceNats` (устаревшего, но всё ещё используемого) и предназначены для прозрачного обращения к соответствующим сервисам, опубликованным в NATS.

## SQL-построители

### SqlQuery

Класс для построения SQL-запроса `SELECT` с поддержкой всех основных секций: `SELECT`, `FROM`, `JOIN`, `WHERE`, `ORDER BY`, `LIMIT/OFFSET`. Предназначен для генерации читаемого и оптимизированного SQL.

```js
import { SqlQuery } from '@morphcluster/db-services';
```

**Методы:**

- `select(expression | [expressions])` — добавить колонки в SELECT.
- `from(tableOrSubquery, alias?)` — добавить таблицу или подзапрос в FROM.
- `innerJoin(table, alias?, condition?)`, `leftJoin(...)`, `addJoin(type, table, alias?, condition?)` — добавить JOIN.
- `where` — публичное свойство, экземпляр `WhereCondition` (по умолчанию пустая AND-группа). Все условия добавляются в него.
- `orderBy(column | [columns])` — добавить сортировку.
- `limit(limit, offset?)` — установить LIMIT и OFFSET.
- `build(options?)` → `string` — собрать готовый SQL.
- `buildPretty(options?)` — собрать SQL с отступами (pretty print).

**Параметры `build`:**  
`{ pretty, indentSize, isSubquery, indent }` (обычно используется внутри для рекурсивного построения подзапросов).

**Пример:**

```js
const q = new SqlQuery()
  .select(['u.id', 'u.name'])
  .from('users', 'u')
  .leftJoin('orders', 'o', new WhereCondition('simple', 'u.id = o.user_id'));

q.where.addSimple("u.active = 1");
q.where.addOr([
  new WhereCondition('simple', "u.role = 'admin'"),
  new WhereCondition('simple', "u.role = 'manager'")
]);

q.orderBy('u.name');
q.limit(10, 20);

console.log(q.build({ pretty: true }));
```

### WhereCondition

Класс для построения деревьев условий WHERE. Поддерживает:
- простые выражения (`simple`)
- `EXISTS` с подзапросом
- логические группы `AND` / `OR`
- отрицание `NOT`
- метод `optimize()` для упрощения дерева (удаление избыточных скобок, `1=1`/`1=0`).

```js
import { WhereCondition } from '@morphcluster/db-services';
```

**Типы условий (поле `type`):**
- `'simple'` — строка SQL (например, `"a = b"`).
- `'exists'` — подзапрос (экземпляр `SqlQuery`). При построении превращается в `EXISTS (subquery)`.
- `'and'`, `'or'` — группа условий, хранятся в массиве `conditions`.
- `'not'` — отрицание другого условия.

**Конструктор:** `new WhereCondition(type, param)`, где `param` зависит от типа.

**Основные методы:**
- `add(condition)` — добавить условие в текущую группу (только для `and`/`or`).
- `addSimple(expr)` — добавить простое условие.
- `addExists(subquery)` — добавить `EXISTS`.
- `addNotExists(subquery)` — добавить `NOT EXISTS`.
- `addNot(condition)` — добавить `NOT`.
- `addOr(conditions)` — создать и добавить OR-группу.
- `addAnd(conditions)` — создать и добавить AND-группу.
- `optimize()` — вернуть оптимизированную копию условия.
- `build(options?)` → `string` — собрать SQL-представление.
- `buildPretty(options?)` — то же с форматированием.

Оптимизация удаляет лишние уровни вложенности, упрощает константные выражения (`1=1`, `1=0`), применяет законы де Моргана для `NOT`.

### SqlModBuilder

Простой класс для формирования запросов `INSERT` и `UPDATE` на основе описания колонок.

```js
import SqlModBuilder from '@morphcluster/db-services';
```

**Конструктор:** `new SqlModBuilder(table)` — указывает имя таблицы.

**Методы:**
- `set(column, type, value)` — добавить значение для установки (для INSERT или UPDATE).
- `setId(column, type, value)` — установить идентификатор (первичный ключ) для WHERE в UPDATE.
- `getInsertQuery()` → `{ sql, params }` — возвращает `INSERT ... VALUES (... :param ...) RETURNING id` и массив объектов `{ name, type, value }`.
- `getUpdateQuery()` → `{ sql, params }` — `UPDATE ... SET ... WHERE id=:ID`.

Параметры для Oracle-нотации формируются в виде именованных параметров (`:VARNAME`).

### SqlEscape

Утилитарная функция `SqlEscape(str)` — экранирует строку для безопасной вставки в SQL: удваивает одинарные кавычки.

```js
import { SqlEscape } from '@morphcluster/db-services';
```

---

## DbServicesLoader

Сервис, который загружает описания DB-сервисов (hard-services) из JSON-файлов на диске и отправляет их в `DbServices` при старте и по событию `onResendHardServices`.

```js
import { DbServicesLoader } from '@morphcluster/db-services';
```

**Конструктор:** `new DbServicesLoader(host, config)`

- Помечается как `local = true`, т.е. не публикуется глобально.
- Требует зависимость `DbServices`.
- Загружает файлы из директории `{Config.packageRoot}/db-services/` с расширением `.json`.
- При старте вызывает `reload()` (чтение файлов) и `sendDbServices()` (отправка каждого сервиса через `DbServices.setHardService`).
- Подписывается на `DbServices.onResendHardServices`, чтобы повторно отправлять сервисы по требованию.

**Структура JSON-файла сервиса** должна соответствовать тому, что ожидает `setHardService` (полное описание сервиса с запросами и т.д.). В примере кода в этих файлах добавляется поле `hostName`.

---

## QueriesHelper

Универсальный хелпер для выполнения запросов к базе данных из любого сервиса. Скрывает детали работы с `OraQueries` и `OraLongTransactions`, предоставляя простые методы `query`, `select`, `querySql` и др.

```js
import { QueriesHelper } from '@morphcluster/db-services';
```

**Конструктор:** `new QueriesHelper(host)`

- Помечается `local = true`.
- Зависимости: `OraLongTransactions`, `RegistryHelper`. Опционально ожидает `OraQueries`, но при необходимости создаёт его сам.
- В `start()` создаёт подписку на схему реестра `registrySchema`, которая содержит поле `queries.type` (например, `"ora"` или `"pg"`).

**Основные методы:**

| Метод | Назначение |
|-------|------------|
| `queryRaw(queryName, params, count, offset, options)` | Выполнить вызов функции (`PKG.FUNC`) и вернуть сырой результат (массив объектов `{paramName, type, value}`) |
| `query(queryName, params, count, offset, options)` | То же, но преобразует курсоры в массивы объектов (колонки → ключи) и возвращает объект с ключами `paramName` |
| `select(queryName, params, count, offset, options)` | Взять первое значение из результата `query` |
| `selectRow(queryName, params, options)` | Вызвать `select` с `count=1` и вернуть первый элемент массива, если есть |
| `querySql(log, SQL, rawParams, options)` | Выполнить сырой SQL через `OraQueries.execSql` (или через транзакцию, если передан `trxId`) |
| `querySqlCursor(log, SQL, rawParams, options)` | Как `querySql`, но ожидает, что первый результат — курсор, и преобразует его через `convertCursor` |
| `transactionWrap({ log, callback, trxId?, session? })` | Выполнить функцию в транзакции: создать транзакцию, вызвать `callback(trxId)`, при успехе — `commit`, при ошибке — `rollback` |
| `convertCursor(cursor)` | Преобразует объект курсора `{ columns, list }` в массив объектов, где ключи — имена колонок |
| `escapeId(str)` | Экранирует идентификатор для PostgreSQL |

**Параметры `options` для методов:**
- `log` — логгер (обязательно).
- `trxId` — идентификатор существующей транзакции. Если передан, запросы выполняются в ней, иначе — в простом пуле.
- `session` — объект сессии (обычно содержит `userId`).
- `userId` — алиас для `session.userId`.

`queryRaw` и `query` автоматически определяют, нужно ли использовать транзакцию (если передан `trxId`), и в этом случае вызывают `OraLongTransactions.execFunc` + `getExecResult` + извлечение курсора. Если без транзакции — вызывают `OraQueries.execFuncInner` (прямое выполнение).

Внутренний метод `_queryExec` выбирает пул в зависимости от `this.DBType`, но в текущей версии поддерживается только `"ora"`, который приводит к вызову `OraQueries.execFuncInner`.

---

## OLTHelper

Помощник для работы с одной длинной транзакцией (Oracle Long Transaction). Упрощает создание транзакции, выполнение функций, извлечение результатов и фетчинг курсоров.

```js
import { OLTHelper } from '@morphcluster/db-services';
```

**Конструктор:** `new OLTHelper(olt, ws, oltId?, log, options?)`
- `olt` — экземпляр клиента `OraLongTransactions`.
- `oltId` — существующий идентификатор транзакции (если есть).
- `log` — логгер (обязателен).
- `options.userId` / `options.noUser` — параметры сессии.

**Методы:**

| Метод | Описание |
|-------|----------|
| `getTrxId()` | Вернуть `oltId`, если нет — создать транзакцию и вернуть |
| `create()` | Создать новую транзакцию и сохранить `oltId` |
| `getResult()` | Получить `getExecResult` с повторными попытками при `OltTimeout` |
| `fetch(OutParams, count, keepColIndex?)` | Извлечь `count` строк из курсора, присутствующего в `OutParams`. Возвращает массив строк (уже преобразованных, если `keepColIndex=false`) |
| `execFuncMulti(queryName, params)` | Выполнить функцию и вернуть объект всех выходных параметров (ключ = `paramName`) |
| `execFunc(queryName, params)` | Выполнить функцию и вернуть значение первого выходного параметра |
| `selectFunc(queryName, params, count)` | Выполнить функцию и выбрать `count` строк из курсора |
| `selectRowFunc(queryName, params)` | `selectFunc` с `count=1`, вернуть первый ряд или `null` |
| `execSql(sql, params)` | Выполнить сырой SQL |
| `selectSql(sql, params, count)` | Выполнить SQL и выбрать строки из курсора |
| `selectRowSql(sql, params)` | Аналогично, одна строка |
| `commit()` | Зафиксировать транзакцию |
| `rollback()` | Откатить транзакцию |

Все методы обрабатывают ошибку `OltTimeout` автоматическими повторами. `execFunc` и `execSql` обновляют `this.result`, который затем используется для фетча. `convertRow` преобразует числовые поля из строк в числа.

## DbFunctionsInstaller

Сервис для автоматической установки хранимых функций в PostgreSQL. Сравнивает хеши функций, хранящиеся в таблице `db_installer.db_functions_1`, с переданными, и при несовпадении выполняет их пересоздание.

```js
import { DbFunctionsInstaller } from '@morphcluster/db-services';
```

**Конструктор:** `new DbFunctionsInstaller(host, config, dbFunctions)`
- `config.disabled` — отключить автоустановку.
- `dbFunctions` — массив объектов схем и функций:
  ```js
  [
    {
      name: 'schema_name',
      functions: [
        { name: 'func_name', source: 'CREATE OR REPLACE FUNCTION ...', hash: 'sha256...' }
      ]
    }
  ]
  ```
- Зависит от `QueriesHelper` (для выполнения SQL).

**Запросы:**
- `install({})` — основной метод: в транзакции создаёт служебную таблицу, получает текущие хеши, находит функции, требующие обновления, и переустанавливает их. При установке используется `SET check_function_bodies = off` для игнорирования ошибок в теле функции. После установки всех функций записывает/обновляет запись в `db_installer.db_functions_1`.

**Схема сервиса** определена в `service-schema.mjs`: запрос `install`, требует `needAdmin: true`.

---

## Примечания

- **Обработка ошибок:** `QueriesHelper` и `OLTHelper` автоматически обрабатывают `OltTimeout` повторными попытками. В случае других ошибок они пробрасываются выше, при этом в `QueriesHelper.queryRaw` к ошибке добавляются поля `query` и `queryParams`.
- **Безопасность:** Функция `SqlEscape` экранирует только одинарные кавычки. Для предотвращения SQL-инъекций всегда используйте параметризованные запросы, передавая значения через `params`, а не встраивая их в SQL.

# Сервис QueriesHelper

`QueriesHelper` — локальный сервис-помощник для выполнения запросов к базе данных в сервисах, предоставляя единый интерфейс для вызова хранимых функций, выполнения SQL и работы с транзакциями.

## 1. Подключение в вашем сервисе

Ваш сервис должен наследоваться от `ServiceRequire` и указать `QueriesHelper` в списке зависимостей.

```javascript
import { ServiceRequire } from '@morphcluster/core'
import { QueriesHelper } from '@morphcluster/carabi'

export default class MyService extends ServiceRequire {
  constructor(host, config) {
    super(host, config)
    this.requirements = ['QueriesHelper']
  }

  async start(log) {
    await super.start(log)
    // this.QueriesHelper уже доступен
  }
}
```

После вызова `super.start(log)` свойство `this.QueriesHelper` будет содержать готовый к использованию экземпляр.

## 2. Конфигурация

`QueriesHelper` получает тип подключения из реестра через `RegistryHelper`. Ключ конфигурации: `queries.type`.  
Возможные значения:
- `"ora"` — работа через `OraQueries` (Oracle-совместимый адаптер поверх PostgreSQL).
- `"pg"` — прямое подключение через `PgQueryPool` (на данный момент не используется; при попытке использования в `_queryExec` выбрасывается ошибка).

## 3. Основные методы запросов

### 3.1. `queryRaw(queryName, params, count, offset, options)`
Базовый метод выполнения хранимой функции или процедуры.  
Формат имени: `'PKG.FUN'` (пакет/схема и имя функции через точку). Для Postgres пакет заменен схемой.

**Параметры:**
- `queryName` (string) – полное имя функции, например `'csp_mainmenu.get_dashboard'`.
- `params` (object) – объект с параметрами, ключи соответствуют именам параметров функции (без префикса `:`).
- `count` (number, по умолчанию 1) – количество строк для извлечения из курсора (если возвращается курсор).
- `offset` (number, по умолчанию 0) – смещение для курсора.
- `options` (object) – дополнительные настройки (см. раздел «Опции запросов»).

**Возвращает:** `Promise<Array<{ paramName: string, type: string, value: any }>>`  
Массив объектов, описывающих все выходные параметры функции. Для курсоров `value` будет содержать структуру `{ columns: Array<[string, string]>, list: Array<Array<any>> }`.

**Пример:**
```javascript
const result = await this.QueriesHelper.queryRaw(
  'csp_mainmenu.get_items',
  { user_id: 42, role_id: 1 },
  10,
  0,
  { log, session }
);
// result[0] может быть { paramName: 'RESULT', type: 'CURSOR', value: {...} }
```

### 3.2. `query(queryName, params, count, offset, options)`
Обёртка над `queryRaw`, которая:
- Преобразует курсоры из сырого формата в массив объектов (через `convertCursor`).
- Упаковывает все выходные параметры в объект, где ключ — `paramName`, а значение — преобразованное значение.

**Возвращает:** `Promise<Object>`  
Объект вида `{ PARAM1: value1, PARAM2: value2 }`. Курсоры превращаются в массив объектов, где ключи — имена колонок.

**Пример:**
```javascript
const data = await this.QueriesHelper.query(
  'csp_mainmenu.get_user_info',
  { user_id: 100 },
  1, 0,
  { log, session }
);
// data.USER_INFO = [ { NAME: 'John', AGE: 30 } ]
// data.STATUS   = 'OK'
```

### 3.3. `select(queryName, params, count, offset, options)`
Упрощённый метод, когда ожидается ровно одно выходное значение. Фактически возвращает первое свойство из результата `query`.

**Возвращает:** значение первого выходного параметра (после преобразования курсора).

**Пример:**
```javascript
const itemCount = await this.QueriesHelper.select(
  'csp_mainmenu.count_items',
  { category: 'books' },
  1, 0,
  { log, session }
);
// itemCount = 12 (если единственный out-параметр — число)
```

### 3.4. `selectRow(queryName, params, options)`
Используется, когда ожидается ровно одна строка из курсора. Возвращает первый элемент массива-результата или `null`.

**Параметры:**
- `queryName`, `params`, `options` (count и offset не принимаются – под капотом используется `count=1, offset=0`).

**Возвращает:** объект строки или `null`.

**Пример:**
```javascript
const user = await this.QueriesHelper.selectRow(
  'csp_mainmenu.get_user_by_id',
  { user_id: 55 },
  { log, session }
);
// user = { NAME: 'Alice', EMAIL: 'alice@example.com' } или null, если не найден
```

### 3.5. `querySql(log, SQL, rawParams, options)`
Выполняет произвольный SQL-запрос (не хранимую функцию). Поддерживает как простые запросы, так и выполнение в транзакции.

**Параметры:**
- `log` (Logger) – обязательно.
- `SQL` (string) – текст запроса с плейсхолдерами в нотации Oracle (`:param_name`).
- `rawParams` (Array) – массив объектов параметров: `{ name: 'PARAM', type: 'number', value: 123 }`.
- `options` (object) – `{ userId, session, trxId, count, offset }`.

**Возвращает:** `Promise<Array<{ paramName, type, value }>>` (как `queryRaw`, но только для SQL).

**Пример:**
```javascript
const result = await this.QueriesHelper.querySql(
  log,
  `UPDATE users SET name = :NEW_NAME WHERE id = :ID`,
  [
    { name: 'NEW_NAME', type: 'varchar2', value: 'Bob' },
    { name: 'ID', type: 'number', value: 100 }
  ],
  { session, trxId: currentTrx } // если нужно в транзакции
);
```

### 3.6. `querySqlCursor(log, SQL, rawParams, options)`
То же, что `querySql`, но ожидает, что единственный выходной параметр — курсор. Сразу преобразует его через `convertCursor` и возвращает массив объектов (строк).

**Возвращает:** массив объектов (строк курсора).

**Пример:**
```javascript
const rows = await this.QueriesHelper.querySqlCursor(
  log,
  `SELECT id, name FROM users WHERE role = :ROLE`,
  [{ name: 'ROLE', type: 'varchar2', value: 'admin' }],
  { log, session }
);
// rows = [ { ID: 1, NAME: 'Alice' }, { ID: 2, NAME: 'Bob' } ]
```

### 3.7. `convertCursor(cursor)`
Вспомогательный метод, который вы можете использовать отдельно, если получили сырой курсор. Преобразует колонки и значения в массив объектов, попутно парся числа.

**Сигнатура:**
```javascript
convertCursor(rawCursor: { columns: Array<[string, string]>, list: Array<Array<any>> }): Array<Object>
```

## 4. Опции запросов

Почти все методы принимают объект `options` со следующими необязательными полями:
- `log` — экземпляр `Logger` (обязателен для методов `querySql`, `querySqlCursor`).
- `session` — объект сессии текущего пользователя (обычно `{ userId, token, isAdmin }`).
- `userId` — числовой ID пользователя; если передан, он будет добавлен в `session.userId`.
- `trxId` — ID открытой транзакции, если запрос нужно выполнить в её контексте.
- `count`, `offset` — для курсоров, если не переданы отдельными аргументами (в `querySql` они берутся из options).

## 5. Работа с транзакциями

### 5.1. `transactionWrap({ log, callback, trxId, session })`
Выполняет переданную функцию в рамках транзакции. Если `trxId` не указан, создаёт новую транзакцию, после успешного выполнения коммитит, при ошибке — откатывает.

**Параметры:**
- `callback` (async function) — принимает `trxId` и выполняет запросы.
- `trxId` — можно передать существующую транзакцию, тогда коммит/откат не управляется автоматически.
- `session` — сессия для создания транзакции.
- `log` — логгер.

**Пример:**
```javascript
await this.QueriesHelper.transactionWrap({
  log,
  session,
  callback: async (trxId) => {
    // Передаём trxId в опции всех запросов
    await this.QueriesHelper.querySql(log,
      `INSERT INTO audit (user_id, action) VALUES (:UID, :ACT)`,
      [
        { name: 'UID', type: 'number', value: session.userId },
        { name: 'ACT', type: 'varchar2', value: 'update' }
      ],
      { trxId, session }
    );
    // ещё запросы...
  }
});
```

Если не передать `trxId`, транзакция будет создана и автоматически завершена.  
Если вы передали внешний `trxId` (например, из длинной транзакции), обёртка не выполняет commit/rollback — управление остаётся за вами.

### 5.2. Прямое управление транзакциями
Вы можете самостоятельно создавать, коммитить и откатывать транзакции через сервис `OraLongTransactions` (доступен как `this.QueriesHelper.OraLongTransactions`). Однако обычно удобнее использовать `transactionWrap`.

## 6. Параметры запросов

Именованные параметры передаются в виде массива объектов:
```typescript
interface QueryParam {
  name: string;   // имя параметра (без двоеточия)
  type: string;   // тип: 'varchar2', 'number', 'numeric', 'json' и т.д.
  value: any;     // значение
}
```
В тексте SQL плейсхолдеры указываются с двоеточием: `:USER_ID`. `QueriesHelper` сам преобразует их в позиционные (`$1`, `$2`, ...) в зависимости от типа БД.

Для вызова хранимых функций (`query`, `queryRaw`) параметры передаются объектом, где ключи соответствуют именам параметров функции (без префикса `p_` или других соглашений – смотрите документацию к вашим функциям). Внутренний механизм сам сопоставит их с аргументами функции.

## 7. Типы данных и преобразования

- **NUMBER** — PostgreSQL-значения числовых типов автоматически преобразуются в `float` при конвертации курсора (`convertCursor`).
- **DATE / TIMESTAMP** — возвращаются как строки в формате ISO (зависит от адаптера OraQueries).
- **VARCHAR2** — строки.
- **CURSOR** — преобразуется в массив объектов.

Если вы используете метод `queryRaw`, вы получаете сырые значения без дополнительных преобразований чисел.

## 8. Обработка ошибок

При ошибке выполнения запроса выбрасывается исключение. В него добавляются поля `query` и `queryParams` для упрощения отладки. Также ошибка автоматически логируется через `log.writeExceptionOnly(e)`.

Рекомендуется оборачивать вызовы в try/catch и при необходимости выбрасывать `ComplexError` для клиентов.

## 9. Полный пример сервиса

```javascript
import { ServiceRequire } from '@morphcluster/core'
import { QueriesHelper } from '@morphcluster/carabi'

export default class ReportService extends ServiceRequire {
  constructor(host, config) {
    super(host, config)
    this.requirements = ['QueriesHelper']
  }

  async start(log) {
    await super.start(log)
    // Можно выполнить начальную загрузку
  }

  async getDailyReport({ date, session }, ws, log) {
    const rows = await this.QueriesHelper.querySqlCursor(log,
      `SELECT product, sum(amount) as total
       FROM sales
       WHERE sale_date = :SALE_DATE
       GROUP BY product`,
      [{ name: 'SALE_DATE', type: 'date', value: date }],
      { session, count: 1000 }
    );
    return rows;
  }

  async updateProductStock({ productId, quantity }, ws, log) {
    await this.QueriesHelper.transactionWrap({
      log,
      session: ws.session,
      callback: async (trxId) => {
        await this.QueriesHelper.querySql(log,
          `UPDATE products SET stock = stock - :QTY WHERE id = :ID`,
          [
            { name: 'QTY', type: 'number', value: quantity },
            { name: 'ID', type: 'number', value: productId }
          ],
          { trxId }
        );
        // Дополнительные действия...
      }
    });
    return { success: true };
  }
}
```

# Сервис SysProcesses

Сервис для асинхронного выполнения фоновых процессов бизнес-логики. Процессы могут быть инициированы через API или автоматически путём вставки записей в служебную таблицу `sysprocesses.sysprocesses`. 

**Конфигурация:**
`SysProcesses.disableCheck` – если `true`, автоматическая проверка очереди процессов отключается (полезно для отладки).

**Основные возможности:**

- Автоматический запуск процессов, ожидающих выполнения в таблице `sysprocesses.sysprocesses`.
- Поддержка очередей (`QUEUE`): внутри одной очереди процессы выполняются строго последовательно.
- Отложенный запуск по полю `START_AT`.
- Ограничение количества одновременно выполняющихся процессов (`maxProcessses`, по умолчанию `1`).
- Сохранение результатов или ошибок в БД.
- API для добавления, удаления и просмотра процессов.
- Оповещение о завершении/ошибке через события `onProcessCompleted` и `onProcessError`.

Сервис поддерживает три типа процессов: 
- `csp` - вызов сервисов
- `db` - выполнение хранимых процедур в БД с транзакцией ()
- `sql` - выполнение произвольного SQL-кода ()


### Методы

#### `addProcess`

Добавляет новый процесс в очередь.

**Параметры:**
- `session` – объект сессии пользователя (обязательно, для записи `user_id`).
- `type` – тип процесса (`'csp'`, `'db'`, `'sql'`).
- `name` – имя процесса:
  - для `csp` – строка вида `'ServiceName.requestName'`.
  - для `db` – полное имя хранимой функции (например, `'PKG_VOCAB.INSERT_VALUE'`).
  - для `sql` – произвольное описание (сам код передаётся в `sourceCode`).
- `params` – объект с параметрами (для `csp` и `db`). При вызове сервиса (`csp`) параметры будут переданы в запрос, а также будет добавлено поле `session` с `userId`.
- `sourceCode` – SQL-код для типа `sql`.
- `queue` – имя очереди (строковый идентификатор). Процессы в одной очереди выполняются последовательно.
- `startAt` – время отложенного запуска в формате ISO.
- `keep` – если `true`, запись о процессе не удаляется автоматически после завершения. Полезно для отслеживания выполнения процесса.

**Возвращает:** идентификатор созданного процесса (`id`).

**Пример:**
```js
const id = await gsvc.sendRequest('addProcess', {
  session: { userId: 123, isAdmin: true },
  type: 'csp',
  name: 'ReportService.generate',
  params: { date: '2026-01-01' },
  queue: 'reports',
  keep: true
}, log);
```

#### `delProcess`

Удаляет процесс по идентификатору. Запрещено удалять выполняющийся в данный момент процесс.

Нужно только для процессов соданых с `keep: true`

**Параметры:**
- `session` – сессия (обязательно).
- `id` – ID процесса.

**Права доступа:** администратор или владелец процесса (совпадение `user_id`).

**Ошибки:**
- `NOT_AUTHORIZED` – отсутствует сессия.
- `PROCESS_IS_RUNNING` – процесс выполняется.
- `PROCESS_NOT_FOUND` – процесс не найден.
- `FORBIDDEN` – недостаточно прав.

**Пример:**
```js
await gsvc.sendRequest('delProcess', {
  session: { userId: 123 },
  id: 42
}, log);
```

#### `list`

Возвращает список процессов с информацией о статусе.

**Параметры:**
- `session` – обязателен.

**Права:** администратор видит все процессы, обычный пользователь – только свои (`user_id` совпадает с `session.userId`).

**Возвращаемый формат:**
```json
[
  {
    "ID": 1,
    "TYPE": "csp",
    "NAME": "ReportService.generate",
    "USER_ID": 123,
    "QUEUE": "reports",
    "START_AT": null,
    "STARTED": "2026-06-22T10:00:00",
    "COMPLETED": null,
    "ERROR_TEXT": null,
    "STATUS": "running",
    "INFO": { ... }
  }
]
```

Поле `STATUS` может принимать значения:
- `'waiting'` – ожидает запуска.
- `'running'` – выполняется в данный момент.
- `'completed'` – успешно завершён.
- `'failed'` – завершён с ошибкой.

Для выполняющихся процессов в `INFO` попадают данные, возвращаемые методом `getInfo()` конкретного обработчика (например, идентификатор транзакции).

#### `checkList`

Инструмент отладки. Принудительно проверяет таблицу процессов и запускает все ожидающие (с учётом очередей и лимитов). 

**Требует прав администратора.**

**Пример:**
```js
await gsvc.sendRequest('checkList', {}, log);
```

### События

- `onProcessCompleted` – возникает при успешном завершении процесса. Параметры обработчика: `{ process, result }`.
- `onProcessError` – возникает при ошибке. Параметры: `{ process, errorText }`.

### Внутреннее устройство

Сервис использует два механизма для обнаружения новых процессов:

1. **Таймер `checkTimer`** – периодически (каждые 60 секунд) вызывает `checkList()`.
2. **Подписка на уведомления PostgreSQL** через клиент `PgNotify`. При вставке новой записи в таблицу `sysprocesses.sysprocesses` (через триггер в БД) отправляется уведомление `new_sysprocess`, которое мгновенно активирует проверку очереди.

При запуске сервиса (`start()`) выполняется первоначальная проверка очереди, затем запускается таймер и оформляется подписка на канал `new_sysprocess`. При остановке (`stop()`) подписка снимается.


## Примечания

- Максимальное число одновременно выполняющихся процессов задаётся жёстко (`maxProcessses = 1`). При необходимости изменения можно унаследовать сервис и переопределить это свойство.
- Автоматическое удаление записи после завершения можно отключить флагом `keep: true` при добавлении процесса.

# Слой информационных объектов



# Пакет @morphcluster/carabi

## Обзор

**carabi** — это клиентская библиотека для работы с **информационными объектами** (документами) в экосистеме CSP. Она предоставляет высокоуровневый API для взаимодействия с сервисами управления документами, а также вспомогательные утилиты для кэширования, работы со структурой и данными.

Библиотека построена поверх **NATS-интеграции** и использует `ServiceNats` для прозрачного вызова удалённых сервисов.

---

## Клиенты

Клиенты наследуются от `ServiceNats` и предоставляют методы для вызова удалённых сервисов через NATS. Все методы принимают параметры, объект `workspace` (устаревший, всегда `null`) и логгер.

### 1. Documents

**Назначение:** Низкоуровневая работа с документами (чтение/запись значений, информация, проверки).

**Методы:**

| Метод | Параметры | Описание |
|-------|-----------|----------|
| `getInfo` | `{ documentId }` | Получить основную информацию (тип, статус, дата, создатель) |
| `getDescr` | `{ documentId }` | Получить описание документа (по правилам именования) |
| `getValues` | `{ dockind, documentId, propNames }` | Получить значения указанных свойств |
| `setValues` | `{ dockind, documentId, values }` | Установить значения свойств |
| `dublicate` | `{ dockind, documentId }` | Создать копию документа |
| `insertRefs` | `{ dockind, documentId, propName, values }` | Добавить ссылки в множественное поле |
| `deleteRefs` | `{ dockind, documentId, propName, values }` | Удалить ссылки |
| `checkNull` | `{ dockind, documentId }` | Проверить обязательные поля |
| `listIdByProps` | `{ dockind, values, count? }` | Найти ID документов по свойствам (AND) |
| `listPropsByProps` | `{ dockind, props, filter?, count? }` | Выбрать документы с указанными полями |
| `listByUniqueFields` | `{ dockind, values }` | Найти по уникальным полям |
| `getUniqueCollisions` | `{ dockind, documentId }` | Найти конфликты уникальности |

---

### 2. DocumentLists

**Назначение:** Расширенный поиск документов с фильтрацией по статусам.

**Методы:**

| Метод | Параметры | Описание |
|-------|-----------|----------|
| `listIdByProps` | `{ dockind, values, statuses?, notStatuses?, count? }` | Найти ID с фильтром по статусам |
| `listPropsByProps` | `{ dockind, props, filter?, statuses?, notStatuses?, count? }` | Вывести поля с фильтрацией |
| `listByUniqueFields` | `{ dockind, values }` | Поиск по уникальным полям |
| `getUniqueCollisions` | `{ dockind, documentId }` | Проверить коллизии |

---

### 3. DockindStructure

**Назначение:** Управление метаданными типа документа (статусы, свойства, функции, действия, права).

**Методы:**

| Метод | Параметры | Описание |
|-------|-----------|----------|
| `getInfo` | `{ docKind }` | Получить базовую информацию |
| `setMain` | `{ oldDocKind, newDocKind, description, parentId }` | Обновить описание и родителя |
| `getStatuses` | `{ docKind }` | Получить список статусов |
| `setStatuses` | `{ docKind, deleteIds, statuses, newOrder }` | Обновить статусы |
| `getProps` | `{ docKind }` | Получить свойства |
| `setProps` | `{ docKind, props, deleteIds }` | Обновить свойства |
| `getFunctions` | `{ docKind }` | Получить функции (DB/CSP) |
| `setFunctions` | `{ docKind, funcs, deleteIds }` | Обновить функции |
| `setActions` | `{ docKind, deleteIds, actions }` | Обновить действия |
| `setNames` | `{ docKind, names }` | Обновить правила именования |
| `setPermissions` | `{ docKind, roles, deleteRoleIds }` | Обновить права доступа |
| `getIdByName` | `{ docKind }` | Получить ID по имени |
| `getNameById` | `{ docKindId }` | Получить имя по ID |
| `updateDockindNames` | `{}` | Отложенное обновление всех имён |

**Событие:** `onChanged` — генерируется при любом изменении структуры.

---

### 4. DockindImporter

**Назначение:** Импорт/экспорт метаданных в форматах XML и JSON.

**Методы:**

| Метод | Параметры | Описание |
|-------|-----------|----------|
| `importXml` | `{ root, vocabs, types }` | Импорт структуры из XML |
| `exportXml` | `{ dockind }` | Экспорт структуры в XML |

---

### 5. DocDataImporter

**Назначение:** Импорт/экспорт данных документов (содержимого).

**Методы:**

| Метод | Параметры | Описание |
|-------|-----------|----------|
| `importXml` | `{ XML }` | Импорт данных из XML |
| `exportXml` | `{ columnsXML, filterXML }` | Экспорт данных в XML |

---

## Классы - Хелперы

### DocumentsHelper

**Назначение:** Высокоуровневый фасад для работы с документами. Предоставляет объектно-ориентированный API, скрывая детали низкоуровневых вызовов.

**Зависимости:** `QueriesHelper`, `DockindCache`, `Documents`, `DocumentLists`.

**Методы:**

| Метод | Описание |
|-------|----------|
| `createDocument(log, session, docKindName, trxId?)` | Создать новый документ. Возвращает `Document`. |
| `loadDocument(log, session, docKindName, documentId, trxId?)` | Загрузить существующий документ. Возвращает `Document`. |
| `loadDocList(log, session, docKindName, options)` | Загрузить список документов по фильтру. Возвращает массив `Document`. |
| `getDocKindByDocumentId(log, documentId)` | Получить тип документа по ID документа. |

**Параметры `options` для `loadDocList`:**
- `props: string[]` — имена свойств для вывода
- `filter: object` — фильтр по значениям
- `statuses: string[]` — ограничение по статусам
- `notStatuses: string[]` — исключение статусов
- `count: number` — максимальное количество (по умолчанию 100000)
- `trxId: number` — идентификатор транзакции (опционально)

**Пример:**
```javascript
const helper = new DocumentsHelper(host);
// ... зависимости подгружаются автоматически
const doc = await helper.createDocument(log, session, 'INVOICE');
await doc.setValues({ amount: 1000 }, true); // autocommit
```

---

### DockindCache

**Назначение:** Кэширование типов документов (`DocumentKind`) с автоматической загрузкой и инвалидацией.

**Особенности:**
- Использует `MemoryCacheAsync` с TTL 10 часов, максимум 100 типов.
- Предоставляет методы `getById(log, docKindId)` и `get(log, docKindName)`.
- Автоматически инвалидируется при изменении структуры через событие `onChanged`. Для принудительной очистки вызовите `clearCache()`.

**Внутренние PromiseSingleton:**
- `vocabNames` — список справочников
- `dockindNames` — список типов документов

**Пример:**
```javascript
const cache = new DockindCache(host);
const dockind = await cache.get(log, 'INVOICE');
// dockind — это DocumentKind
```

---

### DocumentKind

**Назначение:** Модель типа документа, загружаемая из БД. Содержит всю структуру: свойства, статусы, привязки.

**Свойства:**

| Свойство | Тип | Описание |
|----------|-----|----------|
| `id` | `number` | ID типа |
| `name` | `string` | Имя типа |
| `description` | `string` | Описание |
| `tableName` | `string` | Имя таблицы в БД |
| `properties` | `DocProperty[]` | Список свойств |
| `propEvents` | `DocEventProperty[]` | Привязки свойств к статусам (read/write/not null) |
| `statuses` | `Array<{id, name, descr}>` | Список статусов |
| `loaded` | `boolean` | Загружен ли объект |

**Методы:**

| Метод | Описание |
|-------|----------|
| `load(log)` | Загрузить структуру из БД |
| `getTableName()` | Получить имя таблицы |
| `getPropFieldName(prop)` | Получить имя поля для свойства |
| `getUniqueProps()` | Получить уникальные свойства |

**DocProperty:**
- `id`, `name`, `descr` — идентификатор, имя, описание
- `propKind` — тип свойства (1 — строка, 9 — ссылка, 10 — справочник и т.д.)
- `formatBase` — базовый тип (text, numeric, timestamp, ref, int8)
- `formatLogic` — логический тип (text, numeric, date, ref, vocab)
- `multi` — множественное
- `unique` — уникальное
- `refDocKinds` — для ссылок: список допустимых типов
- `fieldName` — имя поля для синхронизации таблиц

---

### Document

**Назначение:** Обёртка над документом с кэшированием значений и отслеживанием изменений.

**Конструктор:**
```javascript
new Document(log, session, docKind, { Documents, QueriesHelper, trxId })
```

**Свойства:**
- `id` — ID документа
- `docKind` — объект `DocumentKind`
- `valuesCache` — кэш значений свойств
- `valuesChanged` — Set имён изменённых свойств

**Методы:**

| Метод | Описание |
|-------|----------|
| `getValues(propNames)` | Получить значения нескольких свойств (с кэшированием) |
| `getValue(propName)` | Получить значение одного свойства |
| `setValues(values, autocommit)` | Установить значения (с отслеживанием изменений) |
| `commitValues()` | Сохранить накопленные изменения в БД |
| `insertRefs(propName, values)` | Добавить ссылки (без кэширования) |
| `setStatus(newStatusName)` | Сменить статус документа |
| `loadFromJson(data)` | Загрузить документ из JSON (полученного из `listPropsByProps`) |

**Важно:** `setValues` не вызывает `commitValues` автоматически, если `autocommit = false`. Это позволяет группировать изменения в рамках одной транзакции.

---

### VocabsHelper

**Назначение:** Загрузка справочников по имени с кэшированием и защитой от конкурентных запросов.

**Методы:**

| Метод | Описание |
|-------|----------|
| `getVocab(log, vocabName)` | Получить объект `Vocab` по имени (загружается асинхронно) |

**Пример:**
```javascript
const helper = new VocabsHelper(host);
const vocab = await helper.getVocab(log, 'CURRENCY');
// vocab.id — ID справочника
```

---

### Vocab

**Назначение:** Модель справочника (пока только загружает ID по имени).

**Свойства:**
- `id` — ID справочника
- `name` — имя
- `loaded` — флаг загрузки

---

## DocInstaller

**Назначение:** Автоматическая установка структуры и данных документов из файлов при старте приложения. Используется для развёртывания типов документов и миграций.

### DocInstaller (главный)

**Зависимости:** `DockindInstaller`, `DocDataInstaller`.

**Методы (запросы):**

| Метод | Описание |
|-------|----------|
| `getStatus()` | Получить статус установки |
| `run()` | Запустить полную установку (типы + данные) |
| `dockindsRun()` | Запустить только установку типов |
| `docDataRun()` | Запустить только установку данных |
| `docDataGetLast()` | Получить последнюю установленную версию данных |

**Конфигурация:**
- `skipInstall: true` — пропустить автоматическую установку при старте.

---

### DockindInstaller

**Назначение:** Установка типов документов из XML-файлов.

**Путь:** `./dockinds/<категория>/`

**Структура каталога:**
```
dockinds/
  └── my_category/
      ├── category.json          # Метаданные категории (опционально)
      ├── INVOICE_types.xml      # Структура типа INVOICE
      └── INVOICE_vocabs.xml     # Справочники для типа (опционально)
```

**Формат `category.json`:**
```json
{
  "name": "Моя категория",
  "roles": ["ROLE_ADMIN"]
}
```

**Логика:**
1. Сканирует подкаталоги в `./dockinds/`.
2. Для каждой категории загружает все пары `*_types.xml` и `*_vocabs.xml`.
3. Вычисляет SHA-256 хеш содержимого.
4. Сравнивает с хешем в таблице `DOCKIND_VERSIONS`.
5. Если изменилось — вызывает `DockindImporter.importXml`.
6. После успешной установки вызывает `PKG_XML_REPL_CS.REVISION_STRUCTURE`.

---

### DocDataInstaller

**Назначение:** Установка данных документов из XML-файлов (миграции).

**Путь:** `./doc-data/<номер_версии>/*.xml`

**Логика:**
1. Проверяет наличие таблицы `MIGRATIONS_DATA`.
2. Сканирует подкаталоги в `./doc-data/` с числовыми именами.
3. Для каждой версии, которая ещё не установлена или была с ошибкой:
   - Выполняет все `*.xml` файлы через `DocDataImporter.importXml`.
   - В случае успеха записывает запись со статусом `done`.
   - В случае ошибки — запись со статусом `error` и текстом ошибки.
4. Если миграция завершилась ошибкой, последующие не выполняются.

**Метод `resolveError`:** Позволяет вручную пометить ошибочную миграцию как выполненную (перезапуск с этой версии пропускается).

# Хост PgDocuments

## Обзор

Хост `PgDocuments` предоставляет набор сервисов для управления информационными объектами (документами) в системе MorphCluster. Основная функциональность включает:

- Управление структурой типов документов (DocKind) — свойства, статусы, действия, права доступа
- Работа с документами — создание, чтение, обновление, удаление
- Импорт/экспорт метаданных и данных в XML/JSON
- Генерация и синхронизация таблиц документов в PostgreSQL
- Выполнение бизнес-логики через функции и действия

---

## Структура сервисов

### Клиенты (ServiceNats)
Клиенты **не описываются** в данной документации, но они используются сервисами для межсервисного взаимодействия:

- **Eventer** — отправка событий, управление пользовательскими соединениями
- **SysProcesses** — управление фоновыми бизнес-процессами

---

## Основные сервисы

### 1. DockindStructure

**Ответственность:** Управление структурой типов документов (DocKind): статусы, свойства, функции, действия, права доступа, правила именования.

**Зависимости:** `QueriesHelper`, `OraLongTransactions`, `SysProcesses`

**События:**
- `onChanged` — генерируется при любом изменении структуры типа документа

**Основные методы:**

| Метод | Описание |
|-------|----------|
| `getInfo({ docKind })` | Получить базовую информацию о типе документа (ID, описание) |
| `setMain({ oldDocKind, newDocKind, description, parentId })` | Обновить основную информацию (только описание и родительскую папку) |
| `getStatuses({ docKind, docKindId })` | Получить список статусов (событий) типа документа |
| `setStatuses({ docKind, deleteIds, statuses, newOrder })` | Обновить статусы: удалить, изменить, добавить, переупорядочить |
| `getProps({ docKind, docKindId })` | Получить список свойств типа документа |
| `setProps({ docKind, props, deleteIds })` | Обновить свойства: удалить, изменить, добавить |
| `setProperty({ docKindId, property })` | Внутренний метод для сохранения одного свойства с его статусными масками и ссылками |
| `getFunctions({ docKind, docKindId })` | Получить функции, привязанные к типу документа |
| `setFunctions({ docKind, funcs, deleteIds })` | Обновить функции (DB или CSP) с их привязкой к статусам и свойствам |
| `setActions({ docKind, deleteIds, actions })` | Обновить действия (переходы между статусами) |
| `setNames({ docKind, names })` | Обновить правила именования документов |
| `setPermissions({ docKind, roles, deleteRoleIds })` | Установить права доступа для ролей (создание, видимость/редактирование статусов и свойств) |
| `getIdByName({ docKind })` | Получить ID типа документа по имени |
| `getNameById({ docKindId })` | Получить имя типа документа по ID |
| `updateDockindNames({ dockindId })` | Отложенное обновление описаний всех документов типа |
| `createDbFunction({ schema, name })` | Создать пустую PL/pgSQL-функцию, если она не существует |

---

### 2. Documents

**Ответственность:** Низкоуровневая работа с документами — чтение/запись значений, информация о документе, проверка обязательных полей.

**Зависимости:** `QueriesHelper`, `DockindCache`

**Основные методы:**

| Метод | Описание |
|-------|----------|
| `getInfo({ documentId })` | Получить основную информацию о документе (тип, статус, дата, создатель) |
| `getDescr({ documentId })` | Получить описание документа (сгенерированное по правилам именования) |
| `getValues({ dockind, documentId, propNames })` | Получить значения указанных свойств документа (поддерживает простые и ссылочные поля) |
| `setValues({ dockind, documentId, values })` | Установить значения свойств документа в рамках транзакции |
| `insertRefs({ dockind, documentId, propName, values })` | Добавить ссылки на другие документы в множественное ссылочное поле |
| `deleteRefs({ dockind, documentId, propName, values })` | Удалить указанные ссылки из множественного ссылочного поля |
| `checkNull({ dockind, documentId })` | Проверить, заполнены ли обязательные для текущего статуса поля |

---

### 3. DocumentLists

**Ответственность:** Поиск и выборка списков документов по значениям свойств.

**Зависимости:** `QueriesHelper`, `DockindCache`, `Documents`

**Основные методы:**

| Метод | Описание |
|-------|----------|
| `listIdByProps({ dockind, values, statuses, notStatuses, count })` | Найти ID документов по значениям свойств (AND-условия) |
| `listPropsByProps({ dockind, props, filter, statuses, notStatuses, count })` | Выбрать документы с указанными полями для вывода |
| `listByUniqueFields({ dockind, values })` | Найти документы по уникальным полям (автоматически определяются по конфигурации) |
| `getUniqueCollisions({ dockind, documentId })` | Найти документы с конфликтами по уникальным полям (исключая переданный ID) |

---

### 4. DocumentFuncs

**Ответственность:** Выполнение бизнес-функций, привязанных к статусам и свойствам документов.

**Зависимости:** `QueriesHelper`, `DockindCache`, `Documents`, `GlobalServices`, `Eventer`

**Основные методы:**

| Метод | Описание |
|-------|----------|
| `processStatus({ dockind, documentId, statusName, params, session, postprocess })` | Выполнить все функции, привязанные к указанному статусу (pre- или post-process) |
| `processProperty({ dockind, documentId, propName, session })` | Выполнить функции, привязанные к изменению свойства |
| `processAnykindFuncs({ dockind, documentId, params, session })` | Выполнить глобальные (anykind) функции для документа |
| `runFunc({ func, dockind, documentId, params, session, trxId })` | Выполнить конкретную функцию (DB или CSP) |

**Поддерживаемые типы функций:**
- **DB** — вызов хранимой процедуры PostgreSQL (формат `schema.function`)
- **CSP** — вызов метода удалённого сервиса (формат `ServiceName.method`)

---

### 5. DocCardValues

**Ответственность:** Высокоуровневая работа со значениями документов в контексте карточки (UI-транзакции).

**Зависимости:** `QueriesHelper`, `DockindCache`, `Documents`, `DocumentFuncs`, `DocumentLists`

**Основные методы:**

| Метод | Описание |
|-------|----------|
| `getValDisplay({ documentId, propName })` | Получить отображаемое значение поля (например, имя справочника вместо ID) |
| `getValues({ documentId, propName })` | Получить массив значений простого поля |
| `setValues({ cardId, documentId, propName, value/values })` | Установить значения простого поля в рамках карточной транзакции |
| `getRefs({ documentId, propName })` | Получить список связанных документов для ссылочного поля |
| `setRefs({ cardId, documentId, propName, refDocumentIds })` | Заменить весь список связанных документов |
| `clearRefs({ cardId, documentId, propName })` | Очистить ссылочное поле |
| `insertRefs({ cardId, documentId, propName, refDocumentIds })` | Добавить ссылки в множественное поле |
| `deleteRefs({ cardId, documentId, propName, refDocumentIds })` | Удалить указанные ссылки |
| `createRefDoc({ cardId, documentId, propName, parentId })` | Создать новый документ, привязанный к ссылочному полю |
| `deleteRefDocs({ cardId, documentId, propName, refDocumentIds })` | Удалить связанные документы (полное удаление) |
| `commit({ cardId })` | Зафиксировать транзакцию карточки (проверка обязательных полей и уникальности) |
| `rollback({ cardId })` | Откатить транзакцию карточки |

---

### 6. DocCardActions

**Ответственность:** Выполнение действий (переходов между статусами) с документами.

**Зависимости:** `QueriesHelper`, `DockindCache`, `Documents`, `DocumentFuncs`, `DocCardValues`, `SysProcesses`, `DocumentLists`

**Основные методы:**

| Метод | Описание |
|-------|----------|
| `runActions({ dockind, documentIds, actionId, actParams, cardId, session })` | Выполнить действие над одним или несколькими документами |
| `runAction({ dockind, documentId, action, actParams, cardId, session })` | Внутренний метод выполнения одного действия (проверка статуса, смена статуса, вызов функций) |
| `runGlobalAction({ action, actParams, cardId, session })` | Выполнить глобальное действие (без привязки к документу) |
| `create({ dockind, parentId, classifPropId, classifId, session })` | Создать новый документ с опциональным родителем и классификатором |
| `deleteDocs({ dockind, documentIds, session })` | Пометить документы как удалённые и запустить фоновую очистку |
| `processDelete({ dockind, documentId, session })` | Отложенная постобработка удаления (вызов функций статуса "deleted", физическое удаление) |

**Логика действия:**
1. Проверка допустимости текущего статуса (`FROM_STATUS_IDS`)
2. (Опционально) Смена статуса на `TO_STATUS_ID` с вызовом pre-process функций
3. (Опционально) Вызов функции, привязанной к действию
4. Запуск post-process функций для нового статуса
5. Запуск глобальных (anykind) функций

---

### 7. DocTables

**Ответственность:** Генерация и синхронизация структур таблиц документов в PostgreSQL для быстрого поиска и отчетов.

**Зависимости:** `QueriesHelper`, `OraLongTransactions`, `DockindCache`

**Основные методы:**

| Метод | Описание |
|-------|----------|
| `recreate({ docKind })` | Полностью пересоздать таблицу, представление и процедуру синхронизации для типа, затем синхронизировать все данные |
| `recreateStruct({ docKind })` | Пересоздать только структуру таблицы (без представления и данных) |
| `recreateView({ docKind })` | Пересоздать представление для типа документа (объединяет основную таблицу и словари) |
| `recreateAll({})` | Пересоздать структуры для всех типов документов (последовательно) |
| `createSyncProcedure({ dockind })` | Сгенерировать PL/pgSQL-процедуру синхронизации для типа |
| `documentSync({ docKind, documentId })` | Синхронизировать один документ с его таблицей |
| `documentSyncKind({ docKind })` | Синхронизировать все документы указанного типа (пакетная обработка) |

**Архитектура таблиц:**
- Основная таблица: `doc_tables.<dockind>_t` — содержит single-свойства
- Дочерние таблицы: `doc_tables.<dockind>_t_<prop>` — для multi-свойств
- Представление: `doc_tables.<dockind>_v` — объединяет данные для удобного чтения

---

### 8. DockindImporter

**Ответственность:** Импорт метаданных из XML и JSON в базу данных.

**Зависимости:** `DockindStructure`, `QueriesHelper`, `DocumentsHelper`, `OraLongTransactions`

**Основные методы:**

| Метод | Описание |
|-------|----------|
| `importXml({ root, vocabs, types })` | Импорт структуры из XML (словари + типы документов) |
| `importJson({ root, data })` | Импорт структуры из JSON |

**Внутренние классы:**
- **XmlImporter** — парсинг XML, создание/обновление словарей, типов, статусов, свойств, действий, имён, функций
- **JsonImporter** — аналогичный функционал для JSON (более современный формат)

**Поддерживаемые сущности при импорте (JSON):**
- Словари (`vocabs`)
- Типы документов (`name`, `descr`)
- Статусы (`statuses` с цветами, функциями, правами)
- Свойства (`props` с типом, мульти, уникальностью, статусными масками, ссылками, правами, функциями)
- Правила именования (`names`)
- Действия (`actions` с условиями, переходами, правами, функциями)
- Права доступа (`permissions` на уровне ролей)

---

### 9. DockindExporter

**Ответственность:** Экспорт метаданных типа документа в JSON.

**Зависимости:** `DockindStructure`, `QueriesHelper`, `DocumentsHelper`, `OraLongTransactions`

**Основные методы:**

| Метод | Описание |
|-------|----------|
| `exportJson({ dockind })` | Экспортировать полную структуру типа документа в JSON |

**Внутренний класс:** `JsonExporter`

**Экспортируемые данные:**
- Основная информация (`name`, `descr`)
- Используемые словари (`vocabs` со значениями)
- Статусы (`statuses` с цветами, правами, функциями)
- Свойства (`props` с типами, параметрами, статусными масками, ссылками, правами, функциями)
- Правила именования (`names`)
- Действия (`actions` с условиями, переходами, правами, функциями)

---

### 10. DocDataImporter

**Ответственность:** Импорт данных документов из внешних источников (XML).

**Зависимости:** `QueriesHelper`, `DocumentsHelper`, `DockindCache`, `Documents`

**Основные методы:**

| Метод | Описание |
|-------|----------|
| `importXml({ dataXml, uniqueProps })` | Импортировать данные документов из XML |

**Логика импорта:**
1. Парсинг XML, извлечение структуры документов
2. Для каждого документа:
   - Поиск существующего документа по уникальным полям (если указаны)
   - Если найден — применение правил слияния (добавление, перезапись, стирание)
   - Если не найден — создание нового документа
3. Заполнение свойств документа (включая ссылки на подчинённые документы)
4. Установка статуса

**Правила слияния:**
- `0` — значение не меняется
- `2` — добавление новых значений в множество
- `3` — перезапись значения
- `4` — стирание значения

---

## Вспомогательные модули

### db-functions.mjs

Содержит определения SQL-функций для `csp_documents` (схема CSP-документов). Эти функции используются сервисами для работы с базой данных:

- `get_all_dockinds` — получить все типы документов
- `get_document_info` — получить информацию о документе
- `get_dockind_id_by_name` / `get_dockind_name_by_id` — преобразование ID/имени
- `get_dockind_props` — свойства типа документа
- `get_dockind_prop_events` — статусные маски свойств
- `get_document_values` / `get_document_refs` — чтение значений и ссылок
- и другие

### DocFunctionsInstaller

Наследуется от `DbFunctionsInstaller` и устанавливает функции из `db-functions.mjs` при старте сервиса.

# Сервис DockindStructure

Сервис предназначен для управления структурой информационных объектов (DocKind) в системе. Он позволяет настраивать атрибуты типов документов: основные свойства, статусы, реквизиты, функции, действия, права доступа, а также переименовывать документы определённого типа.

## Зависимости

- **`QueriesHelper`** — выполнение SQL-запросов и вызов хранимых процедур.
- **`OraLongTransactions`** — управление длительными транзакциями.
- **`SysProcesses`** — запуск фоновых процессов бизнес-логики (используется для отложенного обновления имён документов).

## События

- **`onChanged`** — генерируется при любом изменении структуры информационного объекта. Полезно для сброса кэшей или оповещения других компонентов системы.

## Запросы

Все запросы выполняются через HTTP POST (если в схеме указано `"http": "POST"`), либо доступны только внутренне (если `"http": null`). Почти все запросы требуют авторизации (`"anonymous": false`), административные методы отмечены флагом `"needAdmin": true`.

### `getInfo`

Получить идентификатор и описание типа документа по его системному имени.

**Параметры запроса:**
| Поле     | Тип    | Обязательное | Описание                     |
|----------|--------|--------------|------------------------------|
| `docKind` | string | да           | Системное имя типа документа |

**Ответ:**
| Поле          | Тип    | Описание                 |
|---------------|--------|--------------------------|
| `id`          | number | Идентификатор типа (DOCKIND_ID) |
| `description` | string | Человекочитаемое описание |

**Ошибки:**
- `ComplexError("Не найден тип документа ...")` — если указанный тип не существует.

---

### `setMain`

Обновить основные атрибуты типа документа: описание, родительскую папку. Переименование (изменение `oldDocKind` на `newDocKind`) запрещено.

**Параметры запроса:**
| Поле          | Тип    | Обязательное | Описание                                                                 |
|---------------|--------|--------------|--------------------------------------------------------------------------|
| `oldDocKind`  | string | нет          | Текущее системное имя (должно совпадать с `newDocKind`, если указано)     |
| `newDocKind`  | string | да           | Новое системное имя (не может отличаться от `oldDocKind`)                |
| `description` | string | нет          | Новое описание                                                           |
| `parentId`    | number | нет          | ID родительской папки (категории)                                        |

**Примечания:**
- Требует прав администратора (`needAdmin: true`).
- Выполняется в рамках сессии (`session` передаётся из контекста).

---

### `getStatuses`

Получить список всех статусов (событий) для заданного типа документа.

**Параметры запроса:**
| Поле        | Тип    | Обязательное | Описание                                                                 |
|-------------|--------|--------------|--------------------------------------------------------------------------|
| `docKind`   | string | условно      | Системное имя типа. Используется, если не передан `docKindId`.           |
| `docKindId` | number | условно      | ID типа. Если указан, `docKind` игнорируется.                            |
| `session`   | object | нет          | Сессия пользователя (автоматически подставляется при HTTP-вызове).       |

**Ответ:**
Массив объектов со следующими полями:
| Поле         | Тип    | Описание                                    |
|--------------|--------|---------------------------------------------|
| `EVENT_ID`   | number | Уникальный идентификатор статуса            |
| `EVENT_NAME` | string | Системное имя статуса                       |
| `EVENT_DESCR`| string | Отображаемое описание                       |
| `COLOR_NAME` | string | Название цвета (может быть `null`)          |
| `COLOR_CODE` | string | Код цвета в формате Delphi (например, `$00D6FED6`) |

---

### `setStatuses`

Изменить набор статусов типа документа: удалить, обновить существующие, добавить новые, установить порядок. Операция обёрнута в транзакцию.

**Параметры запроса:**
| Поле        | Тип    | Обязательное | Описание                                                                 |
|-------------|--------|--------------|--------------------------------------------------------------------------|
| `docKind`   | string | условно      | Системное имя типа (если не указан `docKindId`).                         |
| `docKindId` | number | условно      | ID типа.                                                                 |
| `deleteIds` | array of number | нет | Список ID статусов, которые необходимо удалить.                          |
| `statuses`  | array  | нет          | Массив статусов для добавления/обновления. Каждый объект содержит:       |
|             |        |              | · `EVENT_ID` (number, <0 для новых),                                     |
|             |        |              | · `EVENT_NAME` (string),                                                 |
|             |        |              | · `EVENT_DESCR` (string),                                                |
|             |        |              | · `COLOR_NAME` (string),                                                 |
|             |        |              | · `COLOR_CODE` (string).                                                 |
| `newOrder`  | array of number | нет      | Новый порядок ID статусов (включая только что созданные).                 |
| `session`   | object | нет          | Сессия пользователя.                                                     |
| `trxId`     | string | нет          | Идентификатор внешней транзакции (для встраивания в более крупные операции). |

**Требования:** `needAdmin: true`.

---

### `getProps`

Получить все реквизиты (свойства) типа документа.

**Параметры запроса:**
| Поле        | Тип    | Обязательное | Описание                 |
|-------------|--------|--------------|--------------------------|
| `docKind`   | string | условно      | Системное имя типа.      |
| `docKindId` | number | условно      | ID типа.                 |
| `session`   | object | нет          | Сессия пользователя.     |

**Ответ:** массив объектов реквизитов (структура зависит от БД).

---

### `setProps`

Массовое изменение реквизитов типа документа: удаление, создание/обновление, установка порядка. Внутри транзакции последовательно обрабатываются все переданные свойства.

**Параметры запроса:**
| Поле        | Тип    | Обязательное | Описание                                                                 |
|-------------|--------|--------------|--------------------------------------------------------------------------|
| `docKind`   | string | да           | Системное имя типа.                                                      |
| `props`     | array  | нет          | Массив объектов свойств для вставки/обновления. Каждый объект должен содержать поля согласно `PKG_KIND_PROPERTIES_CS.UPDATE_PROPERTY` (см. описание отдельных полей в `setProperty`). Если у свойства указаны `Statuses`, они также обновляются. |
| `deleteIds` | array of number | нет | ID реквизитов, подлежащих удалению.                                      |
| `statuses`  |        |              | (Не используется в текущей реализации, оставлен для совместимости)       |
| `session`   | object | нет          | Сессия.                                                                  |
| `trxId`     | string | нет          | Внешняя транзакция.                                                      |

**Детали полей объекта свойства (property):**
- `DOCPROP_KIND` — тип реквизита (number).
- `DOCPROP_ID` — ID реквизита (number, для существующих).
- `DOCPROP_FPATH`, `DOCPROP_PRESENTATION`, `DOCPROP_SQL`, `DOCPROP_OBJECT`, `DOCPROP_DESCR`, `DOCPROP_NAME`, `DOCPROP_SCRIPT`, `DEFAULT_VALUE`, `DOCPROP_PRESENTATION_OPTIONS` — строковые атрибуты (CLOB).
- `DOCPROP_UNIQUE`, `DOCPROP_MULTI` — булевы флаги (передаются как 1/0).
- `DOCPROP_RULE_CHILD`, `DOCPROP_RULE_PARENT`, `DOCPROP_REPEAT`, `DOCPROP_TREE_KIND`, `DOCPROP_RULE`, `DOCPROP_VALID`, `DOC_FORMAT` — числовые поля.
- `RefLinks` — опциональный массив объектов `{ DocKindId, XmlFilter }`, перед отправкой сериализуется в JSON.
- `Statuses` — массив объектов вида `{ eventId, required, visible, writable }` для настройки доступности реквизита в разных статусах.

**Требования:** `needAdmin: true`.

---

### `getFunctions`

Получить список функций (триггеров/обработчиков), привязанных к типу документа.

**Параметры запроса:**
| Поле        | Тип    | Обязательное | Описание             |
|-------------|--------|--------------|----------------------|
| `docKind`   | string | условно      | Системное имя типа.  |
| `docKindId` | number | условно      | ID типа.             |
| `session`   | object | нет          | Сессия.              |
| `trxId`     | string | нет          | Транзакция.          |

**Ответ:** массив объектов функций. Каждый объект содержит:
- `DOCEVENTKIND_IDS`, `DOCPROP_IDS`, `DK_ACTION_IDS` — массивы чисел, полученные парсингом строк с разделителем `,`.
- Прочие поля, возвращаемые БД.

**Требования:** `needAdmin: true`.

---

### `setFunctions`

Обновить перечень функций типа документа. Предварительно для каждой функции вида `schema.name` создаётся заглушка в БД (если ещё не существует).

**Параметры запроса:**
| Поле              | Тип    | Обязательное | Описание                                                                 |
|-------------------|--------|--------------|--------------------------------------------------------------------------|
| `docKind`         | string | условно      | Системное имя типа.                                                      |
| `docKindId`       | number | условно      | ID типа.                                                                 |
| `funcs`           | array  | нет          | Массив объектов функций (формат определяется БД). Поле `db_function` обязательно должно иметь вид `schema.function_name`. |
| `deleteIds`       | array of number | нет | ID функций для удаления.                                                |
| `notValidateFuncs`| boolean| нет          | Если `true`, пропустить проверку формата `db_function`.                  |
| `session`         | object | нет          | Сессия.                                                                  |
| `trxId`           | string | нет          | Транзакция.                                                              |

**Требования:** `needAdmin: true`.

---

### `setActions`

Управление действиями (actions), доступными для типа документа. Поддерживает добавление, обновление, удаление и изменение порядка.

**Параметры запроса:**
| Поле        | Тип    | Обязательное | Описание                                                                 |
|-------------|--------|--------------|--------------------------------------------------------------------------|
| `docKind`   | string | условно      | Системное имя типа.                                                      |
| `docKindId` | number | условно      | ID типа.                                                                 |
| `deleteIds` | array of number | нет | ID действий для удаления.                                                |
| `actions`   | array  | нет          | Массив объектов действий. Каждый объект должен иметь поле `changed` (boolean). Если `changed === true`, объект будет передан в БД для вставки/обновления. Если `false`, используется только его `id` для сохранения порядка. |
| `session`   | object | нет          | Сессия.                                                                  |
| `trxId`     | string | нет          | Транзакция.                                                              |

**Требования:** `needAdmin: true`.

---

### `setNames`

Запускает процесс обновления имён всех документов заданного типа (например, после изменения правил формирования наименования). Выполняется немедленное сохранение новых правил именования и создание фонового процесса через сервис `SysProcesses`.

**Параметры запроса:**
| Поле        | Тип    | Обязательное | Описание                                                                 |
|-------------|--------|--------------|--------------------------------------------------------------------------|
| `docKind`   | string | условно      | Системное имя типа.                                                      |
| `docKindId` | number | условно      | ID типа.                                                                 |
| `names`     | object | да           | Новые правила формирования имён (структура определяется логикой БД).     |
| `session`   | object | нет          | Сессия.                                                                  |
| `trxId`     | string | нет          | Транзакция.                                                              |

**Требования:** `needAdmin: true`.

---

### `updateDockindNames`

Служебный метод, вызываемый фоновым процессом. Последовательно обновляет описания (`DESCR`) всех документов, принадлежащих указанному типу. Может выполняться долго, поэтому не должен вызываться напрямую из HTTP (хотя endpoint открыт).

**Параметры запроса:**
| Поле         | Тип    | Обязательное | Описание       |
|--------------|--------|--------------|----------------|
| `dockindId`  | number | нет          | ID типа документа. Если не указан, будет ошибка. |

**Требования:** `needAdmin: true`.

---

### `setPermissions`

Управление правами ролей на тип документа: создание, доступ к статусам и реквизитам.

**Параметры запроса:**
| Поле           | Тип    | Обязательное | Описание                                                                 |
|----------------|--------|--------------|--------------------------------------------------------------------------|
| `docKind`      | string | условно      | Системное имя типа.                                                      |
| `docKindId`    | number | условно      | ID типа.                                                                 |
| `roles`        | array  | нет          | Массив объектов прав ролей. Каждый объект: `id` (роль), `creation` (0/1), `statuses` (структура прав на статусы), `properties` (права на реквизиты). |
| `deleteRoleIds`| array of number | нет | ID ролей, для которых нужно удалить все права на данный тип.            |
| `session`      | object | нет          | Сессия.                                                                  |
| `trxId`        | string | нет          | Транзакция.                                                              |

**Требования:** `needAdmin: true`.

---

### `getIdByName`

Получить числовой идентификатор типа документа по его системному имени.

**Параметры запроса:**
| Поле      | Тип    | Обязательное | Описание                |
|-----------|--------|--------------|-------------------------|
| `docKind` | string | да           | Системное имя типа.     |

**Ответ:** число (`RESULT`) — идентификатор.

---

### `getNameById`

Получить системное имя типа документа по его ID.

**Параметры запроса:**
| Поле        | Тип    | Обязательное | Описание |
|-------------|--------|--------------|----------|
| `docKindId` | number | да           | ID типа. |

**Ответ:** строка — системное имя.

# Сервис LegacyXml2Select

**LegacyXml2Select** – это сервис, который предоставляет возможность парсить XML-запросы, сформированные в стиле «формальных выборок», и генерировать соответствующие SQL SELECT-запросы к документам в БД. Основное назначение – обеспечить обратную совместимость со старыми механизмами поиска.

Сервис состоит из нескольких компонентов:
- **LegacyXml2Select** – фасад, предоставляющий внешние методы.
- **LegacyXml2SelectDb** – сервис‑помощник для доступа к базе данных (получение метаданных, выполнение функций).
- **LegacyXml2SelectInnerPg** – ядро парсинга XML и построения SQL.
- **PropertyBuilder** – построитель условий для свойств документов.
- **MacroParser** – парсер макросов (функции, литералы).
- **SqlQuery / WhereCondition** – утилиты для программного конструирования SQL-выражений и оптимизации WHERE-условий.

Все компоненты располагаются в одном модуле и экспортируются как единое целое.

## Сервис LegacyXml2Select

**Назначение:** Принимает XML‑строку, параметры режима запроса и прав доступа, преобразует её в объект `SqlQuery` (или сразу в SQL‑строку) и возвращает результат.

### Зависимости
- `QueriesHelper` – обязательный (указан в `requirements`), предоставляет доступ к выполнению запросов к PostgreSQL.
- `LegacyXml2SelectDb` – обязательный (добавляется как вложенный сервис в конструкторе).

### Методы (запросы)

#### `parseToObj({ session, xml, queryMode=0, permissions=1 })`
Преобразует XML в объект `SqlQuery` (без построения финального SQL‑текста).

**Параметры:**
- `session` (объект, опционально) – должен содержать `userId`, используется для фильтрации прав.
- `xml` (string) – XML‑строка запроса.
- `queryMode` (number, по умолчанию `0`) – режим запроса:
  - `0` – полный запрос (выбирает поля документа),
  - `1` – подсчёт количества (`COUNT`),
  - `2` – только идентификаторы документов,
  - `3‑6` – специальные режимы (логика меняется).
- `permissions` (number, по умолчанию `1`) – уровень проверки прав доступа (`-1` – без проверки, `1` – стандартная).

**Возвращает:** экземпляр `SqlQuery`.

#### `parseToSql({ session, xml, queryMode=0, permissions=1 })`
Выполняет `parseToObj`, а затем вызывает `sql.build()`, возвращая готовый SQL‑запрос.

**Параметры:** те же, что у `parseToObj`.

**Возвращает:** строку SQL.

#### `macroParse({ macro })`
Парсит строку‑макрос в абстрактное синтаксическое дерево.

**Параметры:**
- `macro` (string) – строка, содержащая вызов функции, литерал или идентификатор.

**Возвращает:** объект с полем `tree` – AST макроса (например, `{ type: 'function', value: 'NOW', args: [] }`).

## Формат XML‑запроса для LegacyXml2Select

Сервис `LegacyXml2Select` преобразует XML‑описание выборки документов в SQL‑запрос `SELECT`.  
Корневой элемент — `<query>`, внутри которого обязательно присутствует элемент `<formal>`, задающий условия фильтрации.

---

### 1. Корневая структура

```xml
<?xml version="1.0"?>
<query>
  <formal dockind_id="ID_ВИДА_ДОКУМЕНТА" [name="and|or"]>
     <!-- набор условий -->
  </formal>
</query>
```

**Атрибуты `<formal>`:**

| Атрибут      | Обязательный | Тип       | Описание                                                      |
|--------------|--------------|-----------|---------------------------------------------------------------|
| `dockind_id` | да           | integer   | Идентификатор вида документа (`dockind_id`) целевой таблицы   |
| `name`       | нет          | `and`/`or`| Логическая операция, объединяющая все условия верхнего уровня (по умолчанию `and`) |

---

### 2. Элементы внутри `<formal>` и `<reference>`

Допускаются следующие элементы (в любом порядке и в любых сочетаниях):

| Элемент      | Назначение                                                     |
|--------------|----------------------------------------------------------------|
| `<reference>`| Ссылка на другой документ / тип документа (JOIN / EXISTS)      |
| `<property>` | Условие на значение свойства документа                         |
| `<status>`   | Фильтр по статусу (виду события) документа                     |
| `<operation>`| Логическая группа условий (`AND` / `OR`)                       |
| `<select>`   | (Зарезервирован; влияет на логику outer join, но вывод не меняет)|

Все они могут вкладываться внутрь `<reference>` и `<operation>`.

---

### 3. Элемент `<reference>`

Описывает переход к связанному документу через свойство‑ссылку. Реализуется либо как `EXISTS`/`NOT EXISTS`, либо как `JOIN` (в зависимости от атрибута `referencing` и режима запроса).

```xml
<reference
  docprop_id="ID_СВОЙСТВА_ССЫЛКИ"
  referencing="join|is null"
  dockind_id="ID_ЦЕЛЕВОГО_ВИДА"
  [condition="l|nl|g|ng|e|ne"]
  [count="целое_число"]
  [valuevar="строка"]
  [leftp="строка"]
  [rightp="строка"]
>
  <!-- вложенные reference, property, status, operation -->
</reference>
```

| Атрибут       | Обязат. | Тип               | Значения / Описание                                                                                     |
|---------------|---------|-------------------|---------------------------------------------------------------------------------------------------------|
| `docprop_id`  | да      | integer           | Идентификатор свойства‑ссылки (из `model_documents.doc_kind_properties`)                                 |
| `referencing` | да      | строка            | `join` – обычная связь, проверяется существование связанного документа;<br/>`is null` – анти‑связь, условие `NOT EXISTS` (связанного документа нет) |
| `dockind_id`  | да      | integer           | Идентификатор вида документа, на который ссылаемся                                                      |
| `condition`   | нет     | `l,nl,g,ng,e,ne`  | Условие сравнения **количества** связанных документов (используется вместе с `count`; **реализовано частично** – вызывает ошибку) |
| `count`       | нет     | integer           | Ожидаемое количество документов для сравнения (см. `condition`)                                         |
| `valuevar`    | нет     | строка            | Зарезервировано; в текущей реализации не используется                                                   |
| `leftp`       | нет     | строка            | Зарезервировано; не используется                                                                        |
| `rightp`      | нет     | строка            | Зарезервировано; не используется                                                                        |

**Логика работы:**
- Если внутри `<reference>` нет ни вложенных `<reference>`, `<property>`, `<status>`, `<operation>`, ни `<select>`, и режим запроса = 4–6, то такой элемент при `referencing="join"` или внешнем соединении превращается в условие `1=1`.
- В остальных случаях для `<reference>` генерируется подзапрос `EXISTS` (или `NOT EXISTS` при `referencing="is null"`), в котором проверяется наличие связанных документов и дополнительно накладываются вложенные условия (свойства, статусы, другие ссылки).
- При `referencing="join"` и режимах 4–6 (специальные режимы отчётов) возможен сценарий с `OUTER JOIN`, но детали определяются вложенными условиями.

---

### 4. Элемент `<property>`

Задаёт фильтр по значению конкретного свойства документа (или по служебному идентификатору).

```xml
<property
  docprop_id="ID_СВОЙСТВА"
  condition="код_операции"
  [doc_prop_value="значение"]
  [valuevar="макрос"]
  [value="значение"]
  [type="s|t|n|d|v"]
  [kindtype="число"]
/>
```

| Атрибут          | Обязат. | Тип    | Описание / Возможные значения                                                                                                                                       |
|------------------|---------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `docprop_id`     | да      | integer| Идентификатор свойства документа. **Особые значения:**<br/>`-2` – ID документа (прямая выборка по document_id);<br/>`-4` – дата события (`event_date`);<br/>`-5`, `-6` – пользователь, породивший событие (`event_user`);<br/>`-7` – описание документа (`doc_descr`). Остальные числа – обычные свойства из `doc_kind_properties` |
| `condition`      | да      | строка | Код операции сравнения (см. таблицу ниже)                                                                                                                           |
| `doc_prop_value` | нет*    | строка | Непосредственное значение для сравнения. Допускается использование макросов (см. раздел 6). *Обязательно, если только не используется `IS NULL`/`IS NOT NULL`.       |
| `valuevar`       | нет     | строка | Ссылка на переменную или макрос; если задана и `condition != 'DIRECT'`, подменяет собой `doc_prop_value`.                                                           |
| `value`          | нет     | строка | Альтернативное значение (например, для работы со словарями; приоритет ниже `doc_prop_value`).                                                                       |
| `type`           | нет     | символ | Тип свойства (берётся из БД, если не указан):<br/>`s` – строка, `t` – текст, `n` – число, `d` – дата, `v` – ссылка на словарь и т.п.                               |
| `kindtype`       | нет     | integer| Подтип свойства (берётся из БД). Влияет на построение условия (например, `4` – иерархический словарь, `10` – мультизначное свойство).                               |

**Коды операций (`condition`) и их SQL‑эквиваленты:**

| Код         | SQL оператор                | Примечание                                                                                      |
|-------------|-----------------------------|--------------------------------------------------------------------------------------------------|
| `e`         | `=` или `IN`                | `IN` используется для свойств типа `v`, `t` и `kindtype=4` (словарь)                             |
| `ne`        | `<>` или `NOT IN`           | аналогично                                                                                       |
| `l`         | `<`                         |                                                                                                  |
| `nl`        | `>=`                        |                                                                                                  |
| `g`         | `>`                         |                                                                                                  |
| `ng`        | `<=`                        |                                                                                                  |
| `like`      | `LIKE`                      |                                                                                                  |
| `not like`  | `NOT LIKE`                  |                                                                                                  |
| `in`        | `IN`                        | Только для строковых типов (`s`, `t`), если `kindtype` != 4                                      |
| `d`         | `DIRECT`                    | Специальный режим: прямое присоединение таблицы свойств без `EXISTS` (используется для `docprop_id=-2`) |
| `is null`   | `IS NULL`                   |                                                                                                  |
| `is not null`| `IS NOT NULL`              |                                                                                                  |
| `min`       | `MIN`                       | (зарезервировано, используется в агрегациях)                                                      |
| `max`       | `MAX`                       | (зарезервировано)                                                                                |

**Особые `docprop_id`:**

- `-2` (`DIRECT`): фильтр по идентификаторам документов.  
  * `doc_prop_value` – список ID через запятую, например `"10,20,30"`.  
  * `valuevar` – имя функции, возвращающей массив ID (вызывается через `Xml2SelectDb.execFunc`).
- `-4`: фильтр по дате события (`event_date`). Для `condition='<=...'` значение автоматически корректируется на конец дня.
- `-5`, `-6`: фильтр по пользователю события (`event_user`).
- `-7`: фильтр по текстовому описанию (`doc_descr`). Для операторов `LIKE`/`NOT LIKE` – сравнение без учёта регистра.

---

### 5. Элемент `<status>`

Фильтрация по статусу (виду события) документа.

```xml
<status doceventkind_id="ID_СОБЫТИЯ" />
```

Может встречаться многократно. Каждый экземпляр задаёт одно значение `doceventkind_id`.  
В SQL формируется условие:  
`dt_{level}.doceventkind_id IN (0, список_ID_событий)`

Если список событий в XML совпадает с полным набором событий для данного вида документа (или полный набор пуст), условие **не добавляется** (чтобы не перегружать запрос избыточным перечислением).

---

### 6. Элемент `<operation>`

Группирует несколько условий с заданной логической связкой.

```xml
<operation name="and|or">
   <!-- reference, property, status, другие operation -->
</operation>
```

- `name="and"` – все вложенные условия объединяются через `AND`.
- `name="or"` – через `OR`.
- Атрибут `name` можно опустить; тогда элемент становится прозрачным контейнером (вложенные условия просто передаются на уровень выше без добавления собственной группы).

---

### 7. Элемент `<select>` (зарезервирован)

Присутствует в коде, но не влияет на итоговый SQL. Его наличие/отсутствие используется только во внутренней логике определения, нужно ли создавать `OUTER JOIN`.  
Пример:

```xml
<select />
```

В текущей реализации его содержимое игнорируется.

---

### 8. Подстановка макросов в значениях

В атрибутах `doc_prop_value`, `valuevar`, `value` (а также `@_valuevar` у `<reference>`) могут использоваться макросы.  
Поддерживаются следующие макросы (регистр важен):

| Макрос                | Подстановка                                                      |
|-----------------------|------------------------------------------------------------------|
| `@USERID`             | ID текущего пользователя (`documents.get_user_id`)               |
| `@USERNAME`           | Полное имя пользователя (Фамилия Имя Отчество)                   |
| `@ROLEID`             | ID текущей роли пользователя (`documents.get_role_id`)           |
| `@ROLENAME`           | Название роли                                                   |
| `@DEPARTID`           | Идентификатор подразделения пользователя                         |
| `@DATE`               | Текущая дата/время (`SYSDATE`); может комбинироваться с функциями: `@DATE+1`, `@DATE-7` и т.п. |
| `GET_USER_FILIALS`    | Вызов функции `GET_USER_FILIALS` (и другие подобные)             |
| `NOW`, `TODAY`, `WORKDAY`, `MONTHDAY`, `*YEAR*` | Вызов соответствующей функции БД, возвращающей дату |

Макросы распознаются по точному совпадению (`@USERID`) или по вхождению в строку (например, `TO_DATE('@DATE','DD.MM.YYYY')` приводит к подстановке `SYSDATE`).

Если значение начинается с буквы и содержит скобки (например, `my_func(1,2)`), то оно рассматривается как вызов функции БД и выполняется через `Xml2SelectDb.execFunc` или `execDate`.

---

### 9. Примеры

#### Простейший запрос (получить все документы вида 10)

```xml
<query>
  <formal dockind_id="10" />
</query>
```

#### Фильтр по свойствам и статусу

```xml
<query>
  <formal dockind_id="10" name="and">
    <property docprop_id="100" condition="e" doc_prop_value="Иванов"/>
    <property docprop_id="101" condition="g" doc_prop_value="1000"/>
    <status doceventkind_id="3"/>
    <status doceventkind_id="5"/>
  </formal>
</query>
```

#### Ссылка на связанный документ

```xml
<query>
  <formal dockind_id="10">
    <reference docprop_id="200" referencing="join" dockind_id="20">
      <property docprop_id="201" condition="like" doc_prop_value="%утверждён%"/>
    </reference>
  </formal>
</query>
```

#### Анти‑ссылка (NOT EXISTS)

```xml
<reference docprop_id="200" referencing="is null" dockind_id="20"/>
```

#### Использование макроса

```xml
<property docprop_id="102" condition="e" doc_prop_value="@USERID"/>
```

#### Группировка условий с OR

```xml
<operation name="or">
  <property docprop_id="100" condition="e" doc_prop_value="A"/>
  <property docprop_id="100" condition="e" doc_prop_value="B"/>
</operation>
```

#### Выборка по списку ID

```xml
<property docprop_id="-2" condition="d" doc_prop_value="101,205,330"/>
```

или через функцию:

```xml
<property docprop_id="-2" condition="d" valuevar="my_package.get_docs(55)"/>
```

# Сервисы CarabiQuery и DoclistStructure

## Общие сведения

Сервисы предназначены для формирования динамических SQL-запросов к базе данных документов, получения структурированных списков (доклистов, ссылочных полей), а также вспомогательной информации о колонках и фильтрах.

- **CarabiQuery** – основной сервис для выборки данных из доклистов, референсных списков и справочников типов документов.
- **DoclistStructure** – вспомогательный сервис для получения метаданных колонок и структуры доклистов/рефлистов.

Для запросов, требующих авторизации, в объекте запроса обязательно наличие поля `session` с корректной сессией пользователя (содержит `userId`).

---

## Сервис `CarabiQuery`

### 1. `getDoclistDataSql`
**Назначение:** получить SQL-запрос для выборки данных доклиста без его выполнения.  
**Права:** требует `needAdmin: true`.

**Параметры запроса:**

| Поле          | Тип    | Обязательное | Описание                                                              |
|---------------|--------|--------------|-----------------------------------------------------------------------|
| `docListId`   | number | да           | Идентификатор доклиста                                                |
| `statusList`  | string | нет          | Список ID статусов через запятую (например `"1,2,3"`)                 |
| `filter`      | string | нет          | Текстовый фильтр (поиск по контексту или номеру документа)            |
| `filterCols`  | string | нет          | JSON-фильтр по колонкам (см. формат в `FiltersBuilder`)               |
| `xmlFilter`   | string | нет          | Дополнительный XML-фильтр, подменяющий `XML_SEARCH` доклиста          |
| `orderBy`     | string | нет          | Имя колонки и направление сортировки (напр. `"EVENT_DATE DESC"`)      |
| `resTreeValue`| string | нет          | Код классификатора для фильтрации по дереву ресурсов                  |

**Формат ответа:**
```json
{
  "SQL": "<сгенерированный SQL-запрос>"
}
```

### 2. `fetchDoclistData`
**Назначение:** получить страницу данных доклиста.  
**Права:** доступно авторизованным пользователям.

**Параметры:** те же, что у `getDoclistDataSql`, плюс параметры пагинации:

| Поле     | Тип    | По умолчанию | Описание                      |
|----------|--------|--------------|-------------------------------|
| `count`  | number | 10           | Количество записей на страницу|
| `offset` | number | 0            | Смещение (начиная с 0)        |

**Ответ:**  
Массив объектов, соответствующих строкам доклиста. Поля каждой строки включают:
- `document_id` – идентификатор документа
- `doc_status_descr` – описание статуса
- `event_date` – дата события (строка в формате `DD.MM.YYYY HH24:MI:SS`)
- `doc_status_owner` – ФИО создателя
- `doc_status_modifier` – ФИО изменившего
- `doc_descr` – полное наименование документа
- `doc_status_id`, `doc_status_name` – системные поля статуса
- `doc_status_owner_id` – ID создателя
- `pr_special` – служебное поле
- `attach_count`, `child_count`, `files` – счётчики вложений и дочерних документов
- `PR_<имя_свойства>` – динамические колонки, соответствующие свойствам документа (имена начинаются с `PR_`)

**Пример:**
```json
[
  {
    "document_id": 12345,
    "doc_status_descr": "Утверждён",
    "event_date": "15.01.2025 10:30:00",
    "doc_status_owner": "Иванов Иван Иванович",
    "doc_status_modifier": "Петров Пётр Петрович",
    "doc_descr": "Договор №123 от 10.01.2025",
    "doc_status_id": 2,
    "doc_status_name": "APPROVED",
    "doc_status_owner_id": 101,
    "pr_special": null,
    "attach_count": 3,
    "child_count": 0,
    "files": 2,
    "pr_number": "123",
    "pr_date": "10.01.2025",
    ...
  }
]
```

### 3. `getDoclistDataCnt`
**Назначение:** получить общее количество записей в доклисте с учётом фильтров.  
**Права:** авторизованные пользователи.

**Параметры:** совпадают с `getDoclistDataSql` (без пагинации).  
**Ответ:** число (integer) – количество документов.

### 4. `fetchDoclistDataRow`
**Назначение:** получить одну строку доклиста по идентификатору документа.  
**Права:** авторизованные пользователи.

**Параметры:**

| Поле         | Тип    | Обязательное | Описание                      |
|--------------|--------|--------------|-------------------------------|
| `docListId`  | number | да           | ID доклиста                   |
| `documentId` | number | да           | ID конкретного документа      |

**Ответ:**  
Объект с данными строки (такой же, как в массиве `fetchDoclistData`) или `null`, если документ не найден.

### 5. `fetchReflistData`
**Назначение:** получить данные ссылочного поля (референс-листа) для конкретного документа.  
**Права:** авторизованные пользователи.

**Параметры:**

| Поле         | Тип    | Обязательное | Описание                                                                 |
|--------------|--------|--------------|--------------------------------------------------------------------------|
| `docKind`    | string | да           | Системное имя типа документа, из которого просматривается ссылка          |
| `documentId` | number | да           | ID текущего документа                                                     |
| `docPropId`  | number | да           | ID свойства-ссылки (идентификатор свойства в модели)                     |
| `parentId`   | number | нет          | ID родительского документа для иерархических ссылок (если дерево)         |
| `statusId`   | string | нет          | Фильтр по статусам целевых документов (через запятую)                    |
| `filter`     | string | нет          | Текстовый фильтр                                                         |
| `filterCols` | string | нет          | JSON-фильтр по колонкам                                                  |
| `orderBy`    | string | нет          | Сортировка                                                               |
| `count`      | number | нет (10)     | Размер страницы                                                          |
| `offset`     | number | нет (0)      | Смещение                                                                 |

**Ответ:** массив объектов с данными целевых документов (структура аналогична `fetchDoclistData`).

### 6. `getReflistDataCnt`
**Назначение:** получить количество документов в ссылочном списке.  
**Права:** авторизованные пользователи.  
**Параметры:** аналогичны `fetchReflistData`, но без `count`/`offset`.  
**Ответ:** число.

### 7. `fetchReflistDataRow`
**Назначение:** получить одну строку из ссылочного списка (целевой документ) по его ID.  
**Права:** авторизованные пользователи.  
**Параметры:**

| Поле         | Тип    | Обязательное | Описание                |
|--------------|--------|--------------|-------------------------|
| `documentId` | number | да           | ID целевого документа   |

**Ответ:** объект строки или `null`.

### 8. `fetchReflistOptions`
**Назначение:** получить список возможных документов для назначения в ссылку (диалог выбора).  
**Права:** авторизованные пользователи.  

**Параметры:**

| Поле            | Тип    | Обязательное | Описание                                                                                 |
|-----------------|--------|--------------|------------------------------------------------------------------------------------------|
| `docKind`       | string | да           | Системное имя типа документа-источника                                                   |
| `documentId`    | number | нет          | ID текущего документа (для подстановки переменных в XML-фильтр)                          |
| `docPropId`     | number | да           | ID свойства-ссылки                                                                       |
| `dockindIdProp` | number | да           | ID типа документа, доступного для выбора                                                 |
| `filter`        | object | нет          | Расширенные фильтры. Состав: `xml` (XML-фильтр), `statusIds`, `context`, `columns`, `classifId` |
| `orderBy`       | string | нет          | Сортировка                                                                               |
| `count`         | number | нет (10)     | Размер страницы                                                                          |
| `offset`        | number | нет (0)      | Смещение                                                                                 |

**Ответ:** массив объектов доступных документов.

### 9. `fetchDockindData`
**Назначение:** получить список документов заданного типа (без привязки к конкретному свойству-ссылке).  
**Права:** авторизованные пользователи.  

**Параметры:**

| Поле         | Тип    | Обязательное | Описание                                            |
|--------------|--------|--------------|-----------------------------------------------------|
| `docKind`    | string | да           | Системное имя типа документа                        |
| `statusList` | string | нет          | Список ID статусов через запятую                    |
| `xmlFilter`  | string | нет          | XML-фильтр                                          |
| `filter`     | string | нет          | Текстовый фильтр                                    |
| `filterCols` | string | нет          | JSON-фильтр по колонкам                             |
| `orderBy`    | string | нет          | Сортировка                                          |
| `count`      | number | нет (10)     | Размер страницы                                     |
| `offset`     | number | нет (0)      | Смещение                                            |

**Ответ:** массив объектов документов (формат аналогичен `fetchDoclistData`).

### 10. `getSqlFromXml`
**Назначение:** отладочный метод для получения сгенерированного SQL из произвольного XML.  
**Права:** `needAdmin: true`.  

**Параметры:**

| Поле  | Тип    | Обязательное | Описание                                             |
|-------|--------|--------------|------------------------------------------------------|
| `xml` | string | да           | XML-строка с блоком `<query><formal ...>`            |

**Ответ:** объект с единственным полем `SQL` (строка).

---

## Сервис `DoclistStructure`

### 1. `getDoclistColumns`
**Назначение:** получить полную структуру колонок доклиста с учётом пользовательских настроек (ширина, видимость, порядок).  
**Права:** авторизованные пользователи.

**Параметры:**

| Поле        | Тип    | Обязательное | Описание       |
|-------------|--------|--------------|----------------|
| `doclistId` | number | да           | ID доклиста    |

**Ответ:** массив объектов, каждый из которых описывает одну колонку:

| Поле                   | Тип     | Описание |
|------------------------|---------|----------|
| `SHOW_ORDER`           | number  | Порядковый номер для отображения |
| `DOCPROP_ID`           | number  | ID свойства модели (для системных колонок < 0) |
| `DOCPROP_NAME`         | string  | Человекочитаемое название |
| `PROP_SYS_NAME`        | string  | Системное имя колонки (напр. `DOCUMENT_ID`, `PR_DATE`, `DOC_STATUS_DESCR`) |
| `DOCPROP_KIND`         | number  | Тип свойства |
| `DOCPROP_OBJECT`       | string  | Код типа данных (`'1'` – целое, `'4'` – строка, `'32'` – дата и т.д.) |
| `MULTI`                | number  | Признак множественного значения |
| `SYS_COLUMN`           | number  | Флаг системной колонки |
| `WIDTH`                | number  | Ширина в пикселях (пользовательская или по умолчанию) |
| `VISIBLE`              | number  | Флаг видимости (1 – показывать, 0 – скрыта) |
| `ORDERBY`              | string|null  | Сохранённая пользователем сортировка |
| `DOCLIST_ID`           | number|null  | Идентификатор доклиста (если задан) |
| `IS_FIXED`, `IS_GROUP` и др. | number  | Флаги расширенных настроек колонки |

### 2. `getReflistStructure`
**Назначение:** получить полную информацию для отображения ссылочного списка, включая колонки, статусы и доступные XML-фильтры.  
**Права:** авторизованные пользователи.

**Параметры:**

| Поле        | Тип    | Обязательное | Описание                           |
|-------------|--------|--------------|------------------------------------|
| `docpropId` | number | да           | ID свойства-ссылки                 |
| `dockindId` | number | нет          | Явное указание типа документов (обычно определяется автоматически) |

**Ответ:** объект с полями:

- `columns` – массив колонок целевого типа документов (формат как в `getDoclistColumns`)
- `info` – объект с базовой информацией о типе (поля `DOCKIND_ID`, `DOCKIND_NAME`, …)
- `statusColors` – массив объектов статусов с цветовой индикацией
- `xmlFilters` – массив предустановленных XML-фильтров для выбора

### 3. `getReflistColumns`
**Назначение:** получить колонки для референс-листа (аналогично `getDoclistColumns`, но без привязки к конкретному доклисту).  
**Права:** авторизованные пользователи.

**Параметры:**

| Поле        | Тип    | Обязательное | Описание               |
|-------------|--------|--------------|------------------------|
| `docpropId` | number | нет          | ID свойства-ссылки     |
| `dockindId` | number | нет          | ID типа документов     |

**Ответ:** массив колонок (та же структура, что и в `getDoclistColumns`).

### 4. `getReflistOptionsColumns`
**Назначение:** получить колонки для диалога выбора документа в ссылку (обычно те же, что и в рефлисте, но могут отличаться правами).  
**Права:** авторизованные пользователи.

**Параметры:**

| Поле            | Тип    | Обязательное | Описание                       |
|-----------------|--------|--------------|--------------------------------|
| `docpropId`     | number | да           | ID свойства-ссылки             |
| `propDockindId` | number | да           | ID типа документов для выбора  |

**Ответ:** массив колонок.

---

## Примечания по фильтрации

### Текстовый фильтр (`filter`)
- Если переданная строка состоит только из цифр и длиннее 4 символов, сервис сначала пытается найти документ по `DOCUMENT_ID` в указанном типе.
- Если документ не найден или фильтр не числовой, выполняется поиск подстроки без учёта регистра в поле `doc_descr` (через `LIKE '%...%'`).

### JSON-фильтр по колонкам (`filterCols`)
Ожидается строка в формате JSON с массивом условий:
```json
{
  "filter": [
    {
      "field_name": "PR_NUMBER",
      "value": "123",
      "operator": "="
    }
  ]
}
```
- `field_name` – системное имя колонки (например, `DOCUMENT_ID`, `EVENT_DATE`, `PR_*`).
- `value` – значение для сравнения.
- `operator` – оператор (`=`, `!=`, `<`, `>`, `LIKE`, и т.д.). Если не указан, для строковых полей применяется `LIKE`, для числовых – `=`, для дат – `>=` или `<=`.

### XML-фильтр (`xmlFilter`, `xmlSearch`)
Формат соответствует внутреннему представлению условий в подсистеме `LegacyXml2Select`.  
Пример:
```xml
<query>
  <formal dockind_id="123" namevar="MYDOCTYPE" haschild="0" docrefs="1">
    <property docprop_id="100" condition="e" doc_prop_value="Hello"/>
    <reference docprop_id="200" condition="e" referencing="join">
      <status doceventkind_id="1"/>
    </reference>
  </formal>
</query>
```
Для деталей синтаксиса обратитесь к документации по `LegacyXml2Select`.

---

## Аутентификация и сессия

Все запросы (кроме `getDoclistDataSql` и `getSqlFromXml`, требующих `needAdmin`) доступны обычным авторизованным пользователям.  
Объект запроса должен содержать поле `session` с пользовательской сессией:
```json
{
  "session": {
    "userId": 123,
    ...
  },
  "docListId": 42,
  ...
}
```
При использовании HTTP-транспорта сессия обычно передаётся через механизмы платформы Morphcluster (куки, заголовки) – в документации по ядру уточните способ передачи.

# Планировщик задач DocScheduler (CRON_TABLE)

Планировщик `DocScheduler` предназначен для периодического запуска задач (CSP‑сервисов или DB‑функций) по расписанию, задаваемому с помощью SQL‑выражений. Управление задачами осуществляется через документы типа **CRON_TABLE**.

Планировщик автоматически отслеживает выполнение запущенных процессов, фиксирует ошибки, рассчитывает следующее время запуска и при необходимости переводит задачу в неактивное состояние (по превышению лимита ошибок или невалидному расписанию).

---

## Документ CRON_TABLE: поля и их назначение

Для создания задачи необходимо добавить документ с `dockind = "CRON_TABLE"` и статусом `"CRON_TABLE_ACTIVE"`.  
Поддерживаются следующие свойства:

| Поле | Тип | Описание |
|------|-----|-----------|
| `NUM` | число/строка | Идентификатор (номер) задачи. |
| `DESCR` | строка | Описание задачи (назначение). |
| `SYS_PROCESS_ID` | число | ID запущенного системного процесса (заполняется автоматически). |
| `DB_FUNCTION` | строка | Имя DB‑функции для вызова (если не используется CSP). |
| `CSP_SERVICE` | строка | Имя CSP‑сервиса для вызова. |
| `CSP_SERVICE_PARAMS` | строка (JSON) | Параметры вызова CSP‑сервиса в формате JSON. |
| `INTERVAL` | строка | **SQL‑выражение**, вычисляющее следующую дату/время запуска (см. ниже). |
| `INTERVAL_ERROR` | строка | SQL‑выражение для следующего запуска **после ошибки** (если не задано, используется `INTERVAL`). |
| `MAX_COUNT_ERROR` | число | Максимальное количество последовательных ошибок, после которого задача переводится в статус `CRON_TABLE_ERROR`. |
| `COUNT_ERROR` | число | Текущий счётчик последовательных ошибок (заполняется автоматически). |
| `FIRST_RUN` | datetime | Первый запланированный запуск (опционально, используется для проверки). |
| `LAST_RUN` | datetime | Время последнего запуска (заполняется автоматически). |
| `NEXT_RUN` | datetime | Следующее время запуска (рассчитывается автоматически). |
| `DAILY_START_TIME` | datetime | Время фактического начала выполнения задачи (заполняется автоматически). |
| `DAILY_END_TIME` | datetime | Время окончания выполнения задачи (заполняется автоматически). |
| `ERROR_TEXT` | строка | Текст последней ошибки (заполняется автоматически). |
| `PERIODICAL_RUN` | *не используется* | (зарезервировано) |
| `DEL_ON_FINISH` | *не используется* | (зарезервировано) |

> **Важно:** Для запуска задачи необходимо указать **либо** `CSP_SERVICE` (с возможными `CSP_SERVICE_PARAMS`), **либо** `DB_FUNCTION`.

---

## Принцип работы планировщика

### 1. Загрузка задач

При старте и далее каждые **10 минут или при изменениях в CRON_TABLE** (таймер `reloadTimer`) планировщик перечитывает все активные документы `CRON_TABLE_ACTIVE`.  
Также перезагрузка происходит при любом изменении статуса системного процесса (через триггер `trgProcessChanged`) — например, после завершения задачи.

### 2. Цикл проверки расписания

Каждые **30 секунд** (таймер `checkTimer`) планировщик:
- Собирает задачи, у которых `NEXT_RUN <= текущего времени` **и** `SYS_PROCESS_ID` отсутствует (т.е. процесс не запущен).
- Запускает полную перезагрузку задач и их обработку, при наличии собранных задач

### 3. Обработка задачи (`_processTask`)

Для каждой активной задачи выполняется логика:

```
Если NEXT_RUN в будущем → пропустить
Иначе если SYS_PROCESS_ID отсутствует → запустить задачу (_runTask)
Иначе если процесс с таким ID не найден в sysprocs → запустить задачу заново
Иначе если процесс завершён с ошибкой → вызвать _finishTask с текстом ошибки
Иначе если процесс завершён успешно → вызвать _finishTask без ошибки
Иначе процесс ещё выполняется → ничего не делать
```

### 4. Запуск задачи (`_runTask`)

- Создаётся новый системный процесс:
  - тип `csp` – для вызова CSP‑сервиса (параметры берутся из `CSP_SERVICE_PARAMS`);
  - тип `db` – для вызова DB‑функции.
- В документе CRON_TABLE проставляется `SYS_PROCESS_ID` и текущее время в `DAILY_START_TIME`.
- После этого планировщик перезагружает списки задач и процессов.

Если на этапе запуска возникает исключение, вызывается `_finishTask` с текстом ошибки.

### 5. Завершение задачи / расчёт следующего запуска (`_finishTask`)

**Шаги:**

1. **Определение интервала**  
   - Если есть ошибка → используется `INTERVAL_ERROR` (или `INTERVAL`, если `INTERVAL_ERROR` не задан).  
   - Если ошибки нет → используется `INTERVAL`.

2. **Обновление счётчика ошибок**  
   - При ошибке: `COUNT_ERROR` увеличивается на 1.  
   - При успехе: `COUNT_ERROR` сбрасывается в 0.

3. **Проверка лимита ошибок**  
   - Если `COUNT_ERROR > MAX_COUNT_ERROR` → задача переводится в статус `CRON_TABLE_ERROR` и больше не запускается.

4. **Расчёт следующего времени запуска**  
   - Выполняется SQL‑запрос: `SELECT (<INTERVAL_выражение>) as RESULT`.  
   - Результат преобразуется в `dayjs`‑объект.  
   - Если выражение пустое, результат невалиден или запрос не удался → задача переводится в статус `CRON_TABLE_PASSIVE` (отключена).  
   - Иначе `NEXT_RUN` устанавливается на вычисленную дату/время.

5. **Обновление документа**  
   - Сбрасывается `SYS_PROCESS_ID`, проставляется `DAILY_END_TIME`, `ERROR_TEXT`, новый `NEXT_RUN` и `COUNT_ERROR`.  
   - При необходимости изменяется статус документа (на `CRON_TABLE_ERROR` или `CRON_TABLE_PASSIVE`).  
   - Выполняется перезагрузка списков задач и процессов.

---

## Настройка расписания (INTERVAL и INTERVAL_ERROR)

Планировщик **не использует классические cron‑выражения**. Вместо этого для вычисления следующей даты запуска применяются **произвольные SQL‑выражения**, возвращающие timestamp.

### Формат

Выражение должно быть валидным SQL‑выражением для СУБД, на которой работает MorphCluster. Рекомендуется использовать `NOW()` как точку отсчёта.

### Примеры

| Цель | SQL‑выражение для `INTERVAL` |
|------|-------------------------------|
| Каждые 5 минут | `NOW() + INTERVAL '5 minutes'` |
| Каждый час | `NOW() + INTERVAL '1 hour'` |
| Ежедневно в 03:00 | `DATE_TRUNC('day', NOW()) + INTERVAL '1 day' + INTERVAL '3 hours'` |
| Каждый понедельник в 09:00 | `NEXT_DAY(NOW(), 'MONDAY') + INTERVAL '9 hours'` (зависит от диалекта SQL) |
| Через 1 день после успешного выполнения | `NOW() + INTERVAL '1 day'` |

### Особенность при пустом `INTERVAL`

Если `INTERVAL` (и `INTERVAL_ERROR`) не заданы, то:
- После успешного выполнения задачи `NEXT_RUN` останется `NULL`.
- При следующем цикле проверки планировщик будет считать, что `NEXT_RUN` уже наступил (условие `task.NEXT_RUN && task.NEXT_RUN.isAfter(...)` не сработает, т.к. `NEXT_RUN` — `null`).
- В результате задача **будет запускаться повторно сразу после предыдущего завершения** (зацикливание).

**Рекомендация:** всегда задавайте явное `INTERVAL` или переводите задачу в неактивный статус вручную после разового выполнения.

---

## Обработка ошибок и отказоустойчивость

- **Счётчик ошибок** (`COUNT_ERROR`) увеличивается при любом сбое:
  - Ошибка запуска задачи (исключение в `_runTask`).
  - Завершение системного процесса со статусом `failed`.
- **Раздельный интервал после ошибки** позволяет задать более частое повторение (например, `NOW() + INTERVAL '5 minutes'`) или более длительную паузу.
- **Лимит ошибок** (`MAX_COUNT_ERROR`): по достижении лимита задача автоматически деактивируется (статус `CRON_TABLE_ERROR`). Это предотвращает бесконечные попытки заведомо ошибочной задачи.
- **Невалидное расписание** (ошибка вычисления `NEXT_RUN` или пустой `INTERVAL`) переводит задачу в статус `CRON_TABLE_PASSIVE` — она исключается из обработки до ручного вмешательства.

---

## Жизненный цикл задачи (на примере)

1. **Создание**  
   Добавляется документ `CRON_TABLE` со статусом `ACTIVE`, заполняются поля `CSP_SERVICE`/`DB_FUNCTION`, `INTERVAL`, `MAX_COUNT_ERROR` (опционально).

2. **Первый запуск**  
   - Если `NEXT_RUN` не задан или `NEXT_RUN <= текущего времени` → планировщик запускает задачу.  
   - `SYS_PROCESS_ID` получает ID нового системного процесса.  
   - `DAILY_START_TIME` фиксирует момент старта.

3. **Выполнение**  
   Планировщик не вмешивается в ход работы процесса. Процесс выполняется асинхронно.

4. **Завершение процесса**  
   - Системный процесс переходит в статус `completed` или `failed`.  
   - Триггер `trgProcessChanged` немедленно вызывает перезагрузку планировщика.  
   - Планировщик обрабатывает завершённую задачу через `_finishTask`:
     - Рассчитывается `NEXT_RUN` (с учётом ошибки, если была).  
     - Обновляются `COUNT_ERROR`, `DAILY_END_TIME`, `ERROR_TEXT`.  
     - Сбрасывается `SYS_PROCESS_ID`.  
     - При необходимости меняется статус документа.

5. **Повторный запуск**  
   Когда текущее время достигнет нового `NEXT_RUN`, планировщик снова запустит задачу.

6. **Отключение задачи**  
   - Автоматически: при превышении `MAX_COUNT_ERROR` или невалидном `INTERVAL`.  
   - Вручную: изменить статус документа на любой, кроме `CRON_TABLE_ACTIVE`.

---

## Особенности и ограничения

### 1. Механизм блокировки повторной перезагрузки
- Используется флаг `reloading` и отложенный вызов `needReload` для предотвращения одновременной перезагрузки из разных таймеров/триггеров.

### 2. Сессия выполнения
- Задачи запускаются от имени пользователя `userId = 5056` с правами администратора (`isAdmin: true`).  
- Это зашито в коде и не настраивается через CRON_TABLE.

### 3. Поведение при `NEXT_RUN = NULL`
Как уже отмечено, это приводит к немедленному повторному запуску после завершения. Если вам нужно **однократное выполнение**, после успеха следует вручную перевести задачу в статус `CRON_TABLE_PASSIVE` или `CRON_TABLE_ERROR` (например, через отдельный процесс).

### 4. Типы вызываемых функций
- **CSP‑сервис** – должен быть зарегистрирован в системе. Параметры передаются через `CSP_SERVICE_PARAMS` в виде JSON-строки.  
- **DB‑функция** – хранимая функция в базе данных. Вызов происходит без параметров (но можно передать через глобальные переменные сессии или отдельный механизм).

---

## Рекомендации по настройке

1. **Всегда задавайте `INTERVAL`** даже для периодических задач. Для разовых задач используйте ручное отключение или запланируйте удаление документа после выполнения.
2. **Указывайте `MAX_COUNT_ERROR`** – разумное значение (например, 3–5) для защиты от «зависших» ошибочных задач.
3. **Используйте `INTERVAL_ERROR`**, если после сбоя нужно повторить попытку быстрее, чем обычно.
4. **Проверяйте SQL‑выражения** в консоли базы данных перед внесением в `INTERVAL`.
5. **Избегайте слишком частых запусков** менее 30 секунд

---

## Пример документа CRON_TABLE

```json
{
  "dockind": "CRON_TABLE",
  "status": "CRON_TABLE_ACTIVE",
  "props": {
    "NUM": 101,
    "DESCR": "Ежечасная архивация логов",
    "DB_FUNCTION": "archive_logs",
    "INTERVAL": "NOW() + INTERVAL '1 hour'",
    "INTERVAL_ERROR": "NOW() + INTERVAL '5 minutes'",
    "MAX_COUNT_ERROR": 3,
    "FIRST_RUN": "2025-01-01T00:00:00"
  }
}
```

Этот документ заставит планировщик вызывать DB‑функцию `archive_logs` каждый час. При возникновении ошибки следующая попытка будет через 5 минут. После трёх последовательных ошибок задача перейдёт в статус `CRON_TABLE_ERROR`.

---

## Заключение

Планировщик DocScheduler предоставляет гибкий механизм запуска задач по расписанию на основе SQL‑выражений. Настройка через документы CRON_TABLE позволяет динамически добавлять, изменять и отключать задачи без перезапуска сервиса. Важно правильно заполнять поля `INTERVAL` и контролировать обработку ошибок через `MAX_COUNT_ERROR` и `INTERVAL_ERROR`.