letopis 1.4.0 → 1.4.2
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/AGENT-CHEATSHEET.en.md +15 -7
- package/AGENT-CHEATSHEET.md +19 -11
- package/CHANGELOG.md +299 -1
- package/README.en.md +180 -62
- package/README.md +165 -55
- package/dist/acl.d.ts +11 -5
- package/dist/admin.d.ts +30 -14
- package/dist/admin.js +53 -18
- package/dist/auth.d.ts +61 -17
- package/dist/auth.js +16 -2
- package/dist/cache.d.ts +5 -1
- package/dist/cache.js +3 -2
- package/dist/chain.d.ts +92 -15
- package/dist/chain.js +56 -7
- package/dist/cli.js +16 -2
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +5 -5
- package/dist/import.js +7 -0
- package/dist/index.d.ts +73 -45
- package/dist/index.js +69 -14
- package/dist/internal.d.ts +7 -0
- package/dist/internal.js +5 -0
- package/dist/load.d.ts +10 -3
- package/dist/load.js +39 -11
- package/dist/mcp.d.ts +20 -11
- package/dist/mcp.js +8 -6
- package/dist/model.d.ts +4 -1
- package/dist/model.js +7 -1
- package/dist/ops.d.ts +58 -18
- package/dist/ops.js +58 -18
- package/dist/project.d.ts +39 -11
- package/dist/project.js +75 -15
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +4 -1
- package/dist/signals.d.ts +4 -4
- package/dist/signals.js +4 -4
- package/dist/sql.d.ts +8 -4
- package/dist/sql.js +110 -25
- package/dist/sync.js +11 -4
- package/dist/tx.d.ts +15 -0
- package/dist/tx.js +22 -4
- package/dist/types.d.ts +21 -7
- package/dist/types.js +13 -6
- package/dist/up.d.ts +48 -25
- package/dist/up.js +94 -41
- package/dist/validate.d.ts +9 -2
- package/dist/validate.js +19 -3
- package/dist/watch.d.ts +29 -5
- package/dist/watch.js +62 -13
- package/dist/write.d.ts +3 -2
- package/dist/write.js +52 -19
- package/package.json +8 -4
- package/sql/10-core.sql +6 -1
- package/sql/60-write.sql +47 -18
- package/sql/70-read.sql +13 -7
- package/sql/75-history.sql +106 -28
- package/sql/80-auth.sql +41 -3
- package/sql/88-derived.sql +3 -1
- package/sql/90-time.sql +118 -30
- package/sql/95-seed.system.sql +6 -3
- package/sql/99-revision.sql +1 -1
- package/sql/migrate/0007-secret-account-fk.sql +12 -0
package/AGENT-CHEATSHEET.en.md
CHANGED
|
@@ -32,7 +32,7 @@ const r = await up({ dsn: ADMIN, schema: 'myapp', // schema 'v2
|
|
|
32
32
|
tenant: { name: 'Salon', user: { name: 'Olga', login: 'olga@salon.ru', password: 'Lilac-2026!' } } })
|
|
33
33
|
// r: { schema, created, upgraded, revision, serviceKey, migrations, analyzed, tenant, … }
|
|
34
34
|
// r.serviceKey ('lts_…') is the service key; it is issued at the first installation (again only if the installation has
|
|
35
|
-
// no
|
|
35
|
+
// no API keys left except revoked ones). Save it.
|
|
36
36
|
// r.tenant: { tenant, user, token, created } — the tenant "Salon" and its first user Olga
|
|
37
37
|
```
|
|
38
38
|
- `up()` waits for the database, creates it, checks for PostgreSQL 18+ and UTF8, and installs the roles `letopis_owner`
|
|
@@ -82,10 +82,10 @@ db.tenant // the session tenant: by default the account's own tenant (h
|
|
|
82
82
|
allowBypassRls, models })`. `schema` is the **full** name with `v2.`.
|
|
83
83
|
- `db.auth`: `setPassword`, `verifyPassword`, `issueApiKey`, `verifyApiKey`, `issueKeySecret`, `verifyKeySecret`,
|
|
84
84
|
`enrollTotp`, `verifyTotp`, `totpEnabled`, `issueOtp`, `verifyOtp`, `link`, `lookup`, `credentials`, `revokeCredential`,
|
|
85
|
-
`setFlags`, `sessionFor(account, { ttl })`, `refresh`, `revoke`, `revokeAll`, `switch(tenant)`,
|
|
86
|
-
`purgeAccount`. Password, OTP and TOTP checks, issuing sessions
|
|
87
|
-
**service** (the `auth.*` permissions); a regular user gets `acl_denied`.
|
|
88
|
-
`issueApiKey({ account, name?, ttl? })` → `{ key, id }`; `verifyApiKey(key)` → `{ token, account } | null`;
|
|
85
|
+
`setFlags`, `sessionFor(account, { ttl })`, `session(token)`, `refresh`, `revoke`, `revokeAll`, `switch(tenant)`,
|
|
86
|
+
`tenants()`, `purgeAccount`. Password, OTP and TOTP checks, issuing sessions, session details by token and
|
|
87
|
+
confirming credentials are called by the **service** (the `auth.*` permissions); a regular user gets `acl_denied`.
|
|
88
|
+
Signatures: `issueApiKey({ account, name?, ttl? })` → `{ key, id }`; `verifyApiKey(key)` → `{ token, account } | null`;
|
|
89
89
|
`sessionFor(account, { ttl })` → a token string; then `connect({ dsn, schema, token })`, and to work in another
|
|
90
90
|
tenant through a membership, `db.auth.switch(tenant)` (without it, the session is in the account's own tenant). The
|
|
91
91
|
token for the CLI (`--token`) is the same `token` from `verifyPassword`/`verifyApiKey`/`sessionFor`. An admin
|
|
@@ -99,6 +99,11 @@ db.tenant // the session tenant: by default the account's own tenant (h
|
|
|
99
99
|
`issueKeySecret` with `account` set to the service id; `revokeCredential(id)`). Revoking a key revokes only the
|
|
100
100
|
sessions of that key, so rotation has no downtime: new key → processes onto it → revoke the old one. The last active
|
|
101
101
|
key of a service cannot be revoked (`acl_denied`); to disable a service, set `enabled: false` on its account.
|
|
102
|
+
- Session by token: `svc.auth.session(token)` → `{ account, tenant, member, expiresAt, loginAt, methods } | null`.
|
|
103
|
+
The service (the `auth.session` permission) learns whose session it is, which tenant it works in and whether the
|
|
104
|
+
membership there is intact, without connecting with the token and without extending the session. A wrong, revoked
|
|
105
|
+
or expired token and a disabled account give `null`. This is how a web service checks the session from a cookie at
|
|
106
|
+
the start of every request: reading under a revoked session gives no error, the answers are simply empty.
|
|
102
107
|
- Working in another tenant through a membership: `await user.auth.tenants()` → `[{ tenant, roles }]`, then
|
|
103
108
|
`await user.auth.switch(tenant)` (only from the connection root). Impersonation by a service:
|
|
104
109
|
`const u = await svc.as(account, { tenant, owner })` — the database checks it; in the journal, `author` is the user
|
|
@@ -189,6 +194,9 @@ await db.Wallet(or({ number: 'W-1' }, { number: 'W-200' })).ids()
|
|
|
189
194
|
`{ Shop: id }`, `{ Shop: null }`. Fields are in `row.data`, not at the root of the row.
|
|
190
195
|
- A step is a class name, an alias or an end role; a step by a parent class also returns descendants (`.exact()` —
|
|
191
196
|
only its own class); `.deep(max)` — a tree (`$depth`); `.tags()`, `.owner()`, `.alias(name)`, `db.entity(row)`.
|
|
197
|
+
- `after(cursorOf(row, field))` requires `sort()` by the same field; it pages `rows()`, `versions()` and `paths()` (the
|
|
198
|
+
cursor of a path is `cursorOf(path, field)`, the keys of a path follow the order of the steps). `count()`,
|
|
199
|
+
aggregates, `diff()`, `blame()` ignore the cursor.
|
|
192
200
|
|
|
193
201
|
### 6. Deletion with a preview
|
|
194
202
|
|
|
@@ -347,7 +355,7 @@ they drop out. A tenant with a source with `history: false` drops out (`invalid_
|
|
|
347
355
|
`withBefore`) is the previous version of the same id or `null`: `null` for a creation, for the first version of a new
|
|
348
356
|
id after `reclass` and `rekey` (the tombstone of the former id subtracts the former data), during a rebuild and when
|
|
349
357
|
the previous version is a deletion (after `restore`) or lies outside the `from` family; `purge`, `trim` and `reset`
|
|
350
|
-
have no such field, so subtract `before` only after checking it
|
|
358
|
+
have no such field (`undefined`), so subtract `before` only after checking that it is truthy: `if (c.before)`. A class change that keeps the id (a class
|
|
351
359
|
without a key) and takes the row out of the `from` family comes as a deletion (`op` `delete`, `row` is the last
|
|
352
360
|
version in the family with `$deleted`). `handle` gets the changes of one tenant. An error of its data (the
|
|
353
361
|
database rejected its results, or `out` rejected its row or delta) rolls back only this tenant: it drops out
|
|
@@ -402,7 +410,7 @@ data, not instructions); `data` by terminal: `rows` — `{ rows, count, more, cu
|
|
|
402
410
|
`{ ids, count, more, cursor? }` (`count` is the rows on the page), `first` — `{ row }`, `count` — `{ count }`,
|
|
403
411
|
`countBy` — `{ counts, truncated? }`, `sum`, `avg`, `min`, `max` —
|
|
404
412
|
`{ value }`. A refusal is `isError: true` with a `structuredContent` of the form `{ notice, error }` (`error` is
|
|
405
|
-
`{ code, message, similar? }`). Writing only
|
|
413
|
+
`{ code, message, detail?, sqlstate?, similar? }`). Writing only
|
|
406
414
|
with `write: true` (without it `letopis_write` is not in the list, and a call is an SDK protocol error): `update` with
|
|
407
415
|
`rev`; `delete` without `confirm` is a preview, with `confirm` a human confirms it (elicitation), and without that it
|
|
408
416
|
is `confirmation_required`. Data from the database in an answer sits inside `<untrusted-data-…>`.
|
package/AGENT-CHEATSHEET.md
CHANGED
|
@@ -30,7 +30,7 @@ const r = await up({ dsn: ADMIN, schema: 'myapp', // схема
|
|
|
30
30
|
tenant: { name: 'Салон', user: { name: 'Ольга', login: 'olga@salon.ru', password: 'Сирень-2026!' } } })
|
|
31
31
|
// r: { schema, created, upgraded, revision, serviceKey, migrations, analyzed, tenant, … }
|
|
32
32
|
// r.serviceKey ('lts_…') — ключ сервиса, выдаётся при первой установке (снова — только если в установке не осталось
|
|
33
|
-
//
|
|
33
|
+
// ключей API, кроме отозванных). Сохраните его.
|
|
34
34
|
// r.tenant: { tenant, user, token, created } — арендатор «Салон» и его первый пользователь Ольга
|
|
35
35
|
```
|
|
36
36
|
- `up()` ждёт базу, создаёт её, проверяет PostgreSQL 18+ и UTF8, ставит роли `letopis_owner` и `letopis_app`.
|
|
@@ -77,14 +77,14 @@ db.tenant // арендатор сессии: по умолчанию
|
|
|
77
77
|
allowBypassRls, models })`. `schema` — **полное** имя с `v2.`.
|
|
78
78
|
- `db.auth`: `setPassword`, `verifyPassword`, `issueApiKey`, `verifyApiKey`, `issueKeySecret`, `verifyKeySecret`,
|
|
79
79
|
`enrollTotp`, `verifyTotp`, `totpEnabled`, `issueOtp`, `verifyOtp`, `link`, `lookup`, `credentials`, `revokeCredential`,
|
|
80
|
-
`setFlags`, `sessionFor(account, { ttl })`, `refresh`, `revoke`, `revokeAll`, `switch(tenant)`,
|
|
81
|
-
`purgeAccount`. Проверку паролей, OTP и TOTP, выпуск
|
|
82
|
-
`auth.*`); обычному пользователю — `acl_denied`. Сигнатуры:
|
|
83
|
-
`{ key, id }`; `verifyApiKey(key)` → `{ token, account } | null`;
|
|
84
|
-
дальше `connect({ dsn, schema, token })`, а для работы в чужом
|
|
85
|
-
(без него сессия — в собственном арендаторе аккаунта). Токен для
|
|
86
|
-
`verifyPassword`/`verifyApiKey`/`sessionFor`. Администраторское подключение
|
|
87
|
-
предупреждение «RLS не действует» — так и должно быть.
|
|
80
|
+
`setFlags`, `sessionFor(account, { ttl })`, `session(token)`, `refresh`, `revoke`, `revokeAll`, `switch(tenant)`,
|
|
81
|
+
`tenants()`, `purgeAccount`. Проверку паролей, OTP и TOTP, выпуск сессий, сведения о сессии по токену и
|
|
82
|
+
подтверждение кредов вызывает **сервис** (права `auth.*`); обычному пользователю — `acl_denied`. Сигнатуры:
|
|
83
|
+
`issueApiKey({ account, name?, ttl? })` → `{ key, id }`; `verifyApiKey(key)` → `{ token, account } | null`;
|
|
84
|
+
`sessionFor(account, { ttl })` → строка-токен — дальше `connect({ dsn, schema, token })`, а для работы в чужом
|
|
85
|
+
арендаторе по членству — `db.auth.switch(арендатор)` (без него сессия — в собственном арендаторе аккаунта). Токен для
|
|
86
|
+
CLI (`--token`) — тот же `token` из `verifyPassword`/`verifyApiKey`/`sessionFor`. Администраторское подключение
|
|
87
|
+
(`allowBypassRls`) печатает предупреждение «RLS не действует» — так и должно быть.
|
|
88
88
|
- Второй фактор: `verifyPassword({ identifier, password, totp })` и `lookup({ kind, identifier, totp })` после первой
|
|
89
89
|
проверки сверяют код TOTP (право `auth.totp`, счётчик неудач и блокировка — общие с `verifyTotp`) → сессия со
|
|
90
90
|
способами `[вид, 'TOTP']`, по ней пользователь с TOTP меняет свои креды сам. Неверный или повторный код и код у
|
|
@@ -93,6 +93,11 @@ db.tenant // арендатор сессии: по умолчанию
|
|
|
93
93
|
`issueKeySecret` с `account` — id сервиса; `revokeCredential(id)`). Отзыв ключа гасит только сессии этого ключа —
|
|
94
94
|
ротация без простоя: новый ключ → процессы на него → отзыв старого. Последний действующий ключ сервиса не
|
|
95
95
|
отзывается (`acl_denied`); отключить сервис — `enabled: false` у его аккаунта.
|
|
96
|
+
- Сессия по токену: `svc.auth.session(token)` → `{ account, tenant, member, expiresAt, loginAt, methods } | null`.
|
|
97
|
+
Сервис (право `auth.session`) узнаёт, чья сессия, в каком арендаторе она работает и цело ли там членство, не
|
|
98
|
+
подключаясь по токену и не продлевая сессию. Неверный, погашенный или истёкший токен и отключённый аккаунт —
|
|
99
|
+
`null`. Так веб-сервис проверяет сессию из cookie в начале каждого запроса: чтение по погашенной сессии ошибки не
|
|
100
|
+
даёт, ответы просто пустые.
|
|
96
101
|
- Работа в чужом арендаторе по членству: `await user.auth.tenants()` → `[{ tenant, roles }]`, затем
|
|
97
102
|
`await user.auth.switch(tenant)` (только от корня подключения). Имперсонация сервисом:
|
|
98
103
|
`const u = await svc.as(account, { tenant, owner })` — проверяет база, в журнале `author` — пользователь,
|
|
@@ -182,6 +187,9 @@ await db.Wallet(or({ number: 'W-1' }, { number: 'W-200' })).ids()
|
|
|
182
187
|
`{ Shop: null }`. Поля — в `row.data`, не в корне строки.
|
|
183
188
|
- Шаг — имя класса, алиас или роль конца; шаг по родителю отдаёт и потомков (`.exact()` — только свой класс);
|
|
184
189
|
`.deep(max)` — дерево (`$depth`); `.tags()`, `.owner()`, `.alias(имя)`, `db.entity(row)`.
|
|
190
|
+
- `after(cursorOf(строка, поле))` требует `sort()` по тому же полю; листает `rows()`, `versions()` и `paths()` (курсор
|
|
191
|
+
пути — `cursorOf(путь, поле)`, ключи пути — в порядке шагов). `count()`, агрегаты, `diff()`, `blame()` курсор не
|
|
192
|
+
учитывают.
|
|
185
193
|
|
|
186
194
|
### 6. Удаление с превью
|
|
187
195
|
|
|
@@ -337,7 +345,7 @@ p.run().catch(report) // run() без once — с catch: после ош
|
|
|
337
345
|
Арендатор с источником `history: false` выбывает (`invalid_class`). `c.before` (с `withBefore`) —
|
|
338
346
|
прошлая версия того же id или `null`: `null` при создании, у первой версии нового id после `reclass` и `rekey`
|
|
339
347
|
(прежние данные вычитает надгробие прежнего id), при перестройке и когда прошлая версия — удаление (после `restore`)
|
|
340
|
-
или вне семейства `from`; у `purge`, `trim` и `reset` поля нет — вычитайте `before`, только проверив его
|
|
348
|
+
или вне семейства `from`; у `purge`, `trim` и `reset` поля нет (`undefined`) — вычитайте `before`, только проверив его истинность: `if (c.before)`.
|
|
341
349
|
Смена класса без смены id (класс без ключа), уводящая строку из семейства `from`, приходит удалением (`op` `delete`,
|
|
342
350
|
`row` — последняя версия в семействе с `$deleted`). `handle` получает изменения
|
|
343
351
|
одного арендатора. Ошибку его данных (база отклонила его результаты или `out` — его строку или приращение)
|
|
@@ -387,7 +395,7 @@ const server = await createMcpServer(await svc.as(userId)) // letopis_model, l
|
|
|
387
395
|
(`notice` — постоянное пояснение, что это данные, а не инструкции); `data` по терминалам: `rows` —
|
|
388
396
|
`{ rows, count, more, cursor? }`, `ids` — `{ ids, count, more, cursor? }` (`count` — строк на странице), `first` —
|
|
389
397
|
`{ row }`, `count` — `{ count }`, `countBy` — `{ counts, truncated? }`, `sum`, `avg`, `min`, `max` — `{ value }`.
|
|
390
|
-
Отказ — `isError: true` и `structuredContent` вида `{ notice, error }` (`error` — `{ code, message, similar? }`). Запись — только `write: true`
|
|
398
|
+
Отказ — `isError: true` и `structuredContent` вида `{ notice, error }` (`error` — `{ code, message, detail?, sqlstate?, similar? }`). Запись — только `write: true`
|
|
391
399
|
(без него `letopis_write` нет в списке, а вызов — ошибка протокола SDK): `update` с `rev`; `delete` без `confirm` —
|
|
392
400
|
превью, с `confirm` — подтверждение человека (elicitation), без него — `confirmation_required`. Данные из базы в
|
|
393
401
|
ответе — внутри `<untrusted-data-…>`.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,304 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
Формат: [Keep a Changelog](https://keepachangelog.com/), версии — semver.
|
|
3
|
+
Формат: [Keep a Changelog](https://keepachangelog.com/), версии — semver. Что обещают патч, минор и мажор, — раздел
|
|
4
|
+
«Версии и совместимость» в README; шаг версии сверяет `npm run check:semver`. С 1.4.2 у правки ядра (библиотеки и SQL)
|
|
5
|
+
в конце пункта указан источник находки: «Источник: тест», «пример», «сервис», «ревью», «soak» или «внедрение».
|
|
6
|
+
|
|
7
|
+
## [1.4.2] — 2026-11-01
|
|
8
|
+
|
|
9
|
+
План 2.0 (`reports/PLAN-LETOPIS-2.0.md`): правила версий, проверка перед выпуском, исправления.
|
|
10
|
+
|
|
11
|
+
### Добавлено
|
|
12
|
+
- Правила версий — подраздел «Версии и совместимость» в README (обе версии): патч — только исправления без новой
|
|
13
|
+
ревизии движка и без новых отказов; минор — новые возможности и новая ревизия с `up({ upgrade: true })`; мажор —
|
|
14
|
+
удаление, переименование, смена сигнатур и новые отказы.
|
|
15
|
+
- `npm run check:semver` (`scripts/check-semver.mjs`): контракт API и ревизия движка текущего дерева против тега
|
|
16
|
+
прошлого выпуска — исчезло → мажор, появилось или выросла ревизия → минор, тексты ошибок → патч; шаг версии обязан
|
|
17
|
+
быть не меньше. Входит в `check:docs` (в CI checkout с полной историей) и в `release.yml` до публикации. Сборка
|
|
18
|
+
контракта вынесена в `scripts/contract.mjs`; в `docs/api-contract.json` появилось поле `codes` — коды ошибок.
|
|
19
|
+
- `npm run prerelease` (`scripts/prerelease.mjs`) — полная локальная проверка одной командой, строго по очереди:
|
|
20
|
+
typecheck, сверка документов, весь `npm test`, примеры сайта со снимками, ER-схемы, покрытие, сборка сайта,
|
|
21
|
+
песочница, сценарии четырёх сервисов, проверка пакета с живым прогоном. Первый красный шаг останавливает прогон;
|
|
22
|
+
итог — таблица в `docs/checkpoint-<x.y.z>.md`. Тесты в CI не гоняются (решение владельца 01.11.2026).
|
|
23
|
+
- Долгий прогон `npm run soak` (`bench/soak.mjs`, план 2.0, этап 2): смешанная нагрузка одного процесса на часы —
|
|
24
|
+
писатели с агрегатом и `inc`, читатели, подписки, проектор, `maintain()` с прореживанием под записью, вход и выход,
|
|
25
|
+
`db.as()`, заведение и удаление классов; замер раз в 5 минут, в конце — критерии §7.7 и `verify({ data: true })`.
|
|
26
|
+
Быстрый режим на 15 минут — шаг `soak` в `npm run prerelease`.
|
|
27
|
+
- Метрика стабилизации: у правки ядра в CHANGELOG — источник находки, `release-notes.mjs` считает их в теле релиза.
|
|
28
|
+
- Тест `upgrade` №3: накат схемы 1.4.1 (dev-зависимость `letopis-141`) обычным `up()` без миграций; правило — каждый
|
|
29
|
+
выпуск проверяет накат с предыдущего. Тесты `semver` и `prerelease` проверяют сами сценарии (план 2.0, 7.8).
|
|
30
|
+
- `CONTRIBUTING.md` (и английская версия), шаблон issue об ошибке, форма отчёта о внедрении
|
|
31
|
+
`docs/adoption/TEMPLATE.md`.
|
|
32
|
+
|
|
33
|
+
### Исправлено
|
|
34
|
+
- Строки-замки удалённых классов не убирались: запись строки `Class` заводит две строки `LOCK` в `secret` (замок
|
|
35
|
+
семейства и его шлюз), а удаление класса их оставляло — приложение, которое заводит и удаляет классы на лету, копило
|
|
36
|
+
их без предела. Теперь `maintain()` убирает строки-замки семейств, чей корневой класс удалён (или удалён его
|
|
37
|
+
арендатор); строку, которую держит транзакция, — следующим вызовом; класс с тем же именем заводит их заново
|
|
38
|
+
(7.7 №8). Источник: soak.
|
|
39
|
+
- `maintain()` мог безвозвратно стереть историю: если класс System заводили позже одноимённого класса арендатора,
|
|
40
|
+
строки журнала арендатора попадали в отбор дважды, и промежуточные версии ярусов стирались. Теперь политику выбирают
|
|
41
|
+
прореживание, `verify` и срок курсора подписки так же, как класс строки: свой класс арендатора, иначе System
|
|
42
|
+
(план 2.0, 7.3 №1). Источник: ревью.
|
|
43
|
+
- История удалённого объекта стирается целиком и тогда, когда до `reclass` он был в классе без политики: версии
|
|
44
|
+
прежнего класса больше не остаются и не показывают объект живым в срезе (7.3 №2). Источник: ревью.
|
|
45
|
+
- Стирание истории удалённого ждёт `restore()`, начатый в другой транзакции, и после него историю не трогает: раньше
|
|
46
|
+
цепочка восстановленного объекта начиналась не с версии 1 (7.3 №3). Источник: ревью.
|
|
47
|
+
- `maintain()` чистит истёкшие сессии и коды один раз за вызов, в первой пачке, а не в каждой из пачек (до 100):
|
|
48
|
+
у функции базы `maintain` третий аргумент `p_cleanup`; схема, которую ещё не обновил `up()`, получает прежний
|
|
49
|
+
вызов (7.3 №4). Источник: ревью.
|
|
50
|
+
|
|
51
|
+
- Два `create` одного ключа в батче проходили молча: обе строки ответа были одной и той же строкой. Теперь повтор —
|
|
52
|
+
`exists`, и батч откатывается целиком (7.2 №1). Источник: ревью.
|
|
53
|
+
- Строки ответа `reclass`, `restore` и `rekey` — в порядке целей (`sort()` до глагола, иначе id), как обещает README;
|
|
54
|
+
в 1.4.1 так стало только у `update` и `anonymize` (7.2 №2). Источник: ревью.
|
|
55
|
+
- Цепочка с глаголом и читающим хвостом в значении слота (`.Cat.add(db.Cat().create(…).Parent())`) падала с
|
|
56
|
+
`TypeError`; вложенный план не проходил проверки до базы. Теперь работает, а модификатор сразу после её глагола —
|
|
57
|
+
`invalid_query`, как у терминала (7.2 №3). Источник: ревью.
|
|
58
|
+
- Кэш результатов отдавал общие объекты: правка `rows[0].data` у ответа `.cache()` портила следующие ответы. Теперь
|
|
59
|
+
кэш хранит и отдаёт копии (7.2 №5). Источник: ревью.
|
|
60
|
+
|
|
61
|
+
- Подписка без событий опрашивала журнал сериями по 40 коротких повторов на каждый тик таймера — около 4 запросов в
|
|
62
|
+
секунду вместо одного за `pollMs`. Теперь короткие повторы идут только после сигнала (7.4 №1). Источник: ревью.
|
|
63
|
+
- `close()` подключения не закрывал живые подписки: с чужим пулом (`connect({ sql })`) они опрашивали журнал и после
|
|
64
|
+
`close()`. Теперь подписки закрываются, ожидающий `next()` получает `done: true` (7.4 №8). Источник: ревью.
|
|
65
|
+
|
|
66
|
+
- `reclass` класса без ключа (id тот же) сигналил только новый класс: ответы кэша и подписки прежнего класса не
|
|
67
|
+
сбрасывались и отдавали строку, которая из него ушла. Теперь сигнал идёт по обоим классам, как у пути через
|
|
68
|
+
триггер (7.4 №4). Источник: ревью.
|
|
69
|
+
|
|
70
|
+
- `letopis types` падал на кириллическом имени класса (`invalid_class`, хотя такой идентификатор в TypeScript допустим),
|
|
71
|
+
а для класса без полей, класса с `anyOf`/`oneOf` и открытой схемы выдавал неверный TypeScript
|
|
72
|
+
(`export interface XData Record<string, unknown>`). Теперь имена — буквы Unicode, не-объекты — `export type`, ветки
|
|
73
|
+
`anyOf` из одних `required` у класса с полями — условие, а не форма (7.5 №1). Источник: ревью.
|
|
74
|
+
- `hub('X', { fields: z.object(…) })` без других ключей описания читался как короткая форма — класс с единственным
|
|
75
|
+
полем `fields`. Теперь это полная форма (7.5 №2). Источник: ревью.
|
|
76
|
+
- Понятные ошибки вместо сырых: `connect({ owner })` с ролью, которой нет, — `invalid_data`; `anonymize(['опечатка'])` —
|
|
77
|
+
«поля нет в классе»; `up()` на готовой роли владельца без ADMIN OPTION — объяснение, что нужно; `auth.switch` при
|
|
78
|
+
имперсонации сырым SQL — `acl_denied` из каталога, а не неизвестный код; `--trust` — `LetopisError` (7.5 №3).
|
|
79
|
+
Источник: ревью.
|
|
80
|
+
- CLI: `letopis <команда> --help` возвращает код 0, неизвестная команда — справку с кодом 2 ещё до подключения к базе
|
|
81
|
+
(7.5 №4). Источник: ревью.
|
|
82
|
+
- Сравнение имени проектора в `88-derived.sql` — той же коллацией, что индекс `secret_kind_ident` (7.5 №5). Источник:
|
|
83
|
+
ревью.
|
|
84
|
+
|
|
85
|
+
- Срез `asOf` по классу и по концам показывал объект в классе, из которого он уже ушёл (`reclass` класса без ключа
|
|
86
|
+
сохраняет id): последняя версия выбиралась среди версий классов шага. Теперь она выбирается среди всех версий id, и
|
|
87
|
+
класс проверяется у неё — как уже делала ветка по id; то же в `trimHistory` по классам (7.1 №1). Источник: ревью.
|
|
88
|
+
- `after()` в `versions()` молча не действовал: страница «после курсора» повторяла первую. Теперь курсор листает версии
|
|
89
|
+
в порядке `sort()`. Курсор версии несёт её `rev` (`cursorOf` кладёт его сам), поэтому версии одного объекта с равным
|
|
90
|
+
полем сортировки не теряются. Без `sort()` — `invalid_query`, как у `rows()`. `count()`, агрегаты, `diff()` и
|
|
91
|
+
`blame()` курсор не учитывают, README теперь это описывает (7.1 №4). Источник: ревью.
|
|
92
|
+
- `paths()` с `after()` терял пути с общим последним узлом: курсор помнил только этот узел, и следующая страница
|
|
93
|
+
начиналась после всех путей к нему. Теперь `cursorOf(путь, поле)` кладёт в курсор id всех узлов (`path`), а пути
|
|
94
|
+
упорядочены по ним после поля и последнего узла. Ключи объекта пути идут в порядке шагов, как в примере README;
|
|
95
|
+
прежде они шли в порядке jsonb — по длине имени (7.1 №3). Источник: ревью.
|
|
96
|
+
- `versions({ follow: true })`, `diff({ follow: true })` и `blame()` после `rekey` смешивали историю перенесённого
|
|
97
|
+
объекта с объектом, которого потом завели на прежнем id (тот же ключ): брались все версии всех связанных id.
|
|
98
|
+
Теперь история идёт по отрезкам между переносами — надгробие переноса кончает отрезок, первая версия на новом id
|
|
99
|
+
начинает (функция базы `history_segments`, обычный `up()`); у нового объекта своя история с его создания (7.1 №2).
|
|
100
|
+
Источник: ревью.
|
|
101
|
+
- `blame()` на каждом новом соединении первые пять вызовов шли с квадратичной сложностью: свой план функции базы
|
|
102
|
+
`version_paths` по фильтру видимости оценивал версии в 2 строки и искал предыдущую версию по одному id, проверяя
|
|
103
|
+
номер фильтром, — около 250 мс на 1000 версиях и секунды на 10 000. Теперь обе версии ищутся по `(id, rev)` на
|
|
104
|
+
каждую пару: 21 мс с первого вызова (`bench/results/history.json`; ошибку нашёл замер `bench/history.bench.mjs`
|
|
105
|
+
при проверке 7.1 №2). Источник: тест.
|
|
106
|
+
- Проектор не строил данных арендатора, который стал участником после первой проекции: изменения шли только с
|
|
107
|
+
курсора, строки, заведённые до вступления, в проекцию не попадали. Теперь чекпоинт хранит состав участников
|
|
108
|
+
(`tenants`), и новый участник получает начальную перестройку пачками (7.4 №5). Источник: ревью.
|
|
109
|
+
- Проектор на подключении без LISTEN не перечитывал состав участников: сигнала о новых классах нет, и арендатор,
|
|
110
|
+
вступивший после запуска, не участвовал вовсе. Теперь состав читается в начале `run()` и не реже раза в 5 с (7.4 №6).
|
|
111
|
+
Источник: ревью.
|
|
112
|
+
- Реестры классов `db.as()` копились без предела: процесс, который ходит от имени многих арендаторов, держал реестр
|
|
113
|
+
каждого и перечитывал их все на каждый сигнал классов. Теперь их не больше 1000 на подключение, давно не нужный
|
|
114
|
+
вытесняется; фасад с вытесненным реестром работает — реестр возвращается и перечитывается (7.4 №8). Источник: ревью.
|
|
115
|
+
- Загрузчик с `existing: 'update'` писал в строку другого класса, если у неё тот же id: объект класса без ключа после
|
|
116
|
+
`reclass` сохраняет id, и повторный импорт по `$ext` правил его данными, проверенными по схеме прежнего класса, мимо
|
|
117
|
+
триггеров. Теперь такая строка — `exists` с классом объекта в тексте (7.2 №4). Источник: ревью.
|
|
118
|
+
|
|
119
|
+
- Имя поля, в котором есть `@S@` (например, `a@S@b`), портилось в запросе цепочки: построитель подставлял на это место
|
|
120
|
+
имя схемы, и фильтр, `sort()` или агрегат по такому полю ничего не находили (7.1 №5). Источник: ревью.
|
|
121
|
+
|
|
122
|
+
- После обрыва соединения транзакция проектора, `db.begin()` при открытии и загрузчик слали `rollback` в мёртвое
|
|
123
|
+
соединение и возвращали его в пул — postgres.js 3.4 ронял процесс необработанным `TypeError`. Теперь, как в
|
|
124
|
+
`runTx`: после обрыва в соединение не пишут и в пул его не возвращают, промис отклоняется (7.4 №7). Источник: ревью.
|
|
125
|
+
|
|
126
|
+
- `watch({ from: 'start' })` после первой полной пачки событий старше яруса `all` падал с `cursor_expired`, хотя
|
|
127
|
+
обещан весь журнал. Теперь, пока подписка не догнала журнал, свои чтения она шлёт без срока курсора; курсоры
|
|
128
|
+
событий и `sub.cursor` время хранят, и продолжение с них после прореживания — по-прежнему `cursor_expired` (7.4 №3).
|
|
129
|
+
Источник: ревью.
|
|
130
|
+
|
|
131
|
+
- Подписка на класс не получала события его потомков, заведённых после `watch()`: список потомков фиксировался при
|
|
132
|
+
подписке, и курсор уходил за их события. Теперь классы с потомками берутся из текущего реестра (7.4 №2).
|
|
133
|
+
Источник: ревью.
|
|
134
|
+
|
|
135
|
+
### Изменено
|
|
136
|
+
- CI: у задания `test` нет сервиса PostgreSQL (живой прогон пакета в CI выключен, база не нужна), комментарии
|
|
137
|
+
`ci.yml` описывают то, что задания делают.
|
|
138
|
+
|
|
139
|
+
## [1.4.1] — 2026-11-01
|
|
140
|
+
|
|
141
|
+
Исправления, найденные прогоном примеров документации для людей (`guide/`).
|
|
142
|
+
|
|
143
|
+
По правилам версий, введённым в 1.4.2, часть изменений этого выпуска требовала минора (ревизия движка 7 с миграцией
|
|
144
|
+
`0007`, новый метод `auth.session`), а новые отказы (`invalid_query` для модификатора после глагола, `acl_denied` для
|
|
145
|
+
удаления `Account` System, код 1 у `letopis types` и `letopis describe` без сессии) и убранный код `not_implemented` —
|
|
146
|
+
мажора.
|
|
147
|
+
|
|
148
|
+
### Добавлено
|
|
149
|
+
- `db.auth.session(token)` — сведения о сессии по её токену для сервиса: чья сессия (`account`), в каком арендаторе
|
|
150
|
+
она работает (`tenant`), цело ли там членство (`member`; в собственном арендаторе — всегда `true`), когда она
|
|
151
|
+
истечёт (`expiresAt`), когда и как был вход (`loginAt`, `methods`). Подключение по этому токену не открывается,
|
|
152
|
+
срок сессии не продлевается. Неверный, погашенный или истёкший токен и отключённый аккаунт — `null`, причина не
|
|
153
|
+
различается. Вызывает сервис или системный администратор (право `auth.session`, как у `sessionFor`); пользователю
|
|
154
|
+
отвечают `acl_denied`, вызову без сессии — `no_session`. В базе — функция владельца `auth_session_info(text)`, в
|
|
155
|
+
пакете — новый тип `SessionInfo`; в схеме метод появляется после повторного `up()`. Пробел нашли сервисы-примеры
|
|
156
|
+
сайта (салон, helpdesk). Helpdesk теперь проверяет сессию в начале каждого запроса через `svc.auth.session(token)`,
|
|
157
|
+
а не вызовом `auth.tenants()` по подключению пользователя.
|
|
158
|
+
|
|
159
|
+
### Исправлено
|
|
160
|
+
- `db.auth.purgeAccount(арендатор)` из сессии, которая работает в этом же арендаторе, откатывался с
|
|
161
|
+
`tenant_denied` («нет членства в арендаторе»): функция стирала членство вызывающего вместе с данными, а
|
|
162
|
+
надгробия и передача владения дальше перечитывали контекст. Теперь после проверки прав остаток операции идёт в
|
|
163
|
+
личном арендаторе вызывающего, его сессия больше не указывает на стёртого арендатора, а корень подключения
|
|
164
|
+
перечитывает арендатора и реестр. Подтверждает пример `guide/examples/auth/02-sessions-tenants.ts` (шаг 8):
|
|
165
|
+
без исправления он падает.
|
|
166
|
+
- `commit()` явной транзакции (`db.begin()`), в которой ошибку поймали и проглотили, бросал сырую ошибку
|
|
167
|
+
PostgreSQL `25P02` вместо обещанного README `tx_rolled_back`: чтение `letopis.changed` в одном конвейере с
|
|
168
|
+
COMMIT падало раньше, чем COMMIT отвечал ROLLBACK. Теперь на `25P02` библиотека дожидается ответа COMMIT и
|
|
169
|
+
бросает `tx_rolled_back`. Подтверждает пример `guide/examples/errors/01-api-errors.ts` (шаг 7): без исправления
|
|
170
|
+
он падает с `25P02`.
|
|
171
|
+
- Порядок надгробий одного уровня каскада в журнале (`seq`, время `at`) и строк итога
|
|
172
|
+
`delete({ confirm: true })` зависел от плана запроса и менялся от прогона к прогону: триггер удаления писал
|
|
173
|
+
надгробия вставкой без `order by`, а превью при этом шло по id. Теперь надгробия каждого уровня пишутся в порядке
|
|
174
|
+
id, итог — по уровням каскада, внутри уровня по id (README, раздел 7); цели классов без журнала — в конце, тоже по
|
|
175
|
+
id. Подтверждают примеры `guide/examples/delete/01-delete-cascade.ts` и `02-restore-purge.ts`: без исправления их
|
|
176
|
+
снимки то и дело расходятся порядком строк заказа.
|
|
177
|
+
|
|
178
|
+
- Порядок записей журнала у остальных массовых операций тоже определён, а не зависит от плана запроса: пакетная
|
|
179
|
+
вставка — в порядке входа, правка многих строк, перестройка проектора, загрузчик с `existing: 'update'` — по id;
|
|
180
|
+
смена владельца при выходе из членства и `purgeAccount` идут по арендаторам по порядку; глаголы по списку целей без
|
|
181
|
+
`sort` (`restore`, `purge`, `reclass`, `rekey`) обходят цели по id.
|
|
182
|
+
- Подписка, к которой больше суток (яруса `all` политики хранения её классов) не было событий, получала ложный
|
|
183
|
+
`cursor_expired`, хотя ничего не пропустила: время курсора оставалось временем последнего события, а база проверяет
|
|
184
|
+
его на каждом опросе. Теперь подписка, догнав журнал, ставит в свой курсор время опроса — «сейчас», но не позже самой
|
|
185
|
+
старой записи её классов, которая уже зафиксирована, а горизонт ещё не прошла; курсор события `ev.cursor` по-прежнему
|
|
186
|
+
хранит время события. `sub.cursor` больше не обгоняет события, прочитанные в запас: он стоит после последнего
|
|
187
|
+
события, которое отдал `next()`, поэтому продолжать после перезапуска нужно с него (README, раздел 12 «Подписка»).
|
|
188
|
+
Подтверждает пример `guide/examples/watch/02-quiet-cursor.ts`: без исправления шаг 3 падает с `cursor_expired`.
|
|
189
|
+
- `connect({ maintain })` не проверял интервал: ноль, отрицательное число, `NaN`, `Infinity` или интервал длиннее
|
|
190
|
+
2³¹−1 мс (`'25d'`, `'30d'`) таймер Node.js превращал в 1 мс, и обслуживание шло без перерыва, а `null` и `true`
|
|
191
|
+
давали `TypeError`. Теперь интервал — число или строка, конечный, от 1 мс до 2³¹−1 мс (около 24 суток), иначе
|
|
192
|
+
`invalid_data`; проверка идёт до открытия пула и таймеров. `close()` ждёт идущий по расписанию запуск: он доделывает
|
|
193
|
+
текущую пачку и следующую не начинает, а пул закрывается после него. Подтверждает пример
|
|
194
|
+
`guide/examples/install/03-connect-options.ts` (шаг 6): без исправления подключения с такими интервалами открывались.
|
|
195
|
+
- `createTenant` и `up({ tenant })` принимали любой `user.kind`: вид в другом регистре (`password`), пустой или
|
|
196
|
+
служебный (`SESSION`, `TOTP` …). По такому паролю нельзя было войти, а служебным видом пароль выдавал себя за другой
|
|
197
|
+
кред: с видом `TOTP` аккаунт выглядел так, будто у него включён второй фактор (`totpEnabled: true`), с видом
|
|
198
|
+
`SESSION` пароль пропадал из `auth.credentials()`. Теперь вид проверяется по правилу функций кредов (`^[A-Z][A-Z0-9_]{1,31}$`, не `SESSION`, `APIKEY`,
|
|
199
|
+
`KEYSECRET`, `OTP`, `TOTP`, `KEY`, `PROJECTOR`, `LOCK`), иначе `invalid_data` до записи. Подтверждает пример
|
|
200
|
+
`guide/examples/install/01-up-and-connect.ts` (шаг 4): без исправления арендатор создавался.
|
|
201
|
+
- Параллельные `up()` разных схем одной базы падали с `tuple concurrently updated` (`XX000`): общую часть
|
|
202
|
+
установки — роли `letopis_owner` и `letopis_app`, членство установщика, pgcrypto и право владельца на схему pgcrypto —
|
|
203
|
+
каждый `up()` менял одновременно с другими, а `grant usage on schema` переписывает строку схемы в каталоге, даже если
|
|
204
|
+
право уже выдано. На чистой базе так же столкнулись бы `create role` и `create extension`. Теперь общая часть — одна
|
|
205
|
+
транзакция под advisory-блокировкой: следующий `up()` ждёт, пока предыдущий её зафиксирует. Блокировка действует в
|
|
206
|
+
пределах базы, а роли общие для кластера, поэтому роль или членство, которые одновременно создал `up()` в другой
|
|
207
|
+
базе, считаются уже созданными. Нашёл стенд сайта (`services/compose.yaml`): helpdesk падал при одновременном
|
|
208
|
+
запуске четырёх сервисов; без исправления 4 из 8 параллельных `up()` падали в каждом круге.
|
|
209
|
+
- Одновременные `up()` одной и той же схемы (несколько экземпляров сервиса при запуске) падали: второй
|
|
210
|
+
`create schema` — на уникальном индексе каталога (`23505`), второй `create or replace function` или `comment` —
|
|
211
|
+
`tuple concurrently updated` (`XX000`), а `up({ tenant })` дважды вставлял того же арендатора. Ревизию, от которой
|
|
212
|
+
считаются миграции, `up()` читал до наката, поэтому второй `upgrade` повторил бы миграции, уже накатанные первым.
|
|
213
|
+
Теперь накат схемы — `fresh`, чтение ревизии, `create schema`, миграции, ядро, сиды и ключ сервиса — одна транзакция
|
|
214
|
+
под advisory-блокировкой схемы (ключ — её имя), и `createTenant` берёт ту же блокировку. Следующий `up()` ждёт
|
|
215
|
+
предыдущий и делает повторный накат по готовой схеме: `created` и `upgraded` он считает по ревизии, которую увидел
|
|
216
|
+
под блокировкой. `up()` разных схем друг друга не ждут. Подтверждает пример
|
|
217
|
+
`guide/examples/install/02-up-options.ts` (шаг 6): без исправления он падал 5 раз из 5.
|
|
218
|
+
- `out.delete` и `out.get` проектора у производного класса с ключом, получив один id (строкой или `{ $id }`), искали
|
|
219
|
+
строку с пустым ключом: `get` отвечал `null`, а `delete` выводил арендатора с `id_mismatch`. Теперь это сразу
|
|
220
|
+
`invalid_data` с перечнем полей ключа — ошибка кода `handle`, пачка откатывается целиком; строку класса с ключом
|
|
221
|
+
называют полями ключа, id — только у класса без ключа (README, раздел 12 «Проекторы»). Подтверждает пример
|
|
222
|
+
`guide/examples/derived/03-projector-options.ts` (шаги 3 и 4): без исправления `get` по id отвечал `null`, а
|
|
223
|
+
`run()` проходил без ошибки.
|
|
224
|
+
- Строки `secret` удалённого аккаунта хранились вечно: удаление строки `Account` гасило сессии и ставило способам
|
|
225
|
+
входа `deleted_at`, а `maintain()` физически убирает только сессии и коды вида `OTP` — хэши паролей и ключей,
|
|
226
|
+
секреты TOTP и внешние учётные записи удалённого аккаунта оставались в базе. Теперь их уносит внешний ключ
|
|
227
|
+
`secret.account → entity(id) on delete cascade` в том же операторе удаления. Сессии гаснут с сигналом
|
|
228
|
+
`session:<хэш>` раньше каскада — в новом триггере `entity_bd_account` (до удаления, для всех ролей, в том числе
|
|
229
|
+
владельца: аккаунты удаляют и каскад `trg_entity_ad`, и `purgeAccount`), поэтому процессы по-прежнему сбрасывают кэш
|
|
230
|
+
ответов этих сессий; пометка `deleted_at` в триггере удаления снята. Первая заливка снимает внешний ключ до `TRUNCATE`
|
|
231
|
+
и возвращает его после первичного ключа `entity`; `letopis import` пропускает креды аккаунтов, которых нет в
|
|
232
|
+
`Account` версии 0.21 (считает их в `skipped`). Подтверждает пример `guide/examples/auth/03-delete-account.ts`: без
|
|
233
|
+
исправления шаг 3 находит в `secret` три строки удалённого аккаунта, а шаг 4 не получает `acl_denied`.
|
|
234
|
+
- Описание `maintain()` в README расходилось с кодом и JSDoc: он удаляет не только истёкшие сессии и коды, но и
|
|
235
|
+
погашенные сессии и использованные, заменённые или исчерпавшие попытки коды вида `OTP`. В README перечислены и
|
|
236
|
+
занятые виды кредов `PROJECTOR` и `LOCK`.
|
|
237
|
+
- `.tags(has(…))`, `.tags(hasAny(…))` и `.tags(hasAll(…))` перед `update()` падали сырой ошибкой Node.js
|
|
238
|
+
`ERR_INVALID_ARG_TYPE`: оператор отбора библиотека пыталась записать в колонку тегов. Теперь оператор перед
|
|
239
|
+
`update()` только отбирает строки, а их теги не меняются; строка или список, как и раньше, и отбирают строки, и
|
|
240
|
+
заменяют их теги целиком. Подтверждает пример `guide/examples/write/05-batch-tags-reason.ts` (шаг 6): без
|
|
241
|
+
исправления он падает.
|
|
242
|
+
|
|
243
|
+
- Срез `asOf()` и надгробия `withDeleted()` на обратном переходе отдавали объект столько раз, на сколько объектов
|
|
244
|
+
прошлого шага он ссылается: товар в двух категориях на шаге `db.Category().Product().asOf(t)` приходил в `rows()`
|
|
245
|
+
дважды, `count()` и итоги его удваивали. Без среза повторы отбрасывались, а пары переходов среза журнала шли в узел
|
|
246
|
+
как есть. Теперь узел — одна строка на объект, `paths()` по-прежнему отдаёт путь на каждую ссылку. Подтверждает
|
|
247
|
+
пример `guide/examples/history/01-versions-asof-diff-blame.ts` (шаг 6): без исправления он падает.
|
|
248
|
+
- Строки ответа `update()` и `anonymize()` шли в порядке обхода таблицы, а не целей, поэтому `first()` после глагола
|
|
249
|
+
отдавал случайную из исправленных строк. Теперь ответ — в порядке целей: по `sort()` до глагола, без него — по id.
|
|
250
|
+
Подтверждает пример `guide/examples/write/01-create-update-upsert.ts` (шаг 7): без исправления он падает.
|
|
251
|
+
- `maintain()` удалял только одноразовые коды вида `OTP`, а использованные и истёкшие коды своих видов `OTP_<ИМЯ>`
|
|
252
|
+
(`auth.issueOtp({ kind })`) оставались в `secret` навсегда. Теперь он удаляет коды всех видов, которые выпускает
|
|
253
|
+
`issueOtp`. Подтверждает пример `guide/examples/auth/01-login-factors.ts` (шаг 6): без исправления он падает.
|
|
254
|
+
- `min()` и `max()` по логическому полю падали сырой ошибкой PostgreSQL (у `boolean` нет `min` и `max`). Теперь
|
|
255
|
+
`false` меньше `true`: `min` — `false`, если есть хоть одно `false`, `max` — `true`, если есть хоть одно `true`.
|
|
256
|
+
Подтверждает пример `guide/examples/read/03-sort-pages-aggregates.ts` (шаг 7).
|
|
257
|
+
- `verifyHash()` и вход `auth.verifyPassword()` по хэшу scrypt, которому нужно больше 64 МБ памяти или с
|
|
258
|
+
недопустимыми N, r, p, бросали сырую ошибку Node.js; теперь — `invalid_data` с объяснением. `compileValidator()` с
|
|
259
|
+
шаблоном `pattern`, неверным для JavaScript, бросал `SyntaxError`; теперь — `invalid_class` с путём поля.
|
|
260
|
+
Подтверждает пример `guide/examples/models/06-exports.ts` (шаги 3 и 5).
|
|
261
|
+
|
|
262
|
+
### Изменено
|
|
263
|
+
- `letopis types` и `letopis describe` без действующей сессии (токен истёк, погашен, неверен или не задан) выдавали
|
|
264
|
+
пустой результат с кодом 0, а с `--out` затирали им файл. Теперь они выходят с кодом 1 и ошибкой `no_session`, файл
|
|
265
|
+
не трогают. Подтверждает пример `guide/examples/ops/04-cli.ts` (шаг 5).
|
|
266
|
+
- `connect()` на схеме, ревизия которой новее пакета (её обновил более новый пакет), молчал; теперь он подключается и
|
|
267
|
+
предупреждает «обновите пакет letopis», как предупреждает об отставшей схеме. Подтверждает пример
|
|
268
|
+
`guide/examples/install/05-engine-stamp.ts` (шаг 4).
|
|
269
|
+
- Описания методов (JSDoc), которые справочник сайта показывал непонятными, переписаны простыми словами: цепочка и
|
|
270
|
+
операторы фильтра, `db.as`, `db.verify`, `db.reset`, `db.trimHistory`, способы входа, подписка, проектор, загрузчик,
|
|
271
|
+
кэш, `up()` и `createTenant`, MCP, валидатор, константы. Исправлено неверное описание `InstallResult.serviceKey`:
|
|
272
|
+
новый ключ сервиса выпускается на любом накате, где не осталось ни одного неотозванного ключа API.
|
|
273
|
+
- README: ключ сервиса `up()` выдаёт, когда не осталось ни одного ключа API, кроме отозванных (истёкший или
|
|
274
|
+
выключенный ключ тоже в счёт), — было «действующего»; класс из `into` проектора источником не бывает, даже если
|
|
275
|
+
попадает в `from`, — было «не может быть одновременно источником и результатом», будто это запрет.
|
|
276
|
+
- Модификатор выборки (`limit`, `offset`, `sort`, `after`, `asOf`, `withDeleted`, `cache`) сразу после глагола, за
|
|
277
|
+
которым идёт терминал `rows`, `first`, `ids`, `count` или `paths`, — теперь `invalid_query` до записи. Раньше
|
|
278
|
+
`db.Product().update(…).limit(1).rows()` правил все товары, а в ответе отдавал одну строку; `offset`, `sort` и
|
|
279
|
+
остальные молча пропускались, а у `ids` и `count` — и `limit`. Цели глагола сужают модификаторы до него. В батче
|
|
280
|
+
так же отказывает `run()`. После глагола модификатор по-прежнему действует на чтение за ним (шаг класса,
|
|
281
|
+
`versions()`, `diff()`, `blame()`, агрегаты) и на цели следующего глагола. Подтверждают примеры
|
|
282
|
+
`guide/examples/write/01-create-update-upsert.ts` (шаг 8) и `guide/examples/write/05-batch-tags-reason.ts`
|
|
283
|
+
(шаг 9): без изменения они падают.
|
|
284
|
+
- Ревизия движка 7: внешний ключ `secret.account` (миграция `0007` сначала стирает строки `secret`, чьих аккаунтов уже
|
|
285
|
+
нет в `entity`, затем ставит ключ) и индекс `secret_account`. Схемы ревизии 6 (1.1–1.4.0) обновляются через
|
|
286
|
+
`up({ upgrade: true })`: без него `up()` отказывает, а `connect()` предупреждает об отставшей ревизии.
|
|
287
|
+
- Строку `Account` аккаунта System удалить нельзя — `acl_denied` (как уже отказывал `purgeAccount`): ему принадлежат
|
|
288
|
+
служебные строки установки в `secret` (ключ прав и сигналов, строки-замки, чекпоинты проекторов), которые каскад
|
|
289
|
+
внешнего ключа стёр бы. `seed_system()` создаёт аккаунт System раньше строки ключа.
|
|
290
|
+
- Убран код ошибки `not_implemented` (охранник `notYet`): в 1.4 его не бросал ни один метод.
|
|
291
|
+
- Тексты для людей:
|
|
292
|
+
- ошибка снятой опции `connect()` согласована по роду: «опция connect({ … }) снята в 1.0.0»;
|
|
293
|
+
- предупреждение о схеме, которую не обновили после смены пакета, — «обновите схему — вызовите up(…)» вместо
|
|
294
|
+
«накатите up(…)»;
|
|
295
|
+
- ошибка `rev_ambiguous` — «проверить версию { rev } можно только у одной цели» вместо «строгий режим только
|
|
296
|
+
для одной»;
|
|
297
|
+
- причина выбывания арендатора проектора по `lockRetries` (`status().failed`, код `55P03`) — «держит чужая
|
|
298
|
+
транзакция — в течение 3 проходов подряд дольше lockTimeout» вместо «— 3 проходов подряд…»: число и слово не
|
|
299
|
+
согласовывались («1 проходов», «3 проходов»).
|
|
300
|
+
- Тест `retention` «7.8.1 №7» больше не зависит от даты: 29–31 числа «сейчас» эталона уходило в прошлое, и проверка
|
|
301
|
+
разрыва свежей версии падала при исправной библиотеке.
|
|
4
302
|
|
|
5
303
|
## [1.4.0] — 2026-10-29
|
|
6
304
|
|