letopis 1.4.0 → 1.4.1
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 +10 -5
- package/AGENT-CHEATSHEET.md +14 -9
- package/CHANGELOG.md +159 -0
- package/README.en.md +119 -52
- package/README.md +107 -47
- package/dist/acl.d.ts +11 -5
- package/dist/admin.d.ts +30 -14
- package/dist/admin.js +42 -16
- package/dist/auth.d.ts +61 -17
- package/dist/auth.js +16 -2
- package/dist/cache.d.ts +5 -1
- package/dist/chain.d.ts +91 -15
- package/dist/chain.js +47 -4
- package/dist/cli.js +6 -0
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +5 -5
- package/dist/import.js +7 -0
- package/dist/index.d.ts +74 -45
- package/dist/index.js +12 -5
- package/dist/load.d.ts +10 -3
- package/dist/load.js +19 -4
- package/dist/mcp.d.ts +20 -11
- package/dist/mcp.js +4 -4
- package/dist/model.d.ts +4 -1
- package/dist/ops.d.ts +58 -18
- package/dist/ops.js +58 -18
- package/dist/project.d.ts +30 -11
- package/dist/project.js +30 -4
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +4 -1
- package/dist/sql.d.ts +4 -1
- package/dist/sql.js +12 -4
- package/dist/tx.js +10 -1
- package/dist/types.d.ts +14 -7
- package/dist/types.js +13 -6
- package/dist/up.d.ts +48 -25
- package/dist/up.js +89 -41
- package/dist/validate.d.ts +9 -2
- package/dist/validate.js +19 -3
- package/dist/watch.d.ts +17 -5
- package/dist/watch.js +40 -8
- package/dist/write.d.ts +3 -2
- package/dist/write.js +29 -10
- package/package.json +1 -1
- package/sql/10-core.sql +6 -1
- package/sql/60-write.sql +38 -18
- package/sql/80-auth.sql +39 -2
- package/sql/88-derived.sql +1 -0
- package/sql/90-time.sql +15 -5
- 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
|
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` — пользователь,
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,165 @@
|
|
|
2
2
|
|
|
3
3
|
Формат: [Keep a Changelog](https://keepachangelog.com/), версии — semver.
|
|
4
4
|
|
|
5
|
+
## [1.4.1] — 2026-11-01
|
|
6
|
+
|
|
7
|
+
Исправления, найденные прогоном примеров документации для людей (`guide/`).
|
|
8
|
+
|
|
9
|
+
### Добавлено
|
|
10
|
+
- `db.auth.session(token)` — сведения о сессии по её токену для сервиса: чья сессия (`account`), в каком арендаторе
|
|
11
|
+
она работает (`tenant`), цело ли там членство (`member`; в собственном арендаторе — всегда `true`), когда она
|
|
12
|
+
истечёт (`expiresAt`), когда и как был вход (`loginAt`, `methods`). Подключение по этому токену не открывается,
|
|
13
|
+
срок сессии не продлевается. Неверный, погашенный или истёкший токен и отключённый аккаунт — `null`, причина не
|
|
14
|
+
различается. Вызывает сервис или системный администратор (право `auth.session`, как у `sessionFor`); пользователю
|
|
15
|
+
отвечают `acl_denied`, вызову без сессии — `no_session`. В базе — функция владельца `auth_session_info(text)`, в
|
|
16
|
+
пакете — новый тип `SessionInfo`; в схеме метод появляется после повторного `up()`. Пробел нашли сервисы-примеры
|
|
17
|
+
сайта (салон, helpdesk). Helpdesk теперь проверяет сессию в начале каждого запроса через `svc.auth.session(token)`,
|
|
18
|
+
а не вызовом `auth.tenants()` по подключению пользователя.
|
|
19
|
+
|
|
20
|
+
### Исправлено
|
|
21
|
+
- `db.auth.purgeAccount(арендатор)` из сессии, которая работает в этом же арендаторе, откатывался с
|
|
22
|
+
`tenant_denied` («нет членства в арендаторе»): функция стирала членство вызывающего вместе с данными, а
|
|
23
|
+
надгробия и передача владения дальше перечитывали контекст. Теперь после проверки прав остаток операции идёт в
|
|
24
|
+
личном арендаторе вызывающего, его сессия больше не указывает на стёртого арендатора, а корень подключения
|
|
25
|
+
перечитывает арендатора и реестр. Подтверждает пример `guide/examples/auth/02-sessions-tenants.ts` (шаг 8):
|
|
26
|
+
без исправления он падает.
|
|
27
|
+
- `commit()` явной транзакции (`db.begin()`), в которой ошибку поймали и проглотили, бросал сырую ошибку
|
|
28
|
+
PostgreSQL `25P02` вместо обещанного README `tx_rolled_back`: чтение `letopis.changed` в одном конвейере с
|
|
29
|
+
COMMIT падало раньше, чем COMMIT отвечал ROLLBACK. Теперь на `25P02` библиотека дожидается ответа COMMIT и
|
|
30
|
+
бросает `tx_rolled_back`. Подтверждает пример `guide/examples/errors/01-api-errors.ts` (шаг 7): без исправления
|
|
31
|
+
он падает с `25P02`.
|
|
32
|
+
- Порядок надгробий одного уровня каскада в журнале (`seq`, время `at`) и строк итога
|
|
33
|
+
`delete({ confirm: true })` зависел от плана запроса и менялся от прогона к прогону: триггер удаления писал
|
|
34
|
+
надгробия вставкой без `order by`, а превью при этом шло по id. Теперь надгробия каждого уровня пишутся в порядке
|
|
35
|
+
id, итог — по уровням каскада, внутри уровня по id (README, раздел 7); цели классов без журнала — в конце, тоже по
|
|
36
|
+
id. Подтверждают примеры `guide/examples/delete/01-delete-cascade.ts` и `02-restore-purge.ts`: без исправления их
|
|
37
|
+
снимки то и дело расходятся порядком строк заказа.
|
|
38
|
+
|
|
39
|
+
- Порядок записей журнала у остальных массовых операций тоже определён, а не зависит от плана запроса: пакетная
|
|
40
|
+
вставка — в порядке входа, правка многих строк, перестройка проектора, загрузчик с `existing: 'update'` — по id;
|
|
41
|
+
смена владельца при выходе из членства и `purgeAccount` идут по арендаторам по порядку; глаголы по списку целей без
|
|
42
|
+
`sort` (`restore`, `purge`, `reclass`, `rekey`) обходят цели по id.
|
|
43
|
+
- Подписка, к которой больше суток (яруса `all` политики хранения её классов) не было событий, получала ложный
|
|
44
|
+
`cursor_expired`, хотя ничего не пропустила: время курсора оставалось временем последнего события, а база проверяет
|
|
45
|
+
его на каждом опросе. Теперь подписка, догнав журнал, ставит в свой курсор время опроса — «сейчас», но не позже самой
|
|
46
|
+
старой записи её классов, которая уже зафиксирована, а горизонт ещё не прошла; курсор события `ev.cursor` по-прежнему
|
|
47
|
+
хранит время события. `sub.cursor` больше не обгоняет события, прочитанные в запас: он стоит после последнего
|
|
48
|
+
события, которое отдал `next()`, поэтому продолжать после перезапуска нужно с него (README, раздел 12 «Подписка»).
|
|
49
|
+
Подтверждает пример `guide/examples/watch/02-quiet-cursor.ts`: без исправления шаг 3 падает с `cursor_expired`.
|
|
50
|
+
- `connect({ maintain })` не проверял интервал: ноль, отрицательное число, `NaN`, `Infinity` или интервал длиннее
|
|
51
|
+
2³¹−1 мс (`'25d'`, `'30d'`) таймер Node.js превращал в 1 мс, и обслуживание шло без перерыва, а `null` и `true`
|
|
52
|
+
давали `TypeError`. Теперь интервал — число или строка, конечный, от 1 мс до 2³¹−1 мс (около 24 суток), иначе
|
|
53
|
+
`invalid_data`; проверка идёт до открытия пула и таймеров. `close()` ждёт идущий по расписанию запуск: он доделывает
|
|
54
|
+
текущую пачку и следующую не начинает, а пул закрывается после него. Подтверждает пример
|
|
55
|
+
`guide/examples/install/03-connect-options.ts` (шаг 6): без исправления подключения с такими интервалами открывались.
|
|
56
|
+
- `createTenant` и `up({ tenant })` принимали любой `user.kind`: вид в другом регистре (`password`), пустой или
|
|
57
|
+
служебный (`SESSION`, `TOTP` …). По такому паролю нельзя было войти, а служебным видом пароль выдавал себя за другой
|
|
58
|
+
кред: с видом `TOTP` аккаунт выглядел так, будто у него включён второй фактор (`totpEnabled: true`), с видом
|
|
59
|
+
`SESSION` пароль пропадал из `auth.credentials()`. Теперь вид проверяется по правилу функций кредов (`^[A-Z][A-Z0-9_]{1,31}$`, не `SESSION`, `APIKEY`,
|
|
60
|
+
`KEYSECRET`, `OTP`, `TOTP`, `KEY`, `PROJECTOR`, `LOCK`), иначе `invalid_data` до записи. Подтверждает пример
|
|
61
|
+
`guide/examples/install/01-up-and-connect.ts` (шаг 4): без исправления арендатор создавался.
|
|
62
|
+
- Параллельные `up()` разных схем одной базы падали с `tuple concurrently updated` (`XX000`): общую часть
|
|
63
|
+
установки — роли `letopis_owner` и `letopis_app`, членство установщика, pgcrypto и право владельца на схему pgcrypto —
|
|
64
|
+
каждый `up()` менял одновременно с другими, а `grant usage on schema` переписывает строку схемы в каталоге, даже если
|
|
65
|
+
право уже выдано. На чистой базе так же столкнулись бы `create role` и `create extension`. Теперь общая часть — одна
|
|
66
|
+
транзакция под advisory-блокировкой: следующий `up()` ждёт, пока предыдущий её зафиксирует. Блокировка действует в
|
|
67
|
+
пределах базы, а роли общие для кластера, поэтому роль или членство, которые одновременно создал `up()` в другой
|
|
68
|
+
базе, считаются уже созданными. Нашёл стенд сайта (`services/compose.yaml`): helpdesk падал при одновременном
|
|
69
|
+
запуске четырёх сервисов; без исправления 4 из 8 параллельных `up()` падали в каждом круге.
|
|
70
|
+
- Одновременные `up()` одной и той же схемы (несколько экземпляров сервиса при запуске) падали: второй
|
|
71
|
+
`create schema` — на уникальном индексе каталога (`23505`), второй `create or replace function` или `comment` —
|
|
72
|
+
`tuple concurrently updated` (`XX000`), а `up({ tenant })` дважды вставлял того же арендатора. Ревизию, от которой
|
|
73
|
+
считаются миграции, `up()` читал до наката, поэтому второй `upgrade` повторил бы миграции, уже накатанные первым.
|
|
74
|
+
Теперь накат схемы — `fresh`, чтение ревизии, `create schema`, миграции, ядро, сиды и ключ сервиса — одна транзакция
|
|
75
|
+
под advisory-блокировкой схемы (ключ — её имя), и `createTenant` берёт ту же блокировку. Следующий `up()` ждёт
|
|
76
|
+
предыдущий и делает повторный накат по готовой схеме: `created` и `upgraded` он считает по ревизии, которую увидел
|
|
77
|
+
под блокировкой. `up()` разных схем друг друга не ждут. Подтверждает пример
|
|
78
|
+
`guide/examples/install/02-up-options.ts` (шаг 6): без исправления он падал 5 раз из 5.
|
|
79
|
+
- `out.delete` и `out.get` проектора у производного класса с ключом, получив один id (строкой или `{ $id }`), искали
|
|
80
|
+
строку с пустым ключом: `get` отвечал `null`, а `delete` выводил арендатора с `id_mismatch`. Теперь это сразу
|
|
81
|
+
`invalid_data` с перечнем полей ключа — ошибка кода `handle`, пачка откатывается целиком; строку класса с ключом
|
|
82
|
+
называют полями ключа, id — только у класса без ключа (README, раздел 12 «Проекторы»). Подтверждает пример
|
|
83
|
+
`guide/examples/derived/03-projector-options.ts` (шаги 3 и 4): без исправления `get` по id отвечал `null`, а
|
|
84
|
+
`run()` проходил без ошибки.
|
|
85
|
+
- Строки `secret` удалённого аккаунта хранились вечно: удаление строки `Account` гасило сессии и ставило способам
|
|
86
|
+
входа `deleted_at`, а `maintain()` физически убирает только сессии и коды вида `OTP` — хэши паролей и ключей,
|
|
87
|
+
секреты TOTP и внешние учётные записи удалённого аккаунта оставались в базе. Теперь их уносит внешний ключ
|
|
88
|
+
`secret.account → entity(id) on delete cascade` в том же операторе удаления. Сессии гаснут с сигналом
|
|
89
|
+
`session:<хэш>` раньше каскада — в новом триггере `entity_bd_account` (до удаления, для всех ролей, в том числе
|
|
90
|
+
владельца: аккаунты удаляют и каскад `trg_entity_ad`, и `purgeAccount`), поэтому процессы по-прежнему сбрасывают кэш
|
|
91
|
+
ответов этих сессий; пометка `deleted_at` в триггере удаления снята. Первая заливка снимает внешний ключ до `TRUNCATE`
|
|
92
|
+
и возвращает его после первичного ключа `entity`; `letopis import` пропускает креды аккаунтов, которых нет в
|
|
93
|
+
`Account` версии 0.21 (считает их в `skipped`). Подтверждает пример `guide/examples/auth/03-delete-account.ts`: без
|
|
94
|
+
исправления шаг 3 находит в `secret` три строки удалённого аккаунта, а шаг 4 не получает `acl_denied`.
|
|
95
|
+
- Описание `maintain()` в README расходилось с кодом и JSDoc: он удаляет не только истёкшие сессии и коды, но и
|
|
96
|
+
погашенные сессии и использованные, заменённые или исчерпавшие попытки коды вида `OTP`. В README перечислены и
|
|
97
|
+
занятые виды кредов `PROJECTOR` и `LOCK`.
|
|
98
|
+
- `.tags(has(…))`, `.tags(hasAny(…))` и `.tags(hasAll(…))` перед `update()` падали сырой ошибкой Node.js
|
|
99
|
+
`ERR_INVALID_ARG_TYPE`: оператор отбора библиотека пыталась записать в колонку тегов. Теперь оператор перед
|
|
100
|
+
`update()` только отбирает строки, а их теги не меняются; строка или список, как и раньше, и отбирают строки, и
|
|
101
|
+
заменяют их теги целиком. Подтверждает пример `guide/examples/write/05-batch-tags-reason.ts` (шаг 6): без
|
|
102
|
+
исправления он падает.
|
|
103
|
+
|
|
104
|
+
- Срез `asOf()` и надгробия `withDeleted()` на обратном переходе отдавали объект столько раз, на сколько объектов
|
|
105
|
+
прошлого шага он ссылается: товар в двух категориях на шаге `db.Category().Product().asOf(t)` приходил в `rows()`
|
|
106
|
+
дважды, `count()` и итоги его удваивали. Без среза повторы отбрасывались, а пары переходов среза журнала шли в узел
|
|
107
|
+
как есть. Теперь узел — одна строка на объект, `paths()` по-прежнему отдаёт путь на каждую ссылку. Подтверждает
|
|
108
|
+
пример `guide/examples/history/01-versions-asof-diff-blame.ts` (шаг 6): без исправления он падает.
|
|
109
|
+
- Строки ответа `update()` и `anonymize()` шли в порядке обхода таблицы, а не целей, поэтому `first()` после глагола
|
|
110
|
+
отдавал случайную из исправленных строк. Теперь ответ — в порядке целей: по `sort()` до глагола, без него — по id.
|
|
111
|
+
Подтверждает пример `guide/examples/write/01-create-update-upsert.ts` (шаг 7): без исправления он падает.
|
|
112
|
+
- `maintain()` удалял только одноразовые коды вида `OTP`, а использованные и истёкшие коды своих видов `OTP_<ИМЯ>`
|
|
113
|
+
(`auth.issueOtp({ kind })`) оставались в `secret` навсегда. Теперь он удаляет коды всех видов, которые выпускает
|
|
114
|
+
`issueOtp`. Подтверждает пример `guide/examples/auth/01-login-factors.ts` (шаг 6): без исправления он падает.
|
|
115
|
+
- `min()` и `max()` по логическому полю падали сырой ошибкой PostgreSQL (у `boolean` нет `min` и `max`). Теперь
|
|
116
|
+
`false` меньше `true`: `min` — `false`, если есть хоть одно `false`, `max` — `true`, если есть хоть одно `true`.
|
|
117
|
+
Подтверждает пример `guide/examples/read/03-sort-pages-aggregates.ts` (шаг 7).
|
|
118
|
+
- `verifyHash()` и вход `auth.verifyPassword()` по хэшу scrypt, которому нужно больше 64 МБ памяти или с
|
|
119
|
+
недопустимыми N, r, p, бросали сырую ошибку Node.js; теперь — `invalid_data` с объяснением. `compileValidator()` с
|
|
120
|
+
шаблоном `pattern`, неверным для JavaScript, бросал `SyntaxError`; теперь — `invalid_class` с путём поля.
|
|
121
|
+
Подтверждает пример `guide/examples/models/06-exports.ts` (шаги 3 и 5).
|
|
122
|
+
|
|
123
|
+
### Изменено
|
|
124
|
+
- `letopis types` и `letopis describe` без действующей сессии (токен истёк, погашен, неверен или не задан) выдавали
|
|
125
|
+
пустой результат с кодом 0, а с `--out` затирали им файл. Теперь они выходят с кодом 1 и ошибкой `no_session`, файл
|
|
126
|
+
не трогают. Подтверждает пример `guide/examples/ops/04-cli.ts` (шаг 5).
|
|
127
|
+
- `connect()` на схеме, ревизия которой новее пакета (её обновил более новый пакет), молчал; теперь он подключается и
|
|
128
|
+
предупреждает «обновите пакет letopis», как предупреждает об отставшей схеме. Подтверждает пример
|
|
129
|
+
`guide/examples/install/05-engine-stamp.ts` (шаг 4).
|
|
130
|
+
- Описания методов (JSDoc), которые справочник сайта показывал непонятными, переписаны простыми словами: цепочка и
|
|
131
|
+
операторы фильтра, `db.as`, `db.verify`, `db.reset`, `db.trimHistory`, способы входа, подписка, проектор, загрузчик,
|
|
132
|
+
кэш, `up()` и `createTenant`, MCP, валидатор, константы. Исправлено неверное описание `InstallResult.serviceKey`:
|
|
133
|
+
новый ключ сервиса выпускается на любом накате, где не осталось ни одного неотозванного ключа API.
|
|
134
|
+
- README: ключ сервиса `up()` выдаёт, когда не осталось ни одного ключа API, кроме отозванных (истёкший или
|
|
135
|
+
выключенный ключ тоже в счёт), — было «действующего»; класс из `into` проектора источником не бывает, даже если
|
|
136
|
+
попадает в `from`, — было «не может быть одновременно источником и результатом», будто это запрет.
|
|
137
|
+
- Модификатор выборки (`limit`, `offset`, `sort`, `after`, `asOf`, `withDeleted`, `cache`) сразу после глагола, за
|
|
138
|
+
которым идёт терминал `rows`, `first`, `ids`, `count` или `paths`, — теперь `invalid_query` до записи. Раньше
|
|
139
|
+
`db.Product().update(…).limit(1).rows()` правил все товары, а в ответе отдавал одну строку; `offset`, `sort` и
|
|
140
|
+
остальные молча пропускались, а у `ids` и `count` — и `limit`. Цели глагола сужают модификаторы до него. В батче
|
|
141
|
+
так же отказывает `run()`. После глагола модификатор по-прежнему действует на чтение за ним (шаг класса,
|
|
142
|
+
`versions()`, `diff()`, `blame()`, агрегаты) и на цели следующего глагола. Подтверждают примеры
|
|
143
|
+
`guide/examples/write/01-create-update-upsert.ts` (шаг 8) и `guide/examples/write/05-batch-tags-reason.ts`
|
|
144
|
+
(шаг 9): без изменения они падают.
|
|
145
|
+
- Ревизия движка 7: внешний ключ `secret.account` (миграция `0007` сначала стирает строки `secret`, чьих аккаунтов уже
|
|
146
|
+
нет в `entity`, затем ставит ключ) и индекс `secret_account`. Схемы ревизии 6 (1.1–1.4.0) обновляются через
|
|
147
|
+
`up({ upgrade: true })`: без него `up()` отказывает, а `connect()` предупреждает об отставшей ревизии.
|
|
148
|
+
- Строку `Account` аккаунта System удалить нельзя — `acl_denied` (как уже отказывал `purgeAccount`): ему принадлежат
|
|
149
|
+
служебные строки установки в `secret` (ключ прав и сигналов, строки-замки, чекпоинты проекторов), которые каскад
|
|
150
|
+
внешнего ключа стёр бы. `seed_system()` создаёт аккаунт System раньше строки ключа.
|
|
151
|
+
- Убран код ошибки `not_implemented` (охранник `notYet`): в 1.4 его не бросал ни один метод.
|
|
152
|
+
- Тексты для людей:
|
|
153
|
+
- ошибка снятой опции `connect()` согласована по роду: «опция connect({ … }) снята в 1.0.0»;
|
|
154
|
+
- предупреждение о схеме, которую не обновили после смены пакета, — «обновите схему — вызовите up(…)» вместо
|
|
155
|
+
«накатите up(…)»;
|
|
156
|
+
- ошибка `rev_ambiguous` — «проверить версию { rev } можно только у одной цели» вместо «строгий режим только
|
|
157
|
+
для одной»;
|
|
158
|
+
- причина выбывания арендатора проектора по `lockRetries` (`status().failed`, код `55P03`) — «держит чужая
|
|
159
|
+
транзакция — в течение 3 проходов подряд дольше lockTimeout» вместо «— 3 проходов подряд…»: число и слово не
|
|
160
|
+
согласовывались («1 проходов», «3 проходов»).
|
|
161
|
+
- Тест `retention` «7.8.1 №7» больше не зависит от даты: 29–31 числа «сейчас» эталона уходило в прошлое, и проверка
|
|
162
|
+
разрыва свежей версии падала при исправной библиотеке.
|
|
163
|
+
|
|
5
164
|
## [1.4.0] — 2026-10-29
|
|
6
165
|
|
|
7
166
|
letopis 1.4 — доработки после выпуска 1.3.0. Вошли:
|