letopis 0.20.3 → 1.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.
Files changed (89) hide show
  1. package/AGENT-CHEATSHEET.en.md +368 -0
  2. package/AGENT-CHEATSHEET.md +354 -0
  3. package/CHANGELOG.md +348 -0
  4. package/MIGRATION.md +190 -0
  5. package/README.en.md +1937 -0
  6. package/README.md +1493 -3466
  7. package/dist/acl.d.ts +26 -50
  8. package/dist/acl.js +22 -267
  9. package/dist/admin.d.ts +138 -0
  10. package/dist/admin.js +170 -0
  11. package/dist/auth.d.ts +120 -73
  12. package/dist/auth.js +121 -306
  13. package/dist/cache.d.ts +73 -0
  14. package/dist/cache.js +148 -0
  15. package/dist/chain.d.ts +124 -191
  16. package/dist/chain.js +369 -551
  17. package/dist/cli.d.ts +2 -0
  18. package/dist/cli.js +164 -0
  19. package/dist/demo/booking.d.ts +289 -0
  20. package/dist/demo/booking.js +159 -0
  21. package/dist/errors.d.ts +29 -0
  22. package/dist/errors.js +70 -0
  23. package/dist/import.d.ts +179 -0
  24. package/dist/import.js +792 -0
  25. package/dist/index.d.ts +172 -26
  26. package/dist/index.js +304 -178
  27. package/dist/jsonschema.d.ts +22 -0
  28. package/dist/jsonschema.js +167 -0
  29. package/dist/load.d.ts +76 -0
  30. package/dist/load.js +884 -0
  31. package/dist/model.d.ts +166 -0
  32. package/dist/model.js +224 -0
  33. package/dist/ops.d.ts +7 -6
  34. package/dist/ops.js +7 -51
  35. package/dist/pglite.d.ts +22 -0
  36. package/dist/pglite.js +45 -0
  37. package/dist/registry.d.ts +57 -0
  38. package/dist/registry.js +82 -0
  39. package/dist/sql.d.ts +59 -142
  40. package/dist/sql.js +568 -654
  41. package/dist/sync.d.ts +31 -0
  42. package/dist/sync.js +108 -0
  43. package/dist/tx.d.ts +129 -8
  44. package/dist/tx.js +300 -73
  45. package/dist/typed.d.ts +97 -0
  46. package/dist/typed.js +1 -0
  47. package/dist/types.d.ts +71 -250
  48. package/dist/types.js +27 -108
  49. package/dist/up.d.ts +140 -47
  50. package/dist/up.js +339 -267
  51. package/dist/uuid.d.ts +21 -6
  52. package/dist/uuid.js +48 -64
  53. package/dist/validate.d.ts +24 -0
  54. package/dist/validate.js +251 -0
  55. package/dist/watch.d.ts +62 -0
  56. package/dist/watch.js +168 -0
  57. package/dist/write.d.ts +117 -74
  58. package/dist/write.js +658 -720
  59. package/llms.txt +26 -0
  60. package/package.json +49 -19
  61. package/sql/10-core.sql +136 -0
  62. package/sql/15-errors.sql +60 -0
  63. package/sql/20-context.sql +153 -0
  64. package/sql/30-validate.sql +423 -0
  65. package/sql/40-class.sql +259 -0
  66. package/sql/50-acl.sql +539 -0
  67. package/sql/60-write.sql +1369 -0
  68. package/sql/70-read.sql +245 -0
  69. package/sql/80-auth.sql +827 -0
  70. package/sql/90-time.sql +957 -0
  71. package/sql/95-seed.system.sql +178 -0
  72. package/sql/99-revision.sql +3 -0
  73. package/sql/README.md +56 -0
  74. package/sql/seed.booking.sql +39 -112
  75. package/dist/schema.d.ts +0 -15
  76. package/dist/schema.js +0 -351
  77. package/dist/sessions.d.ts +0 -32
  78. package/dist/sessions.js +0 -114
  79. package/dist/tables.d.ts +0 -105
  80. package/dist/tables.js +0 -248
  81. package/docker/Dockerfile +0 -40
  82. package/docker/start.sh +0 -18
  83. package/scripts/check-docs.mjs +0 -375
  84. package/scripts/gen-api-contract.mjs +0 -226
  85. package/scripts/gen-types.mjs +0 -350
  86. package/scripts/release-notes.mjs +0 -76
  87. package/scripts/schema-sync.mjs +0 -185
  88. package/sql/ddl.sql +0 -600
  89. package/sql/seed.auth.sql +0 -73
