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/tx.js
CHANGED
|
@@ -1,82 +1,309 @@
|
|
|
1
|
+
import { LetopisError, fromDbError } from './errors.js';
|
|
2
|
+
/** Транзиентные ошибки PG, которые снимает повтор: взаимная блокировка, сериализация. */
|
|
3
|
+
export function isTransient(e) {
|
|
4
|
+
const code = e.code;
|
|
5
|
+
return code === '40P01' || code === '40001';
|
|
6
|
+
}
|
|
7
|
+
/** Конфликт пары «id, номер версии» в журнале — параллельные удаление и создание одного id (§2.5). */
|
|
8
|
+
export function isLogConflict(e) {
|
|
9
|
+
const err = e;
|
|
10
|
+
return err.code === '23505' && err.constraint_name === 'log_id_rev';
|
|
11
|
+
}
|
|
12
|
+
/** Обрыв связи: сокет закрыт или серверный процесс завершён (FATAL) — исход отправленного неизвестен. */
|
|
13
|
+
export function isConnectionError(e) {
|
|
14
|
+
const err = e;
|
|
15
|
+
if (err?.severity)
|
|
16
|
+
return err.severity === 'FATAL' || err.severity === 'PANIC';
|
|
17
|
+
const code = String(err?.code ?? '');
|
|
18
|
+
return code.startsWith('CONNECTION_') || ['ECONNRESET', 'EPIPE', 'ECONNREFUSED', 'ETIMEDOUT', 'ECONNABORTED'].includes(code);
|
|
19
|
+
}
|
|
1
20
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
21
|
+
* Вернуть зарезервированное соединение в пул. После обрыва сокета пул postgres.js уже перевёл
|
|
22
|
+
* соединение в закрытые (onclose); release() вернул бы его в открытые, и следующая операция
|
|
23
|
+
* писала бы в сокет null — необработанный TypeError роняет процесс.
|
|
5
24
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
function release(conn, err) {
|
|
26
|
+
const e = err;
|
|
27
|
+
if (e && !e.severity && isConnectionError(e))
|
|
28
|
+
return;
|
|
29
|
+
conn.release();
|
|
30
|
+
}
|
|
31
|
+
const commitUnknown = (cause) => Object.assign(new LetopisError('commit_unknown', 'связь оборвалась во время commit: транзакция могла зафиксироваться — '
|
|
32
|
+
+ 'проверьте исход повтором той же операции с тем же ключом'), { cause });
|
|
33
|
+
/** Взаимная блокировка в явной транзакции: повторить нужно весь блок (подсказка, как hintTxRetry 0.21). */
|
|
34
|
+
function hintTxRetry(e) {
|
|
35
|
+
if (isTransient(e) && e instanceof Error && !e.message.includes('db.begin()')) {
|
|
36
|
+
e.message += ' — откатите транзакцию и повторите весь блок db.begin()';
|
|
37
|
+
}
|
|
38
|
+
return e;
|
|
39
|
+
}
|
|
40
|
+
export const RETRIES = 3;
|
|
41
|
+
export const retryDelay = (attempt) => new Promise((r) => setTimeout(r, 40 * attempt + Math.random() * 40));
|
|
42
|
+
/** Нотификатор процесса: копит имена классов и отправляет их пачкой раз в интервал. */
|
|
43
|
+
export class Notifier {
|
|
44
|
+
sql;
|
|
45
|
+
channel;
|
|
46
|
+
intervalMs;
|
|
47
|
+
onError;
|
|
48
|
+
queue = new Set();
|
|
49
|
+
timer = null;
|
|
50
|
+
stopped = false;
|
|
51
|
+
flushing = Promise.resolve();
|
|
52
|
+
constructor(sql, channel, intervalMs, onError = () => { }) {
|
|
53
|
+
this.sql = sql;
|
|
54
|
+
this.channel = channel;
|
|
55
|
+
this.intervalMs = intervalMs;
|
|
56
|
+
this.onError = onError;
|
|
57
|
+
}
|
|
58
|
+
/** Добавить классы из letopis.changed (строка через запятую). */
|
|
59
|
+
add(changed) {
|
|
60
|
+
if (!changed || this.stopped)
|
|
61
|
+
return;
|
|
62
|
+
for (const c of changed.split(','))
|
|
63
|
+
if (c)
|
|
64
|
+
this.queue.add(c);
|
|
65
|
+
if (!this.timer && this.queue.size)
|
|
66
|
+
this.timer = setTimeout(() => void this.flush(), this.intervalMs);
|
|
67
|
+
}
|
|
68
|
+
/** Отправить накопленное сейчас (вызывается по таймеру и при закрытии). */
|
|
69
|
+
flush() {
|
|
70
|
+
if (this.timer) {
|
|
71
|
+
clearTimeout(this.timer);
|
|
72
|
+
this.timer = null;
|
|
73
|
+
}
|
|
74
|
+
if (!this.queue.size)
|
|
75
|
+
return this.flushing;
|
|
76
|
+
const classes = [...this.queue];
|
|
77
|
+
this.queue.clear();
|
|
78
|
+
this.flushing = this.flushing.then(() => this.sql `select pg_notify(${this.channel}, c) from unnest(${classes}::text[]) as c`.then(() => undefined, this.onError));
|
|
79
|
+
return this.flushing;
|
|
80
|
+
}
|
|
81
|
+
async close() {
|
|
82
|
+
this.stopped = true;
|
|
83
|
+
await this.flush();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Кэш подписанных пропусков прав одного подключения: по токену, актору и арендатору. */
|
|
87
|
+
export class AclPasses {
|
|
88
|
+
schema;
|
|
89
|
+
ttlMs;
|
|
90
|
+
max;
|
|
91
|
+
map = new Map();
|
|
92
|
+
/** schema — имя схемы в кавычках (для вызова acl_token). */
|
|
93
|
+
constructor(schema, ttlMs = 60_000, max = 10_000) {
|
|
94
|
+
this.schema = schema;
|
|
95
|
+
this.ttlMs = ttlMs;
|
|
96
|
+
this.max = max;
|
|
97
|
+
}
|
|
98
|
+
key(c) {
|
|
99
|
+
return `${c.token ?? ''}\u0001${c.account ?? ''}\u0001${c.tenant ?? ''}`;
|
|
100
|
+
}
|
|
101
|
+
get(c) {
|
|
102
|
+
const v = this.map.get(this.key(c));
|
|
103
|
+
if (!v)
|
|
104
|
+
return null;
|
|
105
|
+
if (Date.now() - v.at > this.ttlMs) {
|
|
106
|
+
this.map.delete(this.key(c));
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return v.pass;
|
|
110
|
+
}
|
|
111
|
+
set(c, pass) {
|
|
112
|
+
if (!pass)
|
|
41
113
|
return;
|
|
42
|
-
|
|
114
|
+
if (this.map.size >= this.max)
|
|
115
|
+
this.map.delete(this.map.keys().next().value);
|
|
116
|
+
this.map.set(this.key(c), { pass, at: Date.now() });
|
|
117
|
+
}
|
|
118
|
+
/** Права изменились (сигнал строк прав, auth.switch): все пропуска — заново. */
|
|
119
|
+
clear() {
|
|
120
|
+
this.map.clear();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const changedList = (changed) => (changed ? changed.split(',').filter(Boolean) : []);
|
|
124
|
+
/** Служебные настройки транзакции одним оператором (уходит конвейером с первым запросом). */
|
|
125
|
+
const setupSql = (q, ctx, pass) => q `select set_config('letopis.token', ${ctx.token ?? ''}, true),
|
|
126
|
+
set_config('letopis.account', ${ctx.account ?? ''}, true),
|
|
127
|
+
set_config('letopis.tenant', ${ctx.tenant ?? ''}, true),
|
|
128
|
+
set_config('letopis.acl', ${pass ?? ''}, true),
|
|
129
|
+
set_config('letopis.notify', 'deferred', true),
|
|
130
|
+
set_config('letopis.reason', ${ctx.reason ?? ''}, true),
|
|
131
|
+
set_config('TimeZone', 'UTC', true)`;
|
|
132
|
+
/** Пропуска нет — взять его в этой же транзакции (им пользуются и её запросы). */
|
|
133
|
+
const passSql = (q, ctx) => q.unsafe(`select set_config('letopis.acl', coalesce(${ctx.passes.schema}.acl_token(), ''), true) as p`);
|
|
134
|
+
/**
|
|
135
|
+
* Операция одной транзакцией с контекстом сессии и повтором при транзиентных ошибках и конфликте
|
|
136
|
+
* журнала. Ошибки базы переводятся в LetopisError. read — операция только читает: сигналы не собираются.
|
|
137
|
+
*/
|
|
138
|
+
export async function runTx(ctx, fn, opts = {}) {
|
|
139
|
+
const retries = opts.retries ?? RETRIES;
|
|
140
|
+
for (let attempt = 1;; attempt++) {
|
|
141
|
+
let bodyDone = false;
|
|
142
|
+
let dead = false;
|
|
143
|
+
let failure = null;
|
|
144
|
+
// своё зарезервированное соединение, а не sql.begin: после гибели серверного процесса
|
|
145
|
+
// postgres.js 3.4 шлёт rollback в закрытый сокет и роняет процесс необработанным TypeError
|
|
146
|
+
const conn = await ctx.sql.reserve();
|
|
43
147
|
try {
|
|
44
|
-
|
|
148
|
+
let changed = null;
|
|
149
|
+
// begin, служебный оператор и первый запрос операции уходят конвейером (§2.9); время — в UTC;
|
|
150
|
+
// execute(): запросы postgres.js ленивые — без него первый запрос fn ушёл бы раньше настроек
|
|
151
|
+
const begin = conn.unsafe('begin isolation level read committed').execute();
|
|
152
|
+
const pass = ctx.passes?.get(ctx) ?? null;
|
|
153
|
+
const setup = setupSql(conn, ctx, pass).execute();
|
|
154
|
+
const fetch = !pass && ctx.passes && ctx.token ? passSql(conn, ctx).execute() : null;
|
|
155
|
+
const [, , got, out] = await Promise.all([begin, setup, fetch, fn(conn)]);
|
|
156
|
+
if (got)
|
|
157
|
+
ctx.passes.set(ctx, got[0]?.p);
|
|
158
|
+
bodyDone = true;
|
|
159
|
+
// чтение letopis.changed и COMMIT — одним конвейером: порядок на соединении сохраняется (чтение
|
|
160
|
+
// идёт до фиксации), а круг до базы на запись — один (бенч этапа 7: −0,8 мс на запись)
|
|
161
|
+
if (opts.read) {
|
|
162
|
+
await conn.unsafe('commit');
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const [rows] = await Promise.all([conn `select current_setting('letopis.changed', true) as changed`, conn.unsafe('commit')]);
|
|
166
|
+
changed = rows[0]?.changed ?? null;
|
|
167
|
+
}
|
|
168
|
+
ctx.notifier?.add(changed);
|
|
169
|
+
if (changed)
|
|
170
|
+
ctx.onChanged?.(changedList(changed), opts.raw === true);
|
|
171
|
+
ctx.after?.();
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
failure = e;
|
|
176
|
+
dead = isConnectionError(e);
|
|
177
|
+
if (!dead)
|
|
178
|
+
await conn.unsafe('rollback').catch(() => { });
|
|
179
|
+
// обрыв во время COMMIT: транзакция могла зафиксироваться — не повторяем (§2.5)
|
|
180
|
+
if (bodyDone && !opts.read && dead)
|
|
181
|
+
throw commitUnknown(e);
|
|
182
|
+
if (attempt <= retries && (isTransient(e) || isLogConflict(e))) {
|
|
183
|
+
await retryDelay(attempt);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
throw fromDbError(e);
|
|
45
187
|
}
|
|
46
188
|
finally {
|
|
47
|
-
|
|
189
|
+
release(conn, failure);
|
|
48
190
|
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
userTx: true,
|
|
55
|
-
commit: () => finish('COMMIT'),
|
|
56
|
-
rollback: () => finish('ROLLBACK'),
|
|
57
|
-
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** Исполнитель пула: каждая операция — своя транзакция с повторами. */
|
|
194
|
+
export function poolExecutor(ctx) {
|
|
195
|
+
return { explicit: false, run: (fn, opts) => runTx(ctx, fn, opts) };
|
|
58
196
|
}
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Явная транзакция db.begin() (этап 4, п. 9): одно зарезервированное соединение, read committed.
|
|
199
|
+
* Ошибка любого запроса ломает транзакцию (правило §2.5): дальше каждый вызов бросает исходную
|
|
200
|
+
* ошибку, rollback() проходит, commit() откатывает и бросает её же. Ответ ROLLBACK на COMMIT
|
|
201
|
+
* (PostgreSQL так фиксирует сломанную транзакцию) — тоже ошибка.
|
|
202
|
+
*/
|
|
203
|
+
export class ExplicitTx {
|
|
204
|
+
ctx;
|
|
205
|
+
conn;
|
|
206
|
+
explicit = true;
|
|
207
|
+
broken = null;
|
|
208
|
+
done = false;
|
|
209
|
+
failure = null;
|
|
210
|
+
/** В транзакции был сырой SQL (db.sql): после фиксации — весь кэш. */
|
|
211
|
+
raw = false;
|
|
212
|
+
constructor(ctx, conn) {
|
|
213
|
+
this.ctx = ctx;
|
|
214
|
+
this.conn = conn;
|
|
215
|
+
}
|
|
216
|
+
static async open(ctx) {
|
|
217
|
+
const conn = await ctx.sql.reserve();
|
|
218
|
+
try {
|
|
219
|
+
await conn.unsafe('begin isolation level read committed');
|
|
220
|
+
const pass = ctx.passes?.get(ctx) ?? null;
|
|
221
|
+
await setupSql(conn, ctx, pass);
|
|
222
|
+
if (!pass && ctx.passes && ctx.token)
|
|
223
|
+
ctx.passes.set(ctx, (await passSql(conn, ctx))[0]?.p);
|
|
224
|
+
}
|
|
225
|
+
catch (e) {
|
|
226
|
+
await conn.unsafe('rollback').catch(() => { });
|
|
227
|
+
conn.release();
|
|
228
|
+
throw fromDbError(e);
|
|
229
|
+
}
|
|
230
|
+
return new ExplicitTx(ctx, conn);
|
|
231
|
+
}
|
|
232
|
+
/** Транзакция ещё открыта (не зафиксирована и не откачена). */
|
|
233
|
+
get active() {
|
|
234
|
+
return !this.done;
|
|
235
|
+
}
|
|
236
|
+
async run(fn, opts) {
|
|
237
|
+
if (this.done)
|
|
238
|
+
throw new LetopisError('tx_required', 'транзакция db.begin() уже завершена');
|
|
239
|
+
if (this.broken)
|
|
240
|
+
throw this.broken;
|
|
241
|
+
if (opts?.raw)
|
|
242
|
+
this.raw = true;
|
|
243
|
+
try {
|
|
244
|
+
return await fn(this.conn);
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
const err = fromDbError(hintTxRetry(e));
|
|
248
|
+
this.broken = err;
|
|
249
|
+
throw err;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** Advisory-блокировка до конца транзакции (отдельное от блокировок семейств пространство ключей). */
|
|
253
|
+
async lock(...keys) {
|
|
254
|
+
if (this.done)
|
|
255
|
+
throw new LetopisError('tx_required', 'lock(): транзакция db.begin() уже завершена');
|
|
256
|
+
await this.run((q) => q.unsafe('select pg_advisory_xact_lock(hashtextextended($1, 0))', [keys.join('|')]));
|
|
257
|
+
}
|
|
258
|
+
async commit() {
|
|
259
|
+
if (this.done) {
|
|
260
|
+
if (this.failure)
|
|
261
|
+
throw this.failure;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
this.done = true;
|
|
265
|
+
if (this.broken) {
|
|
266
|
+
if (!isConnectionError(this.broken))
|
|
267
|
+
await this.conn.unsafe('rollback').catch(() => { });
|
|
268
|
+
release(this.conn, this.broken);
|
|
269
|
+
this.failure = this.broken;
|
|
270
|
+
throw this.broken;
|
|
271
|
+
}
|
|
272
|
+
let changed = null;
|
|
273
|
+
try {
|
|
274
|
+
// чтение letopis.changed и COMMIT — одним конвейером (как в runTx); обрыв — исход неизвестен
|
|
275
|
+
const [rows, res] = await Promise.all([this.conn.unsafe(`select current_setting('letopis.changed', true) as changed`), this.conn.unsafe('commit')]);
|
|
276
|
+
changed = rows[0]?.changed ?? null;
|
|
277
|
+
if (res.command === 'ROLLBACK') {
|
|
278
|
+
this.failure = new LetopisError('tx_rolled_back', 'commit: PostgreSQL откатил транзакцию (в ней была ошибка)');
|
|
279
|
+
throw this.failure;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch (e) {
|
|
283
|
+
this.failure ??= isConnectionError(e) ? commitUnknown(e) : fromDbError(e);
|
|
284
|
+
release(this.conn, e);
|
|
285
|
+
throw this.failure;
|
|
286
|
+
}
|
|
287
|
+
this.conn.release();
|
|
288
|
+
this.ctx.notifier?.add(changed);
|
|
289
|
+
if (changed)
|
|
290
|
+
this.ctx.onChanged?.(changedList(changed), this.raw);
|
|
291
|
+
this.ctx.after?.();
|
|
292
|
+
}
|
|
293
|
+
async rollback() {
|
|
294
|
+
if (this.done)
|
|
295
|
+
return;
|
|
296
|
+
this.done = true;
|
|
297
|
+
try {
|
|
298
|
+
// соединение, оборванное ошибкой, не трогаем: postgres.js уронил бы процесс записью в закрытый сокет
|
|
299
|
+
if (!isConnectionError(this.broken))
|
|
300
|
+
await this.conn.unsafe('rollback');
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
// соединение могло оборваться — транзакция откатится сама
|
|
304
|
+
release(this.conn, e);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
release(this.conn, this.broken);
|
|
308
|
+
}
|
|
82
309
|
}
|
package/dist/typed.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Типы цепочек по моделям (план, этап 3, п. 9): `connect({ models })` даёт типы шагов по именам и
|
|
3
|
+
* алиасам классов, типы строк ответа и фильтров. Допустимость переходов проверяется во время
|
|
4
|
+
* выполнения, как в 0.21. Модель описана структурно (ModelLike), чтобы основная точка входа не
|
|
5
|
+
* зависела от Zod: модели letopis/model ей соответствуют.
|
|
6
|
+
*/
|
|
7
|
+
import type { ChainCore, StepFn } from './chain.js';
|
|
8
|
+
import type { Cursor, Op, Path, Row, Scalar } from './types.js';
|
|
9
|
+
import type { IncMarker } from './write.js';
|
|
10
|
+
/** То, что цепочкам нужно от модели: имя, алиас и выведенные типы данных и концов. */
|
|
11
|
+
export interface ModelLike<N extends string = string, D = unknown, L = unknown, A extends string = string> {
|
|
12
|
+
readonly name: N;
|
|
13
|
+
readonly alias?: A;
|
|
14
|
+
readonly __types?: {
|
|
15
|
+
data: D;
|
|
16
|
+
links: L;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export type AnyModelLike = ModelLike<string, any, any, any>;
|
|
20
|
+
type DataOf<C> = C extends ModelLike<string, infer D, unknown, string> ? (D extends Record<string, unknown> ? D : Record<string, unknown>) : Record<string, unknown>;
|
|
21
|
+
type LinksOf<C> = C extends ModelLike<string, unknown, infer L, string> ? (L extends Record<string, unknown> ? L : Record<string, string>) : Record<string, string>;
|
|
22
|
+
/** Строка ответа класса C. */
|
|
23
|
+
export type RowOf<C> = Row<DataOf<C>, LinksOf<C>>;
|
|
24
|
+
/** Фильтр по значению поля: равенство, оператор, вложенный объект любой глубины. */
|
|
25
|
+
export type FieldFilterOf<T> = Op | (T extends readonly unknown[] ? T : T extends Record<string, unknown> ? {
|
|
26
|
+
[P in keyof T]?: FieldFilterOf<T[P]>;
|
|
27
|
+
} : T extends Scalar ? T | null : never);
|
|
28
|
+
/** Фильтр шага класса C: id, список id, строка ответа, or(…) или объект полей и ролей. */
|
|
29
|
+
export type FilterOf<C> = string | string[] | Op | {
|
|
30
|
+
id: string;
|
|
31
|
+
class: string;
|
|
32
|
+
} | ({
|
|
33
|
+
[P in keyof DataOf<C>]?: FieldFilterOf<DataOf<C>[P]>;
|
|
34
|
+
} & {
|
|
35
|
+
[R in keyof LinksOf<C>]?: string | {
|
|
36
|
+
id: string;
|
|
37
|
+
} | null;
|
|
38
|
+
} & {
|
|
39
|
+
id?: string | string[];
|
|
40
|
+
});
|
|
41
|
+
/** Шаги по именам и алиасам моделей M. */
|
|
42
|
+
export type StepsOf<M extends readonly AnyModelLike[]> = {
|
|
43
|
+
[K in M[number] as K['name']]: (filter?: FilterOf<K>) => TypedChain<K, M>;
|
|
44
|
+
} & {
|
|
45
|
+
[K in M[number] as K extends ModelLike<string, unknown, unknown, infer A> ? (string extends A ? never : A) : never]: (filter?: FilterOf<K>) => TypedChain<K, M>;
|
|
46
|
+
};
|
|
47
|
+
/** Патч update: поля любой глубины необязательны; inc() — только у числовых полей (§2.5). */
|
|
48
|
+
export type PatchOf<T> = {
|
|
49
|
+
[P in keyof T]?: T[P] extends number | undefined ? T[P] | IncMarker : T[P] extends readonly unknown[] | undefined ? T[P] : T[P] extends Record<string, unknown> | undefined ? PatchOf<NonNullable<T[P]>> : T[P];
|
|
50
|
+
};
|
|
51
|
+
type Retyped = 'paths' | 'rows' | 'first' | 'versions' | 'limit' | 'offset' | 'sort' | 'asOf' | 'after' | 'withDeleted' | 'deep' | 'exact' | 'alias' | 'tags' | 'owner' | 'create' | 'update' | 'upsert' | 'delete' | 'anonymize' | 'reclass' | 'forUpdate' | 'restore' | 'purge' | 'rekey' | 'cache';
|
|
52
|
+
/** Цепочка с текущим классом C: терминалы отдают RowOf<C>, шаги — по моделям M; роли и прочие классы — без типов. */
|
|
53
|
+
export type TypedChain<C, M extends readonly AnyModelLike[]> = Omit<ChainCore, Retyped> & {
|
|
54
|
+
paths(): Promise<Path[]>;
|
|
55
|
+
rows(): Promise<RowOf<C>[]>;
|
|
56
|
+
first(): Promise<RowOf<C> | null>;
|
|
57
|
+
versions(opts?: {
|
|
58
|
+
follow?: boolean;
|
|
59
|
+
}): Promise<RowOf<C>[]>;
|
|
60
|
+
limit(n: number): TypedChain<C, M>;
|
|
61
|
+
offset(n: number): TypedChain<C, M>;
|
|
62
|
+
sort(field: 'at' | 'rev' | 'id' | `data.${string}`, dir?: 'asc' | 'desc' | boolean): TypedChain<C, M>;
|
|
63
|
+
asOf(t: string | Date): TypedChain<C, M>;
|
|
64
|
+
after(cursor: Cursor): TypedChain<C, M>;
|
|
65
|
+
withDeleted(): TypedChain<C, M>;
|
|
66
|
+
deep(max?: number): TypedChain<C, M>;
|
|
67
|
+
exact(): TypedChain<C, M>;
|
|
68
|
+
alias(name: string): TypedChain<C, M>;
|
|
69
|
+
tags(v: string | string[] | Op): TypedChain<C, M>;
|
|
70
|
+
owner(v: string | {
|
|
71
|
+
id: string;
|
|
72
|
+
}): TypedChain<C, M>;
|
|
73
|
+
create(data: Partial<DataOf<C>>): TypedChain<C, M>;
|
|
74
|
+
update(patch?: PatchOf<DataOf<C>>, opts?: {
|
|
75
|
+
rev?: number;
|
|
76
|
+
}): TypedChain<C, M>;
|
|
77
|
+
upsert(data: Partial<DataOf<C>>): TypedChain<C, M>;
|
|
78
|
+
delete(opts?: {
|
|
79
|
+
confirm?: boolean;
|
|
80
|
+
}): TypedChain<C, M>;
|
|
81
|
+
anonymize(fields: (keyof DataOf<C> & string)[]): TypedChain<C, M>;
|
|
82
|
+
reclass(cls: string, data?: Record<string, unknown>): TypedChain<unknown, M>;
|
|
83
|
+
forUpdate(): TypedChain<C, M>;
|
|
84
|
+
restore(): TypedChain<C, M>;
|
|
85
|
+
purge(opts?: {
|
|
86
|
+
confirm?: boolean;
|
|
87
|
+
}): TypedChain<C, M>;
|
|
88
|
+
rekey(patch: Partial<DataOf<C>> & Partial<Record<keyof LinksOf<C>, string | {
|
|
89
|
+
id: string;
|
|
90
|
+
}>>): TypedChain<C, M>;
|
|
91
|
+
cache(opts?: {
|
|
92
|
+
ttl?: number;
|
|
93
|
+
}): TypedChain<C, M>;
|
|
94
|
+
} & StepsOf<M> & {
|
|
95
|
+
[step: string]: StepFn;
|
|
96
|
+
};
|
|
97
|
+
export {};
|
package/dist/typed.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|