letopis 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT-CHEATSHEET.en.md +10 -5
- package/AGENT-CHEATSHEET.md +14 -9
- package/CHANGELOG.md +159 -0
- package/README.en.md +119 -52
- package/README.md +107 -47
- package/dist/acl.d.ts +11 -5
- package/dist/admin.d.ts +30 -14
- package/dist/admin.js +42 -16
- package/dist/auth.d.ts +61 -17
- package/dist/auth.js +16 -2
- package/dist/cache.d.ts +5 -1
- package/dist/chain.d.ts +91 -15
- package/dist/chain.js +47 -4
- package/dist/cli.js +6 -0
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +5 -5
- package/dist/import.js +7 -0
- package/dist/index.d.ts +74 -45
- package/dist/index.js +12 -5
- package/dist/load.d.ts +10 -3
- package/dist/load.js +19 -4
- package/dist/mcp.d.ts +20 -11
- package/dist/mcp.js +4 -4
- package/dist/model.d.ts +4 -1
- package/dist/ops.d.ts +58 -18
- package/dist/ops.js +58 -18
- package/dist/project.d.ts +30 -11
- package/dist/project.js +30 -4
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +4 -1
- package/dist/sql.d.ts +4 -1
- package/dist/sql.js +12 -4
- package/dist/tx.js +10 -1
- package/dist/types.d.ts +14 -7
- package/dist/types.js +13 -6
- package/dist/up.d.ts +48 -25
- package/dist/up.js +89 -41
- package/dist/validate.d.ts +9 -2
- package/dist/validate.js +19 -3
- package/dist/watch.d.ts +17 -5
- package/dist/watch.js +40 -8
- package/dist/write.d.ts +3 -2
- package/dist/write.js +29 -10
- package/package.json +1 -1
- package/sql/10-core.sql +6 -1
- package/sql/60-write.sql +38 -18
- package/sql/80-auth.sql +39 -2
- package/sql/88-derived.sql +1 -0
- package/sql/90-time.sql +15 -5
- package/sql/95-seed.system.sql +6 -3
- package/sql/99-revision.sql +1 -1
- package/sql/migrate/0007-secret-account-fk.sql +12 -0
package/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 штамп не меняет, и предупреждения нет.
|
|
@@ -376,8 +387,8 @@ const adm = await connect({ dsn: ADMIN, schema: r.schema, token: r.tenant!.token
|
|
|
376
387
|
|
|
377
388
|
Обновление схем 1.1.0 и 1.0.1 проверяет тест `upgrade`. Схему ставит прежний пакет, данные пишет прежний движок, затем
|
|
378
389
|
вызывается `up()` текущей версии. После него версии и хэши прежнего движка на месте, `verify({ data: true })` чист, а
|
|
379
|
-
каталог схемы совпадает с чистой установкой.
|
|
380
|
-
|
|
390
|
+
каталог схемы совпадает с чистой установкой. Обе схемы обновляются через `up({ upgrade: true })`: 1.1.0 (ревизия 6) —
|
|
391
|
+
миграцией `0007`, 1.0.1 (ревизия 5) — миграциями `0006` и `0007`.
|
|
381
392
|
|
|
382
393
|
### Разработка без PostgreSQL: PGlite
|
|
383
394
|
|
|
@@ -602,8 +613,9 @@ npx letopis types --dsn "$APP" --schema v2.shop --token "$TOKEN" --out types.ts
|
|
|
602
613
|
### Имена классов
|
|
603
614
|
|
|
604
615
|
Шаг цепочки называется именем класса, его алиасом или именем роли. Имена методов цепочки и корня (`count`, `sort`,
|
|
605
|
-
`set`, `sql`, `auth` и другие)
|
|
606
|
-
|
|
616
|
+
`set`, `sql`, `auth` и другие) там, где такой метод есть, библиотека разбирает раньше классов: класс `count` доступен
|
|
617
|
+
первым шагом `db.count(…)`, но не после другого шага. Связь с таким именем база не примет. Не давайте классам эти
|
|
618
|
+
имена; полный список — в разделе
|
|
607
619
|
[«Зарезервированные имена»](#зарезервированные-имена).
|
|
608
620
|
|
|
609
621
|
## 5. Запись
|
|
@@ -657,7 +669,8 @@ await db.Shop(shop).Product({ price: gte(2000) }).update({ title: 'Чайник
|
|
|
657
669
|
`db.Product().update(…)` правит все товары арендатора. Шаг ничего не нашёл — ответ пустой и ошибки нет, поэтому
|
|
658
670
|
проверяйте длину ответа. Если `update` затронул меньше строк, чем нашёл шаг, весь план откатывается: строку успели
|
|
659
671
|
удалить — `target_not_found`, строка видна, но править её нельзя — `acl_denied`. Модификатор `.tags(…)` перед
|
|
660
|
-
`update`
|
|
672
|
+
`update` отбирает строки; строка или список ещё и заменяют их теги целиком, а оператор (`has`, `hasAny`, `hasAll`)
|
|
673
|
+
только отбирает — теги не меняются.
|
|
661
674
|
|
|
662
675
|
### Создать или заменить: `upsert`
|
|
663
676
|
|
|
@@ -789,8 +802,14 @@ await db.Order(order).line().create({ qty: 1, price: 900 }).Product.set(sencha).
|
|
|
789
802
|
- Весь план — одна транзакция уровня `read committed`. При взаимной блокировке (`40P01`), ошибке сериализации
|
|
790
803
|
(`40001`) и конфликте журнала библиотека повторяет план целиком, до трёх раз.
|
|
791
804
|
- Ошибка любого звена откатывает весь план. `exists` и `conflict` не повторяются: это ответы для приложения.
|
|
792
|
-
- Модификаторы до глагола (`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()`.
|
|
793
810
|
- Глагол сразу после глагола пишет в те же сущности.
|
|
811
|
+
- Строки ответа глагола идут в порядке его целей: по `sort()` до глагола, без него — по id; `first()` после глагола —
|
|
812
|
+
первая по этому порядку.
|
|
794
813
|
|
|
795
814
|
## 6. Чтение
|
|
796
815
|
|
|
@@ -834,7 +853,7 @@ const roots = await db.Category({ Parent: null }).rows() // корневы
|
|
|
834
853
|
| `ne(v)`, `gt(v)`, `gte(v)`, `lt(v)`, `lte(v)` | ≠ (с учётом `null`), >, ≥, <, ≤ |
|
|
835
854
|
| `between(a, b)`, `inList([…])` | a ≤ x ≤ b; значение из списка |
|
|
836
855
|
| `like(s)`, `ilike(s)`, `starts(s)`, `ends(s)` | шаблон с `%` и `_`; без учёта регистра; начинается с; заканчивается на |
|
|
837
|
-
| `has(v)`, `hasAny([…])`, `hasAll([…])` | поле-массив содержит значение; хотя бы одно; все |
|
|
856
|
+
| `has(v)`, `hasAny([…])`, `hasAll([…])` | поле-массив содержит значение; хотя бы одно; все (`hasAll([])` подходит любой строке, `hasAny([])` — никакой) |
|
|
838
857
|
| `exists(true \| false)`, `isNull()` | поле есть или нет; поле `null` или отсутствует |
|
|
839
858
|
| `not(v)` | отрицание оператора; скаляр — «не равно» |
|
|
840
859
|
| `or(f1, f2, …)` | любой из фильтров шага |
|
|
@@ -904,7 +923,9 @@ const byStatus = await db.Order().countBy('data.status') // { new: 1 }
|
|
|
904
923
|
```
|
|
905
924
|
|
|
906
925
|
Агрегаты считает база, по сущностям последнего шага. `limit(n)` и `offset(n)` перед `countBy()` относятся к группам:
|
|
907
|
-
`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 отвечает ошибкой.
|
|
908
929
|
|
|
909
930
|
### Пути
|
|
910
931
|
|
|
@@ -936,10 +957,11 @@ try {
|
|
|
936
957
|
|
|
937
958
|
`delete()` без `{ confirm: true }` — только превью: всё замыкание каскада. В каждой строке превью `$action`:
|
|
938
959
|
`delete`, `unset` или `restrict`, и `$depth` — уровень каскада (0 — сама цель; у строк `unset` и `restrict` его
|
|
939
|
-
нет). С подтверждением удаляются все цели
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
`acl.checkData(класс, 'DELETE')`.
|
|
960
|
+
нет). С подтверждением удаляются все цели шага одним оператором, а каскад применяет правила концов. Итог —
|
|
961
|
+
надгробия по уровням каскада, внутри уровня по id (в таком порядке они и в журнале); цели классов без журнала
|
|
962
|
+
(`history: false`) — в конце. Превью не учитывает права: строка с `$action: 'delete'` может не удалиться у
|
|
963
|
+
пользователя без права (`acl_denied`), поэтому интерфейсу стоит спросить `acl.checkData(класс, 'DELETE')`.
|
|
964
|
+
Правила концов:
|
|
943
965
|
|
|
944
966
|
- `restrict` — удаление запрещено, пока на цель ссылаются (`delete_restricted` со списком видимых строк и числом
|
|
945
967
|
остальных);
|
|
@@ -1389,8 +1411,10 @@ const viaTelegram = await svc.auth.lookup({ kind: 'TELEGRAM', identifier: '10050
|
|
|
1389
1411
|
<!-- run: guide -->
|
|
1390
1412
|
```ts
|
|
1391
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 }
|
|
1392
1415
|
const boris = await connect({ dsn: APP, schema: r.schema, token: shortToken })
|
|
1393
1416
|
await boris.auth.revoke() // выход: сессия погашена
|
|
1417
|
+
const ended = await svc.auth.session(shortToken) // null
|
|
1394
1418
|
await boris.close()
|
|
1395
1419
|
```
|
|
1396
1420
|
|
|
@@ -1403,6 +1427,15 @@ await boris.close()
|
|
|
1403
1427
|
старые сессии того же вида входа без отметки ключа); текущая сессия остаётся, если не передать
|
|
1404
1428
|
`keepCurrent: false`.
|
|
1405
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
|
+
|
|
1406
1439
|
### Работа в другом арендаторе
|
|
1407
1440
|
|
|
1408
1441
|
Сервис действует от имени пользователя через `svc.as(account, { tenant, owner })`. База проверяет право сервиса
|
|
@@ -1510,7 +1543,14 @@ const canReport = await asOleg.acl.check('shop.reports.daily') // { allow:
|
|
|
1510
1543
|
|
|
1511
1544
|
`db.auth.purgeAccount(аккаунт)` удаляет аккаунт и данные его арендатора. Вызывать может системный администратор или
|
|
1512
1545
|
владелец его арендатора; себя и последнего владельца арендатора удалить нельзя. Владение строками в чужих
|
|
1513
|
-
арендаторах переходит этим арендаторам, сессии гаснут, креды удаляются.
|
|
1546
|
+
арендаторах переходит этим арендаторам, сессии гаснут, креды удаляются. Владелец может стереть и арендатора, в
|
|
1547
|
+
котором сейчас работает: его сессия вернётся в личный арендатор.
|
|
1548
|
+
|
|
1549
|
+
Системный администратор может удалить и саму строку аккаунта — `db.Account(id).delete({ confirm: true })` в
|
|
1550
|
+
арендаторе System; данные арендатора аккаунта при этом остаются. Любой путь сразу стирает строки аккаунта в `secret`:
|
|
1551
|
+
сессии, хэши паролей и ключей, секреты TOTP, внешние учётные записи (внешний ключ `secret.account` с каскадом,
|
|
1552
|
+
ревизия 7). Сессии гаснут до этого с сигналом, и процессы сбрасывают кэш ответов этих сессий. Аккаунт System
|
|
1553
|
+
не удаляется (`acl_denied`): ему принадлежат служебные строки установки в `secret`.
|
|
1514
1554
|
|
|
1515
1555
|
## 11. История и срезы на дату
|
|
1516
1556
|
|
|
@@ -1671,7 +1711,7 @@ const sub = db.watch({ from: 'start', classes: ['Product'] }) // весь жу
|
|
|
1671
1711
|
await db.Product(kettle).update({ price: 2100 }).rows()
|
|
1672
1712
|
let cursor = ''
|
|
1673
1713
|
for await (const ev of sub) { // ev: { cursor, row, id, class, op, rev, at }
|
|
1674
|
-
cursor =
|
|
1714
|
+
cursor = sub.cursor! // курсор подписки: watch({ from: cursor }) продолжит после этого события
|
|
1675
1715
|
if (ev.id === kettle.id && ev.row.data.price === 2100) break // break (или sub.close()) — остановить
|
|
1676
1716
|
}
|
|
1677
1717
|
```
|
|
@@ -1689,10 +1729,13 @@ for await (const ev of sub) { // ev: { cursor, row, id, class, op, r
|
|
|
1689
1729
|
порядке записи. Это не порядок фиксации: версии одного объекта сверяйте по `rev`.
|
|
1690
1730
|
- Долгая пишущая транзакция в любой базе кластера задерживает поток: `await sub.lag()` показывает отставание в мс.
|
|
1691
1731
|
- Курсор старше яруса `all` политики хранения — `cursor_expired`: перечитайте текущее состояние и подпишитесь
|
|
1692
|
-
с `'now'`.
|
|
1732
|
+
с `'now'`. Время в курсоре подписки `sub.cursor` — время последнего события, а когда подписка догнала журнал, —
|
|
1733
|
+
время опроса, поэтому подписка, к которой сутки не было событий, не устаревает. Курсор события `ev.cursor` хранит
|
|
1734
|
+
время самого события.
|
|
1693
1735
|
- Без `LISTEN` (`connect({ listen: false })`) подписка опрашивает журнал.
|
|
1694
|
-
- Подписка без потерь — это сохранённый курсор: храните `
|
|
1695
|
-
с него после перезапуска.
|
|
1736
|
+
- Подписка без потерь — это сохранённый курсор: обработав событие, храните `sub.cursor` (позиция после последнего
|
|
1737
|
+
выданного события) и продолжайте с него после перезапуска. С `ev.cursor` поток тоже продолжится, но после затишья
|
|
1738
|
+
дольше яруса `all` такой курсор старше яруса, и продолжение с него — `cursor_expired`.
|
|
1696
1739
|
|
|
1697
1740
|
### Проекторы
|
|
1698
1741
|
|
|
@@ -1774,7 +1817,9 @@ sales.close() // stop() после текущ
|
|
|
1774
1817
|
`out.inc(класс, строка, приращения, { tenant })` создаёт строку или дополняет её и прибавляет числа, не читая
|
|
1775
1818
|
строку (поля, которых нет, считаются от 0). `out.delete(класс, ключ или id, { tenant })` удаляет строку, а если её
|
|
1776
1819
|
нет, ничего не делает. `await out.get(класс, ключ или id, { tenant })` читает текущую строку вместе с записями
|
|
1777
|
-
пачки.
|
|
1820
|
+
пачки. Строку класса с ключом `delete` и `get` находят по полям ключа (`{ day: '2026-10-29' }`), а по id (строкой
|
|
1821
|
+
или `{ $id }`) — только строку класса без ключа; один id у класса с ключом — `invalid_data`, и пачка откатывается.
|
|
1822
|
+
`out` пишет только в классы `into` и только в арендатора изменений, переданных в `handle`; иначе —
|
|
1778
1823
|
`derived_class` или `tenant_denied`, и пачка откатывается.
|
|
1779
1824
|
- Приращения строки, заданной одними полями ключа (или одним `$id`), `out` складывает до отправки в базу, пока между
|
|
1780
1825
|
ними нет `upsert`, `delete` или `get`: на строку — одна правка за пачку, а правило класса проверяется над итогом её
|
|
@@ -1822,9 +1867,9 @@ sales.close() // stop() после текущ
|
|
|
1822
1867
|
выбирает он: пользователь арендатора сам к проектору не подключится. Опция
|
|
1823
1868
|
`tenants: [id, …]` — явный список участников: проектор работает только с этими арендаторами (их классы `into` тоже
|
|
1824
1869
|
должны быть помечены), а пометку у остальных не учитывает. Источники — семейства `from` у этих арендаторов; класс
|
|
1825
|
-
|
|
1826
|
-
журнала у него нет. Поэтому арендатор, у которого такой класс среди
|
|
1827
|
-
его производных строк проектор не строит. Включите журнал класса и перестройте арендатора `rebuild({ tenant })`.
|
|
1870
|
+
из `into` источником не бывает, даже если попадает в `from`: свои результаты проектор не читает. Изменений класса
|
|
1871
|
+
с `history: false` проектор не видит: журнала у него нет. Поэтому арендатор, у которого такой класс среди
|
|
1872
|
+
источников, выбывает с кодом `invalid_class`, и его производных строк проектор не строит. Включите журнал класса и перестройте арендатора `rebuild({ tenant })`.
|
|
1828
1873
|
- `batch` — сколько изменений в пачке (500), `pollMs` — запасной опрос (1000 мс): сигналы источников будят
|
|
1829
1874
|
проектор раньше.
|
|
1830
1875
|
- `lockTimeout` — сколько транзакция проектора ждёт чужую блокировку строки (5000 мс; 0 — без предела). Транзакция
|
|
@@ -1914,7 +1959,9 @@ const stats = db.cache.stats() // { size, hits,
|
|
|
1914
1959
|
- изменение прав и классов, запись через `db.sql`, погашенная сессия и переподключение слушателя сбрасывают весь
|
|
1915
1960
|
кэш или ответы сессии;
|
|
1916
1961
|
- срок по умолчанию — 60 секунд; внутри `db.begin()` кэш не работает;
|
|
1917
|
-
- без `LISTEN` и в режиме `none` кэш выключен, а `connect({ cache: { ttlOnly: true } })` включает
|
|
1962
|
+
- без `LISTEN` и в режиме `none` кэш выключен, а `connect({ cache: { ttlOnly: true } })` включает его: ответ живёт до
|
|
1963
|
+
конца срока, если его не сбросит запись этого подключения;
|
|
1964
|
+
- `db.load()`, проектор и `db.rebuildAggregates()` сбрасывают кэш, даже этого подключения, только сигналом базы.
|
|
1918
1965
|
|
|
1919
1966
|
Реестр классов процесс перечитывает по сигналу об изменении классов сразу (одно перечитывание за раз), а реестр и
|
|
1920
1967
|
ключ сигналов по `*`, по незнакомому токену и по нагрузке не своего режима (а без ключа в режиме `opaque` — по любому
|
|
@@ -1934,10 +1981,16 @@ const stats = db.cache.stats() // { size, hits,
|
|
|
1934
1981
|
const report = await adm.maintain() // { sessions, codes, thinned, batches, more, queue, skipped }
|
|
1935
1982
|
```
|
|
1936
1983
|
|
|
1937
|
-
`maintain()` удаляет
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
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
|
+
идущий по расписанию запуск: он доделывает текущую пачку и следующую не начинает. Из нескольких процессов работает один:
|
|
1941
1994
|
каждая пачка — своя транзакция, и первым делом она берёт блокировку обслуживания — строку таблицы `secret`, которую
|
|
1942
1995
|
ни прикладная, ни посторонняя роль базы не займёт; блокировка снимается с фиксацией пачки. Если блокировку держит
|
|
1943
1996
|
другой процесс, вызов до первой пачки отвечает `skipped: true`, а между пачками останавливается (продолжит тот
|
|
@@ -2139,6 +2192,9 @@ letopis mcp --dsn <строка> --schema <v2.имя> --token <токен пол
|
|
|
2139
2192
|
(или LETOPIS_DSN, LETOPIS_SCHEMA, LETOPIS_TOKEN; ключ сервиса --api-key не принимается)
|
|
2140
2193
|
```
|
|
2141
2194
|
|
|
2195
|
+
`letopis types` и `letopis describe` строят результат по классам сессии, поэтому без действующей сессии (токен истёк,
|
|
2196
|
+
погашен, неверен или не задан, ключ API неверен) выходят с кодом 1 и ошибкой `no_session`, а файл `--out` не трогают.
|
|
2197
|
+
|
|
2142
2198
|
### Наблюдение
|
|
2143
2199
|
|
|
2144
2200
|
- `connect({ onQuery, slowMs })`: хук получает на каждый запрос цепочки режим, классы, длительность, число строк и
|
|
@@ -2256,17 +2312,19 @@ npx letopis import --from postgres://…/old --from-schema v1.salon --dsn postgr
|
|
|
2256
2312
|
| `link`, `lookup` | `{ account, kind, identifier, meta?, confirmed? }` → id; `{ kind, identifier, requireConfirmed?, ttl?, totp? }` → вход или `null` |
|
|
2257
2313
|
| `credentials`, `revokeCredential`, `setFlags` | `(account)` → креды без секретов; `(id, { keepCurrent? })`; `(id, { confirmed?, enabled? })` |
|
|
2258
2314
|
| `sessionFor`, `refresh`, `revoke`, `revokeAll` | `(account, { ttl? })` → токен; `({ ttl? })` → новое время истечения (токен прежний); `()`; `(account?)` → число сессий |
|
|
2315
|
+
| `session` | `(token)` → `{ account, tenant, member, expiresAt, loginAt, methods }` или `null` |
|
|
2259
2316
|
| `tenants`, `switch` | `()` → `[{ tenant, roles }]`; `(tenant)` |
|
|
2260
2317
|
| `purgeAccount` | `(account)` → `{ account, rows, versions, memberships, owned }` |
|
|
2261
2318
|
|
|
2262
|
-
Проверку паролей, кодов и TOTP, выпуск
|
|
2263
|
-
пользователю отвечают `acl_denied`. Креды System, сервисов и системных администраторов
|
|
2264
|
-
после недавнего интерактивного входа или системный администратор. Исключение — ключи
|
|
2265
|
-
пары «ключ + секрет» сервис выпускает и отзывает сам по сессии своего ключа, прочие свои
|
|
2266
|
-
действующий ключ сервиса не отзывается никем (`acl_denied`). С `totp` вход `verifyPassword` и
|
|
2267
|
-
ещё и код 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`.
|
|
2268
2325
|
Вид креда `kind` — `^[A-Z][A-Z0-9_]{1,31}$` (`PASSWORD`, `EMAIL`, `TELEGRAM` …); `SESSION`, `APIKEY`, `KEYSECRET`,
|
|
2269
|
-
`OTP`, `TOTP` и `
|
|
2326
|
+
`OTP`, `TOTP`, `KEY`, `PROJECTOR` и `LOCK` заняты. То же правило — у вида пароля `user.kind` в `createTenant` и
|
|
2327
|
+
`up({ tenant })`.
|
|
2270
2328
|
|
|
2271
2329
|
### Проектор
|
|
2272
2330
|
|
|
@@ -2283,7 +2341,7 @@ npx letopis import --from postgres://…/old --from-schema v1.salon --dsn postgr
|
|
|
2283
2341
|
| `stop()`, `close()` | остановить `run()` после текущей пачки; то же и отписаться от сигналов |
|
|
2284
2342
|
| `ProjectorReport` | `{ batches, changes, created, updated, deleted, unchanged, skipped, rebuilds, locked }` |
|
|
2285
2343
|
| `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` в формате
|
|
2344
|
+
| `ProjectorOut` | `upsert(класс, строка, { tenant })`, `inc(класс, строка, приращения, { tenant })`, `delete(класс, ключ или id, { tenant })`, `get(класс, ключ или id, { tenant })` → `Row \| null`; строка — `ProjectorRow` в формате загрузчика; у класса с ключом `delete` и `get` — по ключу, id — только у класса без ключа |
|
|
2287
2345
|
|
|
2288
2346
|
### Строка ответа
|
|
2289
2347
|
|
|
@@ -2300,8 +2358,10 @@ npx letopis import --from postgres://…/old --from-schema v1.salon --dsn postgr
|
|
|
2300
2358
|
|
|
2301
2359
|
### Зарезервированные имена
|
|
2302
2360
|
|
|
2303
|
-
|
|
2304
|
-
именем
|
|
2361
|
+
Имя метода закрывает класс только там, где этот метод есть: после шага — методы цепочки, у корня `db` — его методы,
|
|
2362
|
+
у батча — его. Поэтому хаб с именем метода только цепочки (например, `count`) доступен первым шагом `db.count(…)`, а
|
|
2363
|
+
после другого шага недостижим; хаб с именем метода корня первым шагом не вызвать. Связь с любым из этих имён база не
|
|
2364
|
+
примет (`reserved_names()`; список подставляет установщик из `RESERVED_CLASS_NAMES`). Не давайте классам эти имена.
|
|
2305
2365
|
|
|
2306
2366
|
- методы цепочки: `then`, `entity`, `paths`, `rows`, `first`, `ids`, `count`, `limit`, `offset`, `sort`, `asOf`,
|
|
2307
2367
|
`withDeleted`, `deep`, `exact`, `sum`, `avg`, `min`, `max`, `countBy`, `after`, `versions`, `create`, `update`,
|
|
@@ -2358,7 +2418,7 @@ npx letopis import --from postgres://…/old --from-schema v1.salon --dsn postgr
|
|
|
2358
2418
|
| `tx_rolled_back` | PostgreSQL ответил `ROLLBACK` на `COMMIT`: ошибку перехватили внутри `tr.transaction(fn)` |
|
|
2359
2419
|
| `no_key` | `upsert`, `rekey` или `idOf` у класса без ключа |
|
|
2360
2420
|
| `no_path` | между классами соседних шагов нет перехода |
|
|
2361
|
-
| `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 и т. п.) |
|
|
2362
2422
|
| `admin_required` | загрузчику, проектору и `rebuildAggregates` нужно администраторское подключение |
|
|
2363
2423
|
| `rebuild_required` | проекция построена другой версией проектора (`version`): нужна перестройка `rebuild()`; `rebuild({ tenant })`, пока проекции нет или идёт перестройка пачками |
|
|
2364
2424
|
| `projector_busy` | перестройку не начать: проектор сейчас работает в другом процессе |
|
|
@@ -2489,8 +2549,8 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2489
2549
|
- Горячие цели ссылок (организация, на которую ссылается почти всё) копят блокировки `for key share` и MultiXact,
|
|
2490
2550
|
как при обычных внешних ключах; это нужно мониторить. Операции одного кошелька идут друг за другом под
|
|
2491
2551
|
блокировкой его строки ([раздел 8](#8-транзакции-батчи-и-деньги), «Рецепт: кошелёк»).
|
|
2492
|
-
- Связь не может называться зарезервированным именем, а хаб с таким именем
|
|
2493
|
-
([«Зарезервированные имена»](#зарезервированные-имена)).
|
|
2552
|
+
- Связь не может называться зарезервированным именем, а хаб с таким именем доступен не везде: например, `count` —
|
|
2553
|
+
только первым шагом ([«Зарезервированные имена»](#зарезервированные-имена)).
|
|
2494
2554
|
|
|
2495
2555
|
**Доступ**
|
|
2496
2556
|
|
|
@@ -2677,7 +2737,7 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2677
2737
|
# cache — кэш результатов: попадание, свои и чужие записи, ключ с пользователем, права, погашенные сессии, обрыв слушателя, ttl, пометка класса, без LISTEN, гонка, db.sql, предел счётчиков сигналов (этап 6; план 1.1, точка А)
|
|
2678
2738
|
# maintain — обслуживание: истёкшие сессии и коды, права, один процесс под блокировкой, расписание (этап 6)
|
|
2679
2739
|
# up — полный установщик: свежая установка, повторный накат, свои сиды, автосоздание базы, UTF8, версия 17, upgrade с миграциями, PGlite, первый арендатор и пользователь (up({ tenant }), createTenant) (этапы 6, 8)
|
|
2680
|
-
# 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)
|
|
2681
2741
|
# scenario-salon — сквозной сценарий салона: 21 акт 0.21 (salon) с матрицей покрытия API 1.0 (этап 6)
|
|
2682
2742
|
# telemetry — onQuery и slowMs, describe() и метаданные классов с наследованием, describeText (этап 6)
|
|
2683
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 {};
|