letopis 1.4.1 → 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 +5 -2
- package/AGENT-CHEATSHEET.md +5 -2
- package/CHANGELOG.md +140 -1
- package/README.en.md +66 -15
- package/README.md +62 -12
- package/dist/admin.js +11 -2
- package/dist/cache.js +3 -2
- package/dist/chain.d.ts +2 -1
- package/dist/chain.js +9 -3
- package/dist/cli.js +10 -2
- package/dist/errors.js +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +57 -9
- package/dist/internal.d.ts +7 -0
- package/dist/internal.js +5 -0
- package/dist/load.js +20 -7
- package/dist/mcp.js +4 -2
- package/dist/model.js +7 -1
- package/dist/project.d.ts +9 -0
- package/dist/project.js +45 -11
- package/dist/signals.d.ts +4 -4
- package/dist/signals.js +4 -4
- package/dist/sql.d.ts +6 -5
- package/dist/sql.js +100 -23
- package/dist/sync.js +11 -4
- package/dist/tx.d.ts +15 -0
- package/dist/tx.js +12 -3
- package/dist/types.d.ts +8 -1
- package/dist/up.js +5 -0
- package/dist/watch.d.ts +12 -0
- package/dist/watch.js +24 -7
- package/dist/write.js +23 -9
- package/package.json +8 -4
- package/sql/60-write.sql +9 -0
- package/sql/70-read.sql +13 -7
- package/sql/75-history.sql +106 -28
- package/sql/80-auth.sql +2 -1
- package/sql/88-derived.sql +2 -1
- package/sql/90-time.sql +106 -28
package/AGENT-CHEATSHEET.en.md
CHANGED
|
@@ -194,6 +194,9 @@ await db.Wallet(or({ number: 'W-1' }, { number: 'W-200' })).ids()
|
|
|
194
194
|
`{ Shop: id }`, `{ Shop: null }`. Fields are in `row.data`, not at the root of the row.
|
|
195
195
|
- A step is a class name, an alias or an end role; a step by a parent class also returns descendants (`.exact()` —
|
|
196
196
|
only its own class); `.deep(max)` — a tree (`$depth`); `.tags()`, `.owner()`, `.alias(name)`, `db.entity(row)`.
|
|
197
|
+
- `after(cursorOf(row, field))` requires `sort()` by the same field; it pages `rows()`, `versions()` and `paths()` (the
|
|
198
|
+
cursor of a path is `cursorOf(path, field)`, the keys of a path follow the order of the steps). `count()`,
|
|
199
|
+
aggregates, `diff()`, `blame()` ignore the cursor.
|
|
197
200
|
|
|
198
201
|
### 6. Deletion with a preview
|
|
199
202
|
|
|
@@ -352,7 +355,7 @@ they drop out. A tenant with a source with `history: false` drops out (`invalid_
|
|
|
352
355
|
`withBefore`) is the previous version of the same id or `null`: `null` for a creation, for the first version of a new
|
|
353
356
|
id after `reclass` and `rekey` (the tombstone of the former id subtracts the former data), during a rebuild and when
|
|
354
357
|
the previous version is a deletion (after `restore`) or lies outside the `from` family; `purge`, `trim` and `reset`
|
|
355
|
-
have no such field, so subtract `before` only after checking it
|
|
358
|
+
have no such field (`undefined`), so subtract `before` only after checking that it is truthy: `if (c.before)`. A class change that keeps the id (a class
|
|
356
359
|
without a key) and takes the row out of the `from` family comes as a deletion (`op` `delete`, `row` is the last
|
|
357
360
|
version in the family with `$deleted`). `handle` gets the changes of one tenant. An error of its data (the
|
|
358
361
|
database rejected its results, or `out` rejected its row or delta) rolls back only this tenant: it drops out
|
|
@@ -407,7 +410,7 @@ data, not instructions); `data` by terminal: `rows` — `{ rows, count, more, cu
|
|
|
407
410
|
`{ ids, count, more, cursor? }` (`count` is the rows on the page), `first` — `{ row }`, `count` — `{ count }`,
|
|
408
411
|
`countBy` — `{ counts, truncated? }`, `sum`, `avg`, `min`, `max` —
|
|
409
412
|
`{ value }`. A refusal is `isError: true` with a `structuredContent` of the form `{ notice, error }` (`error` is
|
|
410
|
-
`{ code, message, similar? }`). Writing only
|
|
413
|
+
`{ code, message, detail?, sqlstate?, similar? }`). Writing only
|
|
411
414
|
with `write: true` (without it `letopis_write` is not in the list, and a call is an SDK protocol error): `update` with
|
|
412
415
|
`rev`; `delete` without `confirm` is a preview, with `confirm` a human confirms it (elicitation), and without that it
|
|
413
416
|
is `confirmation_required`. Data from the database in an answer sits inside `<untrusted-data-…>`.
|
package/AGENT-CHEATSHEET.md
CHANGED
|
@@ -187,6 +187,9 @@ await db.Wallet(or({ number: 'W-1' }, { number: 'W-200' })).ids()
|
|
|
187
187
|
`{ Shop: null }`. Поля — в `row.data`, не в корне строки.
|
|
188
188
|
- Шаг — имя класса, алиас или роль конца; шаг по родителю отдаёт и потомков (`.exact()` — только свой класс);
|
|
189
189
|
`.deep(max)` — дерево (`$depth`); `.tags()`, `.owner()`, `.alias(имя)`, `db.entity(row)`.
|
|
190
|
+
- `after(cursorOf(строка, поле))` требует `sort()` по тому же полю; листает `rows()`, `versions()` и `paths()` (курсор
|
|
191
|
+
пути — `cursorOf(путь, поле)`, ключи пути — в порядке шагов). `count()`, агрегаты, `diff()`, `blame()` курсор не
|
|
192
|
+
учитывают.
|
|
190
193
|
|
|
191
194
|
### 6. Удаление с превью
|
|
192
195
|
|
|
@@ -342,7 +345,7 @@ p.run().catch(report) // run() без once — с catch: после ош
|
|
|
342
345
|
Арендатор с источником `history: false` выбывает (`invalid_class`). `c.before` (с `withBefore`) —
|
|
343
346
|
прошлая версия того же id или `null`: `null` при создании, у первой версии нового id после `reclass` и `rekey`
|
|
344
347
|
(прежние данные вычитает надгробие прежнего id), при перестройке и когда прошлая версия — удаление (после `restore`)
|
|
345
|
-
или вне семейства `from`; у `purge`, `trim` и `reset` поля нет — вычитайте `before`, только проверив его
|
|
348
|
+
или вне семейства `from`; у `purge`, `trim` и `reset` поля нет (`undefined`) — вычитайте `before`, только проверив его истинность: `if (c.before)`.
|
|
346
349
|
Смена класса без смены id (класс без ключа), уводящая строку из семейства `from`, приходит удалением (`op` `delete`,
|
|
347
350
|
`row` — последняя версия в семействе с `$deleted`). `handle` получает изменения
|
|
348
351
|
одного арендатора. Ошибку его данных (база отклонила его результаты или `out` — его строку или приращение)
|
|
@@ -392,7 +395,7 @@ const server = await createMcpServer(await svc.as(userId)) // letopis_model, l
|
|
|
392
395
|
(`notice` — постоянное пояснение, что это данные, а не инструкции); `data` по терминалам: `rows` —
|
|
393
396
|
`{ rows, count, more, cursor? }`, `ids` — `{ ids, count, more, cursor? }` (`count` — строк на странице), `first` —
|
|
394
397
|
`{ row }`, `count` — `{ count }`, `countBy` — `{ counts, truncated? }`, `sum`, `avg`, `min`, `max` — `{ value }`.
|
|
395
|
-
Отказ — `isError: true` и `structuredContent` вида `{ notice, error }` (`error` — `{ code, message, similar? }`). Запись — только `write: true`
|
|
398
|
+
Отказ — `isError: true` и `structuredContent` вида `{ notice, error }` (`error` — `{ code, message, detail?, sqlstate?, similar? }`). Запись — только `write: true`
|
|
396
399
|
(без него `letopis_write` нет в списке, а вызов — ошибка протокола SDK): `update` с `rev`; `delete` без `confirm` —
|
|
397
400
|
превью, с `confirm` — подтверждение человека (elicitation), без него — `confirmation_required`. Данные из базы в
|
|
398
401
|
ответе — внутри `<untrusted-data-…>`.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,150 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
Формат: [Keep a Changelog](https://keepachangelog.com/), версии — semver.
|
|
3
|
+
Формат: [Keep a Changelog](https://keepachangelog.com/), версии — semver. Что обещают патч, минор и мажор, — раздел
|
|
4
|
+
«Версии и совместимость» в README; шаг версии сверяет `npm run check:semver`. С 1.4.2 у правки ядра (библиотеки и SQL)
|
|
5
|
+
в конце пункта указан источник находки: «Источник: тест», «пример», «сервис», «ревью», «soak» или «внедрение».
|
|
6
|
+
|
|
7
|
+
## [1.4.2] — 2026-11-01
|
|
8
|
+
|
|
9
|
+
План 2.0 (`reports/PLAN-LETOPIS-2.0.md`): правила версий, проверка перед выпуском, исправления.
|
|
10
|
+
|
|
11
|
+
### Добавлено
|
|
12
|
+
- Правила версий — подраздел «Версии и совместимость» в README (обе версии): патч — только исправления без новой
|
|
13
|
+
ревизии движка и без новых отказов; минор — новые возможности и новая ревизия с `up({ upgrade: true })`; мажор —
|
|
14
|
+
удаление, переименование, смена сигнатур и новые отказы.
|
|
15
|
+
- `npm run check:semver` (`scripts/check-semver.mjs`): контракт API и ревизия движка текущего дерева против тега
|
|
16
|
+
прошлого выпуска — исчезло → мажор, появилось или выросла ревизия → минор, тексты ошибок → патч; шаг версии обязан
|
|
17
|
+
быть не меньше. Входит в `check:docs` (в CI checkout с полной историей) и в `release.yml` до публикации. Сборка
|
|
18
|
+
контракта вынесена в `scripts/contract.mjs`; в `docs/api-contract.json` появилось поле `codes` — коды ошибок.
|
|
19
|
+
- `npm run prerelease` (`scripts/prerelease.mjs`) — полная локальная проверка одной командой, строго по очереди:
|
|
20
|
+
typecheck, сверка документов, весь `npm test`, примеры сайта со снимками, ER-схемы, покрытие, сборка сайта,
|
|
21
|
+
песочница, сценарии четырёх сервисов, проверка пакета с живым прогоном. Первый красный шаг останавливает прогон;
|
|
22
|
+
итог — таблица в `docs/checkpoint-<x.y.z>.md`. Тесты в CI не гоняются (решение владельца 01.11.2026).
|
|
23
|
+
- Долгий прогон `npm run soak` (`bench/soak.mjs`, план 2.0, этап 2): смешанная нагрузка одного процесса на часы —
|
|
24
|
+
писатели с агрегатом и `inc`, читатели, подписки, проектор, `maintain()` с прореживанием под записью, вход и выход,
|
|
25
|
+
`db.as()`, заведение и удаление классов; замер раз в 5 минут, в конце — критерии §7.7 и `verify({ data: true })`.
|
|
26
|
+
Быстрый режим на 15 минут — шаг `soak` в `npm run prerelease`.
|
|
27
|
+
- Метрика стабилизации: у правки ядра в CHANGELOG — источник находки, `release-notes.mjs` считает их в теле релиза.
|
|
28
|
+
- Тест `upgrade` №3: накат схемы 1.4.1 (dev-зависимость `letopis-141`) обычным `up()` без миграций; правило — каждый
|
|
29
|
+
выпуск проверяет накат с предыдущего. Тесты `semver` и `prerelease` проверяют сами сценарии (план 2.0, 7.8).
|
|
30
|
+
- `CONTRIBUTING.md` (и английская версия), шаблон issue об ошибке, форма отчёта о внедрении
|
|
31
|
+
`docs/adoption/TEMPLATE.md`.
|
|
32
|
+
|
|
33
|
+
### Исправлено
|
|
34
|
+
- Строки-замки удалённых классов не убирались: запись строки `Class` заводит две строки `LOCK` в `secret` (замок
|
|
35
|
+
семейства и его шлюз), а удаление класса их оставляло — приложение, которое заводит и удаляет классы на лету, копило
|
|
36
|
+
их без предела. Теперь `maintain()` убирает строки-замки семейств, чей корневой класс удалён (или удалён его
|
|
37
|
+
арендатор); строку, которую держит транзакция, — следующим вызовом; класс с тем же именем заводит их заново
|
|
38
|
+
(7.7 №8). Источник: soak.
|
|
39
|
+
- `maintain()` мог безвозвратно стереть историю: если класс System заводили позже одноимённого класса арендатора,
|
|
40
|
+
строки журнала арендатора попадали в отбор дважды, и промежуточные версии ярусов стирались. Теперь политику выбирают
|
|
41
|
+
прореживание, `verify` и срок курсора подписки так же, как класс строки: свой класс арендатора, иначе System
|
|
42
|
+
(план 2.0, 7.3 №1). Источник: ревью.
|
|
43
|
+
- История удалённого объекта стирается целиком и тогда, когда до `reclass` он был в классе без политики: версии
|
|
44
|
+
прежнего класса больше не остаются и не показывают объект живым в срезе (7.3 №2). Источник: ревью.
|
|
45
|
+
- Стирание истории удалённого ждёт `restore()`, начатый в другой транзакции, и после него историю не трогает: раньше
|
|
46
|
+
цепочка восстановленного объекта начиналась не с версии 1 (7.3 №3). Источник: ревью.
|
|
47
|
+
- `maintain()` чистит истёкшие сессии и коды один раз за вызов, в первой пачке, а не в каждой из пачек (до 100):
|
|
48
|
+
у функции базы `maintain` третий аргумент `p_cleanup`; схема, которую ещё не обновил `up()`, получает прежний
|
|
49
|
+
вызов (7.3 №4). Источник: ревью.
|
|
50
|
+
|
|
51
|
+
- Два `create` одного ключа в батче проходили молча: обе строки ответа были одной и той же строкой. Теперь повтор —
|
|
52
|
+
`exists`, и батч откатывается целиком (7.2 №1). Источник: ревью.
|
|
53
|
+
- Строки ответа `reclass`, `restore` и `rekey` — в порядке целей (`sort()` до глагола, иначе id), как обещает README;
|
|
54
|
+
в 1.4.1 так стало только у `update` и `anonymize` (7.2 №2). Источник: ревью.
|
|
55
|
+
- Цепочка с глаголом и читающим хвостом в значении слота (`.Cat.add(db.Cat().create(…).Parent())`) падала с
|
|
56
|
+
`TypeError`; вложенный план не проходил проверки до базы. Теперь работает, а модификатор сразу после её глагола —
|
|
57
|
+
`invalid_query`, как у терминала (7.2 №3). Источник: ревью.
|
|
58
|
+
- Кэш результатов отдавал общие объекты: правка `rows[0].data` у ответа `.cache()` портила следующие ответы. Теперь
|
|
59
|
+
кэш хранит и отдаёт копии (7.2 №5). Источник: ревью.
|
|
60
|
+
|
|
61
|
+
- Подписка без событий опрашивала журнал сериями по 40 коротких повторов на каждый тик таймера — около 4 запросов в
|
|
62
|
+
секунду вместо одного за `pollMs`. Теперь короткие повторы идут только после сигнала (7.4 №1). Источник: ревью.
|
|
63
|
+
- `close()` подключения не закрывал живые подписки: с чужим пулом (`connect({ sql })`) они опрашивали журнал и после
|
|
64
|
+
`close()`. Теперь подписки закрываются, ожидающий `next()` получает `done: true` (7.4 №8). Источник: ревью.
|
|
65
|
+
|
|
66
|
+
- `reclass` класса без ключа (id тот же) сигналил только новый класс: ответы кэша и подписки прежнего класса не
|
|
67
|
+
сбрасывались и отдавали строку, которая из него ушла. Теперь сигнал идёт по обоим классам, как у пути через
|
|
68
|
+
триггер (7.4 №4). Источник: ревью.
|
|
69
|
+
|
|
70
|
+
- `letopis types` падал на кириллическом имени класса (`invalid_class`, хотя такой идентификатор в TypeScript допустим),
|
|
71
|
+
а для класса без полей, класса с `anyOf`/`oneOf` и открытой схемы выдавал неверный TypeScript
|
|
72
|
+
(`export interface XData Record<string, unknown>`). Теперь имена — буквы Unicode, не-объекты — `export type`, ветки
|
|
73
|
+
`anyOf` из одних `required` у класса с полями — условие, а не форма (7.5 №1). Источник: ревью.
|
|
74
|
+
- `hub('X', { fields: z.object(…) })` без других ключей описания читался как короткая форма — класс с единственным
|
|
75
|
+
полем `fields`. Теперь это полная форма (7.5 №2). Источник: ревью.
|
|
76
|
+
- Понятные ошибки вместо сырых: `connect({ owner })` с ролью, которой нет, — `invalid_data`; `anonymize(['опечатка'])` —
|
|
77
|
+
«поля нет в классе»; `up()` на готовой роли владельца без ADMIN OPTION — объяснение, что нужно; `auth.switch` при
|
|
78
|
+
имперсонации сырым SQL — `acl_denied` из каталога, а не неизвестный код; `--trust` — `LetopisError` (7.5 №3).
|
|
79
|
+
Источник: ревью.
|
|
80
|
+
- CLI: `letopis <команда> --help` возвращает код 0, неизвестная команда — справку с кодом 2 ещё до подключения к базе
|
|
81
|
+
(7.5 №4). Источник: ревью.
|
|
82
|
+
- Сравнение имени проектора в `88-derived.sql` — той же коллацией, что индекс `secret_kind_ident` (7.5 №5). Источник:
|
|
83
|
+
ревью.
|
|
84
|
+
|
|
85
|
+
- Срез `asOf` по классу и по концам показывал объект в классе, из которого он уже ушёл (`reclass` класса без ключа
|
|
86
|
+
сохраняет id): последняя версия выбиралась среди версий классов шага. Теперь она выбирается среди всех версий id, и
|
|
87
|
+
класс проверяется у неё — как уже делала ветка по id; то же в `trimHistory` по классам (7.1 №1). Источник: ревью.
|
|
88
|
+
- `after()` в `versions()` молча не действовал: страница «после курсора» повторяла первую. Теперь курсор листает версии
|
|
89
|
+
в порядке `sort()`. Курсор версии несёт её `rev` (`cursorOf` кладёт его сам), поэтому версии одного объекта с равным
|
|
90
|
+
полем сортировки не теряются. Без `sort()` — `invalid_query`, как у `rows()`. `count()`, агрегаты, `diff()` и
|
|
91
|
+
`blame()` курсор не учитывают, README теперь это описывает (7.1 №4). Источник: ревью.
|
|
92
|
+
- `paths()` с `after()` терял пути с общим последним узлом: курсор помнил только этот узел, и следующая страница
|
|
93
|
+
начиналась после всех путей к нему. Теперь `cursorOf(путь, поле)` кладёт в курсор id всех узлов (`path`), а пути
|
|
94
|
+
упорядочены по ним после поля и последнего узла. Ключи объекта пути идут в порядке шагов, как в примере README;
|
|
95
|
+
прежде они шли в порядке jsonb — по длине имени (7.1 №3). Источник: ревью.
|
|
96
|
+
- `versions({ follow: true })`, `diff({ follow: true })` и `blame()` после `rekey` смешивали историю перенесённого
|
|
97
|
+
объекта с объектом, которого потом завели на прежнем id (тот же ключ): брались все версии всех связанных id.
|
|
98
|
+
Теперь история идёт по отрезкам между переносами — надгробие переноса кончает отрезок, первая версия на новом id
|
|
99
|
+
начинает (функция базы `history_segments`, обычный `up()`); у нового объекта своя история с его создания (7.1 №2).
|
|
100
|
+
Источник: ревью.
|
|
101
|
+
- `blame()` на каждом новом соединении первые пять вызовов шли с квадратичной сложностью: свой план функции базы
|
|
102
|
+
`version_paths` по фильтру видимости оценивал версии в 2 строки и искал предыдущую версию по одному id, проверяя
|
|
103
|
+
номер фильтром, — около 250 мс на 1000 версиях и секунды на 10 000. Теперь обе версии ищутся по `(id, rev)` на
|
|
104
|
+
каждую пару: 21 мс с первого вызова (`bench/results/history.json`; ошибку нашёл замер `bench/history.bench.mjs`
|
|
105
|
+
при проверке 7.1 №2). Источник: тест.
|
|
106
|
+
- Проектор не строил данных арендатора, который стал участником после первой проекции: изменения шли только с
|
|
107
|
+
курсора, строки, заведённые до вступления, в проекцию не попадали. Теперь чекпоинт хранит состав участников
|
|
108
|
+
(`tenants`), и новый участник получает начальную перестройку пачками (7.4 №5). Источник: ревью.
|
|
109
|
+
- Проектор на подключении без LISTEN не перечитывал состав участников: сигнала о новых классах нет, и арендатор,
|
|
110
|
+
вступивший после запуска, не участвовал вовсе. Теперь состав читается в начале `run()` и не реже раза в 5 с (7.4 №6).
|
|
111
|
+
Источник: ревью.
|
|
112
|
+
- Реестры классов `db.as()` копились без предела: процесс, который ходит от имени многих арендаторов, держал реестр
|
|
113
|
+
каждого и перечитывал их все на каждый сигнал классов. Теперь их не больше 1000 на подключение, давно не нужный
|
|
114
|
+
вытесняется; фасад с вытесненным реестром работает — реестр возвращается и перечитывается (7.4 №8). Источник: ревью.
|
|
115
|
+
- Загрузчик с `existing: 'update'` писал в строку другого класса, если у неё тот же id: объект класса без ключа после
|
|
116
|
+
`reclass` сохраняет id, и повторный импорт по `$ext` правил его данными, проверенными по схеме прежнего класса, мимо
|
|
117
|
+
триггеров. Теперь такая строка — `exists` с классом объекта в тексте (7.2 №4). Источник: ревью.
|
|
118
|
+
|
|
119
|
+
- Имя поля, в котором есть `@S@` (например, `a@S@b`), портилось в запросе цепочки: построитель подставлял на это место
|
|
120
|
+
имя схемы, и фильтр, `sort()` или агрегат по такому полю ничего не находили (7.1 №5). Источник: ревью.
|
|
121
|
+
|
|
122
|
+
- После обрыва соединения транзакция проектора, `db.begin()` при открытии и загрузчик слали `rollback` в мёртвое
|
|
123
|
+
соединение и возвращали его в пул — postgres.js 3.4 ронял процесс необработанным `TypeError`. Теперь, как в
|
|
124
|
+
`runTx`: после обрыва в соединение не пишут и в пул его не возвращают, промис отклоняется (7.4 №7). Источник: ревью.
|
|
125
|
+
|
|
126
|
+
- `watch({ from: 'start' })` после первой полной пачки событий старше яруса `all` падал с `cursor_expired`, хотя
|
|
127
|
+
обещан весь журнал. Теперь, пока подписка не догнала журнал, свои чтения она шлёт без срока курсора; курсоры
|
|
128
|
+
событий и `sub.cursor` время хранят, и продолжение с них после прореживания — по-прежнему `cursor_expired` (7.4 №3).
|
|
129
|
+
Источник: ревью.
|
|
130
|
+
|
|
131
|
+
- Подписка на класс не получала события его потомков, заведённых после `watch()`: список потомков фиксировался при
|
|
132
|
+
подписке, и курсор уходил за их события. Теперь классы с потомками берутся из текущего реестра (7.4 №2).
|
|
133
|
+
Источник: ревью.
|
|
134
|
+
|
|
135
|
+
### Изменено
|
|
136
|
+
- CI: у задания `test` нет сервиса PostgreSQL (живой прогон пакета в CI выключен, база не нужна), комментарии
|
|
137
|
+
`ci.yml` описывают то, что задания делают.
|
|
4
138
|
|
|
5
139
|
## [1.4.1] — 2026-11-01
|
|
6
140
|
|
|
7
141
|
Исправления, найденные прогоном примеров документации для людей (`guide/`).
|
|
8
142
|
|
|
143
|
+
По правилам версий, введённым в 1.4.2, часть изменений этого выпуска требовала минора (ревизия движка 7 с миграцией
|
|
144
|
+
`0007`, новый метод `auth.session`), а новые отказы (`invalid_query` для модификатора после глагола, `acl_denied` для
|
|
145
|
+
удаления `Account` System, код 1 у `letopis types` и `letopis describe` без сессии) и убранный код `not_implemented` —
|
|
146
|
+
мажора.
|
|
147
|
+
|
|
9
148
|
### Добавлено
|
|
10
149
|
- `db.auth.session(token)` — сведения о сессии по её токену для сервиса: чья сессия (`account`), в каком арендаторе
|
|
11
150
|
она работает (`tenant`), цело ли там членство (`member`; в собственном арендаторе — всегда `true`), когда она
|
package/README.en.md
CHANGED
|
@@ -392,11 +392,30 @@ class name. While the index is being built, writes to the journal wait. On a lar
|
|
|
392
392
|
without stopping writes: `create index concurrently log_class_tenant_at on "v2.<schema>".log (class, tenant, at)` as
|
|
393
393
|
the owner role — then `up()` only drops the old one.
|
|
394
394
|
|
|
395
|
-
The `upgrade` test checks upgrading 1.1.0 and 1.0.1 schemas
|
|
395
|
+
The `upgrade` test checks upgrading 1.4.1, 1.1.0 and 1.0.1 schemas; every release checks the upgrade from the previous
|
|
396
|
+
release. The old package installs the schema and the old engine
|
|
396
397
|
writes the data, then the current version's `up()` runs. After it, the versions and hashes written by the old engine
|
|
397
|
-
are in place, `verify({ data: true })` is clean, and the schema catalog matches a clean installation.
|
|
398
|
-
upgraded with `up(
|
|
399
|
-
`0006` and `0007`.
|
|
398
|
+
are in place, `verify({ data: true })` is clean, and the schema catalog matches a clean installation. A 1.4.1 schema of the
|
|
399
|
+
same revision 7 is upgraded with a plain `up()`, without migrations; 1.1.0 (revision 6) and 1.0.1 (revision 5) — with
|
|
400
|
+
`up({ upgrade: true })`: with migration `0007` and with migrations `0006` and `0007`.
|
|
401
|
+
|
|
402
|
+
### Versions and compatibility
|
|
403
|
+
|
|
404
|
+
letopis versions follow semver. What each version step promises:
|
|
405
|
+
|
|
406
|
+
- **Patch** (1.4.1 → 1.4.2) — fixes only. The engine revision stays the same: a plain `up({ schema })` re-applies the
|
|
407
|
+
fixed functions, there are no migrations; until it runs, `connect()` warns about the stamp mismatch. Working code gets
|
|
408
|
+
no new refusals, and no methods, options, exports or error codes disappear.
|
|
409
|
+
- **Minor** (1.4 → 1.5) — new features: methods, options, error codes; behavior changes that do not break working code;
|
|
410
|
+
a new engine revision. `up({ schema, upgrade: true })` applies the new revision. Nothing is removed or renamed.
|
|
411
|
+
- **Major** (1.x → 2.0) — removed and renamed methods, options, exports and error codes, changed signatures and types,
|
|
412
|
+
new refusals for everyone. `MIGRATION.md` describes what to change in your code.
|
|
413
|
+
|
|
414
|
+
The text of an error message is not part of the API: a patch may make it more precise. Rely on `err.code`.
|
|
415
|
+
|
|
416
|
+
Before every release the `check:semver` script checks the version step: it compares the API contract
|
|
417
|
+
(`docs/api-contract.json`) and the engine revision with the previous release and requires at least the matching step.
|
|
418
|
+
Every release checks the schema upgrade from the previous release (the `upgrade` test).
|
|
400
419
|
|
|
401
420
|
### Development without PostgreSQL: PGlite
|
|
402
421
|
|
|
@@ -917,8 +936,8 @@ const page2 = await db.Product().sort('data.price', 'desc')
|
|
|
917
936
|
|
|
918
937
|
`sort(field, 'asc' | 'desc')` sorts by `'at'`, `'rev'`, `'id'` or `'data.<path>'`. `after(cursorOf(row, field))` is
|
|
919
938
|
pagination "after this row"; it requires `sort()`. Such pagination withstands inserts and deletes between pages, but a
|
|
920
|
-
row whose sort field has changed may drop out or repeat. A cursor is an object `{ v, id }` (the field value
|
|
921
|
-
id): it survives JSON, so you can hand it to a client. The field in `cursorOf` must match the `sort` field. The cursor
|
|
939
|
+
row whose sort field has changed may drop out or repeat. A cursor is an object `{ v, id, rev }` (the field value, the
|
|
940
|
+
row id and its version number): it survives JSON, so you can hand it to a client. The field in `cursorOf` must match the `sort` field. The cursor
|
|
922
941
|
value is checked against the type of the `sort` field before the query, and a wrong one is `invalid_query`: a numeric
|
|
923
942
|
field needs a finite number, `rev` an integer, a boolean field `true` or `false`, a text field a string, uuid fields
|
|
924
943
|
and `id` a uuid, time fields and `at` an ISO string with a time zone, a date field `YYYY-MM-DD`, a field without a
|
|
@@ -926,6 +945,12 @@ description a string, a number or `true`/`false`. `null` fits only data fields,
|
|
|
926
945
|
The check cannot tell apart a cursor of the same type taken from another field. `limit` and `offset` are available
|
|
927
946
|
too.
|
|
928
947
|
|
|
948
|
+
`after()` also pages `versions()`: the cursor of a version carries its `rev`, so versions of one object with an equal
|
|
949
|
+
sort field are not lost. In `paths()` the cursor comes from `cursorOf(path, field)`: the field and the id are those of
|
|
950
|
+
the last node of the path, and `path` holds the ids of all nodes, so paths to the same node are not lost. A cursor
|
|
951
|
+
`cursorOf(path.Node, field)` taken from a single node skips the other paths to that node. `count()`, aggregates,
|
|
952
|
+
`diff()` and `blame()` ignore the cursor.
|
|
953
|
+
|
|
929
954
|
A data field may be `null` or missing. `sort` places such rows the way `order by` in PostgreSQL does: last when
|
|
930
955
|
ascending, first when descending, and `after()` treats `null` as a value after all others (ascending) or before them
|
|
931
956
|
(descending). So pages go through such rows without gaps or repeats, and a cursor with the value `null` gives the next
|
|
@@ -964,7 +989,8 @@ const orderPaths = await db.Order(order).line().Product().paths() // [{ Order:
|
|
|
964
989
|
```
|
|
965
990
|
|
|
966
991
|
`paths()` returns the nodes of every variant of the path; the key of a node is the step name or the name from
|
|
967
|
-
`.alias(name)`.
|
|
992
|
+
`.alias(name)`. The keys follow the order of the steps; if the chain ends by returning to an earlier node, that node
|
|
993
|
+
comes last. Repeating a link in the chain returns to its node: this way you can go from one link in several
|
|
968
994
|
directions. `db.entity(row | chain)` inserts a ready node into the path.
|
|
969
995
|
|
|
970
996
|
## 7. Deleting and restoring
|
|
@@ -1277,7 +1303,7 @@ const changed = await adm.load({ Shop: [{ name: 'Cornflower', city: 'Samara' }]
|
|
|
1277
1303
|
| Option | Values |
|
|
1278
1304
|
|---|---|
|
|
1279
1305
|
| `duplicates`: a repeated id within the load | `'error'` (default): cancel; `'first'` or `'last'`: keep the first or the last row |
|
|
1280
|
-
| `existing`: the object already exists in the database | `'error'` (default): cancel; `'skip'`: skip, do not resurrect deleted objects; `'update'`: a new version if the content differs (unchanged objects are counted in `skipped`), deleted objects are created again |
|
|
1306
|
+
| `existing`: the object already exists in the database | `'error'` (default): cancel; `'skip'`: skip, do not resurrect deleted objects; `'update'`: a new version if the content differs (unchanged objects are counted in `skipped`), deleted objects are created again; an object of another class with the same id (a class without a key after `reclass`) is `exists` |
|
|
1281
1307
|
|
|
1282
1308
|
### Portions, history and verification
|
|
1283
1309
|
|
|
@@ -1486,8 +1512,9 @@ A service acts on behalf of a user through `svc.as(account, { tenant, owner })`.
|
|
|
1486
1512
|
`auth.impersonate` permission (by System rules), that the user exists and is enabled, and that the tenant is available
|
|
1487
1513
|
to the user, either their own or through membership. You cannot act on behalf of System, services, system
|
|
1488
1514
|
administrators or disabled accounts. The user's visibility under the access rules is not checked. In the journal,
|
|
1489
|
-
`author` is the user and `agent` is the service.
|
|
1490
|
-
|
|
1515
|
+
`author` is the user and `agent` is the service. The class registry of `db.as()` is one per tenant, and a connection
|
|
1516
|
+
keeps at most 1000 of them: a long unused one is evicted, and a facade whose registry was evicted keeps working — the
|
|
1517
|
+
registry is re-read. A user's connection by their own token switches between tenants like this:
|
|
1491
1518
|
|
|
1492
1519
|
<!-- run: guide -->
|
|
1493
1520
|
```ts
|
|
@@ -1622,6 +1649,12 @@ Without `sort()`, versions go by id and version number (with `follow`, by time).
|
|
|
1622
1649
|
too, and `limit()` limits them in the query itself: `db.Product(kettle).sort('at', 'desc').limit(5).versions()` gives
|
|
1623
1650
|
the five latest versions, newest first.
|
|
1624
1651
|
|
|
1652
|
+
`follow: true` gives the history of the object across all its ids: before a move, the versions of the former id;
|
|
1653
|
+
after it, of the new one. An object created later on the former id (the same key after `rekey`) is a different one,
|
|
1654
|
+
and its versions are not in this history; its own history with `follow` starts at its creation. `diff({ follow: true
|
|
1655
|
+
})` and `blame()` work the same way. Without `follow`, `versions()` gives every version of the id, whoever it
|
|
1656
|
+
belonged to.
|
|
1657
|
+
|
|
1625
1658
|
### Point-in-time read
|
|
1626
1659
|
|
|
1627
1660
|
<!-- run: guide -->
|
|
@@ -1724,7 +1757,8 @@ none.
|
|
|
1724
1757
|
single-valued end, every id of a multi-valued end (`['links', role, id]`), every tag (`['tags', tag]`), `owner` and
|
|
1725
1758
|
`class`. `rev`, `at`, `author`, `agent`, `op` and `reason` come from the version that changed the leaf last. Empty
|
|
1726
1759
|
`agent` and `reason` are omitted; `id` is present only if it is a version of the former id (the history went
|
|
1727
|
-
through `reclass` or `rekey`).
|
|
1760
|
+
through `reclass` or `rekey`). `blame()` does not look at versions of another object on the same id — one that
|
|
1761
|
+
lived there before a move or was created after it.
|
|
1728
1762
|
- `exact: false` means there are gaps between the current version and the found one (the history is thinned,
|
|
1729
1763
|
trimmed or partly invisible): a missing version may have changed the leaf too. `complete: true` means the whole
|
|
1730
1764
|
history was scanned without gaps; by default at most the 1000 latest versions are scanned.
|
|
@@ -1943,6 +1977,10 @@ sales.close() // stop() after the current bat
|
|
|
1943
1977
|
not see the changes of a class with `history: false`: it has no journal. So a tenant with such a class among the
|
|
1944
1978
|
sources drops out with the code `invalid_class`, and the projector does not build its derived rows. Turn the journal
|
|
1945
1979
|
of the class on and rebuild the tenant with `rebuild({ tenant })`.
|
|
1980
|
+
- A tenant that became a participant after the first projection (the administrator marked its classes later) gets an
|
|
1981
|
+
initial batch rebuild: the rows created before it joined get into the projection too. A projector re-reads its
|
|
1982
|
+
participants on a class signal, and on a connection without LISTEN (`listen: false`) at the start of `run()` and at
|
|
1983
|
+
least every 5 seconds.
|
|
1946
1984
|
- `batch` is how many changes go into a batch (500), `pollMs` is fallback polling (1000 ms): source signals wake the
|
|
1947
1985
|
projector earlier.
|
|
1948
1986
|
- `lockTimeout` is how long a projector transaction waits for another transaction's row lock (5000 ms; 0 — no limit).
|
|
@@ -2062,7 +2100,8 @@ const report = await adm.maintain() // { sessions, codes, thinned, batches,
|
|
|
2062
2100
|
|
|
2063
2101
|
`maintain()` deletes sessions and one-time codes of all kinds (`OTP` and custom `OTP_<NAME>` from `issueOtp({ kind })`)
|
|
2064
2102
|
from the `secret` table — expired ones, as well as
|
|
2065
|
-
revoked sessions and codes that were used, replaced by a new one or ran out of attempts, —
|
|
2103
|
+
revoked sessions and codes that were used, replaced by a new one or ran out of attempts, — and the lock rows of deleted
|
|
2104
|
+
classes (two per class; a row held by a transaction goes on the next call), thins the journal by the
|
|
2066
2105
|
retention policy and checks the PostgreSQL signal queue: if it is more than half full, you get a warning. You do not
|
|
2067
2106
|
need `maintain()` for the rows of a deleted account: the deletion itself erases them ([Deleting an account](#deleting-an-account)).
|
|
2068
2107
|
The system administrator, a role with the `letopis.maintain` permission or an admin connection can call it. An external scheduler
|
|
@@ -2587,7 +2626,7 @@ behavior.
|
|
|
2587
2626
|
**Platform**
|
|
2588
2627
|
|
|
2589
2628
|
- PostgreSQL 18 or newer is required; there are no workarounds for 15–17. AWS RDS, Google Cloud SQL, Azure Flexible
|
|
2590
|
-
Server and Neon support 18, while Supabase, as of October 2026, offers only 15 and 17, so
|
|
2629
|
+
Server and Neon support 18, while Supabase, as of October 2026, offers only 15 and 17, so letopis will not work there.
|
|
2591
2630
|
- The database must be in UTF8 encoding (`casefold()` and the `pg_unicode_fast` collation). If a new major PostgreSQL
|
|
2592
2631
|
version updates the Unicode tables, the unique index of credential identifiers is rebuilt with the `reindex` command.
|
|
2593
2632
|
- PGlite has one superuser connection: no concurrent writers and no `LISTEN`, and RLS does not apply
|
|
@@ -2798,6 +2837,12 @@ behavior.
|
|
|
2798
2837
|
This section is for those who develop letopis itself. Run: `cd lib && npm test`. The harness starts the
|
|
2799
2838
|
`letopis-pg18` container (`postgres:18`, port 15433) by itself if the database does not respond; each test file works in its own schema `v2.t_<name>`.
|
|
2800
2839
|
|
|
2840
|
+
CI runs no tests: before a push and a release, everything that checks behavior runs as one command,
|
|
2841
|
+
`cd lib && npm run prerelease` — typecheck, the documentation check, all tests, the site examples against snapshots, the
|
|
2842
|
+
sandbox, the service scenarios, a quick soak run (15 minutes of mixed load) and the package check with a live run, in
|
|
2843
|
+
order; the first red step stops the run. The same load for a day: `cd lib && npm run soak` (duration — `SOAK_HOURS`,
|
|
2844
|
+
24 hours by default). A test for a fix is named after its test-plan item ("plan 2.0, 7.3 No. 1") and fails without the fix.
|
|
2845
|
+
|
|
2801
2846
|
```
|
|
2802
2847
|
# connect — connecting under a session, refusing roles that bypass RLS, the registry, the deferred signal, error translation (stage 2)
|
|
2803
2848
|
# context — sessions, service sign-in by key, the RLS policies of stage 1 (stage 1)
|
|
@@ -2844,9 +2889,9 @@ This section is for those who develop letopis itself. Run: `cd lib && npm test`.
|
|
|
2844
2889
|
# retention — retention policy: tiers, a finite term, special versions, deleted rows, asOf, cursor_expired, time zone, catch-up run, inheritance and default — versus a reference in TypeScript (stage 6)
|
|
2845
2890
|
# watch — cursor subscription: commit order, a long transaction, resuming, listener drop, two tenants (stage 6)
|
|
2846
2891
|
# cache — result cache: hits, own and others' writes, the key with the user, permissions, revoked sessions, listener drop, ttl, class marking, without LISTEN, a race, db.sql, the limit of signal counters (stage 6; plan 1.1, checkpoint A)
|
|
2847
|
-
# maintain — maintenance: expired sessions and codes, permissions, one process under a lock, schedule (stage 6)
|
|
2892
|
+
# maintain — maintenance: expired sessions and codes, permissions, one process under a lock, schedule (stage 6), lock rows of deleted classes
|
|
2848
2893
|
# up — the full installer: fresh installation, re-apply, custom seeds, automatic database creation, UTF8, version 17, upgrade with migrations, PGlite, the first tenant and user (up({ tenant }), createTenant) (stages 6, 8)
|
|
2849
|
-
# upgrade — schema upgrade from 1.1.0 (upgrade: true, migration 0007) and from 1.0.1 (upgrade: true, migrations 0006 and 0007) with the letopis-11 and letopis-1 packages: versions and hashes written by the old engine, verify(), the catalog against a clean installation, lock rows, secret rows without an account erased, the application, a subscription and a projector over old data, a repeated up() (work after 1.2.0, stage 1; revision 7)
|
|
2894
|
+
# upgrade — schema upgrade from 1.4.1 (a plain up() without migrations, plan 2.0, 7.8 No. 2), from 1.1.0 (upgrade: true, migration 0007) and from 1.0.1 (upgrade: true, migrations 0006 and 0007) with the letopis-141, letopis-11 and letopis-1 packages: versions and hashes written by the old engine, verify(), the catalog against a clean installation, lock rows, secret rows without an account erased, the application, a subscription and a projector over old data, a repeated up() (work after 1.2.0, stage 1; revision 7)
|
|
2850
2895
|
# scenario-salon — end-to-end salon scenario: 21 acts from 0.21 (salon) with a coverage matrix of the 1.0 API (stage 6)
|
|
2851
2896
|
# telemetry — onQuery and slowMs, describe() and class metadata with inheritance, describeText (stage 6)
|
|
2852
2897
|
# history — diff and blame over the log, the reason of a change for verbs, db.begin() and batches; a version hidden by an ACL condition, the changed column, a missing version, row_changes over 20 thousand targets (plan 1.1, stage 1 and checkpoint A)
|
|
@@ -2856,4 +2901,10 @@ This section is for those who develop letopis itself. Run: `cd lib && npm test`.
|
|
|
2856
2901
|
# mcp — MCP tools: the model and meta only in results, tenant rights, the JSON chain and hints of similar names, row and byte limits with a cursor, the untrusted-data wrapper, writing only with write, update only with rev, deletion with a human confirmation (elicitation in revisions 2025 and 2026-07-28), the reason in blame, credentials, tool schemas, letopis without the SDK; letopis mcp over stdio, the HTTP handler; the confirmation window without the alias and meta, countBy and versions limited in the query itself, limits of steps and $or conditions; the permission classes only with allowAclWrite, a session in the System tenant only with allowSystemAdmin; structuredContent with a note, a requestState signature of the server user with an expiry (plan 1.1, stage 5; security review No. 2; checkpoint B and the work after it)
|
|
2857
2902
|
# import — import from 0.21 with a fixture of a real 0.21: key violation, --keyless, per-class counters, verify, self-reference, repeat, ids without a key, a dangling reference, $$strict and fields starting with $, update, credentials, CLI (stage 7)
|
|
2858
2903
|
# security-final — the final security review (stage 8, §7.10 No. 8.4): a family lock by the tenant of the root, account_purge without permission and restore/purge of a foreign id give neither a leak nor a lock; the functions of stage 6, log_refs_at, journal-based find_* reads and watch_read stay within their own tenant; NOTIFY carries only names or mode tokens; the cache key distinguishes the token, the actor and the tenant (stage 8)
|
|
2904
|
+
# semver — version rules: check:semver requires at least the matching version step — a major for a removed method or error code, a minor for a new method and a new engine revision, a patch for texts only (plan 2.0, 7.8 No. 1)
|
|
2905
|
+
# prerelease — the pre-release check: steps run strictly in order, the first red one stops the run and the following ones do not start (plan 2.0, 7.8 No. 3)
|
|
2906
|
+
# write-contract — README promises about writes (plan 2.0, 7.2): a repeated key in a batch gives exists and a rollback, the row order of reclass, restore and rekey responses, a chain with a verb as a slot value, the loader with existing: update does not edit a row of another class, a cached answer is not spoiled by editing an earlier answer
|
|
2907
|
+
# watch-contract — README promises about subscriptions and connections (plan 2.0, 7.4): an idle subscription polls the journal once per pollMs, close() of the connection closes live subscriptions, a reclass without a key resets the cache of the old class, a new projector participant gets an initial rebuild, a projector without LISTEN re-reads its participants, at most a limit of db.as() registries
|
|
2908
|
+
# model-contract — README promises about models, types, the CLI and errors (plan 2.0, 7.5): letopis types gives valid TypeScript for Cyrillic names, a class without fields and anyOf, the full form of hub() with z.object, clear errors instead of raw ones, CLI exit codes
|
|
2909
|
+
# read-contract — README promises about reads and history (plan 2.0, 7.1): an asOf slice by class and by ends after a class change without an id change, versions, diff and blame with follow after rekey, the after() cursor in versions() and paths(), a field name with @S@
|
|
2859
2910
|
```
|
package/README.md
CHANGED
|
@@ -385,10 +385,30 @@ const adm = await connect({ dsn: ADMIN, schema: r.schema, token: r.tenant!.token
|
|
|
385
385
|
записи: `create index concurrently log_class_tenant_at on "v2.<схема>".log (class, tenant, at)` от роли владельца, —
|
|
386
386
|
тогда `up()` только снимет прежний.
|
|
387
387
|
|
|
388
|
-
Обновление схем 1.1.0 и 1.0.1 проверяет тест `upgrade
|
|
388
|
+
Обновление схем 1.4.1, 1.1.0 и 1.0.1 проверяет тест `upgrade`; каждый выпуск проверяет обновление с предыдущего
|
|
389
|
+
выпуска. Схему ставит прежний пакет, данные пишет прежний движок, затем
|
|
389
390
|
вызывается `up()` текущей версии. После него версии и хэши прежнего движка на месте, `verify({ data: true })` чист, а
|
|
390
|
-
каталог схемы совпадает с чистой установкой.
|
|
391
|
-
|
|
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`).
|
|
392
412
|
|
|
393
413
|
### Разработка без PostgreSQL: PGlite
|
|
394
414
|
|
|
@@ -890,13 +910,18 @@ const page2 = await db.Product().sort('data.price', 'desc')
|
|
|
890
910
|
`sort(поле, 'asc' | 'desc')` — по `'at'`, `'rev'`, `'id'` или `'data.<путь>'`. `after(cursorOf(строка, поле))` —
|
|
891
911
|
постраничный вывод «после такой-то строки»; он требует `sort()`. Такой вывод устойчив к вставкам и удалениям между
|
|
892
912
|
страницами, но строка, у которой изменилось поле сортировки, может выпасть или повториться. Курсор — объект
|
|
893
|
-
`{ v, id }` (значение
|
|
913
|
+
`{ v, id, rev }` (значение поля, id и номер версии строки): он переживает JSON, и его можно отдать клиенту. Поле в `cursorOf` должно
|
|
894
914
|
совпадать с полем `sort`. Значение курсора проверяется по типу поля `sort` до запроса, неверное — `invalid_query`:
|
|
895
915
|
числовому полю нужно конечное число, `rev` — целое, логическому — `true` или `false`, текстовому — строка, uuid и
|
|
896
916
|
`id` — uuid, времени и `at` — строка ISO с поясом, дате — `ГГГГ-ММ-ДД`, полю без описания — строка, число или
|
|
897
917
|
`true`/`false`. `null` подходит только полям данных, `id` курсора — всегда uuid. Курсор того же типа, снятый
|
|
898
918
|
с другого поля, проверка не отличит. `limit` и `offset` тоже есть.
|
|
899
919
|
|
|
920
|
+
`after()` листает и `versions()`: курсор версии несёт её `rev`, поэтому версии одного объекта с равным полем
|
|
921
|
+
сортировки не теряются. В `paths()` курсор даёт `cursorOf(путь, поле)`: поле и id — последнего узла пути, а в `path`
|
|
922
|
+
— id всех узлов, поэтому не теряются пути к одному узлу. Курсор `cursorOf(путь.Узел, поле)` с одного узла пропустит
|
|
923
|
+
остальные пути к этому узлу. `count()`, агрегаты, `diff()` и `blame()` курсор не учитывают.
|
|
924
|
+
|
|
900
925
|
Поле данных бывает `null` или отсутствует. Такие строки `sort` ставит, как `order by` в PostgreSQL: при
|
|
901
926
|
возрастании — в конце, при убывании — в начале, а `after()` считает `null` значением после всех прочих (по
|
|
902
927
|
возрастанию) или перед ними (по убыванию). Поэтому страницы проходят такие строки без пропусков и повторов, а
|
|
@@ -934,7 +959,8 @@ const byStatus = await db.Order().countBy('data.status') // { new: 1 }
|
|
|
934
959
|
const orderPaths = await db.Order(order).line().Product().paths() // [{ Order: Row, line: Row, Product: Row }, …]
|
|
935
960
|
```
|
|
936
961
|
|
|
937
|
-
`paths()` отдаёт узлы каждого варианта пути; ключ узла — имя шага или имя из `.alias(имя)`.
|
|
962
|
+
`paths()` отдаёт узлы каждого варианта пути; ключ узла — имя шага или имя из `.alias(имя)`. Ключи идут в порядке
|
|
963
|
+
шагов; если цепочка кончается возвратом к прежнему узлу, этот узел — последний. Повтор связи в
|
|
938
964
|
цепочке возвращает к её узлу: так из одной связи можно пройти в несколько сторон. `db.entity(строка | цепочка)`
|
|
939
965
|
вставляет готовый узел в путь.
|
|
940
966
|
|
|
@@ -1243,7 +1269,7 @@ const changed = await adm.load({ Shop: [{ name: 'Василёк', city: 'Сам
|
|
|
1243
1269
|
| Опция | Значения |
|
|
1244
1270
|
|---|---|
|
|
1245
1271
|
| `duplicates` — повтор id внутри загрузки | `'error'` (по умолчанию) — отменить; `'first'` или `'last'` — оставить первую или последнюю строку |
|
|
1246
|
-
| `existing` — объект уже есть в базе | `'error'` (по умолчанию) — отменить; `'skip'` — пропустить, удалённые не воскрешать; `'update'` — новая версия, если содержимое отличается (неизменённые считаются в `skipped`), удалённые — создать
|
|
1272
|
+
| `existing` — объект уже есть в базе | `'error'` (по умолчанию) — отменить; `'skip'` — пропустить, удалённые не воскрешать; `'update'` — новая версия, если содержимое отличается (неизменённые считаются в `skipped`), удалённые — создать заново; объект другого класса с тем же id (класс без ключа после `reclass`) — `exists` |
|
|
1247
1273
|
|
|
1248
1274
|
### Порции, история и проверка
|
|
1249
1275
|
|
|
@@ -1442,7 +1468,8 @@ await boris.close()
|
|
|
1442
1468
|
`auth.impersonate` (по правилам System), что пользователь существует и включён и что арендатор ему доступен —
|
|
1443
1469
|
собственный или по членству. От имени System, сервисов, системных администраторов и отключённых аккаунтов
|
|
1444
1470
|
действовать нельзя. Видимость пользователя по правилам доступа не проверяется. В журнале `author` — пользователь,
|
|
1445
|
-
`agent` — сервис.
|
|
1471
|
+
`agent` — сервис. Реестр классов `db.as()` — один на арендатора, подключение держит их не больше 1000: давно не
|
|
1472
|
+
нужный вытесняется, а фасад, чей реестр вытеснен, продолжает работать — реестр перечитывается. Подключение
|
|
1446
1473
|
пользователя по своему токену переключается между арендаторами так:
|
|
1447
1474
|
|
|
1448
1475
|
<!-- run: guide -->
|
|
@@ -1571,6 +1598,11 @@ const followed = await db.Product(mug2).versions({ follow: true }) // и чер
|
|
|
1571
1598
|
`limit()` ограничивает их в самом запросе: `db.Product(kettle).sort('at', 'desc').limit(5).versions()` — пять
|
|
1572
1599
|
последних версий, новые первыми.
|
|
1573
1600
|
|
|
1601
|
+
`follow: true` отдаёт историю объекта на всех его id: до переноса — версии прежнего id, после — нового. Объект,
|
|
1602
|
+
который потом завели на прежнем id (тот же ключ после `rekey`), — другой, и его версий в этой истории нет; его
|
|
1603
|
+
собственная история с `follow` начинается с его создания. Так же идут `diff({ follow: true })` и `blame()`. Без
|
|
1604
|
+
`follow` `versions()` отдаёт все версии id, чьи бы они ни были.
|
|
1605
|
+
|
|
1574
1606
|
### Срез на дату
|
|
1575
1607
|
|
|
1576
1608
|
<!-- run: guide -->
|
|
@@ -1667,7 +1699,8 @@ const diffed: DiffResult = {
|
|
|
1667
1699
|
скаляр или массив в данных, пустой объект, роль одиночного конца, каждый id множественного конца
|
|
1668
1700
|
(`['links', роль, id]`), каждый тег (`['tags', тег]`), `owner` и `class`. `rev`, `at`, `author`, `agent`, `op` и
|
|
1669
1701
|
`reason` — от версии, которая последней изменила лист. Пустые `agent` и `reason` опускаются; `id` есть, только если
|
|
1670
|
-
это версия прежнего id (история прошла через `reclass` или `rekey`).
|
|
1702
|
+
это версия прежнего id (история прошла через `reclass` или `rekey`). Версии другого объекта на том же id — того,
|
|
1703
|
+
что жил там до переноса или заведён после, — `blame()` не смотрит.
|
|
1671
1704
|
- `exact: false` — между текущей версией и найденной есть пропуски (история прорежена, обрезана или часть версий не
|
|
1672
1705
|
видна): лист мог менять и выпавшая версия. `complete: true` — история просмотрена целиком и без пропусков; по
|
|
1673
1706
|
умолчанию просматривается не больше 1000 последних версий.
|
|
@@ -1870,6 +1903,10 @@ sales.close() // stop() после текущ
|
|
|
1870
1903
|
из `into` источником не бывает, даже если попадает в `from`: свои результаты проектор не читает. Изменений класса
|
|
1871
1904
|
с `history: false` проектор не видит: журнала у него нет. Поэтому арендатор, у которого такой класс среди
|
|
1872
1905
|
источников, выбывает с кодом `invalid_class`, и его производных строк проектор не строит. Включите журнал класса и перестройте арендатора `rebuild({ tenant })`.
|
|
1906
|
+
- Арендатор, который стал участником после первой проекции (администратор пометил его классы позже), получает
|
|
1907
|
+
начальную перестройку пачками: в проекцию попадают и строки, заведённые до вступления. Состав участников проектор
|
|
1908
|
+
перечитывает по сигналу классов, а на подключении без LISTEN (`listen: false`) — в начале `run()` и не реже раза
|
|
1909
|
+
в 5 секунд.
|
|
1873
1910
|
- `batch` — сколько изменений в пачке (500), `pollMs` — запасной опрос (1000 мс): сигналы источников будят
|
|
1874
1911
|
проектор раньше.
|
|
1875
1912
|
- `lockTimeout` — сколько транзакция проектора ждёт чужую блокировку строки (5000 мс; 0 — без предела). Транзакция
|
|
@@ -1983,7 +2020,8 @@ const report = await adm.maintain() // { sessions, codes, thinned, batches,
|
|
|
1983
2020
|
|
|
1984
2021
|
`maintain()` удаляет из таблицы `secret` сессии и одноразовые коды всех видов (`OTP` и свои `OTP_<ИМЯ>` из
|
|
1985
2022
|
`issueOtp({ kind })`) — истёкшие, а также погашенные сессии и коды, которые уже использованы, заменены новым или
|
|
1986
|
-
исчерпали попытки, —
|
|
2023
|
+
исчерпали попытки, — и строки-замки удалённых классов (по две на класс; строку, которую держит транзакция, — следующим
|
|
2024
|
+
вызовом), прореживает журнал по политике хранения
|
|
1987
2025
|
и проверяет очередь сигналов PostgreSQL: заполнение больше половины — предупреждение. Строки удалённого аккаунта
|
|
1988
2026
|
`maintain()` ждать не нужно: их стирает само удаление ([«Удалить аккаунт»](#удалить-аккаунт)). Вызывать может системный
|
|
1989
2027
|
администратор, роль с правом `letopis.maintain` или администраторское подключение. Запускает его внешний
|
|
@@ -2491,7 +2529,7 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2491
2529
|
**Платформа**
|
|
2492
2530
|
|
|
2493
2531
|
- Нужен PostgreSQL 18 или новее; обходных путей для 15–17 нет. AWS RDS, Google Cloud SQL, Azure Flexible Server
|
|
2494
|
-
и Neon поддерживают 18, а Supabase на октябрь 2026 года — только 15 и 17, поэтому там
|
|
2532
|
+
и Neon поддерживают 18, а Supabase на октябрь 2026 года — только 15 и 17, поэтому там letopis не заработает.
|
|
2495
2533
|
- База — в кодировке UTF8 (`casefold()` и коллация `pg_unicode_fast`). Если новая основная версия PostgreSQL
|
|
2496
2534
|
обновит таблицы Unicode, уникальный индекс идентификаторов кредов перестраивается командой `reindex`.
|
|
2497
2535
|
- PGlite — одно соединение суперпользователя: без параллельных писателей и `LISTEN`, RLS не действует
|
|
@@ -2689,6 +2727,12 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2689
2727
|
Раздел — для тех, кто развивает сам letopis. Запуск: `cd lib && npm test`. Обвязка сама поднимает контейнер `letopis-pg18` (`postgres:18`,
|
|
2690
2728
|
порт 15433), если база не отвечает; каждый тестовый файл работает в своей схеме `v2.t_<имя>`.
|
|
2691
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
|
+
|
|
2692
2736
|
```
|
|
2693
2737
|
# connect — подключение под сессией, отказ ролям в обход RLS, реестр, отложенный сигнал, перевод ошибок (этап 2)
|
|
2694
2738
|
# context — сессии, вход сервиса по ключу, политики RLS этапа 1 (этап 1)
|
|
@@ -2735,9 +2779,9 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2735
2779
|
# retention — политика хранения: ярусы, конечный срок, особые версии, удалённые, asOf, cursor_expired, часовой пояс, догоняющий запуск, наследование и умолчание — против эталона на TypeScript (этап 6)
|
|
2736
2780
|
# watch — подписка по курсору: порядок фиксации, долгая транзакция, возобновление, обрыв слушателя, два арендатора (этап 6)
|
|
2737
2781
|
# cache — кэш результатов: попадание, свои и чужие записи, ключ с пользователем, права, погашенные сессии, обрыв слушателя, ttl, пометка класса, без LISTEN, гонка, db.sql, предел счётчиков сигналов (этап 6; план 1.1, точка А)
|
|
2738
|
-
# maintain — обслуживание: истёкшие сессии и коды, права, один процесс под блокировкой, расписание (этап 6)
|
|
2782
|
+
# maintain — обслуживание: истёкшие сессии и коды, права, один процесс под блокировкой, расписание (этап 6), строки-замки удалённых классов
|
|
2739
2783
|
# up — полный установщик: свежая установка, повторный накат, свои сиды, автосоздание базы, UTF8, версия 17, upgrade с миграциями, PGlite, первый арендатор и пользователь (up({ tenant }), createTenant) (этапы 6, 8)
|
|
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)
|
|
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)
|
|
2741
2785
|
# scenario-salon — сквозной сценарий салона: 21 акт 0.21 (salon) с матрицей покрытия API 1.0 (этап 6)
|
|
2742
2786
|
# telemetry — onQuery и slowMs, describe() и метаданные классов с наследованием, describeText (этап 6)
|
|
2743
2787
|
# history — diff и blame по журналу, причина изменения у глаголов, db.begin() и батча; версия, скрытая условием ACL, колонка changed, пропуск версии, row_changes на 20 тыс. целей (план 1.1, этап 1 и точка А)
|
|
@@ -2747,4 +2791,10 @@ p50 — от −0,28 до +0,21 мс. Бенч против 1.0.1 (`bench/vs-101
|
|
|
2747
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; точка Б и доработки после неё)
|
|
2748
2792
|
# import — импорт из 0.21 по фикстуре настоящей 0.21: нарушение ключа, --keyless, счётчики по классам, verify, самоссылка, повтор, id без ключа, висячая ссылка, $$strict и поля на $, update, креды, CLI (этап 7)
|
|
2749
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@
|
|
2750
2800
|
```
|