letopis 0.13.0 → 0.16.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/CHANGELOG.md +98 -0
- package/README.md +362 -199
- package/dist/chain.d.ts +44 -16
- package/dist/chain.js +197 -26
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/schema.js +96 -4
- package/dist/sql.d.ts +23 -3
- package/dist/sql.js +71 -24
- package/dist/types.d.ts +40 -4
- package/dist/uuid.d.ts +6 -0
- package/dist/uuid.js +32 -0
- package/dist/write.d.ts +12 -5
- package/dist/write.js +213 -95
- package/package.json +1 -1
- package/sql/ddl.sql +53 -11
- package/sql/seed.booking.sql +21 -16
package/dist/sql.js
CHANGED
|
@@ -55,6 +55,9 @@ export async function runQuery(ctx, text, params, mode, classes) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
let nodeSeq = 0;
|
|
59
|
+
/** Уникальная метка узла пути (для pivot-возвратов; переживает нарезку плана). */
|
|
60
|
+
export const nextNodeKey = () => ++nodeSeq;
|
|
58
61
|
/** Накопитель позиционных параметров. */
|
|
59
62
|
class Params {
|
|
60
63
|
list = [];
|
|
@@ -305,6 +308,10 @@ function compileFilter(cls, filter, p) {
|
|
|
305
308
|
}
|
|
306
309
|
return out;
|
|
307
310
|
}
|
|
311
|
+
/** Класс target входит в какой-нибудь конец def (союзы учитываются). */
|
|
312
|
+
export function linksTo(def, target) {
|
|
313
|
+
return def.links.some((end) => end.classes.includes(target));
|
|
314
|
+
}
|
|
308
315
|
/** Правило обхода между шагами (по реестру Schema). */
|
|
309
316
|
export function resolveHop(prev, next) {
|
|
310
317
|
if (prev.category === 'HUB' && next.category === 'LINK')
|
|
@@ -315,9 +322,9 @@ export function resolveHop(prev, next) {
|
|
|
315
322
|
// self-переход (Папка→Папка) = reverse: дети; родитель и так лежит в row.links
|
|
316
323
|
if (prev.id === next.id)
|
|
317
324
|
return 'reverse';
|
|
318
|
-
if (prev
|
|
325
|
+
if (linksTo(prev, next.id))
|
|
319
326
|
return 'forward';
|
|
320
|
-
if (next
|
|
327
|
+
if (linksTo(next, prev.id))
|
|
321
328
|
return 'reverse';
|
|
322
329
|
throw new Error(`letopis: no path ${prev.id} → ${next.id}: neither embeds the other (Schema.links)`);
|
|
323
330
|
}
|
|
@@ -338,25 +345,60 @@ function orderExpr(alias, cls, mods) {
|
|
|
338
345
|
return '';
|
|
339
346
|
return ` ORDER BY ${sortField(alias, cls, mods.order)}${mods.desc ? ' DESC' : ''}`;
|
|
340
347
|
}
|
|
341
|
-
/** Keyset-пагинация: (поле сортировки, id) строго после курсора. Требует .sort(). */
|
|
342
|
-
function
|
|
348
|
+
/** Keyset-пагинация: (поле сортировки, id) строго после курсора. Требует .sort(). Выражение без WHERE. */
|
|
349
|
+
function afterExpr(alias, cls, mods, p) {
|
|
343
350
|
if (!mods.after)
|
|
344
|
-
return
|
|
351
|
+
return undefined;
|
|
345
352
|
if (!mods.order)
|
|
346
353
|
throw new Error('letopis: .after(cursor) requires .sort(field)');
|
|
347
354
|
const expr = sortField(alias, cls, mods.order);
|
|
348
355
|
const cast = mods.order === 'updated' ? '::timestamptz' : castOf(leafType(cls, mods.order.slice(5).split('.')));
|
|
349
356
|
const cmp = mods.desc ? '<' : '>';
|
|
350
|
-
return `
|
|
357
|
+
return `(${expr}, ${alias}.id) ${cmp} (${p.push(mods.after.v)}${cast}, ${p.push(mods.after.id)})`;
|
|
351
358
|
}
|
|
359
|
+
const whereOf = (conds) => {
|
|
360
|
+
const list = conds.filter((c) => !!c);
|
|
361
|
+
return list.length ? ` WHERE ${list.join(' AND ')}` : '';
|
|
362
|
+
};
|
|
352
363
|
/** Построить читающий запрос по цепочке. */
|
|
353
364
|
export function buildRead(ctx, steps, mods, mode) {
|
|
354
365
|
const p = new Params();
|
|
355
366
|
const ent = entityTable(ctx.pgSchema);
|
|
356
367
|
const pPart = p.push(ctx.partition);
|
|
368
|
+
// pivot: карта эффективных блоков. real[i] — индекс блока-узла шага i
|
|
369
|
+
// (pivot-шаг блока не строит, ссылается на узел по pivotKey).
|
|
370
|
+
const real = [];
|
|
371
|
+
const byNodeKey = new Map();
|
|
372
|
+
const tailConds = []; // дофильтры pivot-шагов (alias подставлен) — в хвостовой WHERE
|
|
373
|
+
for (let i = 0; i < steps.length; i++) {
|
|
374
|
+
const s = steps[i];
|
|
375
|
+
if (s.pivotKey !== undefined) {
|
|
376
|
+
const at = byNodeKey.get(s.pivotKey);
|
|
377
|
+
if (at === undefined)
|
|
378
|
+
throw new Error(`letopis: pivot "${s.name}" — node is not in this path`);
|
|
379
|
+
real[i] = at;
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
real[i] = i;
|
|
383
|
+
if (s.nodeKey !== undefined)
|
|
384
|
+
byNodeKey.set(s.nodeKey, i);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
357
387
|
const blocks = [];
|
|
358
388
|
for (let i = 0; i < steps.length; i++) {
|
|
359
389
|
const step = steps[i];
|
|
390
|
+
// pivot-шаг: узел уже есть — только дофильтры (AND на алиас узла)
|
|
391
|
+
if (step.pivotKey !== undefined) {
|
|
392
|
+
const a = `h${real[i]}`;
|
|
393
|
+
const f = compileFilter(step.cls, step.filter, p);
|
|
394
|
+
for (const c of f.idConds)
|
|
395
|
+
tailConds.push(c(a));
|
|
396
|
+
for (const fr of f.finalFrags)
|
|
397
|
+
tailConds.push(fr(a));
|
|
398
|
+
if (step.tagsFilter !== undefined)
|
|
399
|
+
tailConds.push(compileTags(step.tagsFilter, p)(a));
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
360
402
|
const pCls = p.push(step.cls.id);
|
|
361
403
|
const f = compileFilter(step.cls, step.filter, p);
|
|
362
404
|
// модификаторы шага: .tags() / .account() / .owner()
|
|
@@ -420,16 +462,17 @@ export function buildRead(ctx, steps, mods, mode) {
|
|
|
420
462
|
const isDeep = step.deepMax !== undefined;
|
|
421
463
|
if (i > 0 && !isDeep) { // deep строит containment сам (recursive CTE)
|
|
422
464
|
const prev = steps[i - 1];
|
|
465
|
+
const prevAlias = `h${real[i - 1]}`; // pivot: ветвление от узла-возврата
|
|
423
466
|
const hop = resolveHop(prev.cls, step.cls);
|
|
424
467
|
if (hop === 'forward') {
|
|
425
468
|
// id неизменен — условие сразу во внутренний WHERE, перепроверка не нужна
|
|
426
|
-
innerWhere.push(`e.id =
|
|
469
|
+
innerWhere.push(`e.id = ${prevAlias}.links->>${p.push(step.cls.id)}`);
|
|
427
470
|
}
|
|
428
471
|
else {
|
|
429
472
|
// reverse containment: кандидаты по GIN + перепроверка на latest
|
|
430
473
|
const pKey = p.push(prev.cls.id);
|
|
431
|
-
candWhere.push(`c.links @> jsonb_build_object(${pKey}::text,
|
|
432
|
-
outerWhere.push(`t.links @> jsonb_build_object(${pKey}::text,
|
|
474
|
+
candWhere.push(`c.links @> jsonb_build_object(${pKey}::text, ${prevAlias}.id)`);
|
|
475
|
+
outerWhere.push(`t.links @> jsonb_build_object(${pKey}::text, ${prevAlias}.id)`);
|
|
433
476
|
}
|
|
434
477
|
}
|
|
435
478
|
if (candWhere.length) {
|
|
@@ -456,7 +499,7 @@ export function buildRead(ctx, steps, mods, mode) {
|
|
|
456
499
|
` WHERE t.deleted IS NULL AND t.links @> jsonb_build_object(${p.push(step.cls.id)}::text, ${ref}))`;
|
|
457
500
|
block =
|
|
458
501
|
`(WITH RECURSIVE d AS (` +
|
|
459
|
-
`SELECT k.*, 1 AS depth FROM ${kids(`h${i - 1}.id`)} k` +
|
|
502
|
+
`SELECT k.*, 1 AS depth FROM ${kids(`h${real[i - 1]}.id`)} k` +
|
|
460
503
|
` UNION ALL ` +
|
|
461
504
|
`SELECT k.*, d.depth + 1 FROM d JOIN LATERAL ${kids('d.id')} k ON true WHERE d.depth < ${pMax}::int` +
|
|
462
505
|
`) SELECT * FROM d) h${i}`;
|
|
@@ -464,45 +507,49 @@ export function buildRead(ctx, steps, mods, mode) {
|
|
|
464
507
|
blocks.push(i === 0 ? `FROM ${block}` : `JOIN LATERAL ${block} ON true`);
|
|
465
508
|
}
|
|
466
509
|
const fromClause = blocks.join('\n');
|
|
467
|
-
const last = steps.length - 1;
|
|
468
|
-
const lastCls = steps[
|
|
469
|
-
// ключи шагов в путях: alias → имя вызова; дубликаты
|
|
470
|
-
const
|
|
510
|
+
const last = real[steps.length - 1]; // pivot в конце → терминал на его узле
|
|
511
|
+
const lastCls = steps[steps.length - 1].cls;
|
|
512
|
+
// ключи шагов в путях: alias → имя вызова; pivot узла не добавляет; дубликаты — _2, _3…
|
|
513
|
+
const keyed = [];
|
|
471
514
|
const seen = new Map();
|
|
472
|
-
for (
|
|
515
|
+
for (let i = 0; i < steps.length; i++) {
|
|
516
|
+
const s = steps[i];
|
|
517
|
+
if (s.pivotKey !== undefined)
|
|
518
|
+
continue;
|
|
473
519
|
const base = s.aliasKey ?? s.name;
|
|
474
520
|
const n = (seen.get(base) ?? 0) + 1;
|
|
475
521
|
seen.set(base, n);
|
|
476
|
-
|
|
522
|
+
keyed.push({ key: n === 1 ? base : `${base}_${n}`, block: i });
|
|
477
523
|
}
|
|
524
|
+
const keys = keyed.map((k) => k.key);
|
|
478
525
|
const limitOffset = (mods.limit !== undefined ? ` LIMIT ${p.push(mods.limit)}` : '') +
|
|
479
526
|
(mods.offset !== undefined ? ` OFFSET ${p.push(mods.offset)}` : '');
|
|
480
527
|
let text;
|
|
481
528
|
switch (mode) {
|
|
482
529
|
case 'paths': {
|
|
483
|
-
const obj =
|
|
530
|
+
const obj = keyed.map(({ key, block }) => `${p.push(key)}::text, to_jsonb(h${block})`).join(', ');
|
|
484
531
|
text =
|
|
485
532
|
`SELECT jsonb_build_object(${obj}) AS path\n${fromClause}` +
|
|
486
|
-
|
|
533
|
+
whereOf([...tailConds, afterExpr(`h${last}`, lastCls, mods, p)]) +
|
|
487
534
|
orderExpr(`h${last}`, lastCls, mods) +
|
|
488
535
|
limitOffset;
|
|
489
536
|
break;
|
|
490
537
|
}
|
|
491
538
|
case 'rows':
|
|
492
539
|
case 'ids': {
|
|
493
|
-
const inner = `SELECT DISTINCT ON (h${last}.id) h${last}.* \n${fromClause}\n` +
|
|
540
|
+
const inner = `SELECT DISTINCT ON (h${last}.id) h${last}.* \n${fromClause}${whereOf(tailConds)}\n` +
|
|
494
541
|
`ORDER BY h${last}.id, h${last}.updated DESC`;
|
|
495
542
|
const sel = mode === 'rows' ? 'to_jsonb(z) AS row' : 'z.id';
|
|
496
543
|
text =
|
|
497
544
|
`SELECT ${sel} FROM (${inner}) z` +
|
|
498
|
-
|
|
545
|
+
whereOf([afterExpr('z', lastCls, mods, p)]) +
|
|
499
546
|
orderExpr('z', lastCls, mods) +
|
|
500
547
|
limitOffset;
|
|
501
548
|
break;
|
|
502
549
|
}
|
|
503
550
|
case 'versions': {
|
|
504
551
|
// ВСЕ версии (включая tombstone) сущностей последнего шага, по возрастанию updated
|
|
505
|
-
const inner = `SELECT DISTINCT h${last}.id \n${fromClause}`;
|
|
552
|
+
const inner = `SELECT DISTINCT h${last}.id \n${fromClause}${whereOf(tailConds)}`;
|
|
506
553
|
text =
|
|
507
554
|
`SELECT to_jsonb(v) AS row FROM (${inner}) z ` +
|
|
508
555
|
`JOIN ${ent} v ON v.partition = ${pPart} AND v.class = ${p.push(lastCls.id)} AND v.id = z.id ` +
|
|
@@ -511,7 +558,7 @@ export function buildRead(ctx, steps, mods, mode) {
|
|
|
511
558
|
break;
|
|
512
559
|
}
|
|
513
560
|
case 'count':
|
|
514
|
-
text = `SELECT count(*)::int AS n\n${fromClause}`;
|
|
561
|
+
text = `SELECT count(*)::int AS n\n${fromClause}${whereOf(tailConds)}`;
|
|
515
562
|
break;
|
|
516
563
|
case 'agg': {
|
|
517
564
|
const { aggFn, aggField } = mods;
|
|
@@ -521,12 +568,12 @@ export function buildRead(ctx, steps, mods, mode) {
|
|
|
521
568
|
const path = aggField.slice(5).split('.');
|
|
522
569
|
const acc = accessor(path)(`h${last}`); // data-путь любой глубины
|
|
523
570
|
if (aggFn === 'countBy') {
|
|
524
|
-
text = `SELECT ${acc} AS k, count(*)::int AS n\n${fromClause}\nGROUP BY 1 ORDER BY 2 DESC`;
|
|
571
|
+
text = `SELECT ${acc} AS k, count(*)::int AS n\n${fromClause}${whereOf(tailConds)}\nGROUP BY 1 ORDER BY 2 DESC`;
|
|
525
572
|
}
|
|
526
573
|
else {
|
|
527
574
|
// sum/avg — всегда numeric; min/max — по типу листа
|
|
528
575
|
const cast = aggFn === 'sum' || aggFn === 'avg' ? '::numeric' : castOf(leafType(lastCls, path));
|
|
529
|
-
text = `SELECT ${aggFn}((${acc})${cast}) AS v\n${fromClause}`;
|
|
576
|
+
text = `SELECT ${aggFn}((${acc})${cast}) AS v\n${fromClause}${whereOf(tailConds)}`;
|
|
530
577
|
}
|
|
531
578
|
break;
|
|
532
579
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -18,6 +18,32 @@ export interface Row {
|
|
|
18
18
|
}
|
|
19
19
|
/** Результат run(): вариант пути — узел на каждый шаг цепочки. */
|
|
20
20
|
export type Path = Record<string, Row>;
|
|
21
|
+
/**
|
|
22
|
+
* Конец связи класса (Schema.links v2). В БД элемент text[]: JSON-объект
|
|
23
|
+
* `{"class":"Org","cardinality":1}` / `{"classes":["Service","Complex"],"cardinality":1}`
|
|
24
|
+
* (союз ролей); legacy-строка 'Org' — сахар для {classes:['Org']} (старые схемы).
|
|
25
|
+
*/
|
|
26
|
+
export interface LinkEnd {
|
|
27
|
+
/** Допустимые классы конца; ровно один из них присутствует в Entity.links. */
|
|
28
|
+
classes: string[];
|
|
29
|
+
/** Конец может отсутствовать. Default false. */
|
|
30
|
+
optional?: boolean;
|
|
31
|
+
/** ЗАРЕЗЕРВИРОВАНО (не имплементировано): 0 — безлимит, N — точное число. Default 1. */
|
|
32
|
+
cardinality?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Генерация id класса — из attributes.id:
|
|
36
|
+
* "uuid" | {type:'uuid'} → v4 (random, дефолт);
|
|
37
|
+
* {type:'uuid', generate: 7} → v7 (время в старших битах);
|
|
38
|
+
* {type:'uuid', generate: 5, from: ['Slot','Staff']} → v5: детерминированный id из значений
|
|
39
|
+
* from — имена ОБЯЗАТЕЛЬНЫХ концов Schema.links (класс или полное имя союза
|
|
40
|
+
* 'Service|Complex') и/или скалярных полей data.
|
|
41
|
+
*/
|
|
42
|
+
export interface IdGen {
|
|
43
|
+
version: 4 | 5 | 7;
|
|
44
|
+
/** Только v5: источники имени (порядок значим). */
|
|
45
|
+
from?: string[];
|
|
46
|
+
}
|
|
21
47
|
/** Класс из таблицы Schema. */
|
|
22
48
|
export interface ClassDef {
|
|
23
49
|
id: string;
|
|
@@ -27,11 +53,20 @@ export interface ClassDef {
|
|
|
27
53
|
ancestors: string[];
|
|
28
54
|
/** Все потомки (транзитивно) — считает триггер schema_lineage. */
|
|
29
55
|
descendants: string[];
|
|
56
|
+
/** Поля data с НАСЛЕДОВАНИЕМ по ancestor-цепочке (потомок поверх предка). */
|
|
30
57
|
attributes: Record<string, unknown>;
|
|
31
|
-
links:
|
|
58
|
+
links: LinkEnd[];
|
|
59
|
+
/**
|
|
60
|
+
* true — хотя бы один конец объявлен объектом (схема v2): строгая валидация
|
|
61
|
+
* (жадный матчинг по порядку, союзы, optional, лишние связи — ошибка).
|
|
62
|
+
* false — все концы legacy-строками: старое поведение ('Entity' = полиморф, лишние молчат).
|
|
63
|
+
*/
|
|
64
|
+
strictEnds: boolean;
|
|
32
65
|
meta: Record<string, unknown>;
|
|
33
66
|
abstract: boolean;
|
|
34
67
|
order: number;
|
|
68
|
+
/** Как генерить id новой сущности (attributes.id). */
|
|
69
|
+
idGen: IdGen;
|
|
35
70
|
/** Скомпилированный fastest-validator: true | ошибки. */
|
|
36
71
|
check: (data: Record<string, unknown>) => true | {
|
|
37
72
|
field: string;
|
|
@@ -85,12 +120,13 @@ export interface Cursor {
|
|
|
85
120
|
id: string;
|
|
86
121
|
}
|
|
87
122
|
/**
|
|
88
|
-
* Операция записи, привязанная к шагу цепочки:
|
|
123
|
+
* Операция записи, привязанная к шагу цепочки:
|
|
124
|
+
* .create() / .update() / .delete() / .anonymize().
|
|
89
125
|
* Терминал исполняет план (все операции + финальное чтение) одной транзакцией.
|
|
90
126
|
*/
|
|
91
127
|
export interface PlanOp {
|
|
92
|
-
kind: '
|
|
93
|
-
/**
|
|
128
|
+
kind: 'create' | 'update' | 'delete' | 'anonymize';
|
|
129
|
+
/** create/update: данные новой версии (deep-merge листьев). */
|
|
94
130
|
data?: Record<string, unknown>;
|
|
95
131
|
/** anonymize: string-поля под '[erased]'. */
|
|
96
132
|
fields?: string[];
|
package/dist/uuid.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Namespace letopis для uuidv5 (фиксированная константа — стабильность id между релизами). */
|
|
2
|
+
export declare const LETOPIS_NS = "c7a2f9d4-3b61-4e8a-9f05-8d2c1e6b7a90";
|
|
3
|
+
/** RFC 4122 v5: sha1(namespace + name); детерминирован — одно имя → один uuid. */
|
|
4
|
+
export declare function uuidv5(name: string, ns?: string): string;
|
|
5
|
+
/** RFC 9562 v7: 48 бит unix-ms + random — время в старших битах, вставки ложатся в хвост индекса. */
|
|
6
|
+
export declare function uuidv7(): string;
|
package/dist/uuid.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Генерация id по Schema (attributes.id):
|
|
3
|
+
* "uuid" → v4 (random) — дефолт;
|
|
4
|
+
* { type: 'uuid', generate: 7 } → v7 (unix-время в старших битах: btree-локальность);
|
|
5
|
+
* { type: 'uuid', generate: 5, from: […] } → v5: sha1 от «схема:партиция:класс:значения from» —
|
|
6
|
+
* детерминированный id из концов/полей: та же комбинация → тот же id (идемпотентный create,
|
|
7
|
+
* дубль невозможен даже в гонке — оба запроса вычислят один id, второй станет версией).
|
|
8
|
+
*/
|
|
9
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
10
|
+
/** Namespace letopis для uuidv5 (фиксированная константа — стабильность id между релизами). */
|
|
11
|
+
export const LETOPIS_NS = 'c7a2f9d4-3b61-4e8a-9f05-8d2c1e6b7a90';
|
|
12
|
+
const fmt = (b) => {
|
|
13
|
+
const h = b.toString('hex');
|
|
14
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
15
|
+
};
|
|
16
|
+
/** RFC 4122 v5: sha1(namespace + name); детерминирован — одно имя → один uuid. */
|
|
17
|
+
export function uuidv5(name, ns = LETOPIS_NS) {
|
|
18
|
+
const nsBytes = Buffer.from(ns.replace(/-/g, ''), 'hex');
|
|
19
|
+
const h = createHash('sha1').update(nsBytes).update(name, 'utf8').digest().subarray(0, 16);
|
|
20
|
+
h[6] = (h[6] & 0x0f) | 0x50; // version 5
|
|
21
|
+
h[8] = (h[8] & 0x3f) | 0x80; // variant 10xx
|
|
22
|
+
return fmt(h);
|
|
23
|
+
}
|
|
24
|
+
/** RFC 9562 v7: 48 бит unix-ms + random — время в старших битах, вставки ложатся в хвост индекса. */
|
|
25
|
+
export function uuidv7() {
|
|
26
|
+
const b = Buffer.alloc(16);
|
|
27
|
+
b.writeUIntBE(Date.now(), 0, 6);
|
|
28
|
+
randomBytes(10).copy(b, 6);
|
|
29
|
+
b[6] = (b[6] & 0x0f) | 0x70; // version 7
|
|
30
|
+
b[8] = (b[8] & 0x3f) | 0x80; // variant 10xx
|
|
31
|
+
return fmt(b);
|
|
32
|
+
}
|
package/dist/write.d.ts
CHANGED
|
@@ -31,12 +31,19 @@ export declare function readRows(ctx: Ctx, steps: Step[], mods?: ChainMods): Pro
|
|
|
31
31
|
*/
|
|
32
32
|
export declare function deepMerge(base: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
|
|
33
33
|
/**
|
|
34
|
-
* .
|
|
35
|
-
* -
|
|
36
|
-
* - id (
|
|
37
|
-
*
|
|
34
|
+
* .create(data) — «чтобы сущность существовала»:
|
|
35
|
+
* - id не задан → INSERT (id по IdGen класса; links = концы из пути + слоты);
|
|
36
|
+
* - id известен (Класс(id) / data.id / вычислен v5): есть → новая версия (deep-merge),
|
|
37
|
+
* нет → INSERT с этим id (идемпотентный create, REST-PUT семантика);
|
|
38
|
+
* - фильтр-объект / pivot — ошибка: create не ищет, это update().
|
|
38
39
|
*/
|
|
39
|
-
export declare function
|
|
40
|
+
export declare function createOp(ctx: Ctx, steps: Step[], mods: ChainMods, data: Record<string, unknown>): Promise<Row[]>;
|
|
41
|
+
/**
|
|
42
|
+
* .update(data?) — новая версия КАЖДОГО найденного путём (deep-merge листьев);
|
|
43
|
+
* цели: Класс() ≡ Класс({}) — все в границах контекста, id/фильтр/pivot — как в чтении.
|
|
44
|
+
* Не найдено → [] — update НИКОГДА не создаёт.
|
|
45
|
+
*/
|
|
46
|
+
export declare function updateOp(ctx: Ctx, steps: Step[], mods: ChainMods, data: Record<string, unknown>): Promise<Row[]>;
|
|
40
47
|
/**
|
|
41
48
|
* .anonymize(fields): GDPR-затирание — новая версия с '[erased]' в указанных string-полях
|
|
42
49
|
* + тег 'anonymized'. Только string-поля (по Schema); история сохраняется (см. README).
|