letopis 1.4.2 → 3.0.0
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.md +516 -398
- package/CHANGELOG.md +739 -0
- package/LICENSE +75 -21
- package/LICENSE-COMMERCIAL.md +49 -0
- package/README.md +1312 -1190
- package/api-contract.json +381 -0
- package/dist/acl.d.ts +81 -1
- package/dist/acl.js +281 -13
- package/dist/admin.d.ts +11 -141
- package/dist/admin.js +28 -186
- package/dist/auth.d.ts +20 -5
- package/dist/auth.js +80 -21
- package/dist/cache.d.ts +8 -1
- package/dist/cache.js +30 -8
- package/dist/chain.d.ts +46 -48
- package/dist/chain.js +556 -223
- package/dist/cli.js +73 -96
- package/dist/demo/booking.d.ts +352 -36
- package/dist/describe.d.ts +47 -0
- package/dist/describe.js +195 -0
- package/dist/errors.d.ts +77 -13
- package/dist/errors.js +231 -30
- package/dist/index.d.ts +62 -74
- package/dist/index.js +312 -155
- package/dist/internal.d.ts +7 -0
- package/dist/jsonschema.js +18 -17
- package/dist/load.d.ts +29 -17
- package/dist/load.js +235 -190
- package/dist/log.d.ts +121 -0
- package/dist/log.js +633 -0
- package/dist/mcp.d.ts +5 -7
- package/dist/mcp.js +194 -225
- package/dist/messages/access.d.ts +90 -0
- package/dist/messages/access.js +113 -0
- package/dist/messages/api.d.ts +237 -0
- package/dist/messages/api.js +304 -0
- package/dist/messages/boot.d.ts +201 -0
- package/dist/messages/boot.js +256 -0
- package/dist/messages/codes.d.ts +172 -0
- package/dist/messages/codes.js +173 -0
- package/dist/messages/index.d.ts +98 -0
- package/dist/messages/index.js +121 -0
- package/dist/messages/log.d.ts +229 -0
- package/dist/messages/log.js +102 -0
- package/dist/messages/mcp.d.ts +306 -0
- package/dist/messages/mcp.js +387 -0
- package/dist/messages/model.d.ts +256 -0
- package/dist/messages/model.js +314 -0
- package/dist/messages/query.d.ts +141 -0
- package/dist/messages/query.js +171 -0
- package/dist/messages/sql-access.d.ts +70 -0
- package/dist/messages/sql-access.js +87 -0
- package/dist/messages/sql-core.d.ts +164 -0
- package/dist/messages/sql-core.js +198 -0
- package/dist/messages/sql-time.d.ts +86 -0
- package/dist/messages/sql-time.js +111 -0
- package/dist/messages/sql-write.d.ts +246 -0
- package/dist/messages/sql-write.js +270 -0
- package/dist/messages/time.d.ts +23 -0
- package/dist/messages/time.js +29 -0
- package/dist/messages/types.d.ts +27 -0
- package/dist/messages/types.js +5 -0
- package/dist/messages/write.d.ts +452 -0
- package/dist/messages/write.js +572 -0
- package/dist/model.d.ts +77 -60
- package/dist/model.js +43 -34
- package/dist/ops.d.ts +35 -0
- package/dist/ops.js +101 -0
- package/dist/pglite.d.ts +0 -9
- package/dist/pglite.js +3 -1
- package/dist/registry.d.ts +3 -19
- package/dist/registry.js +3 -16
- package/dist/signals.d.ts +1 -5
- package/dist/signals.js +1 -5
- package/dist/sql.d.ts +9 -1
- package/dist/sql.js +175 -107
- package/dist/sync.d.ts +10 -4
- package/dist/sync.js +90 -17
- package/dist/tx.d.ts +21 -2
- package/dist/tx.js +76 -13
- package/dist/typed.d.ts +252 -53
- package/dist/types.d.ts +93 -90
- package/dist/types.js +17 -14
- package/dist/up.d.ts +56 -35
- package/dist/up.js +229 -83
- package/dist/uuid.d.ts +8 -0
- package/dist/uuid.js +14 -6
- package/dist/validate.d.ts +13 -1
- package/dist/validate.js +82 -33
- package/dist/watch.d.ts +10 -16
- package/dist/watch.js +36 -77
- package/dist/write.d.ts +87 -17
- package/dist/write.js +441 -206
- package/llms.txt +48 -31
- package/package.json +13 -14
- package/sql/10-core.sql +7 -50
- package/sql/15-errors.sql +64 -10
- package/sql/20-context.sql +18 -9
- package/sql/30-validate.sql +79 -30
- package/sql/40-class.sql +41 -115
- package/sql/50-acl.sql +8 -13
- package/sql/60-write.sql +220 -676
- package/sql/70-read.sql +11 -13
- package/sql/80-auth.sql +109 -42
- package/sql/90-time.sql +108 -947
- package/sql/95-seed.system.sql +7 -14
- package/sql/99-revision.sql +6 -1
- package/sql/README.md +32 -24
- package/sql/migrate/0009-restore-rev.tenants-name.sql +10 -0
- package/sql/migrate/0010-impersonation-no-account.sql +5 -0
- package/sql/migrate/0011-key-lock.otp-enabled.sql +7 -0
- package/sql/migrate/README.md +18 -0
- package/AGENT-CHEATSHEET.en.md +0 -488
- package/MIGRATION.md +0 -190
- package/README.en.md +0 -2910
- package/dist/import.d.ts +0 -185
- package/dist/import.js +0 -820
- package/dist/project.d.ts +0 -359
- package/dist/project.js +0 -1258
- package/sql/75-history.sql +0 -436
- package/sql/88-derived.sql +0 -1236
- package/sql/migrate/0006-changed-paths.sql +0 -3
- package/sql/migrate/0007-secret-account-fk.sql +0 -12
package/MIGRATION.md
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
# Переход с letopis 0.21 на 1.0
|
|
2
|
-
|
|
3
|
-
letopis 1.0 — новая реализация хранилища: PostgreSQL 18, три таблицы (`entity`, `log`, `secret`), проверки и
|
|
4
|
-
права в самой базе. Схема 0.21 не обновляется на месте: 1.0 ставится в новую схему, данные переносит команда
|
|
5
|
-
`letopis import`, а код приложения переводится по таблице замен ниже. Полный список несовместимостей — раздел
|
|
6
|
-
BREAKING записи `[1.0.0]` в `CHANGELOG.md`; руководство по 1.0 — `README.md`, краткая шпаргалка — `AGENT-CHEATSHEET.md`
|
|
7
|
-
(оба лежат в пакете рядом с этим файлом).
|
|
8
|
-
|
|
9
|
-
## 1. Порядок перехода
|
|
10
|
-
|
|
11
|
-
1. Поднимите PostgreSQL 18 (TimescaleDB, Redis и Docker больше не нужны) и Node 22+.
|
|
12
|
-
2. Установите схему 1.0 без демо-сида: `await up({ dsn: ADMIN, schema: 'salon' })` — схема `v2.salon`. Сохраните
|
|
13
|
-
`serviceKey` из результата: ключ сервиса выдаётся один раз. В 0.21 `up()` без `seeds` заливал демо-домен; в
|
|
14
|
-
1.0 сиды ставятся только явно (`seeds: ['booking']` или путь к своему `.sql`).
|
|
15
|
-
3. Создайте прикладную LOGIN-роль, которая состоит только в `letopis_app`
|
|
16
|
-
(`create role app login password '…'; grant letopis_app to app;`). Приложение подключается ею: роль, которая
|
|
17
|
-
обходит RLS, `connect()` отклоняет (`bypass_rls`), если не передать `allowBypassRls: true`.
|
|
18
|
-
4. Перенесите данные: `letopis import` (раздел 2). Повтор безопасен — он дописывает недостающее.
|
|
19
|
-
5. Переведите код по таблицам разделов 3–5 и проверьте установку: `letopis verify --data`.
|
|
20
|
-
|
|
21
|
-
## 2. Импорт данных: `letopis import`
|
|
22
|
-
|
|
23
|
-
```sh
|
|
24
|
-
npx letopis import --from postgres://…/old --from-schema v1.salon --dsn postgres://…/new --schema v2.salon \
|
|
25
|
-
[--tenant <имя>] [--keyless <Класс>]… [--rename <Старое>=<Новое>]… [--update] [--commit-every <N>] [--partition <имя>]
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
| Опция | Что делает |
|
|
29
|
-
|---|---|
|
|
30
|
-
| `--from`, `--from-schema` | база и схема 0.21 (`v1.<имя>`): PostgreSQL 17 с TimescaleDB или дамп обычных таблиц |
|
|
31
|
-
| `--dsn`, `--schema` | база и схема 1.0 (`v2.<имя>`), поставленная заранее `up()`; оба подключения — администраторские |
|
|
32
|
-
| `--tenant <имя>` | арендатор, в которого переходят данные System 0.21 (в 1.0 у System своих данных нет) |
|
|
33
|
-
| `--keyless <Класс>` | класс, чьи данные нарушают ключ: его объекты получают id от старого id, а не от ключа |
|
|
34
|
-
| `--rename <Старое>=<Новое>` | новое имя класса, совпавшего с методом 1.0 (по умолчанию — имя с `_` на конце) |
|
|
35
|
-
| `--update` | при повторе — новые версии объектов, изменившихся в 0.21 после прошлого импорта |
|
|
36
|
-
| `--commit-every <N>` | размер порции загрузчика |
|
|
37
|
-
| `--partition <имя>` | партиция 0.21 (по умолчанию `entity`) |
|
|
38
|
-
|
|
39
|
-
Что переносится:
|
|
40
|
-
- **Классы** (`Schema`) — строки `Class` арендатора System. Правила fastest-validator переводятся в JSON Schema
|
|
41
|
-
(`optional` пропускает `null`, `$$strict` — `additionalProperties: false`, `record` — `propertyNames`); концы
|
|
42
|
-
становятся ролями с именем класса цели, союз всех потомков класса — полиморфным концом; корни `Entity` и `link`
|
|
43
|
-
опускаются; `description`, `appearance`, `order` — в `meta`.
|
|
44
|
-
- **id**: у класса с ключом (правило v5 из 0.21) — v5 от ключа в арендаторе, у класса без ключа — v5 от старого
|
|
45
|
-
id. uuid внутри `data`, совпадающие со старыми id, импорт не переписывает и перечисляет в отчёте.
|
|
46
|
-
- **Объекты** (`Entity`) — загрузчиком в режиме истории: все версии, надгробия, воскрешения, теги, владелец; агент
|
|
47
|
-
в журнале — `letopis import`. Каждый аккаунт 0.21 становится арендатором своих строк; владелец, не найденный
|
|
48
|
-
среди аккаунтов, заменяется арендатором. `onDelete` концов — `cascade`, как удаляла 0.21.
|
|
49
|
-
- **Аккаунты** — строки `Account` (в System); **креды** — в `secret` как есть (хэши scrypt, ключи);
|
|
50
|
-
одноразовые коды не переносятся.
|
|
51
|
-
- **Ресурсы и правила** ACL — в System, поэтому действуют во всех арендаторах; непереведённые правила — в отчёте.
|
|
52
|
-
Ресурс с маской не строкой импорт не пропустит (`invalid_data`, см. [раздел 4](#4-изменения-поведения)).
|
|
53
|
-
|
|
54
|
-
Проверки до первой записи: импорт останавливается, если два объекта 0.21 дают один id 1.0 (данные нарушают ключ —
|
|
55
|
-
укажите `--keyless <Класс>`), если ключ объекта менялся между версиями или ссылается на отсутствующий объект.
|
|
56
|
-
Висячая ссылка живой строки — `target_not_found` с объектом 0.21.
|
|
57
|
-
|
|
58
|
-
Отчёт — JSON в конце вывода (перед ним — строки хода `letopis.import: …`): объекты и версии по классам в 0.21 и в 1.0, аккаунты, креды, ресурсы, правила, классы без
|
|
59
|
-
ключа, результат `verify()`. Код выхода 1, если `verify()` нашёл расхождения. Шаги импорта (классы, аккаунты,
|
|
60
|
-
креды, права, данные каждого арендатора) — отдельные транзакции; источник читается в память целиком.
|
|
61
|
-
|
|
62
|
-
## 3. Таблица замен API
|
|
63
|
-
|
|
64
|
-
Снятые имена не исчезли молча: вызов бросает `LetopisError` с кодом `removed` и текстом
|
|
65
|
-
`letopis: removed: <имя> снят в 1.0.0 — <замена>`.
|
|
66
|
-
|
|
67
|
-
| 0.21 | 1.0 (текст замены в ошибке) |
|
|
68
|
-
|---|---|
|
|
69
|
-
| `.run()` | `paths()` |
|
|
70
|
-
| `.execute()` | `paths()` |
|
|
71
|
-
| `.account(id)` | арендатор сессии и `auth.switch()` |
|
|
72
|
-
| `batch.execute()` | `batch.run()` |
|
|
73
|
-
| `uuidv7()` | `uuidv5()` и `idOf()`: id объекта вычисляется из ключа |
|
|
74
|
-
| `LETOPIS_NS` | пространство имён v5 — id арендатора: `idOf(класс, ключ, { tenant })` |
|
|
75
|
-
| `db.watch(cb)`, `db.watch(класс, cb)` | `watch({ from })` — подписка по курсору |
|
|
76
|
-
| `db.reloadSchema()` | реестр обновляется сам по сигналу; принудительно — `db.refresh()` |
|
|
77
|
-
| `db.accounts`, `db.credentials`, `db.resources`, `db.rules` | системные классы `Account`, `Resource`, `rule`, `member` и `db.auth` (`accounts.purge` — `db.auth.purgeAccount`) |
|
|
78
|
-
| `db.compact()` | `db.trimHistory(цель, момент)`, политика хранения `history` и `history: false` |
|
|
79
|
-
| `db.acl.reload()` | права обновляются сами: база применяет действующие правила |
|
|
80
|
-
| `db.auth.sessions(store)` | сессии хранятся в базе: `sessionFor`, `refresh`, `revoke`, `revokeAll` |
|
|
81
|
-
| `connect({ partition })` | отдельные схемы установки (`v2.<имя>`) |
|
|
82
|
-
| `connect({ enforceAccount })` | изоляция арендаторов включена всегда (RLS) |
|
|
83
|
-
| `connect({ enforceAcl })` | права включены всегда |
|
|
84
|
-
| `connect({ container })`, `({ image })`, `({ dataDir })` | Docker в `up()` не нужен |
|
|
85
|
-
| `connect({ redisPort })` | Redis не нужен: сессии в базе |
|
|
86
|
-
| `connect({ version })` | версия схемы — константа движка |
|
|
87
|
-
|
|
88
|
-
Замены без охранника — имя осталось, но значит другое, или API устроено иначе:
|
|
89
|
-
|
|
90
|
-
| 0.21 | 1.0 |
|
|
91
|
-
|---|---|
|
|
92
|
-
| `db.schema.define(def)`, CLI `schema-sync` | модели `hub`/`link` из `letopis/model` и `letopis sync <модуль> [--apply]` (или `syncModels(db, модели, { apply })`); класс — строка `Class` с JSON Schema |
|
|
93
|
-
| `db.schema` — фасад таблицы `Schema` | `db.schema` — имя схемы установки |
|
|
94
|
-
| CLI `gen-types` | `letopis types`; типы цепочек — `connect({ models })` |
|
|
95
|
-
| `attributes` на fastest-validator, `id: { generate, from }` | поля на Zod / JSON Schema, ключ класса `key: [...]`; id только v5 |
|
|
96
|
-
| `db.as(account, { owner })` — синхронный | `await db.as(account, { tenant, owner })` — имперсонацию проверяет база (право `auth.impersonate`) |
|
|
97
|
-
| `connect({ dsn, schema })` + `db.as(accountId)` | `connect({ dsn, schema, token })` или `connect({ …, apiKey })` — сессия в базе |
|
|
98
|
-
| `row.account`, `row.updated` | `row.tenant`, `row.at` (в `sort` и `cursorOf` имя `updated` понимается как `at`); новые `rev`, `author`, `op`, `agent`, `reason`, `moved` |
|
|
99
|
-
| `.count()` — число путей | `.count()` — число сущностей; путей — `.count({ paths: true })` |
|
|
100
|
-
| `up({ schema: 'x', version: 1 })` → `v1.x` | `up({ schema: 'x' })` → `v2.x`; Docker-опций нет, есть `pglite: true` |
|
|
101
|
-
| `db.sql` — голый клиент | `` db.sql`…` `` — под сессией и RLS, одной транзакцией; `db.transaction(fn)` |
|
|
102
|
-
| коды ошибок 0.21 | `err.code` — строчный код каталога (`exists`, `conflict`, `invalid_data` …), ошибки базы — SQLSTATE `LT001`–`LT020` |
|
|
103
|
-
| `tr.lock(...keys)` | остался: advisory-блокировка до конца `db.begin()`; для денег — раздел 5 |
|
|
104
|
-
|
|
105
|
-
## 4. Изменения поведения
|
|
106
|
-
|
|
107
|
-
- **`create` — не PUT.** Занятый ключ — ошибка `exists` с `detail.id`; в 0.21 повторный `create` перезаписывал
|
|
108
|
-
объект. Замена целиком — `upsert(data)` (только класс с ключом, иначе `no_key`; в строке ответа `$upsert`:
|
|
109
|
-
`created`, `updated` или `unchanged`). Частичная правка — `update`.
|
|
110
|
-
- **`update` не создаёт** (как и в 0.21), но теперь строже: если глагол затронул меньше строк, чем нашёл шаг,
|
|
111
|
-
весь план откатывается с `target_not_found`, `conflict` или `acl_denied`.
|
|
112
|
-
- **`count()` считает сущности** последнего шага, агрегаты — тоже; число путей — `count({ paths: true })`.
|
|
113
|
-
- **Пути — `paths()`** вместо `run()`: `[{ Шаг: Row, … }]`, ключ — имя шага или `.alias(имя)`.
|
|
114
|
-
- **Арендатор сессии вместо `account()`.** Строки принадлежат арендатору сессии подключения; чужие не видны и
|
|
115
|
-
неотличимы от несуществующих. Работа в другом арендаторе — по членству (`member`): `db.auth.switch(tenant)` или
|
|
116
|
-
`await db.as(account, { tenant })`. System-fallback 0.21 нет.
|
|
117
|
-
- **Права включены всегда** и по умолчанию запрещают — для библиотеки и сырого SQL (политики RLS). После импорта
|
|
118
|
-
правила 0.21 лежат в System; новому арендатору нужны свои правила (`Resource` и `rule` цепочками).
|
|
119
|
-
- **Маска `Resource` — только строка** (`categories`, `roles`, `endpoint`, `class`; например, `roles: '{staff}'`). В 0.21
|
|
120
|
-
маска не строкой (например, массив `roles: ['staff']`) совпадала со всеми аккаунтами, и разрешающее правило с такой
|
|
121
|
-
группой давало право каждому. В 1.0 такую строку `Resource` база отклоняет с `invalid_data` — при записи
|
|
122
|
-
библиотекой, сырым SQL и загрузчиком, в том числе при импорте: перепишите маску строкой в источнике.
|
|
123
|
-
- **Ключ неизменен.** Правка ключевого поля — `immutable_key`; новый ключ — `rekey(patch)` или
|
|
124
|
-
`db.rekeyClass(класс, ключ)`, оба дают новые id и переводят ссылки. Смена класса — `reclass(класс, данные)`.
|
|
125
|
-
- **Явный id** у класса с ключом должен совпасть с вычисленным (`id_mismatch`), класс без ключа id не принимает
|
|
126
|
-
(`id_from_db`). Вычислить id заранее — `db.idOf(класс, ключ)`.
|
|
127
|
-
- **Ошибка ломает `db.begin()`**: следующие вызовы и `commit()` бросают исходную ошибку (`commit()` откатывает),
|
|
128
|
-
`rollback()` проходит. В 0.21 `commit()` после ошибки молча проходил. Обрыв связи во время `COMMIT` —
|
|
129
|
-
`commit_unknown`, без повтора.
|
|
130
|
-
- **Каскад удаления** — по правилам концов `onDelete` (`restrict` по умолчанию, `cascade`, `unset`); превью
|
|
131
|
-
`delete()` показывает `$action` каждой строки. Удаление, `reclass` и `rekey` — только в `read committed`.
|
|
132
|
-
- **`history: false`** отключает ещё и `versions`, `asOf`, `restore` и события подписки.
|
|
133
|
-
- **Подписка** `db.watch({ from, classes })` — асинхронный итератор с курсором `ev.cursor`: без потерь и повторов,
|
|
134
|
-
продолжение — `watch({ from: курсор })`; `onReconnect` не нужен.
|
|
135
|
-
|
|
136
|
-
## 5. Баланс и счётчики
|
|
137
|
-
|
|
138
|
-
В 0.21 баланс правили так: явная транзакция, `tr.lock(...)`, перечитать строку, записать новое значение. Advisory-
|
|
139
|
-
блокировка не останавливает тех, кто её не берёт (другой код, сырой SQL), а перечитывание и запись — два запроса.
|
|
140
|
-
|
|
141
|
-
```ts
|
|
142
|
-
// 0.21
|
|
143
|
-
const tr = await db.begin()
|
|
144
|
-
await tr.lock('wallet', id)
|
|
145
|
-
const w = await tr.Wallet(id).first()
|
|
146
|
-
await tr.Wallet(id).update({ balance: w.data.balance - 300 }).rows()
|
|
147
|
-
await tr.commit()
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
В 1.0 — один из трёх способов.
|
|
151
|
-
|
|
152
|
-
**1. Приращение `inc`** — для пополнения, списания, счётчиков. База прибавляет величину внутри оператора: без
|
|
153
|
-
чтения, без блокировок в приложении, без потерянных обновлений. Нижнюю границу держит описание класса
|
|
154
|
-
(`minimum: 0` → уход в минус — `invalid_data`). Идемпотентность — ключом операции:
|
|
155
|
-
```ts
|
|
156
|
-
import { inc } from 'letopis'
|
|
157
|
-
// Movement: связь с ключом [Wallet, opId]; повтор той же операции — exists, баланс не меняется
|
|
158
|
-
await db.Wallet(id).Movement().create({ opId, kind: 'debit', amount: 300 }).Wallet().update({ balance: inc(-300) }).rows()
|
|
159
|
-
await db.Wallet(id).update({ visits: inc(1, { start: 0 }) }).first() // start — если поля ещё нет
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
**2. Строгий режим `{ rev }`** — когда новое значение вычисляется из прочитанного (корректировка до целевого
|
|
163
|
-
значения). Если строку успели изменить, приходит `conflict` — перечитать и повторить:
|
|
164
|
-
```ts
|
|
165
|
-
for (;;) {
|
|
166
|
-
const w = await db.Wallet(id).first()
|
|
167
|
-
try {
|
|
168
|
-
await db.Wallet(id).update({ balance: target(w!.data) }, { rev: w!.rev }).rows()
|
|
169
|
-
break
|
|
170
|
-
} catch (e) {
|
|
171
|
-
if ((e as { code?: string }).code !== 'conflict') throw e
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
```
|
|
175
|
-
`{ rev }` действует при одной цели шага (иначе `rev_ambiguous`) и не повторяется автоматически.
|
|
176
|
-
|
|
177
|
-
**3. Чтение с блокировкой `forUpdate()`** — когда нужно прочитать несколько строк и записать итог без повторов:
|
|
178
|
-
```ts
|
|
179
|
-
const tr = await db.begin()
|
|
180
|
-
try {
|
|
181
|
-
const w = await tr.Wallet(id).forUpdate().first() // for no key update, строки — в порядке id
|
|
182
|
-
await tr.Wallet(id).update({ balance: w!.data.balance - 300 }).rows()
|
|
183
|
-
await tr.commit()
|
|
184
|
-
} catch (e) {
|
|
185
|
-
await tr.rollback()
|
|
186
|
-
throw e
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
`forUpdate()` — только внутри `db.begin()` (`tx_required`) и только с `rows`, `first`, `ids` (`lock_unsupported`).
|
|
190
|
-
Транзакция — `read committed`; на `40P01` повторите весь блок `db.begin()`.
|