package/dist/cache.js ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Кэш результатов (план, §2.12, этап 6, п. 13): строки ответа цепочки — в памяти процесса, только по
3
+ * явной пометке: `.cache()` / `.cache({ ttl })` или `cache: { ttl }` у всех классов чтения в модели.
4
+ *
5
+ * Ключ — текст запроса, параметры и тот, кто спрашивает (токен, актор, арендатор): под RLS один
6
+ * запрос разным пользователям отдаёт разные строки. Сброс:
7
+ * - свои записи — сразу после фиксации, по классам из letopis.changed (туда попадают и классы
8
+ * каскада, unset, смены класса и ключа); запись через db.sql — весь кэш;
9
+ * - чужие записи — по сигналу базы с именем класса; классы прав и схемы (rule, Resource, member,
10
+ * Account, Class) и «*» — весь кэш; `session:<16 hex>` — ответы погашенной сессии, `session:*` — все;
11
+ * - гонка запроса и сигнала: счётчики сигналов по классам и сессиям снимаются до запроса, ответ
12
+ * кладётся, только если они не изменились;
13
+ * - (пере)подключение слушателя сигналов — весь кэш: сигналы за время обрыва потеряны; без слушателя
14
+ * кэш выключен (с ttlOnly — работает только по сроку);
15
+ * - срок (по умолчанию 60 с) — предохранитель; ответы истёкшей сессии не используются.
16
+ * Внутри db.begin() и в db.sql кэш не работает.
17
+ */
18
+ import { createHash } from 'node:crypto';
19
+ /** Классы, строки которых меняют права и схему: их сигнал сбрасывает весь кэш. */
20
+ export const ACL_CLASSES = new Set(['rule', 'Resource', 'member', 'Account', 'Class']);
21
+ /** Начало sha256 токена — так база называет сессию в сигнале session:<16 hex>. */
22
+ export const sessionTag = (token) => token ? createHash('sha256').update(token, 'utf8').digest('hex').slice(0, 16) : '';
23
+ export class ResultCache {
24
+ map = new Map();
25
+ classEpoch = new Map();
26
+ sessionEpoch = new Map();
27
+ epoch = 0;
28
+ listening = false;
29
+ st = { hits: 0, misses: 0, stores: 0, raced: 0, signals: 0, listens: 0 };
30
+ ttlMs;
31
+ ttlOnly;
32
+ max;
33
+ constructor(opts = {}) {
34
+ this.ttlMs = (opts.ttl ?? 60) * 1000;
35
+ this.ttlOnly = opts.ttlOnly === true;
36
+ this.max = opts.max ?? 10_000;
37
+ }
38
+ /** Кэш работает: слушатель на месте или режим только по сроку. */
39
+ get enabled() {
40
+ return this.listening || this.ttlOnly;
41
+ }
42
+ /** Ключ ответа: запрос, параметры и тот, кто спрашивает. */
43
+ key(text, params, who) {
44
+ return JSON.stringify([text, params, who.token ?? '', who.account ?? '', who.tenant ?? '']);
45
+ }
46
+ /** Снимок счётчиков классов и сессии — до запроса. */
47
+ token(classes, session) {
48
+ return {
49
+ epoch: this.epoch,
50
+ classes: classes.map((c) => [c, this.classEpoch.get(c) ?? 0]),
51
+ session: this.sessionEpoch.get(session) ?? 0,
52
+ };
53
+ }
54
+ get(key) {
55
+ if (!this.enabled)
56
+ return undefined;
57
+ const e = this.map.get(key);
58
+ const now = Date.now();
59
+ if (!e || e.expires <= now || (e.sessionExpires !== null && e.sessionExpires <= now)) {
60
+ if (e)
61
+ this.map.delete(key);
62
+ this.st.misses++;
63
+ return undefined;
64
+ }
65
+ this.st.hits++;
66
+ return e.value;
67
+ }
68
+ /** Положить ответ, если с момента снимка не было сигналов о его классах и сессии. */
69
+ set(key, value, classes, session, tok, ttlMs, sessionExpires) {
70
+ if (!this.enabled)
71
+ return false;
72
+ const stale = tok.epoch !== this.epoch
73
+ || tok.session !== (this.sessionEpoch.get(session) ?? 0)
74
+ || tok.classes.some(([c, n]) => (this.classEpoch.get(c) ?? 0) !== n);
75
+ if (stale) {
76
+ this.st.raced++;
77
+ return false;
78
+ }
79
+ if (this.map.size >= this.max)
80
+ this.map.delete(this.map.keys().next().value);
81
+ this.map.set(key, { value, classes: [...classes], session, expires: Date.now() + ttlMs, sessionExpires });
82
+ this.st.stores++;
83
+ return true;
84
+ }
85
+ /** Сбросить ответы, читавшие эти классы. */
86
+ invalidate(classes) {
87
+ const set = new Set(classes);
88
+ if (!set.size)
89
+ return;
90
+ for (const c of set)
91
+ this.classEpoch.set(c, (this.classEpoch.get(c) ?? 0) + 1);
92
+ for (const [k, e] of this.map)
93
+ if (e.classes.some((c) => set.has(c)))
94
+ this.map.delete(k);
95
+ }
96
+ /** Сбросить ответы сессии (тег — начало sha256 токена). */
97
+ invalidateSession(tag) {
98
+ this.sessionEpoch.set(tag, (this.sessionEpoch.get(tag) ?? 0) + 1);
99
+ for (const [k, e] of this.map)
100
+ if (e.session === tag)
101
+ this.map.delete(k);
102
+ }
103
+ /** Весь кэш. */
104
+ clear() {
105
+ this.epoch++;
106
+ this.map.clear();
107
+ }
108
+ /**
109
+ * Имена из сигнала или из letopis.changed своей транзакции: классы, `*`, `session:<тег>`,
110
+ * `session:*`. raw — запись через db.sql: весь кэш.
111
+ */
112
+ changed(names, raw = false) {
113
+ const classes = [];
114
+ let any = false;
115
+ for (const n of names) {
116
+ if (!n)
117
+ continue;
118
+ any = true;
119
+ if (n === '*' || n === 'session:*' || ACL_CLASSES.has(n)) {
120
+ this.clear();
121
+ return;
122
+ }
123
+ if (n.startsWith('session:'))
124
+ this.invalidateSession(n.slice(8));
125
+ else
126
+ classes.push(n);
127
+ }
128
+ if (raw && any) {
129
+ this.clear();
130
+ return;
131
+ }
132
+ this.invalidate(classes);
133
+ }
134
+ /** Сигнал от базы. */
135
+ signal(name) {
136
+ this.st.signals++;
137
+ this.changed([name]);
138
+ }
139
+ /** Слушатель (пере)подключился: сигналы за время обрыва потеряны — весь кэш. */
140
+ listen() {
141
+ this.st.listens++;
142
+ this.listening = true;
143
+ this.clear();
144
+ }
145
+ stats() {
146
+ return { size: this.map.size, ...this.st, enabled: this.enabled };
147
+ }
148
+ }
package/dist/chain.d.ts CHANGED
@@ -1,225 +1,158 @@
1
- /**
2
- * Dot-цепочки: db.Сотрудник({name:'Вася'}).навык().Услуга().run()
3
- * db и цепочка — Proxy над реестром классов (id и alias работают одинаково).
4
- *
5
- * Выборка настраивается модификаторами: .limit(n) .offset(n) .sort(field, dir?)
6
- * Колонки — модификаторами: .tags(…) .account(…) .owner(…); ключ путей — .alias(…).
7
- * Запись: .create(data) / .update(data) / .delete({confirm}) / .anonymize(fields) — ЗВЕНЬЯ
8
- * (возвращают цепочку, продолжение — от результата); исполняет ТЕРМИНАЛ, весь план
9
- * одной транзакцией.
10
- */
11
- import type { Row, Path, Filter } from './types.js';
12
- import { type Ctx } from './sql.js';
13
- import { type BatchPlan } from './write.js';
14
- import { type Tables } from './tables.js';
15
- import { type AuthApi } from './auth.js';
16
- import { type AclApi } from './acl.js';
17
- import type { Registry } from './schema.js';
1
+ import { type ResultCache } from './cache.js';
2
+ import type { ClassInfo, Registry } from './registry.js';
3
+ import { type Step } from './sql.js';
4
+ import type { Executor } from './tx.js';
5
+ import type { ChainMods, Cursor, Filter, Path, QueryEvent, Row } from './types.js';
6
+ export interface ChainCtx {
7
+ readonly reg: Registry;
8
+ readonly schema: string;
9
+ /** Где исполнять: отдельной транзакцией из пула или в явной транзакции db.begin(). */
10
+ readonly exec: Executor;
11
+ /** Арендатор сессии. */
12
+ readonly tenant: () => string | null;
13
+ /** Владелец новых строк по умолчанию (db.as(…, { owner })). */
14
+ readonly owner?: string;
15
+ readonly onQuery?: (e: QueryEvent) => void;
16
+ readonly slowMs?: number;
17
+ /** Цепочка из батча: план копится в очереди до batch.run(). */
18
+ readonly batch?: BatchRef;
19
+ /** Кэш результатов подключения и тот, кто спрашивает (ключ ответа, §2.12). */
20
+ readonly cache?: {
21
+ store: ResultCache;
22
+ who(): {
23
+ token: string | null;
24
+ account?: string;
25
+ tenant?: string | null;
26
+ };
27
+ };
28
+ }
29
+ /** План в очереди батча; steps обновляется по мере роста цепочки. */
30
+ export interface BatchEntry {
31
+ steps: Step[];
32
+ }
33
+ /** Ссылка цепочки на очередь: первый глагол регистрирует план, следующие — обновляют. */
34
+ export interface BatchRef {
35
+ queue: BatchEntry[];
36
+ entry?: BatchEntry;
37
+ }
38
+ /** Батч db.batch(имя): классы копят планы; run() исполняет очередь одной транзакцией. */
39
+ export type Batch = {
40
+ run(): Promise<Row[][]>;
41
+ discard(): void;
42
+ size(): number;
43
+ } & {
44
+ [className: string]: StepFn;
45
+ };
18
46
  export interface ChainCore {
19
- /** Пути: [{шаг1: Row, шаг2: Row, …}, …] — все узлы каждого варианта (бывший execute). */
20
- run(): Promise<Path[]>;
47
+ /** Пути: [{ шаг1: Row, шаг2: Row, … }] — узлы каждого варианта. */
48
+ paths(): Promise<Path[]>;
21
49
  /** Уникальные сущности последнего шага. */
22
50
  rows(): Promise<Row[]>;
23
51
  first(): Promise<Row | null>;
24
52
  ids(): Promise<string[]>;
25
- count(): Promise<number>;
26
- /** Максимум строк/путей. */
27
- limit(n: number): Chain;
28
- /** Пропустить N. */
29
- offset(n: number): Chain;
30
- /** Сортировка по полю последнего шага: 'updated' | 'data.<поле>' (каст по типу из Schema). */
31
- sort(field: string, dir?: 'asc' | 'desc' | boolean): Chain;
32
- /** Чтение «как было на момент T» (ISO-строка или Date): версии позже T невидимы. */
33
- asOf(t: string | Date): Chain;
34
- /** Keyset-пагинация: строго после курсора (см. cursorOf). Требует .sort(). */
35
- after(cursor: import('./types.js').Cursor): Chain;
36
- /**
37
- * Включить в выдачу последнего шага удалённые (tombstone). Снимает ТОЛЬКО фильтр deleted —
38
- * изоляция арендатора (enforceAccount) и ACL (enforceAcl) остаются в силе.
39
- */
40
- withDeleted(): Chain;
41
- /** ВСЕ версии сущностей последнего шага (включая tombstone → $deleted), по возрастанию updated. */
42
- versions(): Promise<Row[]>;
43
- /** Рекурсивный self-обход: дети любой глубины (тот же класс), $depth в Row. */
44
- deep(max?: number): Chain;
53
+ /** Число сущностей последнего шага; { paths: true } — число путей. */
54
+ count(opts?: {
55
+ paths?: boolean;
56
+ }): Promise<number>;
45
57
  /**
46
- * Только ЭТОТ класс, без классов-потомков. По умолчанию шаг полиморфен: родитель отдаёт
47
- * объединение с потомками (`db.Контрагент()` → Мастера + Клиенты). `.exact()` нужен, когда
48
- * наследование в домене — переиспользование attributes, а не «is-a» для выборки:
49
- * в демо `запись` наследует `окно`, но смена ≠ бронь, поэтому «смены мастера» —
50
- * `db.Мастер(id).окно().exact()`.
58
+ * Все версии сущностей последнего шага (надгробия — $deleted), по id и номеру версии;
59
+ * { follow: true } — и через смену id при reclass и rekey, по времени.
51
60
  */
52
- exact(): Chain;
53
- /** Агрегации по полю последнего шага ('data.<путь>'), считает БД. */
61
+ versions(opts?: {
62
+ follow?: boolean;
63
+ }): Promise<Row[]>;
64
+ /** Агрегаты по полю последнего шага ('data.<путь>') — по сущностям, считает база. */
54
65
  sum(field: string): Promise<number | null>;
55
66
  avg(field: string): Promise<number | null>;
56
67
  min(field: string): Promise<unknown>;
57
68
  max(field: string): Promise<unknown>;
58
69
  countBy(field: string): Promise<Record<string, number>>;
59
- /**
60
- * GDPR-звено: новая версия с затёртыми string-полями ('[erased]') + тег 'anonymized'.
61
- * История остаётся. Возвращает цепочку — исполняет терминал; продолжение — от затёртых версий.
62
- */
63
- anonymize(fields: string[]): Chain;
64
- /** Переименовать ключ ТЕКУЩЕГО шага в выводе путей. */
70
+ limit(n: number): Chain;
71
+ offset(n: number): Chain;
72
+ /** Сортировка по полю последнего шага: 'at' | 'rev' | 'id' | 'data.<путь>'. */
73
+ sort(field: string, dir?: 'asc' | 'desc' | boolean): Chain;
74
+ /** Срез «как было на момент T»: версии журнала. */
75
+ asOf(t: string | Date): Chain;
76
+ /** Keyset-пагинация: строго после курсора (cursorOf). Требует sort(). */
77
+ after(cursor: Cursor): Chain;
78
+ /** Последний шаг включает удалённые (надгробия, $deleted). */
79
+ withDeleted(): Chain;
80
+ /** Дети любой глубины на переходе к тому же классу; $depth в строке. */
81
+ deep(max?: number): Chain;
82
+ /** Только сам класс шага, без потомков. */
83
+ exact(): Chain;
84
+ /** Ключ текущего шага в путях. */
65
85
  alias(name: string): Chain;
66
- /** Колонка tags: строка | string[] (все) | has/hasAny/hasAll. В записи — значение тегов. */
86
+ /** Колонка tags: строка | список (все) | has / hasAny / hasAll. */
67
87
  tags(v: string | string[] | object): Chain;
68
- /** Колонка account (uuid или Row/Account). В create() — значение. */
69
- account(v: string | {
70
- id: string;
71
- }): Chain;
72
- /** Колонка owner (uuid или Row/Account). В create() — значение. */
88
+ /** Колонка owner. */
73
89
  owner(v: string | {
74
90
  id: string;
75
91
  }): Chain;
76
- /**
77
- * Вставить узел в путь: x — ленивая цепочка-паттерн (стык проверяется правилами
78
- * переходов) либо Row (шаг его класса по id). ТА ЖЕ переменная-цепочка повторно
79
- * в одном пути — возврат к её узлу (ветвление); разные переменные — разные узлы.
80
- */
92
+ /** Вставить узел: ленивая цепочка-паттерн или строка ответа; та же переменная повторно — возврат к её узлу. */
81
93
  entity(x: Chain | Row): Chain;
82
- /**
83
- * Создать сущность (ЗВЕНО — вернёт цепочку, исполняет терминал):
84
- * await db.Организация(org).Сотрудник().create({ name: 'Вася' }).rows()
85
- * Связи — контекст-шагами до create() + слотами .Класс.set(…) после. id — по Schema
86
- * (attributes.id: v4 | v7 | v5-вычисляемый); известный id (Класс(id) / data.id / v5)
87
- * уже существует → новая версия (идемпотентно). Фильтр-объект — ошибка: это update().
88
- */
94
+ /** Создать объект: концы — из пути и слотов; занятый ключ — ошибка exists с id. */
89
95
  create(data?: Record<string, unknown>): Chain;
90
- /**
91
- * Новая версия КАЖДОГО найденного путём (deep-merge листьев data):
92
- * await db.Запись(з).позиция({ qty: 1 }).update({ qty: 2 }).rows()
93
- * Класс() ≡ Класс({}) — все в границах контекста. Не найдено → [] (НИКОГДА не создаёт).
94
- * Продолжение цепочки — от записанных строк (fan-out при множестве).
95
- */
96
- update(data?: Record<string, unknown>): Chain;
97
- /**
98
- * Удаление-звено. { confirm: true } — серверный tombstone + каскад (продолжение — от
99
- * затомбстоуненных, $deleted: true); БЕЗ confirm — превью: терминал вернёт кандидатов
100
- * (цели + каскад), БД не тронута.
101
- */
96
+ /** Новая версия каждой цели шага: слияние патча в базе; inc(n) — приращение; { rev } — строгий режим. */
97
+ update(patch?: Record<string, unknown>, opts?: {
98
+ rev?: number;
99
+ }): Chain;
100
+ /** Создать или заменить целиком (класс с ключом); строка ответа несёт $upsert. */
101
+ upsert(data: Record<string, unknown>): Chain;
102
+ /** Удалить цели шага с каскадом; без { confirm: true } — превью замыкания ($action). */
102
103
  delete(opts?: {
103
104
  confirm?: boolean;
104
105
  }): Chain;
105
- /**
106
- * ФИЗИЧЕСКИЙ hard-erase (необратимо): сносит УЖЕ логически удалённые (tombstone) цели + всё
107
- * поддерево по links (все версии). Живую сущность не трогает — сначала .delete(). { confirm: true }
108
- * — стирает; без confirm — превью замыкания (что сотрётся), БД не тронута. Историю НЕ сохраняет
109
- * (в отличие от .delete()). Реализуется серверной purge() (SET LOCAL letopis.purge отключает триггер).
110
- */
106
+ /** Затереть строковые поля ('[erased]') и пометить тегом anonymized. */
107
+ anonymize(fields: string[]): Chain;
108
+ /** Сменить класс: у класса с ключом — новый id и перевод ссылок. */
109
+ reclass(cls: string, data?: Record<string, unknown>): Chain;
110
+ /** Чтение с блокировкой строк последнего шага до конца db.begin() (rows, first, ids). */
111
+ forUpdate(): Chain;
112
+ /** Восстановить удалённые объекты шага: последний снимок новой версией (id прежний). */
113
+ restore(): Chain;
114
+ /** Стереть историю удалённых объектов шага; без { confirm: true } — превью ($versions). */
111
115
  purge(opts?: {
112
116
  confirm?: boolean;
113
117
  }): Chain;
118
+ /** Новое значение ключа: строка с новым id, ссылки переведены (поля — данные, роли — концы). */
119
+ rekey(patch: Record<string, unknown>): Chain;
120
+ /** Кэш результатов (§2.12): ответ хранится в памяти процесса ttl секунд (по умолчанию 60). */
121
+ cache(opts?: {
122
+ ttl?: number;
123
+ }): Chain;
114
124
  }
