letopis 1.4.2 → 2.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 +329 -281
- package/CHANGELOG.md +418 -0
- package/README.md +572 -1053
- package/dist/acl.d.ts +78 -1
- package/dist/acl.js +264 -13
- package/dist/admin.d.ts +10 -141
- package/dist/admin.js +24 -184
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +5 -15
- package/dist/cache.d.ts +8 -1
- package/dist/cache.js +30 -8
- package/dist/chain.d.ts +18 -41
- package/dist/chain.js +165 -203
- package/dist/cli.js +45 -90
- 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 +59 -13
- package/dist/errors.js +190 -29
- package/dist/index.d.ts +52 -70
- package/dist/index.js +189 -122
- package/dist/internal.d.ts +5 -0
- package/dist/jsonschema.js +18 -17
- package/dist/load.d.ts +20 -12
- package/dist/load.js +207 -181
- package/dist/mcp.d.ts +5 -7
- package/dist/mcp.js +168 -214
- package/dist/messages/access.d.ts +90 -0
- package/dist/messages/access.js +113 -0
- package/dist/messages/api.d.ts +171 -0
- package/dist/messages/api.js +215 -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 +95 -0
- package/dist/messages/index.js +118 -0
- package/dist/messages/mcp.d.ts +306 -0
- package/dist/messages/mcp.js +387 -0
- package/dist/messages/model.d.ts +233 -0
- package/dist/messages/model.js +283 -0
- package/dist/messages/query.d.ts +131 -0
- package/dist/messages/query.js +158 -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 +150 -0
- package/dist/messages/sql-core.js +181 -0
- package/dist/messages/sql-time.d.ts +71 -0
- package/dist/messages/sql-time.js +88 -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 +405 -0
- package/dist/messages/write.js +509 -0
- package/dist/model.d.ts +73 -60
- package/dist/model.js +20 -29
- 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 +7 -1
- package/dist/sql.js +101 -85
- package/dist/sync.d.ts +9 -3
- package/dist/sync.js +40 -17
- package/dist/tx.d.ts +11 -1
- package/dist/tx.js +32 -11
- package/dist/typed.d.ts +240 -45
- package/dist/types.d.ts +15 -90
- package/dist/types.js +12 -14
- package/dist/up.d.ts +53 -32
- package/dist/up.js +180 -74
- package/dist/uuid.js +5 -5
- package/dist/validate.d.ts +13 -1
- package/dist/validate.js +61 -29
- package/dist/watch.d.ts +5 -16
- package/dist/watch.js +23 -77
- package/dist/write.d.ts +9 -16
- package/dist/write.js +102 -185
- package/llms.txt +22 -25
- package/package.json +2 -9
- package/sql/10-core.sql +7 -50
- package/sql/15-errors.sql +64 -10
- package/sql/20-context.sql +7 -6
- package/sql/30-validate.sql +35 -27
- package/sql/40-class.sql +20 -114
- 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 +23 -27
- package/sql/90-time.sql +77 -944
- package/sql/95-seed.system.sql +7 -14
- package/sql/99-revision.sql +2 -1
- package/sql/README.md +32 -24
- package/sql/migrate/README.md +14 -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/dist/sql.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface Step {
|
|
|
29
29
|
}
|
|
30
30
|
/** Уникальная метка узла пути. */
|
|
31
31
|
export declare const nextNodeKey: () => number;
|
|
32
|
-
export type ReadMode = 'paths' | 'rows' | 'ids' | 'count' | 'countPaths' | 'versions' | 'agg'
|
|
32
|
+
export type ReadMode = 'paths' | 'rows' | 'ids' | 'count' | 'countPaths' | 'versions' | 'agg';
|
|
33
33
|
export interface AggSpec {
|
|
34
34
|
fn: 'sum' | 'avg' | 'min' | 'max' | 'countBy';
|
|
35
35
|
field: string;
|
|
@@ -48,6 +48,12 @@ export declare function roleOwners(reg: Registry, role: string): ClassInfo[];
|
|
|
48
48
|
export declare function roleTargets(reg: Registry, role: string): string[];
|
|
49
49
|
/** Конкретные классы, которые матчит шаг: класс (роль — её цели) и потомки, если не exact(). */
|
|
50
50
|
export declare function stepClasses(reg: Registry, step: Step): string[];
|
|
51
|
+
/**
|
|
52
|
+
* Отказы построителя, которым не нужны база и путь, — фильтр шага (в том числе неизвестное поле) и поле sort('data.…') или
|
|
53
|
+
* агрегата по классам шага: план записи проверяет их до первого оператора (write.ts:precheckPlan, §2.5), поэтому
|
|
54
|
+
* опечатка в фильтре цели не ломает db.begin(). Построитель при чтении проверяет то же самое ещё раз.
|
|
55
|
+
*/
|
|
56
|
+
export declare function precheckStep(reg: Registry, step: Step, order?: string, agg?: AggSpec): void;
|
|
51
57
|
/** Принимает ли список классов конца класс cls (с учётом его предков). */
|
|
52
58
|
export declare const accepts: (reg: Registry, targets: string[], cls: string) => boolean;
|
|
53
59
|
export interface Hop {
|
package/dist/sql.js
CHANGED
|
@@ -17,15 +17,43 @@
|
|
|
17
17
|
* Значения всегда уходят параметрами, поэтому текст запроса зависит только от формы цепочки: его
|
|
18
18
|
* кэширует план (chain.ts), а postgres.js готовит оператор один раз на соединение.
|
|
19
19
|
*/
|
|
20
|
-
import {
|
|
20
|
+
import { lerr, lmsg, similar } from './errors.js';
|
|
21
21
|
import { isOp } from './ops.js';
|
|
22
22
|
import { OP } from './types.js';
|
|
23
23
|
let nodeSeq = 0;
|
|
24
24
|
/** Уникальная метка узла пути. */
|
|
25
25
|
export const nextNodeKey = () => ++nodeSeq;
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/** Отказ построителя по ключу каталога — там, где нужно выражение: `x ?? refuse(ключ)`; в операторах — throw lerr(…). */
|
|
27
|
+
const refuse = (key, params) => {
|
|
28
|
+
throw lerr(key, params);
|
|
28
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Поля data строк системного класса Class — свойства метасхемы описания класса (class_metaschema() в 40-class.sql): строки
|
|
32
|
+
* Class проверяет она, а в описании самого класса Class только name. Ключи — свойства описания (ClassDescription) и
|
|
33
|
+
* служебные system и resolved: новое свойство описания без строки здесь — ошибка типов.
|
|
34
|
+
*/
|
|
35
|
+
const CLASS_ROW_FIELDS = Object.keys({
|
|
36
|
+
name: true, kind: true, parent: true, abstract: true, alias: true, system: true, fields: true, ends: true, key: true,
|
|
37
|
+
history: true, ownerDefault: true, cache: true, meta: true, resolved: true,
|
|
38
|
+
});
|
|
39
|
+
/** Поля верхнего уровня data строк класса: он закрыт (class_resolve, 40-class.sql) — в нём только поля описания. */
|
|
40
|
+
const dataFields = (c) => c.name === 'Class' ? CLASS_ROW_FIELDS : Object.keys((c.schema.properties ?? {}));
|
|
41
|
+
/**
|
|
42
|
+
* Поле фильтра, sort() или агрегата не описано ни в одном классе шага — такое условие ничего не найдёт: invalid_query с
|
|
43
|
+
* похожими именами (план 2.0, этап 3, п. 3; в 1.5 было предупреждение, 7.9 №6). Проверяется только верхний уровень:
|
|
44
|
+
* путь внутрь поля-объекта не сверяется. classes — класс шага и все классы, которые он матчит (потомки, если не exact(),
|
|
45
|
+
* у роли — её цели); ends — роли концов тоже годятся (фильтр).
|
|
46
|
+
*/
|
|
47
|
+
function checkField(classes, field, where, ends) {
|
|
48
|
+
if (!classes.length)
|
|
49
|
+
return;
|
|
50
|
+
if (classes.some((c) => dataFields(c).includes(field) || (ends && c.ends[field] !== undefined)))
|
|
51
|
+
return;
|
|
52
|
+
const names = classes.flatMap((c) => [...dataFields(c), ...(ends ? Object.keys(c.ends) : [])]);
|
|
53
|
+
throw lerr('query.unknown_field', { where, field, cls: classes[0].name, similar: similar(field, names) });
|
|
54
|
+
}
|
|
55
|
+
/** Поле sort('data.<путь>') или агрегата: первый ключ пути. */
|
|
56
|
+
const topField = (field) => field.slice(5).split('.')[0];
|
|
29
57
|
// ---------------------------------------------------------------------------
|
|
30
58
|
// Классы шага и правила переходов
|
|
31
59
|
// ---------------------------------------------------------------------------
|
|
@@ -47,6 +75,24 @@ export function stepClasses(reg, step) {
|
|
|
47
75
|
return base;
|
|
48
76
|
return uniq(base.flatMap((b) => (reg.has(b) ? reg.family(b).map((c) => c.name) : [b])));
|
|
49
77
|
}
|
|
78
|
+
/** Классы шага для типов листьев и сверки полей: класс шага первым, затем прочие классы, которые шаг матчит. */
|
|
79
|
+
function typingOf(reg, step, names = stepClasses(reg, step)) {
|
|
80
|
+
const infos = names.map((n) => reg.find(n)).filter((c) => !!c);
|
|
81
|
+
return [step.cls, ...infos.filter((c) => c !== step.cls)];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Отказы построителя, которым не нужны база и путь, — фильтр шага (в том числе неизвестное поле) и поле sort('data.…') или
|
|
85
|
+
* агрегата по классам шага: план записи проверяет их до первого оператора (write.ts:precheckPlan, §2.5), поэтому
|
|
86
|
+
* опечатка в фильтре цели не ломает db.begin(). Построитель при чтении проверяет то же самое ещё раз.
|
|
87
|
+
*/
|
|
88
|
+
export function precheckStep(reg, step, order, agg) {
|
|
89
|
+
const typing = typingOf(reg, step);
|
|
90
|
+
compileFilter(reg, typing, step.filter, new Params());
|
|
91
|
+
if (order?.startsWith('data.'))
|
|
92
|
+
checkField(typing, topField(order), `sort('${order}')`, false);
|
|
93
|
+
if (agg?.field.startsWith('data.'))
|
|
94
|
+
checkField(typing, topField(agg.field), `${agg.fn}('${agg.field}')`, false);
|
|
95
|
+
}
|
|
50
96
|
/** Принимает ли список классов конца класс cls (с учётом его предков). */
|
|
51
97
|
export const accepts = (reg, targets, cls) => (reg.find(cls)?.ancestors ?? [cls]).some((a) => targets.includes(a));
|
|
52
98
|
/** Роли классов from, цели которых принимают хотя бы один класс to. */
|
|
@@ -85,7 +131,7 @@ export function resolveHop(reg, prev, next) {
|
|
|
85
131
|
if ((dir === 'forward' ? fwd : rev).length)
|
|
86
132
|
return { dir, roles: null };
|
|
87
133
|
}
|
|
88
|
-
throw
|
|
134
|
+
throw lerr('query.no_path', { from: prev.name, to: next.name });
|
|
89
135
|
}
|
|
90
136
|
// ---------------------------------------------------------------------------
|
|
91
137
|
// Фильтры и операторы
|
|
@@ -188,7 +234,7 @@ function compileOp(path, leaf, o, p) {
|
|
|
188
234
|
return compileOp(path, leaf, { [OP]: 'ne', args: [args[0]] }, p);
|
|
189
235
|
}
|
|
190
236
|
default:
|
|
191
|
-
|
|
237
|
+
throw lerr('query.unknown_op', { op: String(o[OP]) });
|
|
192
238
|
}
|
|
193
239
|
}
|
|
194
240
|
/** Колонка tags: строка | список (все) | has / hasAny / hasAll. */
|
|
@@ -215,7 +261,7 @@ function compileTags(v, p) {
|
|
|
215
261
|
}
|
|
216
262
|
}
|
|
217
263
|
}
|
|
218
|
-
|
|
264
|
+
throw lerr('query.tags_arg');
|
|
219
265
|
}
|
|
220
266
|
const isPlain = (v) => typeof v === 'object' && v !== null && !Array.isArray(v) && !isOp(v);
|
|
221
267
|
/** Объект-строка ответа как значение: берётся id. */
|
|
@@ -227,7 +273,7 @@ function compileFilter(reg, classes, filter, p) {
|
|
|
227
273
|
// or(…): дизъюнкция под-фильтров — только перепроверка
|
|
228
274
|
if (isOp(filter)) {
|
|
229
275
|
if (filter[OP] !== 'or')
|
|
230
|
-
|
|
276
|
+
throw lerr('query.step_op', { op: String(filter[OP]) });
|
|
231
277
|
const parts = filter.args[0].map((f) => {
|
|
232
278
|
const sub = compileFilter(reg, classes, f, p);
|
|
233
279
|
const frags = [...asFrags(sub, p)];
|
|
@@ -241,7 +287,7 @@ function compileFilter(reg, classes, filter, p) {
|
|
|
241
287
|
return out;
|
|
242
288
|
}
|
|
243
289
|
if (Array.isArray(filter)) {
|
|
244
|
-
out.ids = filter.map((x) => idOfValue(x) ??
|
|
290
|
+
out.ids = filter.map((x) => idOfValue(x) ?? refuse('query.id_list'));
|
|
245
291
|
return out;
|
|
246
292
|
}
|
|
247
293
|
const props = (classes[0]?.schema.properties ?? {});
|
|
@@ -281,9 +327,9 @@ function compileFilter(reg, classes, filter, p) {
|
|
|
281
327
|
if (typeof value === 'string')
|
|
282
328
|
out.ids = [value];
|
|
283
329
|
else if (Array.isArray(value))
|
|
284
|
-
out.ids = value.map((x) => idOfValue(x) ??
|
|
330
|
+
out.ids = value.map((x) => idOfValue(x) ?? refuse('query.filter_id_list'));
|
|
285
331
|
else
|
|
286
|
-
|
|
332
|
+
throw lerr('query.filter_id');
|
|
287
333
|
continue;
|
|
288
334
|
}
|
|
289
335
|
if (isRole(key)) {
|
|
@@ -292,12 +338,14 @@ function compileFilter(reg, classes, filter, p) {
|
|
|
292
338
|
out.frags.push((a) => `coalesce(${a}.links->${role}, 'null'::jsonb) in ('null'::jsonb, '[]'::jsonb)`);
|
|
293
339
|
continue;
|
|
294
340
|
}
|
|
295
|
-
const id = idOfValue(value) ??
|
|
341
|
+
const id = idOfValue(value) ?? refuse('query.end_value', { role: key });
|
|
296
342
|
out.ends.push(id);
|
|
297
343
|
const ph = p.push(id);
|
|
298
344
|
out.frags.push((a) => `${SCHEMA_MARK}.role_has(${a}.links, array[${role}], ${ph}::uuid)`);
|
|
299
345
|
continue;
|
|
300
346
|
}
|
|
347
|
+
// поле данных: не описано ни в одном классе шага — invalid_query с похожими (план 2.0, этап 3, п. 3)
|
|
348
|
+
checkField(classes, key, `${classes[0]?.name ?? ''}({ ${key} })`, true);
|
|
301
349
|
if (isOp(value)) {
|
|
302
350
|
out.frags.push(compileOp([key], leafOf(reg, classes, [key]), value, p));
|
|
303
351
|
continue;
|
|
@@ -318,8 +366,12 @@ function compileFilter(reg, classes, filter, p) {
|
|
|
318
366
|
function asFrags(cf, p) {
|
|
319
367
|
const frags = [...cf.frags];
|
|
320
368
|
if (cf.ids) {
|
|
321
|
-
|
|
322
|
-
|
|
369
|
+
// один id — равенство, а не = any (массив): общий план подготовленного запроса для = any PostgreSQL считает на ~10
|
|
370
|
+
// элементов, и при неудачной выборке ANALYZE чтение по ключу навсегда оставалось на планах под каждый вызов —
|
|
371
|
+
// 0,5–0,8 мс планирования на чтение (замер против 1.0.1); общий план равенства стоит как план вызова и выбирается всегда
|
|
372
|
+
const one = cf.ids.length === 1;
|
|
373
|
+
const ph = p.push(one ? cf.ids[0] : cf.ids);
|
|
374
|
+
frags.push((a) => (one ? `${a}.id = ${ph}::uuid` : `${a}.id = any (${ph}::uuid[])`));
|
|
323
375
|
}
|
|
324
376
|
if (cf.eq) {
|
|
325
377
|
const ph = p.push(cf.eq);
|
|
@@ -341,7 +393,7 @@ function sortSpec(order, leaf) {
|
|
|
341
393
|
const t = leaf(path);
|
|
342
394
|
return { expr: (a) => (CAST[t] ? `(${accessor(path)(a)})${CAST[t]}` : accessor(path)(a)), pcast: PCAST[t], field: order, type: t, nullable: true };
|
|
343
395
|
}
|
|
344
|
-
|
|
396
|
+
throw lerr('query.sort_field', { field: order });
|
|
345
397
|
}
|
|
346
398
|
/** Формы значений курсора — как у форматов uuid, date и date-time валидатора (validate.ts). */
|
|
347
399
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -398,9 +450,11 @@ function shown(v) {
|
|
|
398
450
|
}
|
|
399
451
|
return s.length > 60 ? `${s.slice(0, 59)}…` : s;
|
|
400
452
|
}
|
|
453
|
+
/** Какое значение курсора ждёт поле sort() по своему типу — запись каталога (параметр expect ошибки курсора). */
|
|
401
454
|
const CURSOR_EXPECT = {
|
|
402
|
-
numeric: '
|
|
403
|
-
|
|
455
|
+
numeric: 'query.cursor_expect_numeric', int: 'query.cursor_expect_int', boolean: 'query.cursor_expect_boolean',
|
|
456
|
+
text: 'query.cursor_expect_text', uuid: 'query.cursor_expect_uuid', timestamptz: 'query.cursor_expect_timestamptz',
|
|
457
|
+
date: 'query.cursor_expect_date', jsonb: 'query.cursor_expect_jsonb',
|
|
404
458
|
};
|
|
405
459
|
/**
|
|
406
460
|
* Параметры keyset из курсора: значение проверяется по типу поля сортировки до запроса. Курсор
|
|
@@ -409,16 +463,15 @@ const CURSOR_EXPECT = {
|
|
|
409
463
|
* Курсор того же типа с другого поля проверка не отличит.
|
|
410
464
|
*/
|
|
411
465
|
function cursorParams(c, sort) {
|
|
412
|
-
const how = `cursorOf(строка, '${sort.field}')`;
|
|
413
466
|
if (typeof c !== 'object' || c === null || c.v === undefined)
|
|
414
|
-
|
|
467
|
+
throw lerr('query.cursor_shape', { field: sort.field });
|
|
415
468
|
const { v, id } = c;
|
|
416
469
|
if (typeof id !== 'string' || !UUID_RE.test(id))
|
|
417
|
-
|
|
470
|
+
throw lerr('query.cursor_id', { id: shown(id) });
|
|
418
471
|
const value = v === null && sort.nullable ? null : cursorValue(v, sort.type);
|
|
419
472
|
if (value === undefined) {
|
|
420
|
-
|
|
421
|
-
|
|
473
|
+
// у поля данных подходит и курсор null — своя запись, а не склейка « или null»
|
|
474
|
+
throw lerr(sort.nullable ? 'query.cursor_value_nullable' : 'query.cursor_value', { value: shown(v), field: sort.field, expect: lmsg(CURSOR_EXPECT[sort.type]) });
|
|
422
475
|
}
|
|
423
476
|
return { v: value, id };
|
|
424
477
|
}
|
|
@@ -428,7 +481,7 @@ function cursorRev(c) {
|
|
|
428
481
|
if (rev === undefined)
|
|
429
482
|
return undefined;
|
|
430
483
|
if (!Number.isInteger(rev) || rev < 1)
|
|
431
|
-
|
|
484
|
+
throw lerr('query.cursor_rev', { rev: shown(rev) });
|
|
432
485
|
return rev;
|
|
433
486
|
}
|
|
434
487
|
/** id узлов пути из курсора paths() по ключам узлов (план 2.0, 7.1 №3); курсор без path — undefined. */
|
|
@@ -439,7 +492,7 @@ function cursorPath(c, keys) {
|
|
|
439
492
|
return keys.map((k) => {
|
|
440
493
|
const id = typeof path === 'object' && path !== null ? path[k] : undefined;
|
|
441
494
|
if (typeof id !== 'string' || !UUID_RE.test(id))
|
|
442
|
-
|
|
495
|
+
throw lerr('query.cursor_path', { key: k });
|
|
443
496
|
return id;
|
|
444
497
|
});
|
|
445
498
|
}
|
|
@@ -466,7 +519,7 @@ export function cursorOf(row, field) {
|
|
|
466
519
|
v = v?.[k];
|
|
467
520
|
return { v: (v ?? null), id: row.id, ...rev };
|
|
468
521
|
}
|
|
469
|
-
|
|
522
|
+
throw lerr('query.cursor_of_field', { field });
|
|
470
523
|
}
|
|
471
524
|
// ---------------------------------------------------------------------------
|
|
472
525
|
// Построение запроса
|
|
@@ -476,14 +529,14 @@ export function cursorOf(row, field) {
|
|
|
476
529
|
* заденет литералы — имя поля вида a@S@b прежде портилось (план 2.0, 7.1 №5).
|
|
477
530
|
*/
|
|
478
531
|
const SCHEMA_MARK = '\u0000S\u0000';
|
|
479
|
-
const COLS = ['id', 'rev', 'class', 'tenant', 'owner', 'links', 'data', 'tags', 'at', 'author', 'agent', 'op', 'reason'
|
|
480
|
-
/** Колонки
|
|
481
|
-
const LOG_COLS = ['seq', 'tx', 'id', 'rev', 'class', 'cv', 'tenant', 'owner', 'links', 'data', 'tags', 'at', 'author', 'agent', 'op', 'reason'
|
|
482
|
-
|
|
532
|
+
const COLS = ['id', 'rev', 'class', 'tenant', 'owner', 'links', 'data', 'tags', 'at', 'author', 'agent', 'op', 'reason'];
|
|
533
|
+
/** Колонки журнала (выданные приложению — 10-core.sql). */
|
|
534
|
+
const LOG_COLS = ['seq', 'tx', 'id', 'rev', 'class', 'cv', 'tenant', 'owner', 'links', 'data', 'tags', 'at', 'author', 'agent', 'op', 'reason']
|
|
535
|
+
.map((c) => 'l.' + c).join(', ');
|
|
483
536
|
const colsOf = (a) => COLS.map((c) => `${a}.${c}`).join(', ');
|
|
484
537
|
export function buildRead(reg, schema, steps, mods, mode, agg, opts = {}) {
|
|
485
538
|
if (!steps.length)
|
|
486
|
-
|
|
539
|
+
throw lerr('query.empty_chain');
|
|
487
540
|
const S = `"${schema.replace(/"/g, '""')}"`;
|
|
488
541
|
const p = new Params();
|
|
489
542
|
const cur = mods.asOf === undefined;
|
|
@@ -495,7 +548,7 @@ export function buildRead(reg, schema, steps, mods, mode, agg, opts = {}) {
|
|
|
495
548
|
if (s.pivotKey !== undefined) {
|
|
496
549
|
const at = byNodeKey.get(s.pivotKey);
|
|
497
550
|
if (at === undefined)
|
|
498
|
-
|
|
551
|
+
throw lerr('query.pivot_missing', { name: s.name });
|
|
499
552
|
real[i] = at;
|
|
500
553
|
}
|
|
501
554
|
else {
|
|
@@ -513,8 +566,7 @@ export function buildRead(reg, schema, steps, mods, mode, agg, opts = {}) {
|
|
|
513
566
|
for (let i = 0; i < steps.length; i++) {
|
|
514
567
|
const step = steps[i];
|
|
515
568
|
const names = stepClasses(reg, step);
|
|
516
|
-
const
|
|
517
|
-
const typing = [step.cls, ...infos.filter((c) => c !== step.cls)];
|
|
569
|
+
const typing = typingOf(reg, step, names);
|
|
518
570
|
// возврат к узлу: только дофильтры
|
|
519
571
|
if (step.pivotKey !== undefined) {
|
|
520
572
|
const a = `h${real[i]}`;
|
|
@@ -563,7 +615,7 @@ export function buildRead(reg, schema, steps, mods, mode, agg, opts = {}) {
|
|
|
563
615
|
}
|
|
564
616
|
// по классу: id-кандидаты классов шага, последняя версия — любого класса (как find_ids, план 2.0, 7.1 №1)
|
|
565
617
|
return `(select v.id, v.rev from ${versionsAt(at, ids ? `l.id = any (${ids})` : `l.id in (select c.id from ${S}.log c where c.class = any (${clsPh}))`)} v `
|
|
566
|
-
+ `where v.class = any (${clsPh}) and (v.op = 'delete') = ${isDead}
|
|
618
|
+
+ `where v.class = any (${clsPh}) and (v.op = 'delete') = ${isDead}${dataCond('v')}${endsCond(`${S}.link_ids(v.links)`)})`;
|
|
567
619
|
};
|
|
568
620
|
const entityPart = (src, depth = 'null::int', from = `${S}.entity x`) => `select ${colsOf('x')}, ${depth} as depth from ${from} where ${where('x', src)}`;
|
|
569
621
|
/**
|
|
@@ -580,7 +632,7 @@ export function buildRead(reg, schema, steps, mods, mode, agg, opts = {}) {
|
|
|
580
632
|
const parts = [];
|
|
581
633
|
if (i === 0) {
|
|
582
634
|
if (step.deepMax !== undefined)
|
|
583
|
-
|
|
635
|
+
throw lerr('query.deep_first_step');
|
|
584
636
|
if (cur) {
|
|
585
637
|
// условие id уже в conds — первичный ключ; вложение — поиск по индексу GIN
|
|
586
638
|
parts.push(!cf.ids && (cf.eq || cf.ends.length) ? entityByIds(`select f.id from ${findIds(null, 'null', false)} f`) : entityPart(undefined));
|
|
@@ -601,15 +653,15 @@ export function buildRead(reg, schema, steps, mods, mode, agg, opts = {}) {
|
|
|
601
653
|
const rolesCond = (v, src) => (hop.roles ? ` and ${S}.role_has(${v}.links, ${rolesPh}, ${src})` : '');
|
|
602
654
|
if (step.deepMax !== undefined) {
|
|
603
655
|
if (hop.dir !== 'reverse' || !stepClasses(reg, steps[a]).some((c) => names.includes(c))) {
|
|
604
|
-
|
|
656
|
+
throw lerr('query.deep_hop', { from: steps[a].name, to: step.name });
|
|
605
657
|
}
|
|
606
658
|
if (dead)
|
|
607
|
-
|
|
659
|
+
throw lerr('query.deep_with_deleted');
|
|
608
660
|
const maxPh = `${p.push(step.deepMax)}::int`;
|
|
609
661
|
let deepSrc = `${S}.find_deep(${clsPh}, ${rolesPh}, ${srcArr}, ${maxPh}, ${atPh})`;
|
|
610
662
|
if (opts.direct) {
|
|
611
663
|
// тот же обход под RLS: невидимый узел обрывает поддерево политикой
|
|
612
|
-
const node = cur ? `${S}.entity` : `(select w.id, w.rev, w.links from ${versionsAt(atPh, `l.class = any (${clsPh})`)} w where w.op
|
|
664
|
+
const node = cur ? `${S}.entity` : `(select w.id, w.rev, w.links from ${versionsAt(atPh, `l.class = any (${clsPh})`)} w where w.op <> 'delete')`;
|
|
613
665
|
const hasSrc = (v, src) => (cur ? `${v}.ends @> array[${src}]` : `${S}.link_ids(${v}.links) @> array[${src}]`);
|
|
614
666
|
const cls = (v) => (cur ? ` and ${v}.class = any (${clsPh})` : '');
|
|
615
667
|
deepSrc = `(with recursive d(src, id, rev, depth, seen) as (`
|
|
@@ -645,7 +697,7 @@ export function buildRead(reg, schema, steps, mods, mode, agg, opts = {}) {
|
|
|
645
697
|
}
|
|
646
698
|
return `(select s.src, v.id, v.rev from ${versionsAt(at, `l.class = any (${clsPh})`)} v `
|
|
647
699
|
+ `cross join lateral unnest(${S}.link_ids(v.links)) t(x) join ${srcs} on s.src = t.x `
|
|
648
|
-
+ `where (v.op = 'delete') = ${isDead}
|
|
700
|
+
+ `where (v.op = 'delete') = ${isDead}${dataCond('v')}${endsCond(`${S}.link_ids(v.links)`)}${rolesCond('v', 's.src')})`;
|
|
649
701
|
};
|
|
650
702
|
ctes.push(`p${i}a as (select q.src, q.id, q.rev from ${hopCall(atPh, false)} q)`);
|
|
651
703
|
// пары переходов — по одной на (источник, цель): объект, который ссылается на несколько объектов прошлого шага,
|
|
@@ -686,6 +738,8 @@ export function buildRead(reg, schema, steps, mods, mode, agg, opts = {}) {
|
|
|
686
738
|
const single = tail.length === 0 && !steps.some((st) => st.pivotKey !== undefined);
|
|
687
739
|
const ents = single ? `n${lastNode}` : `(select z0.* from n${lastNode} z0 where z0.id in (select h${lastNode}.id ${from}${whereOf(tail)}))`;
|
|
688
740
|
const leaf = (path) => leafOf(reg, rowsNode(lastNode), path);
|
|
741
|
+
if (mods.order?.startsWith('data.'))
|
|
742
|
+
checkField(rowsNode(lastNode), topField(mods.order), `sort('${mods.order}')`, false);
|
|
689
743
|
const sort = mods.order ? sortSpec(mods.order, leaf) : undefined;
|
|
690
744
|
const dir = mods.desc ? ' desc' : '';
|
|
691
745
|
/** Порядок: поле sort, id строки и разделители равных (id прочих узлов пути). */
|
|
@@ -700,7 +754,7 @@ export function buildRead(reg, schema, steps, mods, mode, agg, opts = {}) {
|
|
|
700
754
|
if (!mods.after)
|
|
701
755
|
return undefined;
|
|
702
756
|
if (!sort)
|
|
703
|
-
|
|
757
|
+
throw lerr('query.after_without_sort');
|
|
704
758
|
const c = cursorParams(mods.after, sort);
|
|
705
759
|
const t = ties(mods.after);
|
|
706
760
|
const lt = mods.desc ? '<' : '>';
|
|
@@ -724,28 +778,7 @@ export function buildRead(reg, schema, steps, mods, mode, agg, opts = {}) {
|
|
|
724
778
|
const others = keyed.filter((k) => k.node !== lastNode);
|
|
725
779
|
const pathKeys = [...others, ...keyed.filter((k) => k.node === lastNode)].map((k) => k.key);
|
|
726
780
|
const limitOffset = () => (mods.limit !== undefined ? ` limit ${p.push(mods.limit)}::bigint` : '') + (mods.offset !== undefined ? ` offset ${p.push(mods.offset)}::bigint` : '');
|
|
727
|
-
|
|
728
|
-
// id с номерами (lo, hi]. Надгробие переноса кончает отрезок, первая версия на новом id начинает: объект, заведённый
|
|
729
|
-
// на прежнем id после переноса, в историю не попадает (план 2.0, 7.1 №2). Тот же обход — history_segments в
|
|
730
|
-
// lib/sql/75-history.sql (diff и blame); здесь он в самом запросе, чтобы работал и на схеме до up() этой версии.
|
|
731
|
-
if (mode === 'versions' && mods.follow) {
|
|
732
|
-
// границы отрезка с версией r: строки переноса id — через парную строку по индексу log_moved (moved друг на друга),
|
|
733
|
-
// без прохода по версиям id
|
|
734
|
-
const bounds = (i, r) => `cross join lateral (select greatest(coalesce(max(x.rev) filter (where x.op = 'delete' and x.rev < ${r}), 0), `
|
|
735
|
-
+ `coalesce(max(x.rev - 1) filter (where x.op <> 'delete' and x.rev <= ${r}), 0)) as lo, `
|
|
736
|
-
+ `least(min(x.rev) filter (where x.op = 'delete' and x.rev >= ${r}), min(x.rev - 1) filter (where x.op <> 'delete' and x.rev > ${r})) as hi `
|
|
737
|
-
+ `from ${S}.log m join ${S}.log x on x.moved = m.id and x.id = ${i} where m.moved = ${i}) b`;
|
|
738
|
-
ctes.push(`f(id, lo, hi, n) as (select z.id, b.lo, b.hi, 0 from ${ents} z ${bounds('z.id', 'z.rev')} `
|
|
739
|
-
+ `union select g.id, b.lo, b.hi, g.n from f cross join lateral (`
|
|
740
|
-
+ `select a.moved as id, (select t.rev from ${S}.log t where t.id = a.moved and t.op = 'delete' and t.moved = f.id `
|
|
741
|
-
+ `order by abs(t.seq - a.seq) limit 1) as r, f.n - 1 as n from ${S}.log a `
|
|
742
|
-
+ `where f.n <= 0 and a.id = f.id and a.rev = f.lo + 1 and a.moved is not null and a.op <> 'delete' `
|
|
743
|
-
+ `union all select t.moved, (select a.rev from ${S}.log a where a.id = t.moved and a.op <> 'delete' and a.moved = f.id `
|
|
744
|
-
+ `order by abs(a.seq - t.seq) limit 1), f.n + 1 from ${S}.log t `
|
|
745
|
-
+ `where f.n >= 0 and t.id = f.id and t.rev = f.hi and t.op = 'delete' and t.moved is not null) g ${bounds('g.id', 'g.r')} `
|
|
746
|
-
+ `where g.r is not null and abs(g.n) <= 1000)`);
|
|
747
|
-
}
|
|
748
|
-
const withClause = `with ${mode === 'versions' && mods.follow ? 'recursive ' : ''}${ctes.join(',\n')}\n`;
|
|
781
|
+
const withClause = `with ${ctes.join(',\n')}\n`;
|
|
749
782
|
let text;
|
|
750
783
|
let aggLeaf;
|
|
751
784
|
switch (mode) {
|
|
@@ -772,37 +805,22 @@ export function buildRead(reg, schema, steps, mods, mode, agg, opts = {}) {
|
|
|
772
805
|
break;
|
|
773
806
|
case 'versions': {
|
|
774
807
|
// с sort — по полю и направлению (последние версии первыми — sort('at', 'desc'), с limit — в самом запросе);
|
|
775
|
-
// без sort
|
|
776
|
-
const order = sort ? `${sort.expr('v')}${dir}, v.id${dir}, v.rev${dir}` :
|
|
808
|
+
// без sort — по id и номеру версии
|
|
809
|
+
const order = sort ? `${sort.expr('v')}${dir}, v.id${dir}, v.rev${dir}` : 'v.id, v.rev';
|
|
777
810
|
// курсор — поле sort, id и номер версии: версии одного объекта с равным полем не теряются (план 2.0, 7.1 №4)
|
|
778
811
|
const cond = whereOf([after('v', (c) => {
|
|
779
812
|
const rev = cursorRev(c);
|
|
780
813
|
return rev === undefined ? [] : [{ col: 'v.rev', val: rev, cast: '::int' }];
|
|
781
814
|
})]);
|
|
782
|
-
text =
|
|
783
|
-
|
|
784
|
-
+ `cross join lateral (select ${colsOf('l')} from ${S}.log l where l.id = s.id and l.rev > s.lo and l.rev <= coalesce(s.hi, 2147483647)) l) v`
|
|
785
|
-
+ `${cond} order by ${order}${limitOffset()}`
|
|
786
|
-
: `${withClause}select to_jsonb(v) as row from (select ${colsOf('l')}, null::int as depth from ${S}.log l `
|
|
787
|
-
+ `where l.id in (select z.id from ${ents} z)) v${cond} order by ${order}${limitOffset()}`;
|
|
788
|
-
break;
|
|
789
|
-
}
|
|
790
|
-
case 'diff':
|
|
791
|
-
case 'blame': {
|
|
792
|
-
// план 1.1, §2.1: функции базы считают по журналу под RLS — по одному документу на объект шага
|
|
793
|
-
const h = mods.hist ?? {};
|
|
794
|
-
const num = (v) => (typeof v === 'number' ? `${p.push(v)}::int` : 'null');
|
|
795
|
-
const ts = (v) => (typeof v === 'string' ? `${p.push(v)}::text::timestamptz` : 'null');
|
|
796
|
-
const call = mode === 'diff'
|
|
797
|
-
? `${S}.diff_versions(z.id, ${num(h.from)}, ${num(h.to)}, ${ts(h.from)}, ${ts(h.to)}, ${h.follow ? 'true' : 'false'})`
|
|
798
|
-
: `${S}.blame(z.id, ${ts(h.at)}, ${num(h.limit)})`;
|
|
799
|
-
text = `${withClause}select ${call} as row from (select distinct z.id from ${ents} z) z order by z.id`;
|
|
815
|
+
text = `${withClause}select to_jsonb(v) as row from (select ${colsOf('l')}, null::int as depth from ${S}.log l `
|
|
816
|
+
+ `where l.id in (select z.id from ${ents} z)) v${cond} order by ${order}${limitOffset()}`;
|
|
800
817
|
break;
|
|
801
818
|
}
|
|
802
819
|
case 'agg': {
|
|
803
820
|
if (!agg || !agg.field.startsWith('data.'))
|
|
804
|
-
|
|
821
|
+
throw lerr('query.agg_field');
|
|
805
822
|
const path = agg.field.slice(5).split('.');
|
|
823
|
+
checkField(rowsNode(lastNode), path[0], `${agg.fn}('${agg.field}')`, false);
|
|
806
824
|
const acc = accessor(path)('z');
|
|
807
825
|
aggLeaf = leaf(path);
|
|
808
826
|
if (agg.fn === 'countBy') {
|
|
@@ -822,14 +840,12 @@ export function buildRead(reg, schema, steps, mods, mode, agg, opts = {}) {
|
|
|
822
840
|
}
|
|
823
841
|
/** Строка из to_jsonb(узла): служебные поля в $deleted / $depth, пустые необязательные — без ключа. */
|
|
824
842
|
export function toRow(raw) {
|
|
825
|
-
const { depth, agent, reason,
|
|
843
|
+
const { depth, agent, reason, ...rest } = raw;
|
|
826
844
|
const row = rest;
|
|
827
845
|
if (agent != null)
|
|
828
846
|
row.agent = agent;
|
|
829
847
|
if (reason != null)
|
|
830
848
|
row.reason = reason;
|
|
831
|
-
if (moved != null)
|
|
832
|
-
row.moved = moved;
|
|
833
849
|
if (row.op === 'delete')
|
|
834
850
|
row.$deleted = true;
|
|
835
851
|
if (depth != null)
|
package/dist/sync.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* мешающими строками отклоняет сама база (`tightening_conflict` с перечнем) уже в сухом прогоне.
|
|
7
7
|
* generateTypes: объявления TypeScript для классов из реестра — в том числе созданных данными.
|
|
8
8
|
*/
|
|
9
|
-
import type {
|
|
9
|
+
import type { DbCore } from './index.js';
|
|
10
10
|
import type { AnyModel, LostRule } from './model.js';
|
|
11
11
|
import type { JsonSchema } from './jsonschema.js';
|
|
12
12
|
import type { Registry } from './registry.js';
|
|
@@ -20,12 +20,18 @@ export interface SyncResult {
|
|
|
20
20
|
/** Правила refine, которых нет в базе. */
|
|
21
21
|
lost: LostRule[];
|
|
22
22
|
}
|
|
23
|
-
export declare function syncModels(db:
|
|
23
|
+
export declare function syncModels(db: DbCore, models: readonly AnyModel[], opts?: {
|
|
24
24
|
apply?: boolean;
|
|
25
25
|
}): Promise<SyncResult>;
|
|
26
26
|
/** Тип TypeScript по JSON Schema подмножества. */
|
|
27
27
|
export declare function schemaToTs(s: JsonSchema | undefined, indent?: string): string;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Объявления `<Класс>Data` и `<Класс>Links` для классов реестра и `<Класс>Model` — имя, алиас, концы с классами целей
|
|
30
|
+
* и предки (ModelLike, typed.ts), — а в конце список `Models`: строгие типы цепочек без моделей Zod, `connect<Models>()`
|
|
31
|
+
* (план 2.0, этап 4, п. 1). Классы арендатора System (системные и классы сидов пакета) — только с includeSystem
|
|
32
|
+
* (`letopis types --system`); без него системные классы даёт встроенный набор SystemModels. Пометка в первой строке —
|
|
33
|
+
* запись каталога model.types_header.
|
|
34
|
+
*/
|
|
29
35
|
export declare function generateTypes(registry: Registry, opts?: {
|
|
30
36
|
includeSystem?: boolean;
|
|
31
37
|
}): string;
|
package/dist/sync.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lerr, lmsg } from './errors.js';
|
|
2
|
+
import { internalsOf } from './internal.js';
|
|
3
|
+
import { RESERVED_CLASS_NAMES } from './types.js';
|
|
2
4
|
const ROLLBACK = Symbol('sync-dry-run');
|
|
3
5
|
/** Канонический JSON: ключи по алфавиту — сравнение описаний без учёта порядка ключей. */
|
|
4
6
|
function canon(v) {
|
|
@@ -9,11 +11,17 @@ function canon(v) {
|
|
|
9
11
|
return JSON.stringify(v);
|
|
10
12
|
}
|
|
11
13
|
export async function syncModels(db, models, opts = {}) {
|
|
14
|
+
// хаб с именем метода цепочки или корня db (модель собрана мимо hub()) — отказ до записи (план 2.0, этап 4, п. 2;
|
|
15
|
+
// 7.10 №3); связь с таким именем отклоняет база
|
|
16
|
+
for (const m of models) {
|
|
17
|
+
if (m.kind === 'hub' && RESERVED_CLASS_NAMES.includes(m.name))
|
|
18
|
+
throw lerr('model.sync_hub_name_reserved', { name: m.name });
|
|
19
|
+
}
|
|
12
20
|
const res = { applied: Boolean(opts.apply), created: [], updated: [], unchanged: [], skipped: [], lost: models.flatMap((m) => m.lostRules()) };
|
|
13
21
|
const S = `"${db.schema.replace(/"/g, '""')}"`;
|
|
14
|
-
// предок раньше
|
|
15
|
-
//
|
|
16
|
-
//
|
|
22
|
+
// предок раньше потомка: потомка класса, которого ещё нет, база отклоняет. Круг родителей (модель, собранная мимо
|
|
23
|
+
// hub()) так не упорядочить — отказ до записи с самим кругом (хвост ревью доработок после 1.2.0: раньше обход молча
|
|
24
|
+
// ставил потомка раньше предка)
|
|
17
25
|
const ordered = [];
|
|
18
26
|
const path = [];
|
|
19
27
|
const visit = (m) => {
|
|
@@ -21,23 +29,21 @@ export async function syncModels(db, models, opts = {}) {
|
|
|
21
29
|
return;
|
|
22
30
|
const at = path.indexOf(m);
|
|
23
31
|
if (at >= 0) {
|
|
24
|
-
throw
|
|
25
|
-
+ 'родителя и источник агрегата база ждёт раньше класса');
|
|
32
|
+
throw lerr('model.models_cycle', { cycle: [...path.slice(at), m].map((x) => x.name).join(' → ') });
|
|
26
33
|
}
|
|
27
34
|
path.push(m);
|
|
28
35
|
if (m.parent && models.includes(m.parent))
|
|
29
36
|
visit(m.parent);
|
|
30
|
-
for (const a of Object.values(m.describe().aggregate ?? {})) {
|
|
31
|
-
const src = models.find((x) => x.name === a.of);
|
|
32
|
-
if (src)
|
|
33
|
-
visit(src);
|
|
34
|
-
}
|
|
35
37
|
path.pop();
|
|
36
38
|
ordered.push(m);
|
|
37
39
|
};
|
|
38
40
|
models.forEach(visit);
|
|
41
|
+
// у корня connect({ admin }) классы пишет администраторский пул с той же сессией (план 2.0, 7.9 №3): правка класса
|
|
42
|
+
// со строками требует прав, которых у роли приложения может не быть
|
|
43
|
+
const adminTx = internalsOf(db)?.adminTx;
|
|
44
|
+
const run = (fn) => (adminTx ? adminTx(fn) : db.transaction(fn));
|
|
39
45
|
try {
|
|
40
|
-
await
|
|
46
|
+
await run(async (tx) => {
|
|
41
47
|
for (const m of ordered) {
|
|
42
48
|
const desc = m.describe();
|
|
43
49
|
const [cur] = await tx.unsafe(`select tenant = ${S}.ctx_tenant() as own, data - 'resolved' as data from ${S}.entity
|
|
@@ -108,23 +114,40 @@ export function schemaToTs(s, indent = '') {
|
|
|
108
114
|
}
|
|
109
115
|
}).join(' | ');
|
|
110
116
|
}
|
|
111
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* Объявления `<Класс>Data` и `<Класс>Links` для классов реестра и `<Класс>Model` — имя, алиас, концы с классами целей
|
|
119
|
+
* и предки (ModelLike, typed.ts), — а в конце список `Models`: строгие типы цепочек без моделей Zod, `connect<Models>()`
|
|
120
|
+
* (план 2.0, этап 4, п. 1). Классы арендатора System (системные и классы сидов пакета) — только с includeSystem
|
|
121
|
+
* (`letopis types --system`); без него системные классы даёт встроенный набор SystemModels. Пометка в первой строке —
|
|
122
|
+
* запись каталога model.types_header.
|
|
123
|
+
*/
|
|
112
124
|
export function generateTypes(registry, opts = {}) {
|
|
113
|
-
const out = [
|
|
125
|
+
const out = [`// ${lmsg('model.types_header')}`, ''];
|
|
126
|
+
const models = [];
|
|
127
|
+
const lit = (v) => JSON.stringify(v);
|
|
114
128
|
for (const c of registry.all().sort((a, b) => a.name.localeCompare(b.name))) {
|
|
115
129
|
if (!opts.includeSystem && c.tenant === '2eba6d0a-1edd-4bb6-a85a-a703dab49035')
|
|
116
130
|
continue;
|
|
117
131
|
// идентификатор TypeScript — буквы Unicode, цифры, _ и $: кириллические имена классов допустимы (план 2.0, 7.5 №1)
|
|
118
132
|
if (!/^[\p{L}_$][\p{L}\p{N}_$]*$/u.test(c.name))
|
|
119
|
-
throw
|
|
133
|
+
throw lerr('model.types_class_name', { name: c.name });
|
|
120
134
|
// additionalProperties: false верхнего уровня в тип не переносится — лишние ключи и так ошибка типов
|
|
121
135
|
const { additionalProperties: _closed, ...open } = c.schema;
|
|
122
136
|
void _closed;
|
|
123
137
|
// объектный литерал — интерфейс; класс без полей, union и открытая схема — псевдоним типа (иначе неверный TypeScript)
|
|
124
138
|
const data = schemaToTs(open);
|
|
125
139
|
out.push(/^\{[\s\S]*\}$/.test(data) && !/\}\s*&/.test(data) ? `export interface ${c.name}Data ${data}` : `export type ${c.name}Data = ${data};`);
|
|
126
|
-
const
|
|
127
|
-
|
|
140
|
+
const required = (e) => e.required === true || (e.min ?? 0) > 0;
|
|
141
|
+
const ends = Object.entries(c.ends).map(([role, e]) => ` ${lit(role)}${required(e) ? '' : '?'}: ${e.many ? 'string[]' : 'string'};`);
|
|
142
|
+
out.push(`export interface ${c.name}Links {${ends.length ? `\n${ends.join('\n')}\n` : ''}}`);
|
|
143
|
+
// концы с классами целей и предки — переходы, слоты и семейства строгих типов (typed.ts: ModelLike)
|
|
144
|
+
const specs = Object.entries(c.ends).map(([role, e]) => {
|
|
145
|
+
const targets = (Array.isArray(e.class) ? e.class : [e.class]).map(lit).join(' | ');
|
|
146
|
+
return ` ${lit(role)}: { targets: ${targets}; many: ${e.many === true}; required: ${required(e)} };`;
|
|
147
|
+
});
|
|
148
|
+
out.push(`export interface ${c.name}Model {`, ` readonly name: ${lit(c.name)};`, ...(c.alias ? [` readonly alias: ${lit(c.alias)};`] : []), ' readonly __types?: {', ` data: ${c.name}Data;`, ` links: ${c.name}Links;`, ` ends: {${specs.length ? `\n${specs.join('\n')}\n ` : ''}};`, ` ancestors: ${(c.ancestors.length ? c.ancestors : [c.name]).map(lit).join(' | ')};`, ' };', '}', '');
|
|
149
|
+
models.push(`${c.name}Model`);
|
|
128
150
|
}
|
|
151
|
+
out.push(`export type Models = readonly [${models.join(', ')}];`, '');
|
|
129
152
|
return out.join('\n');
|
|
130
153
|
}
|
package/dist/tx.d.ts
CHANGED
|
@@ -93,6 +93,16 @@ export interface TxContext {
|
|
|
93
93
|
passes?: AclPasses;
|
|
94
94
|
/** После успешной операции (продление сессии раз в минуту). */
|
|
95
95
|
after?: () => void;
|
|
96
|
+
/**
|
|
97
|
+
* Сессия кончилась (no_session): выдать новую и вписать её токен в failed — у подключения по ключу APIKEY ключ
|
|
98
|
+
* обменивается заново. true — операцию можно повторить (один раз). Нет — no_session как есть.
|
|
99
|
+
*/
|
|
100
|
+
renew?: (failed: TxContext) => Promise<boolean>;
|
|
101
|
+
/**
|
|
102
|
+
* Перед операцией (runTx, ExplicitTx.open): после простоя подключение проверяет, жива ли сессия, — иначе чтение под
|
|
103
|
+
* истёкшей сессией шло бы как анонимное и молча отдавало пустой ответ. Обновляет токен ctx, если сессию выдали заново.
|
|
104
|
+
*/
|
|
105
|
+
before?: (ctx: TxContext) => Promise<void>;
|
|
96
106
|
/**
|
|
97
107
|
* После фиксации записи: нагрузки из letopis.changed (сигналы записи, session:<тег>) — кэш
|
|
98
108
|
* результатов сбрасывает свои ответы сразу (§2.12); raw — запись через db.sql.
|
|
@@ -136,7 +146,7 @@ export declare class ExplicitTx implements Executor {
|
|
|
136
146
|
/** В транзакции был сырой SQL (db.sql): после фиксации — весь кэш. */
|
|
137
147
|
private raw;
|
|
138
148
|
private constructor();
|
|
139
|
-
static open(ctx: TxContext): Promise<ExplicitTx>;
|
|
149
|
+
static open(ctx: TxContext, renewed?: boolean): Promise<ExplicitTx>;
|
|
140
150
|
/** Транзакция ещё открыта (не зафиксирована и не откачена). */
|
|
141
151
|
get active(): boolean;
|
|
142
152
|
run<T>(fn: (tx: Tx) => Promise<T>, opts?: {
|