letopis 0.5.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,177 @@
2
2
 
3
3
  Формат: [Keep a Changelog](https://keepachangelog.com/), версии — semver.
4
4
 
5
+ ## [0.13.0] — 2026-07-11
6
+
7
+ ### Added — `up()`: одна точка входа
8
+ - **`up(opts): Promise<EntityDb>`** — от пустой машины до готового `db` одной функцией:
9
+ probe `dsn` (живой postgres — docker пропускается: CI-сервисы, внешние БД) → docker
10
+ ensure через CLI (`inspect`/`build` из пакованного Dockerfile/`run`/`start`) → ожидание
11
+ готовности PG + Redis → автосоздание базы из `dsn` (3D000) → версионная схема
12
+ `"v<version>.<schema>"` из шаблонов пакета (маркер `"<SCHEMA-NAME>"`) → сквозной
13
+ `connect()`. Идемпотентно на каждом шаге; `fresh: true` — дроп схемы и накат заново;
14
+ `seeds: string[] | false` — свои сиды вместо демо. Живые тайминги: с нуля
15
+ (build+initdb+apply) 7.4 s, `docker start` 1.2 s, всё готово ~110 ms.
16
+ - **Данные PG — на хосте**: контейнер поднимается с named volume **`letopis-pgdata`**
17
+ (кроссплатформенно, переживает пересоздание контейнера; физически — внутри
18
+ docker-диска WSL2/ext4), `dataDir: '/path'` — bind mount папки (на Windows/NTFS —
19
+ на свой риск). Redis без персиста — сессии эфемерны.
20
+ - SQL-шаблоны и Dockerfile теперь **пакуются в npm** (`lib/sql/`, `lib/docker/`,
21
+ `files += sql, docker`) — `up()` работает у любого потребителя пакета из коробки.
22
+
23
+ ### Changed
24
+ - Dev-контейнер переименован: образ `clockz-db` → **`letopis-db`**, контейнер
25
+ `clockz-timescale` → **`letopis-timescale`** (данные перенесены дампом, полигон
26
+ `v1.article` 1 112 952 строк цел).
27
+ - Шаблоны переехали `db/*.sql` → `lib/sql/`, докер-файлы `db/docker/` → `lib/docker/`;
28
+ `db/apply.mjs` остался CLI-обёрткой и читает шаблоны из `lib/sql/`.
29
+
30
+ ## [0.12.0] — 2026-07-11
31
+
32
+ ### Changed — BREAKING: шаблон SQL + версия движка в имени схемы
33
+ - **SQL-шаблоны (`db/ddl.sql`, сиды) держат маркер `"<SCHEMA-NAME>"`** вместо реального
34
+ имени: подстановка тотальная и безопасна by construction — jsonb-литералы (`'booking'`
35
+ в enum, ключи settings) совпасть с маркером не могут (старый text-replace по живому
36
+ имени уже кусался).
37
+ - **`db/apply.mjs`: обязательный `--version=N`** (целое ≥ 1, версия ДВИЖКА — бамп руками
38
+ при breaking-изменении DDL); итоговая PG-схема — **`vN.<имя>`**: `--schema=booking
39
+ --version=1` → `"v1.booking"` (точка в имени — валидный идентификатор в кавычках;
40
+ v1 и v2 живут в БД рядом). `--schema` принимает базовое имя без точек.
41
+ - **`connect({ schema })` принимает ПОЛНОЕ имя** (`'v1.booking'`) — либа префикс не
42
+ достраивает и о версиях не знает. Канал `pg_notify`/LISTEN = полное имя схемы
43
+ (`pg_notify('v1.booking', …)` — строковый литерал; LISTEN-идентификатор postgres.js
44
+ кавычит сам).
45
+ - Все тестовые/бенч-схемы переехали: `v1.booking`, `v1.article`, `v1.acl`, `v1.depth`,
46
+ `v1.plan`, `v1.wave4`, `v1.bench`; безверсионные дропнуты.
47
+
48
+ ## [0.11.0] — 2026-07-11
49
+
50
+ ### Changed — BREAKING: цепочка = план, операции = звенья
51
+ - **`.set(data?)` / `.delete(opts?)` / `.anonymize(fields)` возвращают ЦЕПОЧКУ** (Chain),
52
+ а не исполняются сами: операция применяется к шагу, к которому приклеена точкой;
53
+ **исполняет терминал** (`rows/first/ids/count/run/versions/агрегации`) — весь план
54
+ **одной транзакцией** (deny/валидация любого сегмента откатывает всё). Голый
55
+ `await …set(…)` без терминала больше НЕ пишет (цепочка не thenable). Миграция:
56
+ `await db.X(id).set({…})` → `await db.X(id).set({…}).rows()`.
57
+ - **Продолжение цепочки — от результата операции** (fan-out по строкам):
58
+ `db.Клиент({vip: true}).set({bonus: 500}).Запись().delete({confirm: true}).rows()` —
59
+ обновить всех vip и снести записи каждого. Повторная операция без шага — к тем же
60
+ строкам: `X(id).set({a}).set({b}).rows()` — две версии подряд.
61
+ - **`execute()` → `run()`** (пути; то же у батча: `batch.execute()` → `batch.run()`);
62
+ старые имена бросают понятную ошибку.
63
+ - **`delete({ confirm: true })` — удалить; БЕЗ confirm — превью**: терминал возвращает
64
+ кандидатов (цели + каскад), БД не тронута.
65
+ - **Пост-довесы связей удалены** (`set({}).Окно(w)`, z-форма, тип `SetChain`): имя класса
66
+ после операции — переход-шаг. Связи — контекст-шагами до операции; для значения связи
67
+ БЕЗ участия в фильтре целей — новый модификатор **`.link(Класс, target)`**:
68
+ `tr.Запись(b).позиция({}).link('Сотрудник', новый).set({}).rows()`.
69
+ - Ошибки записи (ValidationError, abstract, context step, acl) летят из терминала (async).
70
+ - В батче `await …set(…)` больше не возвращает номер очереди (план в очереди; Chain).
71
+ - Модификатор шага сразу после операции — ошибка `step modifier after set()`.
72
+
73
+ ## [0.10.0] — 2026-07-11
74
+
75
+ ### Added
76
+ - **ACL по Resource/Rule** (формат согласован повторно, закрыт последний гэп RESEARCH.md):
77
+ - `db.acl.check(account, endpoint)` — legacy-семантика 1:1: субъект-группы по
78
+ `Account.categories` (`{A,B}` / `!{A,B}` / NULL), маски эндпоинтов (`*`, `{a,b}`),
79
+ победа ровно одного правила (max weight, при равенстве deny), без правил — deny.
80
+ - **Операции над данными = категории ресурсов `READ`/`WRITE`/`DELETE`**; pattern —
81
+ **шаблон строки Entity**: реальные колонки (`class`, `owner`, `account`, `tags`,
82
+ `data`, `links`…) с литералами или `"$account"` (подставляется динамически);
83
+ `class`-маска действует на потомков (lineage). Никаких выдуманных полей.
84
+ - `db.acl.checkData(account, class, op)` → `{allow, filter?}` — filter = остаточный
85
+ шаблон строк победившего правила с подставленным `$account`.
86
+ - **`connect({ account, enforceAcl: true })`**: READ на каждый шаг цепочки, WRITE на
87
+ `set()`/anonymize/батчи, DELETE на цели и все классы каскадного замыкания (deny
88
+ откатывает транзакцию). **Предикат вливается в SQL до сортировки/лимита** — пагинация,
89
+ count, keyset честные. INSERT пришпиливает `$account`-колонки; upsert по явному id
90
+ НЕ перехватывает существующую недоступную сущность. `watch()` отдаёт события только
91
+ безусловных allow-классов. Deny-by-default; правила фиксируются на connect.
92
+ - `bench/acl.bench.mjs` (1.1M): безусловное правило — бесплатно; предикат в worst-case
93
+ (100% строк) +33–99% на full-scan; в реальности предикат сужает выборку — дешевле базы.
94
+ - `test/acl.test.ts` — 9 сцен; суммарно 114 тестов. Сид не менялся.
95
+
96
+ ## [0.9.0] — 2026-07-11
97
+
98
+ ### Added
99
+ - **OTP** в `db.auth` (node:crypto, без зависимостей):
100
+ - **TOTP** (RFC 6238, authenticator-приложения): `enrollTotp` (секрет base32 + otpauth-URI
101
+ для QR; re-enroll сбрасывает), `verifyTotp` (окно ±1 шаг, replay принятого шага отбит
102
+ через `meta.lastStep`, первая успешная проверка активирует фактор), `totpEnabled`;
103
+ хелпер `totpCode(secret, atMs?)` экспортирован (тесты/серверная генерация).
104
+ - **Одноразовые коды** (email/SMS/сброс — доставка на приложении): `issueOtp` (в БД
105
+ только sha256 + expires + attempts; повторный выпуск затирает старый код),
106
+ `verifyOtp` (успех сжигает код; 5 неудач сжигают; истёкший TTL сжигает).
107
+ - `test/auth.test.ts`: +2 сцены (TOTP: enroll/verify/replay/окно/re-enroll; OTP:
108
+ одноразовость/лимит/TTL/перевыпуск); суммарно 105 тестов.
109
+
110
+ ## [0.8.0] — 2026-07-11
111
+
112
+ ### Added
113
+ - **Устойчивость к сбоям** (§ 10.12 README):
114
+ - transient-ошибки PG (deadlock `40P01`, serialization `40001`) ретраятся там, где
115
+ повтор безопасен: чтения в автокоммите (до 3 попыток, backoff 40–160 ms), `insertOne`
116
+ (плюс прежний 23505), внутренние транзакции delete-каскада и батчей — повтор целиком;
117
+ - внутри `db.begin()` повтор невозможен (транзакция aborted) — ошибка уходит сразу
118
+ с подсказкой `retry the whole db.begin() block`; `tr.lock()` при deadlock — та же подсказка;
119
+ - ретрай 23505/transient в `insertOne` больше не срабатывает внутри транзакции
120
+ (раньше повтор в aborted-транзакции маскировал исходную ошибку кодом 25P02).
121
+ - **`watch(…, { onReconnect })`** — LISTEN-соединение и так переживает обрывы
122
+ (postgres.js: reconnect + повторный LISTEN), но NOTIFY за время разрыва потеряны;
123
+ `onReconnect` зовётся после каждого восстановления — точка дочитать пропущенное.
124
+ Типы `WatchEvent`/`WatchOpts` экспортированы.
125
+ - `test/resilience.test.ts` — юнит-ретраи, настоящий deadlock двух транзакций
126
+ (встречные `lock()`), обрыв LISTEN через `pg_terminate_backend` (reconnect ~0.4 s);
127
+ суммарно 103 теста.
128
+
129
+ ## [0.7.0] — 2026-07-10
130
+
131
+ ### Added
132
+ - **`db.auth`** — вход по таблице Credential, на аккаунт много способов:
133
+ - пароль (`setPassword`/`verifyPassword`) — scrypt из node:crypto (без зависимостей),
134
+ формат `scrypt$N$r$p$salt$hash`; время ответа выровнено dummy-verify;
135
+ - api-ключ (`issueApiKey`/`verifyApiKey`) — `lts_<48hex>` показывается один раз,
136
+ в БД только sha256 (legacy хранил открыто — исправлено);
137
+ - ключ-секрет (`issueKeySecret`/`verifyKeySecret`) — key открытый id, secret хэшем;
138
+ - внешние identity (`link`/`lookup`) — oauth/sso/telegram: токен проверяет приложение;
139
+ - все verify: кред жив + `confirmed` (отключаемо `requireConfirmed:false`) + аккаунт `enabled`.
140
+ - **Сессии в Redis**: `db.auth.sessions(store)` → `start/check/revoke/revokeAll`;
141
+ клиент инжектируется (интерфейс `SessionStore`, ioredis подходит как есть; в prod-зависимости
142
+ не входит); в Redis — sha256 токена, не сам токен.
143
+ - `credential_identity_udx` — один живой кред на `(category, identifier)` во всей схеме:
144
+ вход по identifier однозначен; мягко удалённый identifier освобождается.
145
+ - Dev-контейнер `db/docker/Dockerfile`: TimescaleDB + Redis в одном контейнере
146
+ (порты 15432/16379); CI — сервис redis:7.
147
+ - `test/auth.test.ts` — 7 сцен, включая живой Redis (expire/revoke); суммарно 98 тестов.
148
+
149
+ ### Fixed
150
+ - CI: артефакт пакета собирался по паттерну `entity-chain-*.tgz` и был пуст после
151
+ переименования пакета — теперь `letopis-*.tgz`.
152
+ - `bench/article-demo.mjs`: битый относительный импорт `./src/index.js` → `../src/index.js`.
153
+
154
+ ## [0.6.0] — 2026-07-10
155
+
156
+ ### Added
157
+ - **data-пути любой глубины** во всём API: containment-фильтры, операторы (включая
158
+ `exists`/`has*` на вложенных ключах), `.sort()`, `.after()`/`cursorOf()`,
159
+ `.sum/.avg/.min/.max/.countBy`. Тип листа (и SQL-каст) берётся из вложенных
160
+ `{type:'object', props:{…}}` схемы — новый `FieldType 'object'`.
161
+ - Пример в booking-схеме: `Org.settings.booking.deposit.{amount,currency}` (лист глубины 4),
162
+ строгая валидация и default-ы на каждом уровне; deep-merge обновляет один вложенный лист,
163
+ не трогая соседей (покрыто тестом).
164
+ - Наследование классов 3+ уровней покрыто тестом lineage (`Vehicle → Car → SportsCar`).
165
+ - `gen-types`: `{type:'object', props}` → вложенные TS-литералы.
166
+ - `test/depth.test.ts` — 5 сцен глубины; суммарно 91 тест.
167
+
168
+ ## [0.5.1] — 2026-07-10
169
+
170
+ ### Fixed
171
+ - `.sort('data.price.RUB')` — сортировка по вложенному record-пути молча давала ORDER BY NULL
172
+ (ключ с точкой вместо пути); теперь строится `data->'price'->>'RUB'` с кастом по типу листа.
173
+ Тот же путь понимают `.after()` и `cursorOf()`. Дефект пойман живым прогоном примеров статьи.
174
+ - `.min()/.max()` возвращали numeric строкой (`"800"`); для number-полей по Schema — число.
175
+
5
176
  ## [0.5.0] — 2026-07-10
6
177
 
7
178
  ### Changed