115
125
  /**
116
- * Свойство-класс на цепочке:
117
- * ВЫЗОВ `Класс(фильтр?)` — шаг-навигация; повтор LINK-класса в пути = возврат
118
- * к его узлу (pivot: ветвление к другому концу, дофильтровка AND);
119
- * СЛОТ `Класс` без скобок — конец links записываемой версии:
120
- * .set(target) — установить конец (id | Row | вложенная цепочка: та же транзакция,
121
- * ровно одна сущность класса конца); союз-конец замещается целиком;
122
- * .unset() — снять optional-конец.
123
- * Слот валиден только для конца из Schema.links владельца и ТОЛЬКО после операции
124
- * записи: …create(…).Класс.set(x) / …update(…).Класс.unset() — та же версия строки.
126
+ * Шаг цепочки: вызов — переход (фильтр); после глагола записи без вызова — слот конца версии:
127
+ * `.Роль.set(id | [id] | строка | цепочка)`, `.unset()`, у множественного конца `.add()` и `.remove()`.
125
128
  */
126
- export type StepProp = ((filter?: Filter) => Chain) & {
127
- set(target: string | {
128
- id: string;
129
- } | Chain): Chain;
129
+ export type StepFn = ((filter?: Filter) => Chain) & {
130
+ set(value: unknown): Chain;
130
131
  unset(): Chain;
132
+ add(value: unknown): Chain;
133
+ remove(value: unknown): Chain;
131
134
  };
132
135
  export type Chain = ChainCore & {
133
- [className: string]: StepProp;
134
- };
135
- export interface DbCore {
136
- /**
137
- * Идентичность ВЫЗОВА: хендл того же пула, работающий от имени account.
138
- * Арендатор — свойство вызова, а не подключения (в connect() опции account нет):
139
- *
140
- * const db = await connect({ dsn, schema }) // пул, безличный
141
- * const t = await db.as(req.accountId) // кто именно делает этот вызов
142
- * await t.Клиент().rows() // изолировано по account
143
- *
144
- * При enforceAccount (default) чтение/запись возможны только с такого хендла.
145
- * При enforceAcl энфорсер компилится под ЭТОГО субъекта (у каждого scope свой).
146
- * owner по умолчанию = account; переопределяется вторым аргументом.
147
- */
148
- as(account: string | {
149
- id: string;
150
- }, opts?: {
151
- owner?: string;
152
- }): Promise<EntityDb>;
153
- /** Открыть транзакцию: tr — тот же API + commit/rollback/lock. */
154
- begin(): Promise<EntityTx>;
155
- commit(tr: EntityTx): Promise<void>;
156
- rollback(tr: EntityTx): Promise<void>;
157
- /** Именованный батч: те же цепочки, планы записи копятся до run(). */
158
- batch(name: string): Batch;
159
- /**
160
- * Realtime: события каждой вставленной версии (insert/update/delete-tombstone).
161
- * watch(cb) — все классы; watch('Запись', cb) — один. Возврат — stop-функция.
162
- * LISTEN-соединение переживает обрывы (re-listen автоматом), но NOTIFY за время
163
- * разрыва потеряны — opts.onReconnect зовётся после восстановления: дочитайте пропущенное.
164
- */
165
- watch(cb: (e: WatchEvent) => void, opts?: WatchOpts): Promise<() => void>;
166
- watch(cls: string, cb: (e: WatchEvent) => void, opts?: WatchOpts): Promise<() => void>;
167
- close(): Promise<void>;
168
- /**
169
- * Перечитать определения классов из таблицы Schema (после правки её простым SQL / db.schema.define):
170
- * пересобирает registry и, при enforceAcl, ACL-резолвер — без реконнекта. Подхват «сразу».
171
- */
172
- reloadSchema(): Promise<void>;
173
- registry: Registry;
174
- /** Голый postgres-клиент (тесты, EXPLAIN). */
175
- sql: Ctx['sql'];
176
- /** Служебные таблицы auth/ACL (обычные, вне версий/цепочек). */
177
- accounts: Tables['accounts'];
178
- credentials: Tables['credentials'];
179
- resources: Tables['resources'];
180
- rules: Tables['rules'];
181
- /** Определения классов: db.schema.define(def) — upsert в таблицу Schema + reloadSchema(). */
182
- schema: Tables['schema'];
183
- /** Вход по кредам (пароль/api-key/key-secret/внешние identity) + сессии в Redis. */
184
- auth: AuthApi;
185
- /** ACL по Resource/Rule: check(эндпоинт) / checkData(класс, READ|WRITE|DELETE) / reload. */
186
- acl: AclApi;
187
- /** Начать путь с готового узла/паттерна: db.entity(pos).… (pos — ленивая цепочка или Row). */
188
- entity(x: Chain | Row): Chain;
189
- }
190
- export type EntityDb = DbCore & {
191
- [className: string]: (filter?: Filter) => Chain;
136
+ [className: string]: StepFn;
192
137
  };
193
- export type EntityTx = EntityDb & {
194
- commit(): Promise<void>;
195
- rollback(): Promise<void>;
196
- lock(...keys: (string | number)[]): Promise<void>;
197
- };
198
- /** Событие watch(): факт новой версии; данные дочитываются запросом при надобности. */
199
- export interface WatchEvent {
200
- partition: string;
201
- class: string;
202
- id: string;
203
- updated: string;
204
- deleted: boolean;
205
- }
206
- /** Опции watch(): реакция на восстановление LISTEN-соединения. */
207
- export interface WatchOpts {
208
- /** Зовётся после каждого re-listen (не на первом подключении): NOTIFY за разрыв потеряны. */
209
- onReconnect?: () => void;
210
- }
211
- export type Batch = {
212
- /** Исполнить всю очередь планов одной транзакцией (бывший execute). */
213
- run(): Promise<Row[][]>;
214
- discard(): void;
215
- size(): number;
216
- } & {
217
- [className: string]: (filter?: Filter) => Chain;
218
- };
219
- interface RootState {
220
- batches: Map<string, BatchPlan[]>;
221
- }
222
- /** Внутренний канал: план чужой ленивой цепочки (слот-значения, entity()). */
138
+ /** Внутренний канал: план чужой ленивой цепочки (entity()). */
223
139
  export declare const PLAN: unique symbol;
224
- export declare function makeDb(ctx: Ctx, root?: RootState): EntityDb;
140
+ /** entity-идентичность: переменная-цепочка → nodeKey её узла в этом пути. */
141
+ type NodeMap = ReadonlyMap<object, number>;
142
+ /** Класс, алиас или роль → класс шага (для роли — первый класс её целей). */
143
+ export declare function lookupStep(reg: Registry, name: string): {
144
+ cls: ClassInfo;
145
+ role?: string;
146
+ } | undefined;
147
+ export declare function makeChain(ctx: ChainCtx, steps: Step[], mods: ChainMods, nodes?: NodeMap): Chain;
148
+ /**
149
+ * Батч: очередь планов; каждый вызов класса — своя ссылка на очередь (свой план). run() исполняет очередь
150
+ * в контексте ctx (актор, арендатор, владелец, транзакция) — поэтому очередь принадлежит фасаду этого ctx
151
+ * и общей с другими фасадами быть не может: иначе план исполнился бы от имени чужого актора.
152
+ */
153
+ export declare function makeBatch(ctx: ChainCtx, queue: BatchEntry[]): Batch;
154
+ /** Старт пути от корня db: класс, алиас или роль. */
155
+ export declare function startStep(ctx: ChainCtx, name: string): StepFn;
156
+ /** Старт пути с готового узла: db.entity(row | цепочка). */
157
+ export declare function startEntity(ctx: ChainCtx, x: Chain | Row): Chain;
225
158
  export {};