letopis 1.3.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 +14 -6
- package/AGENT-CHEATSHEET.md +18 -10
- package/CHANGELOG.md +247 -0
- package/README.en.md +146 -58
- package/README.md +129 -51
- 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 +18 -4
- 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 +44 -12
- package/dist/project.js +90 -8
- 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/sync.js +11 -5
- package/dist/tx.js +10 -1
- package/dist/types.d.ts +14 -7
- package/dist/types.js +13 -6
- package/dist/up.d.ts +65 -25
- package/dist/up.js +152 -40
- 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/llms.txt +1 -1
- package/package.json +1 -1
- package/sql/10-core.sql +6 -1
- package/sql/50-acl.sql +5 -1
- package/sql/60-write.sql +77 -22
- package/sql/80-auth.sql +39 -2
- package/sql/88-derived.sql +82 -1
- package/sql/90-time.sql +75 -38
- package/sql/95-seed.system.sql +6 -3
- package/sql/99-revision.sql +3 -2
- package/sql/README.md +2 -0
- package/sql/migrate/0007-secret-account-fk.sql +12 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# letopis 1.
|
|
1
|
+
# letopis 1.4
|
|
2
2
|
|
|
3
3
|
**letopis** — библиотека для Node.js, хранилище версионированных сущностей на PostgreSQL 18. Каждое изменение
|
|
4
4
|
сохраняется версией в журнале. Поэтому у любого объекта есть история, состояние на любую дату и подписка на
|
|
@@ -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,11 +363,21 @@ 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`), и ставит ключ.
|
|
375
|
+
|
|
376
|
+
Ревизия меняется не с каждой версией: у 1.1–1.4.0 она одна (6), а функции разные. Поэтому `up()` пишет в комментарий
|
|
377
|
+
схемы ещё и штамп движка: версию пакета и хэш его файлов SQL. Если штамп не совпадает с файлами пакета, `connect()`
|
|
378
|
+
предупреждает. Когда схему накатила более старая версия (или версия до 1.4, у которой штампа нет), он советует
|
|
379
|
+
`up({ schema })`. Когда схему уже обновила более новая версия, он советует обновить пакет: `up()` старой версией вернул
|
|
380
|
+
бы прежние функции. Новая версия пакета без правок SQL штамп не меняет, и предупреждения нет.
|
|
364
381
|
|
|
365
382
|
Первый `up()` после перехода на 1.2 строит индекс журнала `log_class_tenant_at (class, tenant, at)` вместо
|
|
366
383
|
`log_class_at`: срез `asOf` и `withDeleted` по классу больше не читает журнал других арендаторов с тем же именем
|
|
@@ -370,8 +387,8 @@ const adm = await connect({ dsn: ADMIN, schema: r.schema, token: r.tenant!.token
|
|
|
370
387
|
|
|
371
388
|
Обновление схем 1.1.0 и 1.0.1 проверяет тест `upgrade`. Схему ставит прежний пакет, данные пишет прежний движок, затем
|
|
372
389
|
вызывается `up()` текущей версии. После него версии и хэши прежнего движка на месте, `verify({ data: true })` чист, а
|
|
373
|
-
каталог схемы совпадает с чистой установкой.
|
|
374
|
-
|
|
390
|
+
каталог схемы совпадает с чистой установкой. Обе схемы обновляются через `up({ upgrade: true })`: 1.1.0 (ревизия 6) —
|
|
391
|
+
миграцией `0007`, 1.0.1 (ревизия 5) — миграциями `0006` и `0007`.
|
|
375
392
|
|
|
376
393
|
### Разработка без PostgreSQL: PGlite
|
|
377
394
|
|
|
@@ -550,12 +567,18 @@ const Movement = link('Movement', { amount: z.number().int() }, { ends: { Wallet
|
|
|
550
567
|
- Описание проверяет база (`invalid_class`): поле есть в схеме, оно числовое и не входит в ключ; конец `via` —
|
|
551
568
|
одиночный и принимает класс цели; поле `sum` — число; агрегаты не образуют цикл; цель и источник — не системные и
|
|
552
569
|
не производные классы. Класс-источник описывают раньше агрегата: агрегат над классом, которого ещё нет, отклоняется.
|
|
553
|
-
У класса арендатора источник ищут среди классов арендатора. `syncModels` сам описывает источники раньше
|
|
570
|
+
У класса арендатора источник ищут среди классов арендатора. `syncModels` сам описывает источники раньше целей, а
|
|
571
|
+
модели по кругу (агрегат над собственным потомком, цикл агрегатов) отклоняет до записи (`invalid_class` с кругом).
|
|
554
572
|
Класс `of` не удаляется, пока его называет агрегат класса того же арендатора: сначала агрегат убирают из описания цели.
|
|
555
573
|
- Строки попадают в агрегат только с ведома того, кто читает их все: поле считает и строки, которые автору не видны.
|
|
556
574
|
Описать или изменить агрегат может только тот, кто без условий читает все строки источника и его потомков. Сделать
|
|
557
575
|
класс источником (новый потомок источника, смена `parent`) может только тот, кто без условий читает все
|
|
558
576
|
строки этого класса и его потомков — и до правки, и после. Иначе `acl_denied`.
|
|
577
|
+
- Новый источник проверяется и по тому, кто описал агрегат: он тоже должен без условий читать все строки класса.
|
|
578
|
+
Описавший — автор версии строки `Class` цели, с которой описание агрегата стало нынешним. Если он их не читает, класс
|
|
579
|
+
источником не станет (`acl_denied`), пока агрегат не опишет заново тот, кто читает. У агрегата класса System
|
|
580
|
+
описавший проверяется по классам System, как и при описании. Класс арендатора под агрегатом System проверяется
|
|
581
|
+
только у того, кто его заводит.
|
|
559
582
|
- Опция наследуется, потомок заменяет её целиком. Добавить или изменить агрегат — ужесточение: запись в цель и в
|
|
560
583
|
источники ждёт, пока поле пересчитывается у всех строк.
|
|
561
584
|
- `db.rebuildAggregates(класс)` пересчитывает поле из текущих строк — для ремонта. Версия пишется только там, где
|
|
@@ -590,8 +613,9 @@ npx letopis types --dsn "$APP" --schema v2.shop --token "$TOKEN" --out types.ts
|
|
|
590
613
|
### Имена классов
|
|
591
614
|
|
|
592
615
|
Шаг цепочки называется именем класса, его алиасом или именем роли. Имена методов цепочки и корня (`count`, `sort`,
|
|
593
|
-
`set`, `sql`, `auth` и другие)
|
|
594
|
-
|
|
616
|
+
`set`, `sql`, `auth` и другие) там, где такой метод есть, библиотека разбирает раньше классов: класс `count` доступен
|
|
617
|
+
первым шагом `db.count(…)`, но не после другого шага. Связь с таким именем база не примет. Не давайте классам эти
|
|
618
|
+
имена; полный список — в разделе
|
|
595
619
|
[«Зарезервированные имена»](#зарезервированные-имена).
|
|
596
620
|
|
|
597
621
|
## 5. Запись
|
|
@@ -645,7 +669,8 @@ await db.Shop(shop).Product({ price: gte(2000) }).update({ title: 'Чайник
|
|
|
645
669
|
`db.Product().update(…)` правит все товары арендатора. Шаг ничего не нашёл — ответ пустой и ошибки нет, поэтому
|
|
646
670
|
проверяйте длину ответа. Если `update` затронул меньше строк, чем нашёл шаг, весь план откатывается: строку успели
|
|
647
671
|
удалить — `target_not_found`, строка видна, но править её нельзя — `acl_denied`. Модификатор `.tags(…)` перед
|
|
648
|
-
`update`
|
|
672
|
+
`update` отбирает строки; строка или список ещё и заменяют их теги целиком, а оператор (`has`, `hasAny`, `hasAll`)
|
|
673
|
+
только отбирает — теги не меняются.
|
|
649
674
|
|
|
650
675
|
### Создать или заменить: `upsert`
|
|
651
676
|
|
|
@@ -777,8 +802,14 @@ await db.Order(order).line().create({ qty: 1, price: 900 }).Product.set(sencha).
|
|
|
777
802
|
- Весь план — одна транзакция уровня `read committed`. При взаимной блокировке (`40P01`), ошибке сериализации
|
|
778
803
|
(`40001`) и конфликте журнала библиотека повторяет план целиком, до трёх раз.
|
|
779
804
|
- Ошибка любого звена откатывает весь план. `exists` и `conflict` не повторяются: это ответы для приложения.
|
|
780
|
-
- Модификаторы до глагола (`limit`, `sort`) сужают его цели.
|
|
805
|
+
- Модификаторы до глагола (`limit`, `offset`, `sort`) сужают его цели.
|
|
806
|
+
- Модификатор после глагола относится к тому, что идёт за ним: к шагу класса, к `versions()`, `diff()`, `blame()` и
|
|
807
|
+
агрегатам или к целям следующего глагола. Если за ним сразу терминал `rows`, `first`, `ids`, `count` или `paths`,
|
|
808
|
+
сужать уже нечего — глагол прошёл по всем целям шага, — поэтому `db.Product().update(…).limit(1).rows()` даёт
|
|
809
|
+
`invalid_query` и ничего не пишет. В батче так же отказывает `run()`.
|
|
781
810
|
- Глагол сразу после глагола пишет в те же сущности.
|
|
811
|
+
- Строки ответа глагола идут в порядке его целей: по `sort()` до глагола, без него — по id; `first()` после глагола —
|
|
812
|
+
первая по этому порядку.
|
|
782
813
|
|
|
783
814
|
## 6. Чтение
|
|
784
815
|
|
|
@@ -822,7 +853,7 @@ const roots = await db.Category({ Parent: null }).rows() // корневы
|
|
|
822
853
|
| `ne(v)`, `gt(v)`, `gte(v)`, `lt(v)`, `lte(v)` | ≠ (с учётом `null`), >, ≥, <, ≤ |
|
|
823
854
|
| `between(a, b)`, `inList([…])` | a ≤ x ≤ b; значение из списка |
|
|
824
855
|
| `like(s)`, `ilike(s)`, `starts(s)`, `ends(s)` | шаблон с `%` и `_`; без учёта регистра; начинается с; заканчивается на |
|
|
825
|
-
| `has(v)`, `hasAny([…])`, `hasAll([…])` | поле-массив содержит значение; хотя бы одно; все |
|
|
856
|
+
| `has(v)`, `hasAny([…])`, `hasAll([…])` | поле-массив содержит значение; хотя бы одно; все (`hasAll([])` подходит любой строке, `hasAny([])` — никакой) |
|
|
826
857
|
| `exists(true \| false)`, `isNull()` | поле есть или нет; поле `null` или отсутствует |
|
|
827
858
|
| `not(v)` | отрицание оператора; скаляр — «не равно» |
|
|
828
859
|
| `or(f1, f2, …)` | любой из фильтров шага |
|
|
@@ -892,7 +923,9 @@ const byStatus = await db.Order().countBy('data.status') // { new: 1 }
|
|
|
892
923
|
```
|
|
893
924
|
|
|
894
925
|
Агрегаты считает база, по сущностям последнего шага. `limit(n)` и `offset(n)` перед `countBy()` относятся к группам:
|
|
895
|
-
`db.Order().limit(3).countBy('data.status')` — три самых частых значения.
|
|
926
|
+
`db.Order().limit(3).countBy('data.status')` — три самых частых значения. У логического поля `min()` — `false`, если
|
|
927
|
+
есть хоть одно `false` (все ли значения `true`), а `max()` — `true`, если есть хоть одно `true`; `sum()` и `avg()`
|
|
928
|
+
логическое поле не считают — PostgreSQL отвечает ошибкой.
|
|
896
929
|
|
|
897
930
|
### Пути
|
|
898
931
|
|
|
@@ -924,10 +957,11 @@ try {
|
|
|
924
957
|
|
|
925
958
|
`delete()` без `{ confirm: true }` — только превью: всё замыкание каскада. В каждой строке превью `$action`:
|
|
926
959
|
`delete`, `unset` или `restrict`, и `$depth` — уровень каскада (0 — сама цель; у строк `unset` и `restrict` его
|
|
927
|
-
нет). С подтверждением удаляются все цели
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
`acl.checkData(класс, 'DELETE')`.
|
|
960
|
+
нет). С подтверждением удаляются все цели шага одним оператором, а каскад применяет правила концов. Итог —
|
|
961
|
+
надгробия по уровням каскада, внутри уровня по id (в таком порядке они и в журнале); цели классов без журнала
|
|
962
|
+
(`history: false`) — в конце. Превью не учитывает права: строка с `$action: 'delete'` может не удалиться у
|
|
963
|
+
пользователя без права (`acl_denied`), поэтому интерфейсу стоит спросить `acl.checkData(класс, 'DELETE')`.
|
|
964
|
+
Правила концов:
|
|
931
965
|
|
|
932
966
|
- `restrict` — удаление запрещено, пока на цель ссылаются (`delete_restricted` со списком видимых строк и числом
|
|
933
967
|
остальных);
|
|
@@ -1377,8 +1411,10 @@ const viaTelegram = await svc.auth.lookup({ kind: 'TELEGRAM', identifier: '10050
|
|
|
1377
1411
|
<!-- run: guide -->
|
|
1378
1412
|
```ts
|
|
1379
1413
|
const shortToken = await svc.auth.sessionFor(BORIS, { ttl: 600 }) // сессия на 10 минут простоя — для другого процесса
|
|
1414
|
+
const whose = await svc.auth.session(shortToken) // { account, tenant, member, expiresAt, loginAt, methods }
|
|
1380
1415
|
const boris = await connect({ dsn: APP, schema: r.schema, token: shortToken })
|
|
1381
1416
|
await boris.auth.revoke() // выход: сессия погашена
|
|
1417
|
+
const ended = await svc.auth.session(shortToken) // null
|
|
1382
1418
|
await boris.close()
|
|
1383
1419
|
```
|
|
1384
1420
|
|
|
@@ -1391,6 +1427,15 @@ await boris.close()
|
|
|
1391
1427
|
старые сессии того же вида входа без отметки ключа); текущая сессия остаётся, если не передать
|
|
1392
1428
|
`keepCurrent: false`.
|
|
1393
1429
|
|
|
1430
|
+
Сервис, у которого есть токен пользователя (например, в cookie), узнаёт о сессии через `svc.auth.session(token)`:
|
|
1431
|
+
подключение по этому токену не открывается, срок сессии не продлевается. Ответ — чья сессия (`account`), в каком
|
|
1432
|
+
арендаторе она работает (`tenant`: выбранный `auth.switch()` или собственный арендатор аккаунта), цело ли там членство
|
|
1433
|
+
(`member`; в собственном арендаторе — всегда `true`), когда она истечёт (`expiresAt`), когда и как был вход
|
|
1434
|
+
(`loginAt`, `methods`: `PASSWORD`, `APIKEY`, `SERVICE`, `TOTP` …). Неверный, погашенный или истёкший токен и
|
|
1435
|
+
отключённый аккаунт — `null`, причина не различается. Так веб-сервис проверяет сессию в начале каждого запроса:
|
|
1436
|
+
чтение по погашенной сессии ошибки не даёт, ответы просто пустые. Вызывает метод сервис или системный администратор
|
|
1437
|
+
(право `auth.session`); пользователю отвечают `acl_denied`.
|
|
1438
|
+
|
|
1394
1439
|
### Работа в другом арендаторе
|
|
1395
1440
|
|
|
1396
1441
|
Сервис действует от имени пользователя через `svc.as(account, { tenant, owner })`. База проверяет право сервиса
|
|
@@ -1498,7 +1543,14 @@ const canReport = await asOleg.acl.check('shop.reports.daily') // { allow:
|
|
|
1498
1543
|
|
|
1499
1544
|
`db.auth.purgeAccount(аккаунт)` удаляет аккаунт и данные его арендатора. Вызывать может системный администратор или
|
|
1500
1545
|
владелец его арендатора; себя и последнего владельца арендатора удалить нельзя. Владение строками в чужих
|
|
1501
|
-
арендаторах переходит этим арендаторам, сессии гаснут, креды удаляются.
|
|
1546
|
+
арендаторах переходит этим арендаторам, сессии гаснут, креды удаляются. Владелец может стереть и арендатора, в
|
|
1547
|
+
котором сейчас работает: его сессия вернётся в личный арендатор.
|
|
1548
|
+
|
|
1549
|
+
Системный администратор может удалить и саму строку аккаунта — `db.Account(id).delete({ confirm: true })` в
|
|
1550
|
+
арендаторе System; данные арендатора аккаунта при этом остаются. Любой путь сразу стирает строки аккаунта в `secret`:
|
|
1551
|
+
сессии, хэши паролей и ключей, секреты TOTP, внешние учётные записи (внешний ключ `secret.account` с каскадом,
|
|
1552
|
+
ревизия 7). Сессии гаснут до этого с сигналом, и процессы сбрасывают кэш ответов этих сессий. Аккаунт System
|
|
1553
|
+
не удаляется (`acl_denied`): ему принадлежат служебные строки установки в `secret`.
|
|
1502
1554
|
|
|
1503
1555
|
## 11. История и срезы на дату
|
|
1504
1556
|
|
|
@@ -1659,7 +1711,7 @@ const sub = db.watch({ from: 'start', classes: ['Product'] }) // весь жу
|
|
|
1659
1711
|
await db.Product(kettle).update({ price: 2100 }).rows()
|
|
1660
1712
|
let cursor = ''
|
|
1661
1713
|
for await (const ev of sub) { // ev: { cursor, row, id, class, op, rev, at }
|
|
1662
|
-
cursor =
|
|
1714
|
+
cursor = sub.cursor! // курсор подписки: watch({ from: cursor }) продолжит после этого события
|
|
1663
1715
|
if (ev.id === kettle.id && ev.row.data.price === 2100) break // break (или sub.close()) — остановить
|
|
1664
1716
|
}
|
|
1665
1717
|
```
|
|
@@ -1677,10 +1729,13 @@ for await (const ev of sub) { // ev: { cursor, row, id, class, op, r
|
|
|
1677
1729
|
порядке записи. Это не порядок фиксации: версии одного объекта сверяйте по `rev`.
|
|
1678
1730
|
- Долгая пишущая транзакция в любой базе кластера задерживает поток: `await sub.lag()` показывает отставание в мс.
|
|
1679
1731
|
- Курсор старше яруса `all` политики хранения — `cursor_expired`: перечитайте текущее состояние и подпишитесь
|
|
1680
|
-
с `'now'`.
|
|
1732
|
+
с `'now'`. Время в курсоре подписки `sub.cursor` — время последнего события, а когда подписка догнала журнал, —
|
|
1733
|
+
время опроса, поэтому подписка, к которой сутки не было событий, не устаревает. Курсор события `ev.cursor` хранит
|
|
1734
|
+
время самого события.
|
|
1681
1735
|
- Без `LISTEN` (`connect({ listen: false })`) подписка опрашивает журнал.
|
|
1682
|
-
- Подписка без потерь — это сохранённый курсор: храните `
|
|
1683
|
-
с него после перезапуска.
|
|
1736
|
+
- Подписка без потерь — это сохранённый курсор: обработав событие, храните `sub.cursor` (позиция после последнего
|
|
1737
|
+
выданного события) и продолжайте с него после перезапуска. С `ev.cursor` поток тоже продолжится, но после затишья
|
|
1738
|
+
дольше яруса `all` такой курсор старше яруса, и продолжение с него — `cursor_expired`.
|
|
1684
1739
|
|
|
1685
1740
|
### Проекторы
|
|
1686
1741
|
|
|
@@ -1762,7 +1817,9 @@ sales.close() // stop() после текущ
|
|
|
1762
1817
|
`out.inc(класс, строка, приращения, { tenant })` создаёт строку или дополняет её и прибавляет числа, не читая
|
|
1763
1818
|
строку (поля, которых нет, считаются от 0). `out.delete(класс, ключ или id, { tenant })` удаляет строку, а если её
|
|
1764
1819
|
нет, ничего не делает. `await out.get(класс, ключ или id, { tenant })` читает текущую строку вместе с записями
|
|
1765
|
-
пачки.
|
|
1820
|
+
пачки. Строку класса с ключом `delete` и `get` находят по полям ключа (`{ day: '2026-10-29' }`), а по id (строкой
|
|
1821
|
+
или `{ $id }`) — только строку класса без ключа; один id у класса с ключом — `invalid_data`, и пачка откатывается.
|
|
1822
|
+
`out` пишет только в классы `into` и только в арендатора изменений, переданных в `handle`; иначе —
|
|
1766
1823
|
`derived_class` или `tenant_denied`, и пачка откатывается.
|
|
1767
1824
|
- Приращения строки, заданной одними полями ключа (или одним `$id`), `out` складывает до отправки в базу, пока между
|
|
1768
1825
|
ними нет `upsert`, `delete` или `get`: на строку — одна правка за пачку, а правило класса проверяется над итогом её
|
|
@@ -1810,14 +1867,20 @@ sales.close() // stop() после текущ
|
|
|
1810
1867
|
выбирает он: пользователь арендатора сам к проектору не подключится. Опция
|
|
1811
1868
|
`tenants: [id, …]` — явный список участников: проектор работает только с этими арендаторами (их классы `into` тоже
|
|
1812
1869
|
должны быть помечены), а пометку у остальных не учитывает. Источники — семейства `from` у этих арендаторов; класс
|
|
1813
|
-
|
|
1814
|
-
журнала у него нет. Поэтому арендатор, у которого такой класс среди
|
|
1815
|
-
его производных строк проектор не строит. Включите журнал класса и перестройте арендатора `rebuild({ tenant })`.
|
|
1870
|
+
из `into` источником не бывает, даже если попадает в `from`: свои результаты проектор не читает. Изменений класса
|
|
1871
|
+
с `history: false` проектор не видит: журнала у него нет. Поэтому арендатор, у которого такой класс среди
|
|
1872
|
+
источников, выбывает с кодом `invalid_class`, и его производных строк проектор не строит. Включите журнал класса и перестройте арендатора `rebuild({ tenant })`.
|
|
1816
1873
|
- `batch` — сколько изменений в пачке (500), `pollMs` — запасной опрос (1000 мс): сигналы источников будят
|
|
1817
1874
|
проектор раньше.
|
|
1818
1875
|
- `lockTimeout` — сколько транзакция проектора ждёт чужую блокировку строки (5000 мс; 0 — без предела). Транзакция
|
|
1819
1876
|
любого арендатора, которая держит строку итога или цель конца, иначе держала бы пачку всех арендаторов. Дольше —
|
|
1820
|
-
пачка откатывается, курсор остаётся, `run()` не отклоняется, а пропускает проход (`skipped`) и повторяет его.
|
|
1877
|
+
пачка откатывается, курсор остаётся, `run()` не отклоняется, а пропускает проход (`skipped`) и повторяет его. Отчёт
|
|
1878
|
+
называет арендатора, на чьей строке встала пачка (`locked`). `rebuild({ tenant, chunked: true })` такую блокировку
|
|
1879
|
+
пережидает.
|
|
1880
|
+
- `lockRetries` — сколько проходов подряд запись одного арендатора может упираться в `lockTimeout`, прежде чем он
|
|
1881
|
+
выбудет (по умолчанию 0 — не выбывает). Выбывший получает код `55P03`, и пачка идёт для остальных; его изменения
|
|
1882
|
+
пропускаются до `rebuild({ tenant })`. Счёт ведёт процесс, удачная запись арендатора его сбрасывает. Учтите: долгая
|
|
1883
|
+
исключительная блокировка семейства класса System (ужесточение описания) упрётся в каждого арендатора по очереди.
|
|
1821
1884
|
- Каждая правка, которую `out` отправляет в базу, — отдельная правка строки, и скорость проектора задаёт число
|
|
1822
1885
|
правок, а не число изменений. Свёртка `inc` сводит правки строки к одной за пачку, поэтому handle из примера выше
|
|
1823
1886
|
(`out.inc` на каждое изменение) на машине замеров догонял 6 тысяч изменений в секунду при `batch` 500 и 33 тысячи
|
|
@@ -1896,7 +1959,9 @@ const stats = db.cache.stats() // { size, hits,
|
|
|
1896
1959
|
- изменение прав и классов, запись через `db.sql`, погашенная сессия и переподключение слушателя сбрасывают весь
|
|
1897
1960
|
кэш или ответы сессии;
|
|
1898
1961
|
- срок по умолчанию — 60 секунд; внутри `db.begin()` кэш не работает;
|
|
1899
|
-
- без `LISTEN` и в режиме `none` кэш выключен, а `connect({ cache: { ttlOnly: true } })` включает
|
|
1962
|
+
- без `LISTEN` и в режиме `none` кэш выключен, а `connect({ cache: { ttlOnly: true } })` включает его: ответ живёт до
|
|
1963
|
+
конца срока, если его не сбросит запись этого подключения;
|
|
1964
|
+
- `db.load()`, проектор и `db.rebuildAggregates()` сбрасывают кэш, даже этого подключения, только сигналом базы.
|
|
1900
1965
|
|
|
1901
1966
|
Реестр классов процесс перечитывает по сигналу об изменении классов сразу (одно перечитывание за раз), а реестр и
|
|
1902
1967
|
ключ сигналов по `*`, по незнакомому токену и по нагрузке не своего режима (а без ключа в режиме `opaque` — по любому
|
|
@@ -1916,10 +1981,16 @@ const stats = db.cache.stats() // { size, hits,
|
|
|
1916
1981
|
const report = await adm.maintain() // { sessions, codes, thinned, batches, more, queue, skipped }
|
|
1917
1982
|
```
|
|
1918
1983
|
|
|
1919
|
-
`maintain()` удаляет
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1984
|
+
`maintain()` удаляет из таблицы `secret` сессии и одноразовые коды всех видов (`OTP` и свои `OTP_<ИМЯ>` из
|
|
1985
|
+
`issueOtp({ kind })`) — истёкшие, а также погашенные сессии и коды, которые уже использованы, заменены новым или
|
|
1986
|
+
исчерпали попытки, — прореживает журнал по политике хранения
|
|
1987
|
+
и проверяет очередь сигналов PostgreSQL: заполнение больше половины — предупреждение. Строки удалённого аккаунта
|
|
1988
|
+
`maintain()` ждать не нужно: их стирает само удаление ([«Удалить аккаунт»](#удалить-аккаунт)). Вызывать может системный
|
|
1989
|
+
администратор, роль с правом `letopis.maintain` или администраторское подключение. Запускает его внешний
|
|
1990
|
+
планировщик (`letopis maintain`) или сама библиотека: `connect({ maintain: '1h' })`. Интервал — число миллисекунд или
|
|
1991
|
+
строка (`'500ms'`, `'30s'`, `'15m'`, `'1h'`, `'1d'`) от 1 мс до 2³¹−1 мс (около 24 суток): другое значение, в том
|
|
1992
|
+
числе ноль, отрицательное или `NaN`, `connect()` отклоняет с `invalid_data`, не открывая соединений. `close()` ждёт
|
|
1993
|
+
идущий по расписанию запуск: он доделывает текущую пачку и следующую не начинает. Из нескольких процессов работает один:
|
|
1923
1994
|
каждая пачка — своя транзакция, и первым делом она берёт блокировку обслуживания — строку таблицы `secret`, которую
|
|
1924
1995
|
ни прикладная, ни посторонняя роль базы не займёт; блокировка снимается с фиксацией пачки. Если блокировку держит
|
|
1925
1996
|
другой процесс, вызов до первой пачки отвечает `skipped: true`, а между пачками останавливается (продолжит тот
|
|
@@ -2121,6 +2192,9 @@ letopis mcp --dsn <строка> --schema <v2.имя> --token <токен пол
|
|
|
2121
2192
|
(или LETOPIS_DSN, LETOPIS_SCHEMA, LETOPIS_TOKEN; ключ сервиса --api-key не принимается)
|
|
2122
2193
|
```
|
|
2123
2194
|
|
|
2195
|
+
`letopis types` и `letopis describe` строят результат по классам сессии, поэтому без действующей сессии (токен истёк,
|
|
2196
|
+
погашен, неверен или не задан, ключ API неверен) выходят с кодом 1 и ошибкой `no_session`, а файл `--out` не трогают.
|
|
2197
|
+
|
|
2124
2198
|
### Наблюдение
|
|
2125
2199
|
|
|
2126
2200
|
- `connect({ onQuery, slowMs })`: хук получает на каждый запрос цепочки режим, классы, длительность, число строк и
|
|
@@ -2238,17 +2312,19 @@ npx letopis import --from postgres://…/old --from-schema v1.salon --dsn postgr
|
|
|
2238
2312
|
| `link`, `lookup` | `{ account, kind, identifier, meta?, confirmed? }` → id; `{ kind, identifier, requireConfirmed?, ttl?, totp? }` → вход или `null` |
|
|
2239
2313
|
| `credentials`, `revokeCredential`, `setFlags` | `(account)` → креды без секретов; `(id, { keepCurrent? })`; `(id, { confirmed?, enabled? })` |
|
|
2240
2314
|
| `sessionFor`, `refresh`, `revoke`, `revokeAll` | `(account, { ttl? })` → токен; `({ ttl? })` → новое время истечения (токен прежний); `()`; `(account?)` → число сессий |
|
|
2315
|
+
| `session` | `(token)` → `{ account, tenant, member, expiresAt, loginAt, methods }` или `null` |
|
|
2241
2316
|
| `tenants`, `switch` | `()` → `[{ tenant, roles }]`; `(tenant)` |
|
|
2242
2317
|
| `purgeAccount` | `(account)` → `{ account, rows, versions, memberships, owned }` |
|
|
2243
2318
|
|
|
2244
|
-
Проверку паролей, кодов и TOTP, выпуск
|
|
2245
|
-
пользователю отвечают `acl_denied`. Креды System, сервисов и системных администраторов
|
|
2246
|
-
после недавнего интерактивного входа или системный администратор. Исключение — ключи
|
|
2247
|
-
пары «ключ + секрет» сервис выпускает и отзывает сам по сессии своего ключа, прочие свои
|
|
2248
|
-
действующий ключ сервиса не отзывается никем (`acl_denied`). С `totp` вход `verifyPassword` и
|
|
2249
|
-
ещё и код TOTP (право `auth.totp`) и выдаёт сессию со способом `TOTP`.
|
|
2319
|
+
Проверку паролей, кодов и TOTP, выпуск сессий, сведения о сессии по токену и подтверждение кредов вызывает сервис
|
|
2320
|
+
(права `auth.*`); обычному пользователю отвечают `acl_denied`. Креды System, сервисов и системных администраторов
|
|
2321
|
+
меняет только сам аккаунт после недавнего интерактивного входа или системный администратор. Исключение — ключи
|
|
2322
|
+
сервиса: свои ключи API и пары «ключ + секрет» сервис выпускает и отзывает сам по сессии своего ключа, прочие свои
|
|
2323
|
+
креды — нет; последний действующий ключ сервиса не отзывается никем (`acl_denied`). С `totp` вход `verifyPassword` и
|
|
2324
|
+
`lookup` проверяет ещё и код TOTP (право `auth.totp`) и выдаёт сессию со способом `TOTP`.
|
|
2250
2325
|
Вид креда `kind` — `^[A-Z][A-Z0-9_]{1,31}$` (`PASSWORD`, `EMAIL`, `TELEGRAM` …); `SESSION`, `APIKEY`, `KEYSECRET`,
|
|
2251
|
-
`OTP`, `TOTP` и `
|
|
2326
|
+
`OTP`, `TOTP`, `KEY`, `PROJECTOR` и `LOCK` заняты. То же правило — у вида пароля `user.kind` в `createTenant` и
|
|
2327
|
+
`up({ tenant })`.
|
|
2252
2328
|
|
|
2253
2329
|
### Проектор
|
|
2254
2330
|
|
|
@@ -2258,14 +2334,14 @@ npx letopis import --from postgres://…/old --from-schema v1.salon --dsn postgr
|
|
|
2258
2334
|
|
|
2259
2335
|
| Член | Что делает |
|
|
2260
2336
|
|---|---|
|
|
2261
|
-
| опции `ProjectorOptions` | `from`, `into` — классы с потомками; `version` — целое от 1; `handle(changes, out)`; `withBefore`; `onExpired: 'rebuild' \| 'error'` (`'rebuild'` — пачками); `batch` — изменений в пачке (500, от 1 до 10 000); `pollMs` — запасной опрос (1000 мс); `lockTimeout` — ожидание чужой блокировки строки (5000 мс, 0 — без предела); `tenants` — id арендаторов-участников |
|
|
2337
|
+
| опции `ProjectorOptions` | `from`, `into` — классы с потомками; `version` — целое от 1; `handle(changes, out)`; `withBefore`; `onExpired: 'rebuild' \| 'error'` (`'rebuild'` — пачками); `batch` — изменений в пачке (500, от 1 до 10 000); `pollMs` — запасной опрос (1000 мс); `lockTimeout` — ожидание чужой блокировки строки (5000 мс, 0 — без предела); `lockRetries` — проходов подряд до выбывания арендатора с чужой блокировкой (0 — не выбывает); `tenants` — id арендаторов-участников |
|
|
2262
2338
|
| `run({ once })` | догнать журнал (`once`) или следовать за ним до `stop()` → `ProjectorReport` |
|
|
2263
2339
|
| `rebuild({ chunked, tenant })` | перестроить с текущего состояния: атомарно, пачками или одного арендатора → `ProjectorReport` |
|
|
2264
2340
|
| `status()` | → `ProjectorStatus`: `{ name, state, version, cursor, at, lag, failed, rebuildingTenants }`; `failed` — `{ [id арендатора]: ProjectorFailure }`, где `ProjectorFailure` — `{ cursor, code, message }` |
|
|
2265
2341
|
| `stop()`, `close()` | остановить `run()` после текущей пачки; то же и отписаться от сигналов |
|
|
2266
|
-
| `ProjectorReport` | `{ batches, changes, created, updated, deleted, unchanged, skipped, rebuilds }` |
|
|
2342
|
+
| `ProjectorReport` | `{ batches, changes, created, updated, deleted, unchanged, skipped, rebuilds, locked }` |
|
|
2267
2343
|
| `ProjectorChange` | `{ cursor, id, class, tenant, op, rev, at, author, row }` и, если есть, `agent`, `reason`, `moved`, `before`, `rebuild` |
|
|
2268
|
-
| `ProjectorOut` | `upsert(класс, строка, { tenant })`, `inc(класс, строка, приращения, { tenant })`, `delete(класс, ключ или id, { tenant })`, `get(класс, ключ или id, { tenant })` → `Row \| null`; строка — `ProjectorRow` в формате
|
|
2344
|
+
| `ProjectorOut` | `upsert(класс, строка, { tenant })`, `inc(класс, строка, приращения, { tenant })`, `delete(класс, ключ или id, { tenant })`, `get(класс, ключ или id, { tenant })` → `Row \| null`; строка — `ProjectorRow` в формате загрузчика; у класса с ключом `delete` и `get` — по ключу, id — только у класса без ключа |
|
|
2269
2345
|
|
|
2270
2346
|
### Строка ответа
|
|
2271
2347
|
|
|
@@ -2282,8 +2358,10 @@ npx letopis import --from postgres://…/old --from-schema v1.salon --dsn postgr
|
|
|
2282
2358
|
|
|
2283
2359
|
### Зарезервированные имена
|
|
2284
2360
|
|
|
2285
|
-
|
|
2286
|
-
именем
|
|
2361
|
+
Имя метода закрывает класс только там, где этот метод есть: после шага — методы цепочки, у корня `db` — его методы,
|
|
2362
|
+
у батча — его. Поэтому хаб с именем метода только цепочки (например, `count`) доступен первым шагом `db.count(…)`, а
|
|
2363
|
+
после другого шага недостижим; хаб с именем метода корня первым шагом не вызвать. Связь с любым из этих имён база не
|
|
2364
|
+
примет (`reserved_names()`; список подставляет установщик из `RESERVED_CLASS_NAMES`). Не давайте классам эти имена.
|
|
2287
2365
|
|
|
2288
2366
|
- методы цепочки: `then`, `entity`, `paths`, `rows`, `first`, `ids`, `count`, `limit`, `offset`, `sort`, `asOf`,
|
|
2289
2367
|
`withDeleted`, `deep`, `exact`, `sum`, `avg`, `min`, `max`, `countBy`, `after`, `versions`, `create`, `update`,
|
|
@@ -2340,7 +2418,7 @@ npx letopis import --from postgres://…/old --from-schema v1.salon --dsn postgr
|
|
|
2340
2418
|
| `tx_rolled_back` | PostgreSQL ответил `ROLLBACK` на `COMMIT`: ошибку перехватили внутри `tr.transaction(fn)` |
|
|
2341
2419
|
| `no_key` | `upsert`, `rekey` или `idOf` у класса без ключа |
|
|
2342
2420
|
| `no_path` | между классами соседних шагов нет перехода |
|
|
2343
|
-
| `invalid_query` | цепочку или API используют неверно (модификатор шага `deep`, `exact`, `alias`, `tags` или `owner` после глагола, слот не после глагола, курсор `after()` со значением не того типа, что поле `sort`, без `v` или с `id` не uuid и т. п.) |
|
|
2421
|
+
| `invalid_query` | цепочку или API используют неверно (модификатор шага `deep`, `exact`, `alias`, `tags` или `owner` после глагола, модификатор выборки между глаголом и терминалом `rows`, `first`, `ids`, `count` или `paths`, слот не после глагола, курсор `after()` со значением не того типа, что поле `sort`, без `v` или с `id` не uuid и т. п.) |
|
|
2344
2422
|
| `admin_required` | загрузчику, проектору и `rebuildAggregates` нужно администраторское подключение |
|
|
2345
2423
|
| `rebuild_required` | проекция построена другой версией проектора (`version`): нужна перестройка `rebuild()`; `rebuild({ tenant })`, пока проекции нет или идёт перестройка пачками |
|
|
2346
2424
|
| `projector_busy` | перестройку не начать: проектор сейчас работает в другом процессе |
|
|
@@ -2408,7 +2486,7 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2408
2486
|
|
|
2409
2487
|
## 18. Ограничения
|
|
2410
2488
|
|
|
2411
|
-
Здесь собрано то, что letopis 1.
|
|
2489
|
+
Здесь собрано то, что letopis 1.4 сознательно не делает или делает с оговорками, и где за поведением нужно следить.
|
|
2412
2490
|
|
|
2413
2491
|
**Платформа**
|
|
2414
2492
|
|
|
@@ -2471,8 +2549,8 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2471
2549
|
- Горячие цели ссылок (организация, на которую ссылается почти всё) копят блокировки `for key share` и MultiXact,
|
|
2472
2550
|
как при обычных внешних ключах; это нужно мониторить. Операции одного кошелька идут друг за другом под
|
|
2473
2551
|
блокировкой его строки ([раздел 8](#8-транзакции-батчи-и-деньги), «Рецепт: кошелёк»).
|
|
2474
|
-
- Связь не может называться зарезервированным именем, а хаб с таким именем
|
|
2475
|
-
([«Зарезервированные имена»](#зарезервированные-имена)).
|
|
2552
|
+
- Связь не может называться зарезервированным именем, а хаб с таким именем доступен не везде: например, `count` —
|
|
2553
|
+
только первым шагом ([«Зарезервированные имена»](#зарезервированные-имена)).
|
|
2476
2554
|
|
|
2477
2555
|
**Доступ**
|
|
2478
2556
|
|
|
@@ -2659,7 +2737,7 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2659
2737
|
# cache — кэш результатов: попадание, свои и чужие записи, ключ с пользователем, права, погашенные сессии, обрыв слушателя, ttl, пометка класса, без LISTEN, гонка, db.sql, предел счётчиков сигналов (этап 6; план 1.1, точка А)
|
|
2660
2738
|
# maintain — обслуживание: истёкшие сессии и коды, права, один процесс под блокировкой, расписание (этап 6)
|
|
2661
2739
|
# up — полный установщик: свежая установка, повторный накат, свои сиды, автосоздание базы, UTF8, версия 17, upgrade с миграциями, PGlite, первый арендатор и пользователь (up({ tenant }), createTenant) (этапы 6, 8)
|
|
2662
|
-
# upgrade — обновление схемы с 1.1.0 (
|
|
2740
|
+
# upgrade — обновление схемы с 1.1.0 (upgrade: true, миграция 0007) и с 1.0.1 (upgrade: true, миграции 0006 и 0007) пакетами letopis-11 и letopis-1: версии и хэши прежнего движка, verify(), каталог против чистой установки, строки-замки, строки secret без аккаунта стёрты, приложение, подписка и проектор по старым данным, повторный up() (доработки после 1.2.0, этап 1; ревизия 7)
|
|
2663
2741
|
# scenario-salon — сквозной сценарий салона: 21 акт 0.21 (salon) с матрицей покрытия API 1.0 (этап 6)
|
|
2664
2742
|
# telemetry — onQuery и slowMs, describe() и метаданные классов с наследованием, describeText (этап 6)
|
|
2665
2743
|
# history — diff и blame по журналу, причина изменения у глаголов, db.begin() и батча; версия, скрытая условием ACL, колонка changed, пропуск версии, row_changes на 20 тыс. целей (план 1.1, этап 1 и точка А)
|
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;
|
package/dist/admin.d.ts
CHANGED
|
@@ -17,22 +17,28 @@ export interface VerifyIssue {
|
|
|
17
17
|
export interface VerifyResult {
|
|
18
18
|
ok: boolean;
|
|
19
19
|
issues: VerifyIssue[];
|
|
20
|
-
/**
|
|
20
|
+
/** Один хэш текущих версий всех объектов — храните его вне базы: подмену самых свежих версий выдаст его расхождение. */
|
|
21
21
|
anchor: string;
|
|
22
22
|
}
|
|
23
23
|
export interface MaintainOptions {
|
|
24
|
-
/** Момент «сейчас» для прореживания — только в установке с allowReset (тесты). */
|
|
24
|
+
/** Момент «сейчас» для прореживания — только в установке с `allowReset` (тесты). */
|
|
25
25
|
now?: string | Date;
|
|
26
|
-
/**
|
|
26
|
+
/** Сколько пачек прореживания журнала самое большее сделать за один вызов; каждая пачка — своя транзакция. */
|
|
27
27
|
batches?: number;
|
|
28
|
-
/**
|
|
28
|
+
/** Сколько версий самое большее удалить в одной пачке прореживания. */
|
|
29
29
|
limit?: number;
|
|
30
30
|
}
|
|
31
|
+
/** Внутренняя опция maintain(): остановка запуска по расписанию — текущая пачка доделывается, следующая не начинается. */
|
|
32
|
+
export declare const MAINTAIN_STOP: unique symbol;
|
|
31
33
|
export interface MaintainReport {
|
|
32
34
|
/** Другой процесс уже обслуживает установку (держит строку-замок обслуживания): этот вызов ничего не делал. */
|
|
33
35
|
skipped: boolean;
|
|
34
|
-
/** Удалено истёкших и погашенных
|
|
36
|
+
/** Удалено сессий: истёкших и погашенных (выход, отзыв, смена пароля, отключение или удаление аккаунта). */
|
|
35
37
|
sessions: number;
|
|
38
|
+
/**
|
|
39
|
+
* Удалено одноразовых кодов (вид `OTP` и свои виды `OTP_<ИМЯ>` из `issueOtp({ kind })`): истёкших, использованных,
|
|
40
|
+
* заменённых новым и исчерпавших попытки.
|
|
41
|
+
*/
|
|
36
42
|
codes: number;
|
|
37
43
|
/** Прорежено версий журнала. */
|
|
38
44
|
thinned: number;
|
|
@@ -46,7 +52,7 @@ export interface ResetOptions {
|
|
|
46
52
|
level: 'sessions' | 'tenant' | 'all';
|
|
47
53
|
/** Подтверждение — имя схемы установки. */
|
|
48
54
|
confirm: string;
|
|
49
|
-
/** level: 'tenant' —
|
|
55
|
+
/** Какого арендатора сбросить при `level: 'tenant'` — его id; на других уровнях не учитывается. */
|
|
50
56
|
tenant?: string;
|
|
51
57
|
}
|
|
52
58
|
export interface ResetResult {
|
|
@@ -55,10 +61,10 @@ export interface ResetResult {
|
|
|
55
61
|
tenant?: string;
|
|
56
62
|
rows?: number;
|
|
57
63
|
versions?: number;
|
|
58
|
-
/** level: 'all' — новый ключ сервиса: прежние сессии и ключи больше не действуют. */
|
|
64
|
+
/** `level: 'all'` — новый ключ сервиса: прежние сессии и ключи больше не действуют. */
|
|
59
65
|
serviceKey?: string;
|
|
60
66
|
}
|
|
61
|
-
/** Машинное описание модели (db.describe()). */
|
|
67
|
+
/** Машинное описание модели (`db.describe()`). */
|
|
62
68
|
export interface ModelDescription {
|
|
63
69
|
schema: string;
|
|
64
70
|
tenant: string | null;
|
|
@@ -117,13 +123,23 @@ export declare function verify(a: AdminCtx, opts?: {
|
|
|
117
123
|
* (skipped у всех). Блокировку держит другой процесс: до первой пачки — skipped; между пачками —
|
|
118
124
|
* вызов останавливается (продолжит тот процесс), отчёт — о сделанном, more: true.
|
|
119
125
|
*/
|
|
120
|
-
export declare function maintain(a: AdminCtx, opts?: MaintainOptions
|
|
126
|
+
export declare function maintain(a: AdminCtx, opts?: MaintainOptions & {
|
|
127
|
+
[MAINTAIN_STOP]?: () => boolean;
|
|
128
|
+
}): Promise<MaintainReport>;
|
|
121
129
|
/** Предупреждение о заполненной очереди сигналов (§2.12): больше половины — зависший слушатель. */
|
|
122
130
|
export declare function queueWarning(queue: number): string | null;
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Интервал расписания: число мс или строка '500ms', '30s', '15m', '1h', '1d' — конечный, от 1 мс до 2³¹−1 мс (около 24
|
|
133
|
+
* суток); иначе invalid_data. Ноль, отрицательное, NaN и слишком длинный интервал таймер Node.js превращает в 1 мс —
|
|
134
|
+
* обслуживание шло бы без перерыва.
|
|
135
|
+
*/
|
|
136
|
+
export declare function parseInterval(v: unknown): number;
|
|
137
|
+
/**
|
|
138
|
+
* Запуск maintain() по расписанию (connect({ maintain: '1h' }); ms — уже проверенный интервал): ошибки — предупреждением,
|
|
139
|
+
* следующий запуск не начинается, пока идёт прежний. Возвращает остановку: она снимает таймер, просит идущий запуск не
|
|
140
|
+
* начинать новых пачек и ждёт его конца — close() закрывает пул после неё, а не посреди пачки.
|
|
141
|
+
*/
|
|
142
|
+
export declare function scheduleMaintain(run: (stopped: () => boolean) => Promise<unknown>, ms: number): () => Promise<void>;
|
|
127
143
|
/**
|
|
128
144
|
* Сброс (этап 6, п. 6). Предохранители — в базе: флаг allowReset установки, право letopis.reset,
|
|
129
145
|
* подтверждение именем схемы. tenant — пачками (длинная операция не держит горизонт подписок).
|
|
@@ -153,6 +169,6 @@ export declare function rebuildAggregates(env: {
|
|
|
153
169
|
}, cls: string): Promise<number>;
|
|
154
170
|
/** Машинное описание модели арендатора вместе с метаданными классов (этап 6, п. 12). */
|
|
155
171
|
export declare function describe(a: AdminCtx): ModelDescription;
|
|
156
|
-
/** Описание модели текстом для языковых
|
|
172
|
+
/** Описание модели текстом для языковых моделей. */
|
|
157
173
|
export declare function describeText(d: ModelDescription): string;
|
|
158
174
|
export {};
|