letopis 0.21.0 → 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.
- package/AGENT-CHEATSHEET.en.md +368 -0
- package/AGENT-CHEATSHEET.md +354 -0
- package/CHANGELOG.md +313 -0
- package/MIGRATION.md +190 -0
- package/README.en.md +1937 -0
- package/README.md +1493 -3469
- package/dist/acl.d.ts +26 -50
- package/dist/acl.js +22 -267
- package/dist/admin.d.ts +138 -0
- package/dist/admin.js +170 -0
- package/dist/auth.d.ts +120 -73
- package/dist/auth.js +121 -306
- package/dist/cache.d.ts +73 -0
- package/dist/cache.js +148 -0
- package/dist/chain.d.ts +124 -191
- package/dist/chain.js +362 -563
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +164 -0
- package/dist/demo/booking.d.ts +289 -0
- package/dist/demo/booking.js +159 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +70 -0
- package/dist/import.d.ts +179 -0
- package/dist/import.js +792 -0
- package/dist/index.d.ts +172 -26
- package/dist/index.js +304 -178
- package/dist/jsonschema.d.ts +22 -0
- package/dist/jsonschema.js +167 -0
- package/dist/load.d.ts +76 -0
- package/dist/load.js +884 -0
- package/dist/model.d.ts +166 -0
- package/dist/model.js +224 -0
- package/dist/ops.d.ts +7 -6
- package/dist/ops.js +7 -51
- package/dist/pglite.d.ts +22 -0
- package/dist/pglite.js +45 -0
- package/dist/registry.d.ts +57 -0
- package/dist/registry.js +82 -0
- package/dist/sql.d.ts +59 -142
- package/dist/sql.js +568 -654
- package/dist/sync.d.ts +31 -0
- package/dist/sync.js +108 -0
- package/dist/tx.d.ts +129 -8
- package/dist/tx.js +300 -73
- package/dist/typed.d.ts +97 -0
- package/dist/typed.js +1 -0
- package/dist/types.d.ts +71 -252
- package/dist/types.js +27 -108
- package/dist/up.d.ts +140 -47
- package/dist/up.js +339 -267
- package/dist/uuid.d.ts +21 -6
- package/dist/uuid.js +48 -64
- package/dist/validate.d.ts +24 -0
- package/dist/validate.js +251 -0
- package/dist/watch.d.ts +62 -0
- package/dist/watch.js +168 -0
- package/dist/write.d.ts +117 -74
- package/dist/write.js +658 -720
- package/llms.txt +26 -0
- package/package.json +49 -19
- package/sql/10-core.sql +136 -0
- package/sql/15-errors.sql +60 -0
- package/sql/20-context.sql +153 -0
- package/sql/30-validate.sql +423 -0
- package/sql/40-class.sql +259 -0
- package/sql/50-acl.sql +539 -0
- package/sql/60-write.sql +1369 -0
- package/sql/70-read.sql +245 -0
- package/sql/80-auth.sql +827 -0
- package/sql/90-time.sql +957 -0
- package/sql/95-seed.system.sql +178 -0
- package/sql/99-revision.sql +3 -0
- package/sql/README.md +56 -0
- package/sql/seed.booking.sql +39 -112
- package/dist/schema.d.ts +0 -15
- package/dist/schema.js +0 -352
- package/dist/sessions.d.ts +0 -32
- package/dist/sessions.js +0 -114
- package/dist/tables.d.ts +0 -105
- package/dist/tables.js +0 -248
- package/docker/Dockerfile +0 -40
- package/docker/start.sh +0 -18
- package/scripts/check-docs.mjs +0 -375
- package/scripts/gen-api-contract.mjs +0 -226
- package/scripts/gen-types.mjs +0 -350
- package/scripts/release-notes.mjs +0 -76
- package/scripts/schema-sync.mjs +0 -185
- package/sql/ddl.sql +0 -685
- 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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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, …}
|
|
20
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
*
|
|
48
|
-
* наследование в домене — переиспользование attributes, а не «is-a» для выборки:
|
|
49
|
-
* в демо `запись` наследует `окно`, но смена ≠ бронь, поэтому «смены мастера» —
|
|
50
|
-
* `db.Мастер(id).окно().exact()`.
|
|
58
|
+
* Все версии сущностей последнего шага (надгробия — $deleted), по id и номеру версии;
|
|
59
|
+
* { follow: true } — и через смену id при reclass и rekey, по времени.
|
|
51
60
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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: строка |
|
|
86
|
+
/** Колонка tags: строка | список (все) | has / hasAny / hasAll. */
|
|
67
87
|
tags(v: string | string[] | object): Chain;
|
|
68
|
-
/** Колонка
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
*
|
|
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
|
|
127
|
-
set(
|
|
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]:
|
|
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
|
-
|
|
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
|
-
|
|
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 {};
|