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/README.md
CHANGED
|
@@ -129,7 +129,7 @@ await svc.close()
|
|
|
129
129
|
уменьшил цену внутри оператора `UPDATE`, без предварительного чтения. `versions()` прочитал историю из журнала.
|
|
130
130
|
|
|
131
131
|
Повторный вызов `up()` безопасен: он повторно накатывает ту же ревизию и данные не трогает, а ключ сервиса снова не
|
|
132
|
-
выдаёт, пока в установке есть
|
|
132
|
+
выдаёт, пока в установке есть хоть один ключ API, кроме отозванных. Дальше раздел 2 объясняет устройство, а разделы 3–13 разбирают задачи по порядку.
|
|
133
133
|
|
|
134
134
|
## 2. Как устроен letopis
|
|
135
135
|
|
|
@@ -239,11 +239,16 @@ LOGIN-роль состоит только в ней. Всё, что выпол
|
|
|
239
239
|
| `owner`, `app` | имена ролей владельца и приложения (по умолчанию `letopis_owner`, `letopis_app`) |
|
|
240
240
|
|
|
241
241
|
Результат: `schema` (полное имя), `serviceKey` (при первой установке и при накате, когда в установке не осталось ни
|
|
242
|
-
одного
|
|
242
|
+
одного ключа API, кроме отозванных: истёкший или выключенный ключ тоже в счёт; иначе `null`), `created`,
|
|
243
243
|
`upgraded`, `revision`, `migrations`, `analyzed`, `owner`, `app`, `signals` (режим сигналов установки), `warnings`
|
|
244
244
|
(предупреждения, без `quiet` они печатаются), а также `tenant` и `pglite`, если их просили. На PostgreSQL ниже 18.2
|
|
245
245
|
`up()` предупреждает: такой сервер мог усечь CLOG, который ещё нужен непрочитанным сигналам, — обновите его.
|
|
246
246
|
|
|
247
|
+
Несколько экземпляров сервиса могут звать `up()` одной схемы одновременно: накат идёт под блокировкой схемы. Первый
|
|
248
|
+
`up()` ставит или обновляет её, остальные ждут и делают повторный накат по готовой (`created: false`,
|
|
249
|
+
`upgraded: false`); первого арендатора `tenant` заводит один из них, остальные находят его по имени. `up()` разных
|
|
250
|
+
схем одной базы друг друга не ждут.
|
|
251
|
+
|
|
247
252
|
Скрыть из сигналов имена классов — режим `opaque` (или `tenant`, если имена не секрет, а нужна точность кэша по
|
|
248
253
|
арендатору); после смены режима перезапустите процессы приложения — режим читается при подключении:
|
|
249
254
|
|
|
@@ -285,7 +290,9 @@ const ANNA = r.tenant!.user! // его первый пользовател
|
|
|
285
290
|
Повтор с теми же именем и логином ничего не дублирует и пароль не меняет, только выдаёт новую сессию. Логин входит в
|
|
286
291
|
id как есть: `Anna@shop.ru` и `anna@shop.ru` дали бы два аккаунта, и у второго не было бы пароля, — пишите логин в
|
|
287
292
|
одном регистре. Поле `created` в ответе — создан ли арендатор этим вызовом: для существующего арендатора с новым
|
|
288
|
-
пользователем оно `false`. Роли задаёт `user.roles` — непустой список имён (не масок), иначе `invalid_data`.
|
|
293
|
+
пользователем оно `false`. Роли задаёт `user.roles` — непустой список имён (не масок), иначе `invalid_data`. Вид
|
|
294
|
+
пароля `user.kind` (по умолчанию `PASSWORD`) подчиняется правилу вида креда, как у `auth.setPassword`
|
|
295
|
+
([«Справочник API»](#dbauth)), иначе — тоже `invalid_data`. Роли
|
|
289
296
|
уже существующего членства повтор не меняет: их меняет правка членства ([«Выдать право»](#выдать-право)).
|
|
290
297
|
Удалённое повтор не воскрешает: если арендатор, аккаунт пользователя или членство удалены (строки нет, а история в
|
|
291
298
|
журнале есть), `createTenant` отвечает `invalid_data` и не записывает ничего. Текст ошибки называет удалённое и
|
|
@@ -356,14 +363,18 @@ const adm = await connect({ dsn: ADMIN, schema: r.schema, token: r.tenant!.token
|
|
|
356
363
|
|
|
357
364
|
### Обновление пакета и схемы
|
|
358
365
|
|
|
359
|
-
У схемы есть ревизия движка (сейчас
|
|
366
|
+
У схемы есть ревизия движка (сейчас 7). Если новая версия пакета принесла новую ревизию, `connect()` только
|
|
360
367
|
предупредит и назовёт команду. Отказывает `up()`: на отставшую схему без `upgrade: true` и на схему новее пакета.
|
|
368
|
+
К схеме новее пакета (её уже обновил более новый пакет, а старые экземпляры приложения ещё работают) `connect()`
|
|
369
|
+
подключается с предупреждением «движок схемы … ревизии N новее библиотеки (она знает M) — обновите пакет letopis».
|
|
361
370
|
Поэтому скрипт запуска, который вызывает `up()` при каждом старте, после обновления пакета остановится, пока не
|
|
362
371
|
получит `upgrade: true`. `up({ dsn, schema, upgrade: true })` применяет миграции из `sql/migrate/` и повторно
|
|
363
|
-
накатывает функции; данные сохраняются.
|
|
372
|
+
накатывает функции; данные сохраняются. Ревизия 7 (1.4.1) добавляет внешний ключ `secret.account`: миграция `0007`
|
|
373
|
+
сначала стирает строки `secret`, чьих аккаунтов уже нет (креды удалённых раньше аккаунтов хранились с пометкой
|
|
374
|
+
`deleted_at`), и ставит ключ.
|
|
364
375
|
|
|
365
|
-
Ревизия меняется не с каждой версией: у 1.1–1.4 она
|
|
366
|
-
ещё и штамп движка: версию пакета и хэш его файлов SQL. Если штамп не совпадает с файлами пакета, `connect()`
|
|
376
|
+
Ревизия меняется не с каждой версией: у 1.1–1.4.0 она одна (6), а функции разные. Поэтому `up()` пишет в комментарий
|
|
377
|
+
схемы ещё и штамп движка: версию пакета и хэш его файлов SQL. Если штамп не совпадает с файлами пакета, `connect()`
|
|
367
378
|
предупреждает. Когда схему накатила более старая версия (или версия до 1.4, у которой штампа нет), он советует
|
|
368
379
|
`up({ schema })`. Когда схему уже обновила более новая версия, он советует обновить пакет: `up()` старой версией вернул
|
|
369
380
|
бы прежние функции. Новая версия пакета без правок SQL штамп не меняет, и предупреждения нет.
|
|
@@ -374,10 +385,30 @@ const adm = await connect({ dsn: ADMIN, schema: r.schema, token: r.tenant!.token
|
|
|
374
385
|
записи: `create index concurrently log_class_tenant_at on "v2.<схема>".log (class, tenant, at)` от роли владельца, —
|
|
375
386
|
тогда `up()` только снимет прежний.
|
|
376
387
|
|
|
377
|
-
Обновление схем 1.1.0 и 1.0.1 проверяет тест `upgrade
|
|
388
|
+
Обновление схем 1.4.1, 1.1.0 и 1.0.1 проверяет тест `upgrade`; каждый выпуск проверяет обновление с предыдущего
|
|
389
|
+
выпуска. Схему ставит прежний пакет, данные пишет прежний движок, затем
|
|
378
390
|
вызывается `up()` текущей версии. После него версии и хэши прежнего движка на месте, `verify({ data: true })` чист, а
|
|
379
|
-
каталог схемы совпадает с чистой установкой. Схема 1.1
|
|
380
|
-
|
|
391
|
+
каталог схемы совпадает с чистой установкой. Схема 1.4.1 той же ревизии 7 обновляется обычным `up()`, без миграций;
|
|
392
|
+
1.1.0 (ревизия 6) и 1.0.1 (ревизия 5) — через `up({ upgrade: true })`: миграцией `0007` и миграциями `0006` и `0007`.
|
|
393
|
+
|
|
394
|
+
### Версии и совместимость
|
|
395
|
+
|
|
396
|
+
Версии letopis следуют semver. Что обещает каждый шаг версии:
|
|
397
|
+
|
|
398
|
+
- **Патч** (1.4.1 → 1.4.2) — только исправления. Ревизия движка та же: исправленные функции накатывает обычный
|
|
399
|
+
`up({ schema })`, миграций нет; до наката `connect()` предупреждает о расхождении штампа. Работающий код не получает
|
|
400
|
+
новых отказов, методы, опции, экспорты и коды ошибок не исчезают.
|
|
401
|
+
- **Минор** (1.4 → 1.5) — новые возможности: методы, опции, коды ошибок; изменения поведения, которые не ломают
|
|
402
|
+
работающий код; новая ревизия движка. Новую ревизию накатывает `up({ schema, upgrade: true })`. Удалённого и
|
|
403
|
+
переименованного нет.
|
|
404
|
+
- **Мажор** (1.x → 2.0) — удаление и переименование методов, опций, экспортов и кодов ошибок, смена сигнатур и типов,
|
|
405
|
+
новые отказы для всех. Что менять в коде, описывает `MIGRATION.md`.
|
|
406
|
+
|
|
407
|
+
Текст сообщения об ошибке в API не входит: в патче он может стать точнее. Полагайтесь на `err.code`.
|
|
408
|
+
|
|
409
|
+
Шаг версии перед каждым выпуском сверяет сценарий `check:semver`: он сравнивает контракт API (`docs/api-contract.json`)
|
|
410
|
+
и ревизию движка с прошлым выпуском и требует шаг не меньше нужного. Каждый выпуск проверяет обновление схемы с
|
|
411
|
+
предыдущего выпуска (тест `upgrade`).
|
|
381
412
|
|
|
382
413
|
### Разработка без PostgreSQL: PGlite
|
|
383
414
|
|
|
@@ -602,8 +633,9 @@ npx letopis types --dsn "$APP" --schema v2.shop --token "$TOKEN" --out types.ts
|
|
|
602
633
|
### Имена классов
|
|
603
634
|
|
|
604
635
|
Шаг цепочки называется именем класса, его алиасом или именем роли. Имена методов цепочки и корня (`count`, `sort`,
|
|
605
|
-
`set`, `sql`, `auth` и другие)
|
|
606
|
-
|
|
636
|
+
`set`, `sql`, `auth` и другие) там, где такой метод есть, библиотека разбирает раньше классов: класс `count` доступен
|
|
637
|
+
первым шагом `db.count(…)`, но не после другого шага. Связь с таким именем база не примет. Не давайте классам эти
|
|
638
|
+
имена; полный список — в разделе
|
|
607
639
|
[«Зарезервированные имена»](#зарезервированные-имена).
|
|
608
640
|
|
|
609
641
|
## 5. Запись
|
|
@@ -657,7 +689,8 @@ await db.Shop(shop).Product({ price: gte(2000) }).update({ title: 'Чайник
|
|
|
657
689
|
`db.Product().update(…)` правит все товары арендатора. Шаг ничего не нашёл — ответ пустой и ошибки нет, поэтому
|
|
658
690
|
проверяйте длину ответа. Если `update` затронул меньше строк, чем нашёл шаг, весь план откатывается: строку успели
|
|
659
691
|
удалить — `target_not_found`, строка видна, но править её нельзя — `acl_denied`. Модификатор `.tags(…)` перед
|
|
660
|
-
`update`
|
|
692
|
+
`update` отбирает строки; строка или список ещё и заменяют их теги целиком, а оператор (`has`, `hasAny`, `hasAll`)
|
|
693
|
+
только отбирает — теги не меняются.
|
|
661
694
|
|
|
662
695
|
### Создать или заменить: `upsert`
|
|
663
696
|
|
|
@@ -789,8 +822,14 @@ await db.Order(order).line().create({ qty: 1, price: 900 }).Product.set(sencha).
|
|
|
789
822
|
- Весь план — одна транзакция уровня `read committed`. При взаимной блокировке (`40P01`), ошибке сериализации
|
|
790
823
|
(`40001`) и конфликте журнала библиотека повторяет план целиком, до трёх раз.
|
|
791
824
|
- Ошибка любого звена откатывает весь план. `exists` и `conflict` не повторяются: это ответы для приложения.
|
|
792
|
-
- Модификаторы до глагола (`limit`, `sort`) сужают его цели.
|
|
825
|
+
- Модификаторы до глагола (`limit`, `offset`, `sort`) сужают его цели.
|
|
826
|
+
- Модификатор после глагола относится к тому, что идёт за ним: к шагу класса, к `versions()`, `diff()`, `blame()` и
|
|
827
|
+
агрегатам или к целям следующего глагола. Если за ним сразу терминал `rows`, `first`, `ids`, `count` или `paths`,
|
|
828
|
+
сужать уже нечего — глагол прошёл по всем целям шага, — поэтому `db.Product().update(…).limit(1).rows()` даёт
|
|
829
|
+
`invalid_query` и ничего не пишет. В батче так же отказывает `run()`.
|
|
793
830
|
- Глагол сразу после глагола пишет в те же сущности.
|
|
831
|
+
- Строки ответа глагола идут в порядке его целей: по `sort()` до глагола, без него — по id; `first()` после глагола —
|
|
832
|
+
первая по этому порядку.
|
|
794
833
|
|
|
795
834
|
## 6. Чтение
|
|
796
835
|
|
|
@@ -834,7 +873,7 @@ const roots = await db.Category({ Parent: null }).rows() // корневы
|
|
|
834
873
|
| `ne(v)`, `gt(v)`, `gte(v)`, `lt(v)`, `lte(v)` | ≠ (с учётом `null`), >, ≥, <, ≤ |
|
|
835
874
|
| `between(a, b)`, `inList([…])` | a ≤ x ≤ b; значение из списка |
|
|
836
875
|
| `like(s)`, `ilike(s)`, `starts(s)`, `ends(s)` | шаблон с `%` и `_`; без учёта регистра; начинается с; заканчивается на |
|
|
837
|
-
| `has(v)`, `hasAny([…])`, `hasAll([…])` | поле-массив содержит значение; хотя бы одно; все |
|
|
876
|
+
| `has(v)`, `hasAny([…])`, `hasAll([…])` | поле-массив содержит значение; хотя бы одно; все (`hasAll([])` подходит любой строке, `hasAny([])` — никакой) |
|
|
838
877
|
| `exists(true \| false)`, `isNull()` | поле есть или нет; поле `null` или отсутствует |
|
|
839
878
|
| `not(v)` | отрицание оператора; скаляр — «не равно» |
|
|
840
879
|
| `or(f1, f2, …)` | любой из фильтров шага |
|
|
@@ -871,13 +910,18 @@ const page2 = await db.Product().sort('data.price', 'desc')
|
|
|
871
910
|
`sort(поле, 'asc' | 'desc')` — по `'at'`, `'rev'`, `'id'` или `'data.<путь>'`. `after(cursorOf(строка, поле))` —
|
|
872
911
|
постраничный вывод «после такой-то строки»; он требует `sort()`. Такой вывод устойчив к вставкам и удалениям между
|
|
873
912
|
страницами, но строка, у которой изменилось поле сортировки, может выпасть или повториться. Курсор — объект
|
|
874
|
-
`{ v, id }` (значение
|
|
913
|
+
`{ v, id, rev }` (значение поля, id и номер версии строки): он переживает JSON, и его можно отдать клиенту. Поле в `cursorOf` должно
|
|
875
914
|
совпадать с полем `sort`. Значение курсора проверяется по типу поля `sort` до запроса, неверное — `invalid_query`:
|
|
876
915
|
числовому полю нужно конечное число, `rev` — целое, логическому — `true` или `false`, текстовому — строка, uuid и
|
|
877
916
|
`id` — uuid, времени и `at` — строка ISO с поясом, дате — `ГГГГ-ММ-ДД`, полю без описания — строка, число или
|
|
878
917
|
`true`/`false`. `null` подходит только полям данных, `id` курсора — всегда uuid. Курсор того же типа, снятый
|
|
879
918
|
с другого поля, проверка не отличит. `limit` и `offset` тоже есть.
|
|
880
919
|
|
|
920
|
+
`after()` листает и `versions()`: курсор версии несёт её `rev`, поэтому версии одного объекта с равным полем
|
|
921
|
+
сортировки не теряются. В `paths()` курсор даёт `cursorOf(путь, поле)`: поле и id — последнего узла пути, а в `path`
|
|
922
|
+
— id всех узлов, поэтому не теряются пути к одному узлу. Курсор `cursorOf(путь.Узел, поле)` с одного узла пропустит
|
|
923
|
+
остальные пути к этому узлу. `count()`, агрегаты, `diff()` и `blame()` курсор не учитывают.
|
|
924
|
+
|
|
881
925
|
Поле данных бывает `null` или отсутствует. Такие строки `sort` ставит, как `order by` в PostgreSQL: при
|
|
882
926
|
возрастании — в конце, при убывании — в начале, а `after()` считает `null` значением после всех прочих (по
|
|
883
927
|
возрастанию) или перед ними (по убыванию). Поэтому страницы проходят такие строки без пропусков и повторов, а
|
|
@@ -904,7 +948,9 @@ const byStatus = await db.Order().countBy('data.status') // { new: 1 }
|
|
|
904
948
|
```
|
|
905
949
|
|
|
906
950
|
Агрегаты считает база, по сущностям последнего шага. `limit(n)` и `offset(n)` перед `countBy()` относятся к группам:
|
|
907
|
-
`db.Order().limit(3).countBy('data.status')` — три самых частых значения.
|
|
951
|
+
`db.Order().limit(3).countBy('data.status')` — три самых частых значения. У логического поля `min()` — `false`, если
|
|
952
|
+
есть хоть одно `false` (все ли значения `true`), а `max()` — `true`, если есть хоть одно `true`; `sum()` и `avg()`
|
|
953
|
+
логическое поле не считают — PostgreSQL отвечает ошибкой.
|
|
908
954
|
|
|
909
955
|
### Пути
|
|
910
956
|
|
|
@@ -913,7 +959,8 @@ const byStatus = await db.Order().countBy('data.status') // { new: 1 }
|
|
|
913
959
|
const orderPaths = await db.Order(order).line().Product().paths() // [{ Order: Row, line: Row, Product: Row }, …]
|
|
914
960
|
```
|
|
915
961
|
|
|
916
|
-
`paths()` отдаёт узлы каждого варианта пути; ключ узла — имя шага или имя из `.alias(имя)`.
|
|
962
|
+
`paths()` отдаёт узлы каждого варианта пути; ключ узла — имя шага или имя из `.alias(имя)`. Ключи идут в порядке
|
|
963
|
+
шагов; если цепочка кончается возвратом к прежнему узлу, этот узел — последний. Повтор связи в
|
|
917
964
|
цепочке возвращает к её узлу: так из одной связи можно пройти в несколько сторон. `db.entity(строка | цепочка)`
|
|
918
965
|
вставляет готовый узел в путь.
|
|
919
966
|
|
|
@@ -936,10 +983,11 @@ try {
|
|
|
936
983
|
|
|
937
984
|
`delete()` без `{ confirm: true }` — только превью: всё замыкание каскада. В каждой строке превью `$action`:
|
|
938
985
|
`delete`, `unset` или `restrict`, и `$depth` — уровень каскада (0 — сама цель; у строк `unset` и `restrict` его
|
|
939
|
-
нет). С подтверждением удаляются все цели
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
`acl.checkData(класс, 'DELETE')`.
|
|
986
|
+
нет). С подтверждением удаляются все цели шага одним оператором, а каскад применяет правила концов. Итог —
|
|
987
|
+
надгробия по уровням каскада, внутри уровня по id (в таком порядке они и в журнале); цели классов без журнала
|
|
988
|
+
(`history: false`) — в конце. Превью не учитывает права: строка с `$action: 'delete'` может не удалиться у
|
|
989
|
+
пользователя без права (`acl_denied`), поэтому интерфейсу стоит спросить `acl.checkData(класс, 'DELETE')`.
|
|
990
|
+
Правила концов:
|
|
943
991
|
|
|
944
992
|
- `restrict` — удаление запрещено, пока на цель ссылаются (`delete_restricted` со списком видимых строк и числом
|
|
945
993
|
остальных);
|
|
@@ -1221,7 +1269,7 @@ const changed = await adm.load({ Shop: [{ name: 'Василёк', city: 'Сам
|
|
|
1221
1269
|
| Опция | Значения |
|
|
1222
1270
|
|---|---|
|
|
1223
1271
|
| `duplicates` — повтор id внутри загрузки | `'error'` (по умолчанию) — отменить; `'first'` или `'last'` — оставить первую или последнюю строку |
|
|
1224
|
-
| `existing` — объект уже есть в базе | `'error'` (по умолчанию) — отменить; `'skip'` — пропустить, удалённые не воскрешать; `'update'` — новая версия, если содержимое отличается (неизменённые считаются в `skipped`), удалённые — создать
|
|
1272
|
+
| `existing` — объект уже есть в базе | `'error'` (по умолчанию) — отменить; `'skip'` — пропустить, удалённые не воскрешать; `'update'` — новая версия, если содержимое отличается (неизменённые считаются в `skipped`), удалённые — создать заново; объект другого класса с тем же id (класс без ключа после `reclass`) — `exists` |
|
|
1225
1273
|
|
|
1226
1274
|
### Порции, история и проверка
|
|
1227
1275
|
|
|
@@ -1389,8 +1437,10 @@ const viaTelegram = await svc.auth.lookup({ kind: 'TELEGRAM', identifier: '10050
|
|
|
1389
1437
|
<!-- run: guide -->
|
|
1390
1438
|
```ts
|
|
1391
1439
|
const shortToken = await svc.auth.sessionFor(BORIS, { ttl: 600 }) // сессия на 10 минут простоя — для другого процесса
|
|
1440
|
+
const whose = await svc.auth.session(shortToken) // { account, tenant, member, expiresAt, loginAt, methods }
|
|
1392
1441
|
const boris = await connect({ dsn: APP, schema: r.schema, token: shortToken })
|
|
1393
1442
|
await boris.auth.revoke() // выход: сессия погашена
|
|
1443
|
+
const ended = await svc.auth.session(shortToken) // null
|
|
1394
1444
|
await boris.close()
|
|
1395
1445
|
```
|
|
1396
1446
|
|
|
@@ -1403,13 +1453,23 @@ await boris.close()
|
|
|
1403
1453
|
старые сессии того же вида входа без отметки ключа); текущая сессия остаётся, если не передать
|
|
1404
1454
|
`keepCurrent: false`.
|
|
1405
1455
|
|
|
1456
|
+
Сервис, у которого есть токен пользователя (например, в cookie), узнаёт о сессии через `svc.auth.session(token)`:
|
|
1457
|
+
подключение по этому токену не открывается, срок сессии не продлевается. Ответ — чья сессия (`account`), в каком
|
|
1458
|
+
арендаторе она работает (`tenant`: выбранный `auth.switch()` или собственный арендатор аккаунта), цело ли там членство
|
|
1459
|
+
(`member`; в собственном арендаторе — всегда `true`), когда она истечёт (`expiresAt`), когда и как был вход
|
|
1460
|
+
(`loginAt`, `methods`: `PASSWORD`, `APIKEY`, `SERVICE`, `TOTP` …). Неверный, погашенный или истёкший токен и
|
|
1461
|
+
отключённый аккаунт — `null`, причина не различается. Так веб-сервис проверяет сессию в начале каждого запроса:
|
|
1462
|
+
чтение по погашенной сессии ошибки не даёт, ответы просто пустые. Вызывает метод сервис или системный администратор
|
|
1463
|
+
(право `auth.session`); пользователю отвечают `acl_denied`.
|
|
1464
|
+
|
|
1406
1465
|
### Работа в другом арендаторе
|
|
1407
1466
|
|
|
1408
1467
|
Сервис действует от имени пользователя через `svc.as(account, { tenant, owner })`. База проверяет право сервиса
|
|
1409
1468
|
`auth.impersonate` (по правилам System), что пользователь существует и включён и что арендатор ему доступен —
|
|
1410
1469
|
собственный или по членству. От имени System, сервисов, системных администраторов и отключённых аккаунтов
|
|
1411
1470
|
действовать нельзя. Видимость пользователя по правилам доступа не проверяется. В журнале `author` — пользователь,
|
|
1412
|
-
`agent` — сервис.
|
|
1471
|
+
`agent` — сервис. Реестр классов `db.as()` — один на арендатора, подключение держит их не больше 1000: давно не
|
|
1472
|
+
нужный вытесняется, а фасад, чей реестр вытеснен, продолжает работать — реестр перечитывается. Подключение
|
|
1413
1473
|
пользователя по своему токену переключается между арендаторами так:
|
|
1414
1474
|
|
|
1415
1475
|
<!-- run: guide -->
|
|
@@ -1510,7 +1570,14 @@ const canReport = await asOleg.acl.check('shop.reports.daily') // { allow:
|
|
|
1510
1570
|
|
|
1511
1571
|
`db.auth.purgeAccount(аккаунт)` удаляет аккаунт и данные его арендатора. Вызывать может системный администратор или
|
|
1512
1572
|
владелец его арендатора; себя и последнего владельца арендатора удалить нельзя. Владение строками в чужих
|
|
1513
|
-
арендаторах переходит этим арендаторам, сессии гаснут, креды удаляются.
|
|
1573
|
+
арендаторах переходит этим арендаторам, сессии гаснут, креды удаляются. Владелец может стереть и арендатора, в
|
|
1574
|
+
котором сейчас работает: его сессия вернётся в личный арендатор.
|
|
1575
|
+
|
|
1576
|
+
Системный администратор может удалить и саму строку аккаунта — `db.Account(id).delete({ confirm: true })` в
|
|
1577
|
+
арендаторе System; данные арендатора аккаунта при этом остаются. Любой путь сразу стирает строки аккаунта в `secret`:
|
|
1578
|
+
сессии, хэши паролей и ключей, секреты TOTP, внешние учётные записи (внешний ключ `secret.account` с каскадом,
|
|
1579
|
+
ревизия 7). Сессии гаснут до этого с сигналом, и процессы сбрасывают кэш ответов этих сессий. Аккаунт System
|
|
1580
|
+
не удаляется (`acl_denied`): ему принадлежат служебные строки установки в `secret`.
|
|
1514
1581
|
|
|
1515
1582
|
## 11. История и срезы на дату
|
|
1516
1583
|
|
|
@@ -1531,6 +1598,11 @@ const followed = await db.Product(mug2).versions({ follow: true }) // и чер
|
|
|
1531
1598
|
`limit()` ограничивает их в самом запросе: `db.Product(kettle).sort('at', 'desc').limit(5).versions()` — пять
|
|
1532
1599
|
последних версий, новые первыми.
|
|
1533
1600
|
|
|
1601
|
+
`follow: true` отдаёт историю объекта на всех его id: до переноса — версии прежнего id, после — нового. Объект,
|
|
1602
|
+
который потом завели на прежнем id (тот же ключ после `rekey`), — другой, и его версий в этой истории нет; его
|
|
1603
|
+
собственная история с `follow` начинается с его создания. Так же идут `diff({ follow: true })` и `blame()`. Без
|
|
1604
|
+
`follow` `versions()` отдаёт все версии id, чьи бы они ни были.
|
|
1605
|
+
|
|
1534
1606
|
### Срез на дату
|
|
1535
1607
|
|
|
1536
1608
|
<!-- run: guide -->
|
|
@@ -1627,7 +1699,8 @@ const diffed: DiffResult = {
|
|
|
1627
1699
|
скаляр или массив в данных, пустой объект, роль одиночного конца, каждый id множественного конца
|
|
1628
1700
|
(`['links', роль, id]`), каждый тег (`['tags', тег]`), `owner` и `class`. `rev`, `at`, `author`, `agent`, `op` и
|
|
1629
1701
|
`reason` — от версии, которая последней изменила лист. Пустые `agent` и `reason` опускаются; `id` есть, только если
|
|
1630
|
-
это версия прежнего id (история прошла через `reclass` или `rekey`).
|
|
1702
|
+
это версия прежнего id (история прошла через `reclass` или `rekey`). Версии другого объекта на том же id — того,
|
|
1703
|
+
что жил там до переноса или заведён после, — `blame()` не смотрит.
|
|
1631
1704
|
- `exact: false` — между текущей версией и найденной есть пропуски (история прорежена, обрезана или часть версий не
|
|
1632
1705
|
видна): лист мог менять и выпавшая версия. `complete: true` — история просмотрена целиком и без пропусков; по
|
|
1633
1706
|
умолчанию просматривается не больше 1000 последних версий.
|
|
@@ -1671,7 +1744,7 @@ const sub = db.watch({ from: 'start', classes: ['Product'] }) // весь жу
|
|
|
1671
1744
|
await db.Product(kettle).update({ price: 2100 }).rows()
|
|
1672
1745
|
let cursor = ''
|
|
1673
1746
|
for await (const ev of sub) { // ev: { cursor, row, id, class, op, rev, at }
|
|
1674
|
-
cursor =
|
|
1747
|
+
cursor = sub.cursor! // курсор подписки: watch({ from: cursor }) продолжит после этого события
|
|
1675
1748
|
if (ev.id === kettle.id && ev.row.data.price === 2100) break // break (или sub.close()) — остановить
|
|
1676
1749
|
}
|
|
1677
1750
|
```
|
|
@@ -1689,10 +1762,13 @@ for await (const ev of sub) { // ev: { cursor, row, id, class, op, r
|
|
|
1689
1762
|
порядке записи. Это не порядок фиксации: версии одного объекта сверяйте по `rev`.
|
|
1690
1763
|
- Долгая пишущая транзакция в любой базе кластера задерживает поток: `await sub.lag()` показывает отставание в мс.
|
|
1691
1764
|
- Курсор старше яруса `all` политики хранения — `cursor_expired`: перечитайте текущее состояние и подпишитесь
|
|
1692
|
-
с `'now'`.
|
|
1765
|
+
с `'now'`. Время в курсоре подписки `sub.cursor` — время последнего события, а когда подписка догнала журнал, —
|
|
1766
|
+
время опроса, поэтому подписка, к которой сутки не было событий, не устаревает. Курсор события `ev.cursor` хранит
|
|
1767
|
+
время самого события.
|
|
1693
1768
|
- Без `LISTEN` (`connect({ listen: false })`) подписка опрашивает журнал.
|
|
1694
|
-
- Подписка без потерь — это сохранённый курсор: храните `
|
|
1695
|
-
с него после перезапуска.
|
|
1769
|
+
- Подписка без потерь — это сохранённый курсор: обработав событие, храните `sub.cursor` (позиция после последнего
|
|
1770
|
+
выданного события) и продолжайте с него после перезапуска. С `ev.cursor` поток тоже продолжится, но после затишья
|
|
1771
|
+
дольше яруса `all` такой курсор старше яруса, и продолжение с него — `cursor_expired`.
|
|
1696
1772
|
|
|
1697
1773
|
### Проекторы
|
|
1698
1774
|
|
|
@@ -1774,7 +1850,9 @@ sales.close() // stop() после текущ
|
|
|
1774
1850
|
`out.inc(класс, строка, приращения, { tenant })` создаёт строку или дополняет её и прибавляет числа, не читая
|
|
1775
1851
|
строку (поля, которых нет, считаются от 0). `out.delete(класс, ключ или id, { tenant })` удаляет строку, а если её
|
|
1776
1852
|
нет, ничего не делает. `await out.get(класс, ключ или id, { tenant })` читает текущую строку вместе с записями
|
|
1777
|
-
пачки.
|
|
1853
|
+
пачки. Строку класса с ключом `delete` и `get` находят по полям ключа (`{ day: '2026-10-29' }`), а по id (строкой
|
|
1854
|
+
или `{ $id }`) — только строку класса без ключа; один id у класса с ключом — `invalid_data`, и пачка откатывается.
|
|
1855
|
+
`out` пишет только в классы `into` и только в арендатора изменений, переданных в `handle`; иначе —
|
|
1778
1856
|
`derived_class` или `tenant_denied`, и пачка откатывается.
|
|
1779
1857
|
- Приращения строки, заданной одними полями ключа (или одним `$id`), `out` складывает до отправки в базу, пока между
|
|
1780
1858
|
ними нет `upsert`, `delete` или `get`: на строку — одна правка за пачку, а правило класса проверяется над итогом её
|
|
@@ -1822,9 +1900,13 @@ sales.close() // stop() после текущ
|
|
|
1822
1900
|
выбирает он: пользователь арендатора сам к проектору не подключится. Опция
|
|
1823
1901
|
`tenants: [id, …]` — явный список участников: проектор работает только с этими арендаторами (их классы `into` тоже
|
|
1824
1902
|
должны быть помечены), а пометку у остальных не учитывает. Источники — семейства `from` у этих арендаторов; класс
|
|
1825
|
-
|
|
1826
|
-
журнала у него нет. Поэтому арендатор, у которого такой класс среди
|
|
1827
|
-
его производных строк проектор не строит. Включите журнал класса и перестройте арендатора `rebuild({ tenant })`.
|
|
1903
|
+
из `into` источником не бывает, даже если попадает в `from`: свои результаты проектор не читает. Изменений класса
|
|
1904
|
+
с `history: false` проектор не видит: журнала у него нет. Поэтому арендатор, у которого такой класс среди
|
|
1905
|
+
источников, выбывает с кодом `invalid_class`, и его производных строк проектор не строит. Включите журнал класса и перестройте арендатора `rebuild({ tenant })`.
|
|
1906
|
+
- Арендатор, который стал участником после первой проекции (администратор пометил его классы позже), получает
|
|
1907
|
+
начальную перестройку пачками: в проекцию попадают и строки, заведённые до вступления. Состав участников проектор
|
|
1908
|
+
перечитывает по сигналу классов, а на подключении без LISTEN (`listen: false`) — в начале `run()` и не реже раза
|
|
1909
|
+
в 5 секунд.
|
|
1828
1910
|
- `batch` — сколько изменений в пачке (500), `pollMs` — запасной опрос (1000 мс): сигналы источников будят
|
|
1829
1911
|
проектор раньше.
|
|
1830
1912
|
- `lockTimeout` — сколько транзакция проектора ждёт чужую блокировку строки (5000 мс; 0 — без предела). Транзакция
|
|
@@ -1914,7 +1996,9 @@ const stats = db.cache.stats() // { size, hits,
|
|
|
1914
1996
|
- изменение прав и классов, запись через `db.sql`, погашенная сессия и переподключение слушателя сбрасывают весь
|
|
1915
1997
|
кэш или ответы сессии;
|
|
1916
1998
|
- срок по умолчанию — 60 секунд; внутри `db.begin()` кэш не работает;
|
|
1917
|
-
- без `LISTEN` и в режиме `none` кэш выключен, а `connect({ cache: { ttlOnly: true } })` включает
|
|
1999
|
+
- без `LISTEN` и в режиме `none` кэш выключен, а `connect({ cache: { ttlOnly: true } })` включает его: ответ живёт до
|
|
2000
|
+
конца срока, если его не сбросит запись этого подключения;
|
|
2001
|
+
- `db.load()`, проектор и `db.rebuildAggregates()` сбрасывают кэш, даже этого подключения, только сигналом базы.
|
|
1918
2002
|
|
|
1919
2003
|
Реестр классов процесс перечитывает по сигналу об изменении классов сразу (одно перечитывание за раз), а реестр и
|
|
1920
2004
|
ключ сигналов по `*`, по незнакомому токену и по нагрузке не своего режима (а без ключа в режиме `opaque` — по любому
|
|
@@ -1934,10 +2018,17 @@ const stats = db.cache.stats() // { size, hits,
|
|
|
1934
2018
|
const report = await adm.maintain() // { sessions, codes, thinned, batches, more, queue, skipped }
|
|
1935
2019
|
```
|
|
1936
2020
|
|
|
1937
|
-
`maintain()` удаляет
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2021
|
+
`maintain()` удаляет из таблицы `secret` сессии и одноразовые коды всех видов (`OTP` и свои `OTP_<ИМЯ>` из
|
|
2022
|
+
`issueOtp({ kind })`) — истёкшие, а также погашенные сессии и коды, которые уже использованы, заменены новым или
|
|
2023
|
+
исчерпали попытки, — и строки-замки удалённых классов (по две на класс; строку, которую держит транзакция, — следующим
|
|
2024
|
+
вызовом), прореживает журнал по политике хранения
|
|
2025
|
+
и проверяет очередь сигналов PostgreSQL: заполнение больше половины — предупреждение. Строки удалённого аккаунта
|
|
2026
|
+
`maintain()` ждать не нужно: их стирает само удаление ([«Удалить аккаунт»](#удалить-аккаунт)). Вызывать может системный
|
|
2027
|
+
администратор, роль с правом `letopis.maintain` или администраторское подключение. Запускает его внешний
|
|
2028
|
+
планировщик (`letopis maintain`) или сама библиотека: `connect({ maintain: '1h' })`. Интервал — число миллисекунд или
|
|
2029
|
+
строка (`'500ms'`, `'30s'`, `'15m'`, `'1h'`, `'1d'`) от 1 мс до 2³¹−1 мс (около 24 суток): другое значение, в том
|
|
2030
|
+
числе ноль, отрицательное или `NaN`, `connect()` отклоняет с `invalid_data`, не открывая соединений. `close()` ждёт
|
|
2031
|
+
идущий по расписанию запуск: он доделывает текущую пачку и следующую не начинает. Из нескольких процессов работает один:
|
|
1941
2032
|
каждая пачка — своя транзакция, и первым делом она берёт блокировку обслуживания — строку таблицы `secret`, которую
|
|
1942
2033
|
ни прикладная, ни посторонняя роль базы не займёт; блокировка снимается с фиксацией пачки. Если блокировку держит
|
|
1943
2034
|
другой процесс, вызов до первой пачки отвечает `skipped: true`, а между пачками останавливается (продолжит тот
|
|
@@ -2139,6 +2230,9 @@ letopis mcp --dsn <строка> --schema <v2.имя> --token <токен пол
|
|
|
2139
2230
|
(или LETOPIS_DSN, LETOPIS_SCHEMA, LETOPIS_TOKEN; ключ сервиса --api-key не принимается)
|
|
2140
2231
|
```
|
|
2141
2232
|
|
|
2233
|
+
`letopis types` и `letopis describe` строят результат по классам сессии, поэтому без действующей сессии (токен истёк,
|
|
2234
|
+
погашен, неверен или не задан, ключ API неверен) выходят с кодом 1 и ошибкой `no_session`, а файл `--out` не трогают.
|
|
2235
|
+
|
|
2142
2236
|
### Наблюдение
|
|
2143
2237
|
|
|
2144
2238
|
- `connect({ onQuery, slowMs })`: хук получает на каждый запрос цепочки режим, классы, длительность, число строк и
|
|
@@ -2256,17 +2350,19 @@ npx letopis import --from postgres://…/old --from-schema v1.salon --dsn postgr
|
|
|
2256
2350
|
| `link`, `lookup` | `{ account, kind, identifier, meta?, confirmed? }` → id; `{ kind, identifier, requireConfirmed?, ttl?, totp? }` → вход или `null` |
|
|
2257
2351
|
| `credentials`, `revokeCredential`, `setFlags` | `(account)` → креды без секретов; `(id, { keepCurrent? })`; `(id, { confirmed?, enabled? })` |
|
|
2258
2352
|
| `sessionFor`, `refresh`, `revoke`, `revokeAll` | `(account, { ttl? })` → токен; `({ ttl? })` → новое время истечения (токен прежний); `()`; `(account?)` → число сессий |
|
|
2353
|
+
| `session` | `(token)` → `{ account, tenant, member, expiresAt, loginAt, methods }` или `null` |
|
|
2259
2354
|
| `tenants`, `switch` | `()` → `[{ tenant, roles }]`; `(tenant)` |
|
|
2260
2355
|
| `purgeAccount` | `(account)` → `{ account, rows, versions, memberships, owned }` |
|
|
2261
2356
|
|
|
2262
|
-
Проверку паролей, кодов и TOTP, выпуск
|
|
2263
|
-
пользователю отвечают `acl_denied`. Креды System, сервисов и системных администраторов
|
|
2264
|
-
после недавнего интерактивного входа или системный администратор. Исключение — ключи
|
|
2265
|
-
пары «ключ + секрет» сервис выпускает и отзывает сам по сессии своего ключа, прочие свои
|
|
2266
|
-
действующий ключ сервиса не отзывается никем (`acl_denied`). С `totp` вход `verifyPassword` и
|
|
2267
|
-
ещё и код TOTP (право `auth.totp`) и выдаёт сессию со способом `TOTP`.
|
|
2357
|
+
Проверку паролей, кодов и TOTP, выпуск сессий, сведения о сессии по токену и подтверждение кредов вызывает сервис
|
|
2358
|
+
(права `auth.*`); обычному пользователю отвечают `acl_denied`. Креды System, сервисов и системных администраторов
|
|
2359
|
+
меняет только сам аккаунт после недавнего интерактивного входа или системный администратор. Исключение — ключи
|
|
2360
|
+
сервиса: свои ключи API и пары «ключ + секрет» сервис выпускает и отзывает сам по сессии своего ключа, прочие свои
|
|
2361
|
+
креды — нет; последний действующий ключ сервиса не отзывается никем (`acl_denied`). С `totp` вход `verifyPassword` и
|
|
2362
|
+
`lookup` проверяет ещё и код TOTP (право `auth.totp`) и выдаёт сессию со способом `TOTP`.
|
|
2268
2363
|
Вид креда `kind` — `^[A-Z][A-Z0-9_]{1,31}$` (`PASSWORD`, `EMAIL`, `TELEGRAM` …); `SESSION`, `APIKEY`, `KEYSECRET`,
|
|
2269
|
-
`OTP`, `TOTP` и `
|
|
2364
|
+
`OTP`, `TOTP`, `KEY`, `PROJECTOR` и `LOCK` заняты. То же правило — у вида пароля `user.kind` в `createTenant` и
|
|
2365
|
+
`up({ tenant })`.
|
|
2270
2366
|
|
|
2271
2367
|
### Проектор
|
|
2272
2368
|
|
|
@@ -2283,7 +2379,7 @@ npx letopis import --from postgres://…/old --from-schema v1.salon --dsn postgr
|
|
|
2283
2379
|
| `stop()`, `close()` | остановить `run()` после текущей пачки; то же и отписаться от сигналов |
|
|
2284
2380
|
| `ProjectorReport` | `{ batches, changes, created, updated, deleted, unchanged, skipped, rebuilds, locked }` |
|
|
2285
2381
|
| `ProjectorChange` | `{ cursor, id, class, tenant, op, rev, at, author, row }` и, если есть, `agent`, `reason`, `moved`, `before`, `rebuild` |
|
|
2286
|
-
| `ProjectorOut` | `upsert(класс, строка, { tenant })`, `inc(класс, строка, приращения, { tenant })`, `delete(класс, ключ или id, { tenant })`, `get(класс, ключ или id, { tenant })` → `Row \| null`; строка — `ProjectorRow` в формате
|
|
2382
|
+
| `ProjectorOut` | `upsert(класс, строка, { tenant })`, `inc(класс, строка, приращения, { tenant })`, `delete(класс, ключ или id, { tenant })`, `get(класс, ключ или id, { tenant })` → `Row \| null`; строка — `ProjectorRow` в формате загрузчика; у класса с ключом `delete` и `get` — по ключу, id — только у класса без ключа |
|
|
2287
2383
|
|
|
2288
2384
|
### Строка ответа
|
|
2289
2385
|
|
|
@@ -2300,8 +2396,10 @@ npx letopis import --from postgres://…/old --from-schema v1.salon --dsn postgr
|
|
|
2300
2396
|
|
|
2301
2397
|
### Зарезервированные имена
|
|
2302
2398
|
|
|
2303
|
-
|
|
2304
|
-
именем
|
|
2399
|
+
Имя метода закрывает класс только там, где этот метод есть: после шага — методы цепочки, у корня `db` — его методы,
|
|
2400
|
+
у батча — его. Поэтому хаб с именем метода только цепочки (например, `count`) доступен первым шагом `db.count(…)`, а
|
|
2401
|
+
после другого шага недостижим; хаб с именем метода корня первым шагом не вызвать. Связь с любым из этих имён база не
|
|
2402
|
+
примет (`reserved_names()`; список подставляет установщик из `RESERVED_CLASS_NAMES`). Не давайте классам эти имена.
|
|
2305
2403
|
|
|
2306
2404
|
- методы цепочки: `then`, `entity`, `paths`, `rows`, `first`, `ids`, `count`, `limit`, `offset`, `sort`, `asOf`,
|
|
2307
2405
|
`withDeleted`, `deep`, `exact`, `sum`, `avg`, `min`, `max`, `countBy`, `after`, `versions`, `create`, `update`,
|
|
@@ -2358,7 +2456,7 @@ npx letopis import --from postgres://…/old --from-schema v1.salon --dsn postgr
|
|
|
2358
2456
|
| `tx_rolled_back` | PostgreSQL ответил `ROLLBACK` на `COMMIT`: ошибку перехватили внутри `tr.transaction(fn)` |
|
|
2359
2457
|
| `no_key` | `upsert`, `rekey` или `idOf` у класса без ключа |
|
|
2360
2458
|
| `no_path` | между классами соседних шагов нет перехода |
|
|
2361
|
-
| `invalid_query` | цепочку или API используют неверно (модификатор шага `deep`, `exact`, `alias`, `tags` или `owner` после глагола, слот не после глагола, курсор `after()` со значением не того типа, что поле `sort`, без `v` или с `id` не uuid и т. п.) |
|
|
2459
|
+
| `invalid_query` | цепочку или API используют неверно (модификатор шага `deep`, `exact`, `alias`, `tags` или `owner` после глагола, модификатор выборки между глаголом и терминалом `rows`, `first`, `ids`, `count` или `paths`, слот не после глагола, курсор `after()` со значением не того типа, что поле `sort`, без `v` или с `id` не uuid и т. п.) |
|
|
2362
2460
|
| `admin_required` | загрузчику, проектору и `rebuildAggregates` нужно администраторское подключение |
|
|
2363
2461
|
| `rebuild_required` | проекция построена другой версией проектора (`version`): нужна перестройка `rebuild()`; `rebuild({ tenant })`, пока проекции нет или идёт перестройка пачками |
|
|
2364
2462
|
| `projector_busy` | перестройку не начать: проектор сейчас работает в другом процессе |
|
|
@@ -2431,7 +2529,7 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2431
2529
|
**Платформа**
|
|
2432
2530
|
|
|
2433
2531
|
- Нужен PostgreSQL 18 или новее; обходных путей для 15–17 нет. AWS RDS, Google Cloud SQL, Azure Flexible Server
|
|
2434
|
-
и Neon поддерживают 18, а Supabase на октябрь 2026 года — только 15 и 17, поэтому там
|
|
2532
|
+
и Neon поддерживают 18, а Supabase на октябрь 2026 года — только 15 и 17, поэтому там letopis не заработает.
|
|
2435
2533
|
- База — в кодировке UTF8 (`casefold()` и коллация `pg_unicode_fast`). Если новая основная версия PostgreSQL
|
|
2436
2534
|
обновит таблицы Unicode, уникальный индекс идентификаторов кредов перестраивается командой `reindex`.
|
|
2437
2535
|
- PGlite — одно соединение суперпользователя: без параллельных писателей и `LISTEN`, RLS не действует
|
|
@@ -2489,8 +2587,8 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2489
2587
|
- Горячие цели ссылок (организация, на которую ссылается почти всё) копят блокировки `for key share` и MultiXact,
|
|
2490
2588
|
как при обычных внешних ключах; это нужно мониторить. Операции одного кошелька идут друг за другом под
|
|
2491
2589
|
блокировкой его строки ([раздел 8](#8-транзакции-батчи-и-деньги), «Рецепт: кошелёк»).
|
|
2492
|
-
- Связь не может называться зарезервированным именем, а хаб с таким именем
|
|
2493
|
-
([«Зарезервированные имена»](#зарезервированные-имена)).
|
|
2590
|
+
- Связь не может называться зарезервированным именем, а хаб с таким именем доступен не везде: например, `count` —
|
|
2591
|
+
только первым шагом ([«Зарезервированные имена»](#зарезервированные-имена)).
|
|
2494
2592
|
|
|
2495
2593
|
**Доступ**
|
|
2496
2594
|
|
|
@@ -2629,6 +2727,12 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2629
2727
|
Раздел — для тех, кто развивает сам letopis. Запуск: `cd lib && npm test`. Обвязка сама поднимает контейнер `letopis-pg18` (`postgres:18`,
|
|
2630
2728
|
порт 15433), если база не отвечает; каждый тестовый файл работает в своей схеме `v2.t_<имя>`.
|
|
2631
2729
|
|
|
2730
|
+
В CI тестов нет: перед пушем и выпуском всё, что проверяет поведение, идёт одной командой `cd lib && npm run prerelease` —
|
|
2731
|
+
по очереди typecheck, сверка документов, все тесты, примеры сайта со снимками, песочница, сценарии сервисов, быстрый
|
|
2732
|
+
долгий прогон (15 минут смешанной нагрузки) и проверка пакета с живым прогоном; первый красный шаг останавливает
|
|
2733
|
+
прогон. Та же нагрузка на сутки — `cd lib && npm run soak` (длительность — `SOAK_HOURS`, по умолчанию 24 часа). Тест
|
|
2734
|
+
исправления называется пунктом тест-плана («план 2.0, 7.3 №1») и без исправления падает.
|
|
2735
|
+
|
|
2632
2736
|
```
|
|
2633
2737
|
# connect — подключение под сессией, отказ ролям в обход RLS, реестр, отложенный сигнал, перевод ошибок (этап 2)
|
|
2634
2738
|
# context — сессии, вход сервиса по ключу, политики RLS этапа 1 (этап 1)
|
|
@@ -2675,9 +2779,9 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2675
2779
|
# retention — политика хранения: ярусы, конечный срок, особые версии, удалённые, asOf, cursor_expired, часовой пояс, догоняющий запуск, наследование и умолчание — против эталона на TypeScript (этап 6)
|
|
2676
2780
|
# watch — подписка по курсору: порядок фиксации, долгая транзакция, возобновление, обрыв слушателя, два арендатора (этап 6)
|
|
2677
2781
|
# cache — кэш результатов: попадание, свои и чужие записи, ключ с пользователем, права, погашенные сессии, обрыв слушателя, ttl, пометка класса, без LISTEN, гонка, db.sql, предел счётчиков сигналов (этап 6; план 1.1, точка А)
|
|
2678
|
-
# maintain — обслуживание: истёкшие сессии и коды, права, один процесс под блокировкой, расписание (этап 6)
|
|
2782
|
+
# maintain — обслуживание: истёкшие сессии и коды, права, один процесс под блокировкой, расписание (этап 6), строки-замки удалённых классов
|
|
2679
2783
|
# up — полный установщик: свежая установка, повторный накат, свои сиды, автосоздание базы, UTF8, версия 17, upgrade с миграциями, PGlite, первый арендатор и пользователь (up({ tenant }), createTenant) (этапы 6, 8)
|
|
2680
|
-
# upgrade — обновление схемы с 1.1.0 (
|
|
2784
|
+
# upgrade — обновление схемы с 1.4.1 (обычный up() без миграций, план 2.0, 7.8 №2), с 1.1.0 (upgrade: true, миграция 0007) и с 1.0.1 (upgrade: true, миграции 0006 и 0007) пакетами letopis-141, letopis-11 и letopis-1: версии и хэши прежнего движка, verify(), каталог против чистой установки, строки-замки, строки secret без аккаунта стёрты, приложение, подписка и проектор по старым данным, повторный up() (доработки после 1.2.0, этап 1; ревизия 7)
|
|
2681
2785
|
# scenario-salon — сквозной сценарий салона: 21 акт 0.21 (salon) с матрицей покрытия API 1.0 (этап 6)
|
|
2682
2786
|
# telemetry — onQuery и slowMs, describe() и метаданные классов с наследованием, describeText (этап 6)
|
|
2683
2787
|
# history — diff и blame по журналу, причина изменения у глаголов, db.begin() и батча; версия, скрытая условием ACL, колонка changed, пропуск версии, row_changes на 20 тыс. целей (план 1.1, этап 1 и точка А)
|
|
@@ -2687,4 +2791,10 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2687
2791
|
# mcp — инструменты MCP: модель и meta только в результатах, права арендатора, JSON-цепочка и подсказки похожих имён, предел строк и байт с курсором, обёртка недоверенных данных, запись только с write, update только с rev, удаление с подтверждением человека (elicitation в ревизиях 2025 и 2026-07-28), причина в blame, учётные данные, схемы инструментов, letopis без SDK; letopis mcp по stdio, обработчик HTTP; окно подтверждения без алиаса и meta, countBy и versions с пределом в самом запросе, предел шагов и условий $or; классы прав — только с allowAclWrite, сессия в арендаторе System — только с allowSystemAdmin; structuredContent с пояснением, подпись requestState пользователя сервера со сроком (план 1.1, этап 5; ревью безопасности №2; точка Б и доработки после неё)
|
|
2688
2792
|
# import — импорт из 0.21 по фикстуре настоящей 0.21: нарушение ключа, --keyless, счётчики по классам, verify, самоссылка, повтор, id без ключа, висячая ссылка, $$strict и поля на $, update, креды, CLI (этап 7)
|
|
2689
2793
|
# security-final — итоговое ревью безопасности (этап 8, §7.10 №8.4): блокировка семейства по арендатору корня, account_purge без права и restore/purge чужого id не дают ни утечки, ни блокировки; функции этапа 6, log_refs_at, срезы find_* по журналу и watch_read не выходят за свой арендатор; NOTIFY несёт только имена или токены режима; ключ кэша различает токен, актора и арендатора (этап 8)
|
|
2794
|
+
# semver — правила версий: check:semver требует шаг версии не меньше нужного — мажор за исчезнувший метод или код ошибки, минор за новый метод и новую ревизию движка, патча хватает на тексты (план 2.0, 7.8 №1)
|
|
2795
|
+
# prerelease — проверка перед выпуском: шаги идут строго по очереди, первый красный останавливает прогон, следующие не запускаются (план 2.0, 7.8 №3)
|
|
2796
|
+
# write-contract — обещания README о записи (план 2.0, 7.2): повтор ключа в батче — exists и откат, порядок строк ответа reclass, restore и rekey, цепочка с глаголом в значении слота, загрузчик с existing: update не правит строку другого класса, ответ из кэша не портится правкой прошлого ответа
|
|
2797
|
+
# watch-contract — обещания README о подписке и подключении (план 2.0, 7.4): молчащая подписка опрашивает журнал раз в pollMs, close() подключения закрывает живые подписки, reclass без ключа сбрасывает кэш прежнего класса, новый участник проектора получает начальную перестройку, проектор без LISTEN перечитывает состав, реестров db.as() не больше предела
|
|
2798
|
+
# model-contract — обещания README о моделях, типах, CLI и ошибках (план 2.0, 7.5): letopis types — верный TypeScript для кириллицы, класса без полей и anyOf, полная форма hub() с z.object, понятные ошибки вместо сырых, коды выхода CLI
|
|
2799
|
+
# read-contract — обещания README о чтении и истории (план 2.0, 7.1): срез asOf по классу и по концам после смены класса без смены id, versions, diff и blame с follow после rekey, курсор after() в versions() и paths(), имя поля с @S@
|
|
2690
2800
|
```
|
package/dist/acl.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Executor } from './tx.js';
|
|
2
2
|
export type AclOp = 'READ' | 'WRITE' | 'DELETE';
|
|
3
|
-
/** Решение по эндпоинту
|
|
3
|
+
/** Решение по ресурсу приложения (эндпоинту): можно ли к нему обратиться. */
|
|
4
4
|
export interface AclDecision {
|
|
5
5
|
allow: boolean;
|
|
6
|
-
/** Победившее правило: id, группа и объект (alias ресурсов), вес, разрешение. */
|
|
6
|
+
/** Победившее правило: id, группа и объект (`alias` ресурсов), вес, разрешение. */
|
|
7
7
|
rule?: {
|
|
8
8
|
id: string;
|
|
9
9
|
group: string;
|
|
@@ -11,7 +11,7 @@ export interface AclDecision {
|
|
|
11
11
|
weight: number;
|
|
12
12
|
permission: 'allow' | 'deny';
|
|
13
13
|
};
|
|
14
|
-
/** Код и сообщение запрещающего правила (поля code
|
|
14
|
+
/** Код и сообщение запрещающего правила (поля `code`, `message` строки `rule`). */
|
|
15
15
|
code?: number;
|
|
16
16
|
message?: string;
|
|
17
17
|
}
|
|
@@ -21,9 +21,15 @@ export interface AclDataDecision {
|
|
|
21
21
|
filter?: Record<string, unknown>;
|
|
22
22
|
}
|
|
23
23
|
export interface AclApi {
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Можно ли тому, от чьего имени идёт запрос, обратиться к ресурсу приложения (эндпоинту). Решают правила доступа
|
|
26
|
+
* System и текущего арендатора.
|
|
27
|
+
*/
|
|
25
28
|
check(endpoint: string): Promise<AclDecision>;
|
|
26
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Можно ли тому, от чьего имени идёт запрос, выполнять операцию `op` с объектами класса — со всеми или только с теми,
|
|
31
|
+
* что подходят под условие `filter` в ответе.
|
|
32
|
+
*/
|
|
27
33
|
checkData(className: string, op: AclOp): Promise<AclDataDecision>;
|
|
28
34
|
}
|
|
29
35
|
export declare function makeAcl(S: string, exec: Executor): AclApi;
|