letopis 1.0.1 → 1.2.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 +117 -10
- package/AGENT-CHEATSHEET.md +110 -8
- package/CHANGELOG.md +739 -0
- package/README.en.md +873 -68
- package/README.md +830 -66
- package/dist/admin.d.ts +24 -4
- package/dist/admin.js +39 -9
- package/dist/cache.d.ts +33 -9
- package/dist/cache.js +81 -32
- package/dist/chain.d.ts +29 -7
- package/dist/chain.js +61 -12
- package/dist/cli.js +59 -7
- package/dist/errors.js +4 -2
- package/dist/import.d.ts +6 -0
- package/dist/import.js +196 -175
- package/dist/index.d.ts +27 -5
- package/dist/index.js +191 -29
- package/dist/internal.d.ts +16 -0
- package/dist/internal.js +14 -0
- package/dist/load.d.ts +63 -0
- package/dist/load.js +331 -44
- package/dist/mcp.d.ts +28 -0
- package/dist/mcp.js +810 -0
- package/dist/model.d.ts +34 -0
- package/dist/model.js +17 -0
- package/dist/project.d.ts +309 -0
- package/dist/project.js +1097 -0
- package/dist/registry.d.ts +17 -0
- package/dist/registry.js +13 -0
- package/dist/signals.d.ts +73 -0
- package/dist/signals.js +169 -0
- package/dist/sql.d.ts +1 -1
- package/dist/sql.js +26 -7
- package/dist/tx.d.ts +9 -7
- package/dist/tx.js +15 -5
- package/dist/typed.d.ts +21 -6
- package/dist/types.d.ts +77 -4
- package/dist/types.js +8 -3
- package/dist/up.d.ts +16 -2
- package/dist/up.js +34 -4
- package/dist/uuid.js +19 -8
- package/dist/watch.d.ts +23 -2
- package/dist/watch.js +44 -7
- package/dist/write.d.ts +11 -2
- package/dist/write.js +57 -10
- package/llms.txt +9 -2
- package/package.json +13 -2
- package/sql/10-core.sql +13 -2
- package/sql/15-errors.sql +4 -1
- package/sql/40-class.sql +57 -2
- package/sql/60-write.sql +611 -82
- package/sql/70-read.sql +33 -8
- package/sql/75-history.sql +358 -0
- package/sql/80-auth.sql +7 -7
- package/sql/88-derived.sql +1154 -0
- package/sql/90-time.sql +134 -21
- package/sql/95-seed.system.sql +24 -6
- package/sql/99-revision.sql +1 -1
- package/sql/README.md +5 -1
- package/sql/migrate/0006-changed-paths.sql +3 -0
package/dist/import.js
CHANGED
|
@@ -26,6 +26,7 @@ import { normalizeSchema } from './jsonschema.js';
|
|
|
26
26
|
import { RESERVED_CLASS_NAMES } from './types.js';
|
|
27
27
|
import { base32Decode } from './auth.js';
|
|
28
28
|
import { v5Id } from './uuid.js';
|
|
29
|
+
import { initialSession } from './load.js';
|
|
29
30
|
const SYSTEM_ID = '2eba6d0a-1edd-4bb6-a85a-a703dab49035';
|
|
30
31
|
const SYSTEM_CLASSES = new Set(['Class', 'Account', 'member', 'Resource', 'rule']);
|
|
31
32
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -427,6 +428,9 @@ const quote = (s) => `"${s.replace(/"/g, '""')}"`;
|
|
|
427
428
|
export async function import021(opts) {
|
|
428
429
|
const t0 = performance.now();
|
|
429
430
|
const log = opts.quiet ? () => { } : (m) => console.log(`letopis.import: ${m}`);
|
|
431
|
+
if (opts.initial && (opts.update || opts.commitEvery)) {
|
|
432
|
+
throw new LetopisError('invalid_data', 'import --initial несовместим с --update и --commit-every: первая заливка — одна транзакция в пустую схему');
|
|
433
|
+
}
|
|
430
434
|
const partition = opts.partition ?? 'entity';
|
|
431
435
|
const owner = opts.owner ?? 'letopis_owner';
|
|
432
436
|
const src = typeof opts.from === 'string' ? postgres(opts.from, { max: 2, onnotice: () => { } }) : opts.from;
|
|
@@ -557,195 +561,212 @@ export async function import021(opts) {
|
|
|
557
561
|
resources: { loaded: 0, skipped: 0 }, rules: { loaded: 0, skipped: 0 }, objects: { loaded: 0, skipped: 0, updated: 0 },
|
|
558
562
|
uuidFields: {}, droppedFields: {}, ownerLost: 0, verify: { ok: false, issues: 0, anchor: '' }, ms: 0,
|
|
559
563
|
};
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
await
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
continue;
|
|
571
|
-
}
|
|
572
|
-
await tx.unsafe(`select ${S}.sys_create(${S}.v5_id($1::uuid, 'Class', array[$2]), 'Class', $1::uuid, '{}', $3::jsonb)`, [SYSTEM_ID, c.name, c.desc]).catch((e) => {
|
|
573
|
-
e.message = `import: класс ${c.name}: ${e.message}`;
|
|
574
|
-
throw e;
|
|
575
|
-
});
|
|
576
|
-
report.classes.created.push(c.name);
|
|
577
|
-
}
|
|
578
|
-
await tx.unsafe(`select pg_notify($1, 'Class')`, [opts.schema]);
|
|
579
|
-
});
|
|
580
|
-
log(`классы: создано ${report.classes.created.length}, уже были ${report.classes.existing.length}, опущено ${b.omitted.length}`);
|
|
581
|
-
// --- шаг 2: аккаунты (System) и арендатор данных System ---
|
|
582
|
-
const accRows = [];
|
|
583
|
-
for (const a of accounts) {
|
|
584
|
-
if (systemOld.has(a.id))
|
|
585
|
-
continue;
|
|
586
|
-
const data = (a.data ?? {});
|
|
587
|
-
const name = typeof data.name === 'string' ? data.name : typeof data.id === 'string' ? data.id : a.id;
|
|
588
|
-
accRows.push({ $class: 'Account', $ext: a.id, name, categories: a.categories.filter((c) => c !== 'System'), enabled: a.enabled !== false });
|
|
589
|
-
}
|
|
590
|
-
if (sysTenant)
|
|
591
|
-
accRows.push({ $class: 'Account', $ext: `tenant:${opts.tenant}`, name: opts.tenant, categories: [], enabled: true });
|
|
592
|
-
if (accRows.length) {
|
|
593
|
-
const r = await db.load(accRows, { tenant: SYSTEM_ID, existing: 'skip', agent: 'letopis import' });
|
|
594
|
-
report.accounts = { loaded: r.loaded, skipped: r.skipped };
|
|
595
|
-
}
|
|
596
|
-
// --- шаг 3: креды → secret (от владельца; хэши как есть); совпадение без учёта регистра — первым идёт
|
|
597
|
-
// подтверждённый, затем более ранний, остальные — в отчёт ---
|
|
598
|
-
const creds = await src.unsafe(`select account::text as account, category, identifier, meta, confirmed from ${F}."Credential" where deleted is null order by confirmed desc, created, id`);
|
|
599
|
-
const secretRows = [];
|
|
600
|
-
for (const c of creds) {
|
|
601
|
-
const m = (c.meta ?? {});
|
|
602
|
-
const account = accId(c.account);
|
|
603
|
-
const kind = String(c.category);
|
|
604
|
-
const confirmed = c.confirmed !== false;
|
|
605
|
-
if (kind === 'OTP' || kind.startsWith('OTP_') || typeof m.code === 'string') {
|
|
606
|
-
report.credentials.otp++;
|
|
607
|
-
continue;
|
|
608
|
-
}
|
|
609
|
-
if (typeof m.password === 'string')
|
|
610
|
-
secretRows.push({ account, kind, ident: c.identifier, material: Buffer.from(m.password, 'utf8'), meta: { confirmed } });
|
|
611
|
-
else if (kind === 'APIKEY')
|
|
612
|
-
secretRows.push({ account, kind, ident: String(m.prefix ?? ''), material: Buffer.from(String(c.identifier), 'hex'), meta: { ...(m.name ? { name: m.name } : {}), confirmed: true } });
|
|
613
|
-
else if (kind === 'KEYSECRET')
|
|
614
|
-
secretRows.push({ account, kind, ident: c.identifier, material: Buffer.from(String(m.secret ?? ''), 'hex'), meta: { ...(m.name ? { name: m.name } : {}), confirmed: true } });
|
|
615
|
-
else if (kind === 'TOTP')
|
|
616
|
-
secretRows.push({ account, kind, ident: account, material: base32Decode(String(m.secret ?? '')), meta: { confirmed, last_step: Number(m.lastStep ?? 0) } });
|
|
617
|
-
else
|
|
618
|
-
secretRows.push({ account, kind, ident: c.identifier, material: null, meta: { ...m, confirmed } });
|
|
619
|
-
}
|
|
620
|
-
if (secretRows.length) {
|
|
621
|
-
await dst.begin(async (tx) => {
|
|
622
|
-
await tx.unsafe(`set local role ${quote(owner)}`);
|
|
623
|
-
for (const s of secretRows) {
|
|
624
|
-
const [dup] = await tx.unsafe(`select account::text as account from ${S}.secret
|
|
625
|
-
where kind = $1 and casefold(ident) collate pg_unicode_fast = casefold($2) collate pg_unicode_fast and deleted_at is null`, [s.kind, s.ident]);
|
|
626
|
-
if (dup) {
|
|
627
|
-
if (dup.account === s.account)
|
|
628
|
-
report.credentials.skipped++;
|
|
629
|
-
else
|
|
630
|
-
report.credentials.conflicts.push({ account: s.account, kind: s.kind, ident: s.ident });
|
|
564
|
+
const steps = async (x) => {
|
|
565
|
+
// --- шаг 1: классы (System, от владельца), предки раньше потомков ---
|
|
566
|
+
await x.begin(async (tx) => {
|
|
567
|
+
for (const r of schemaRows) {
|
|
568
|
+
const c = b.classes.get(r.id);
|
|
569
|
+
if (!c)
|
|
570
|
+
continue;
|
|
571
|
+
const [ex] = await tx.unsafe(`select 1 from ${S}.entity where class = 'Class' and data->>'name' = $1`, [c.name]);
|
|
572
|
+
if (ex) {
|
|
573
|
+
report.classes.existing.push(c.name);
|
|
631
574
|
continue;
|
|
632
575
|
}
|
|
633
|
-
await tx.unsafe(`
|
|
634
|
-
|
|
576
|
+
await tx.unsafe(`select ${S}.sys_create(${S}.v5_id($1::uuid, 'Class', array[$2]), 'Class', $1::uuid, '{}', $3::jsonb)`, [SYSTEM_ID, c.name, c.desc]).catch((e) => {
|
|
577
|
+
e.message = `import: класс ${c.name}: ${e.message}`;
|
|
578
|
+
throw e;
|
|
579
|
+
});
|
|
580
|
+
report.classes.created.push(c.name);
|
|
635
581
|
}
|
|
582
|
+
// сигнал — функцией базы: нагрузка по режиму сигналов установки (план 1.1)
|
|
583
|
+
await tx.unsafe(`select ${S}.signal(array['Class'], array[$1::uuid])`, [SYSTEM_ID]);
|
|
636
584
|
});
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
const
|
|
644
|
-
|
|
585
|
+
log(`классы: создано ${report.classes.created.length}, уже были ${report.classes.existing.length}, опущено ${b.omitted.length}`);
|
|
586
|
+
// --- шаг 2: аккаунты (System) и арендатор данных System ---
|
|
587
|
+
const accRows = [];
|
|
588
|
+
for (const a of accounts) {
|
|
589
|
+
if (systemOld.has(a.id))
|
|
590
|
+
continue;
|
|
591
|
+
const data = (a.data ?? {});
|
|
592
|
+
const name = typeof data.name === 'string' ? data.name : typeof data.id === 'string' ? data.id : a.id;
|
|
593
|
+
accRows.push({ $class: 'Account', $ext: a.id, name, categories: a.categories.filter((c) => c !== 'System'), enabled: a.enabled !== false });
|
|
645
594
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
const
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
src21.versions += o.versions.length;
|
|
688
|
-
if (!o.versions[o.versions.length - 1].del)
|
|
689
|
-
src21.live++;
|
|
690
|
-
let afterDelete = false;
|
|
691
|
-
o.versions.forEach((v, k) => {
|
|
692
|
-
const data = {};
|
|
693
|
-
for (const [f, x] of Object.entries(v.data)) {
|
|
694
|
-
if (f === 'id')
|
|
695
|
-
continue;
|
|
696
|
-
if (f.startsWith('$')) {
|
|
697
|
-
report.droppedFields[`${c.name}.${f}`] = (report.droppedFields[`${c.name}.${f}`] ?? 0) + 1;
|
|
595
|
+
if (sysTenant)
|
|
596
|
+
accRows.push({ $class: 'Account', $ext: `tenant:${opts.tenant}`, name: opts.tenant, categories: [], enabled: true });
|
|
597
|
+
if (accRows.length) {
|
|
598
|
+
const fresh = accRows.filter((row) => !x.taken(v5Id(SYSTEM_ID, 'Account', [String(row.$ext)])));
|
|
599
|
+
const r = fresh.length ? await x.load(fresh, { tenant: SYSTEM_ID, existing: 'skip', agent: 'letopis import' }) : null;
|
|
600
|
+
report.accounts = { loaded: r?.loaded ?? 0, skipped: (r?.skipped ?? 0) + accRows.length - fresh.length };
|
|
601
|
+
}
|
|
602
|
+
// --- шаг 3: креды → secret (от владельца; хэши как есть); совпадение без учёта регистра — первым идёт
|
|
603
|
+
// подтверждённый, затем более ранний, остальные — в отчёт ---
|
|
604
|
+
const creds = await src.unsafe(`select account::text as account, category, identifier, meta, confirmed from ${F}."Credential" where deleted is null order by confirmed desc, created, id`);
|
|
605
|
+
const secretRows = [];
|
|
606
|
+
for (const c of creds) {
|
|
607
|
+
const m = (c.meta ?? {});
|
|
608
|
+
const account = accId(c.account);
|
|
609
|
+
const kind = String(c.category);
|
|
610
|
+
const confirmed = c.confirmed !== false;
|
|
611
|
+
if (kind === 'OTP' || kind.startsWith('OTP_') || typeof m.code === 'string') {
|
|
612
|
+
report.credentials.otp++;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (typeof m.password === 'string')
|
|
616
|
+
secretRows.push({ account, kind, ident: c.identifier, material: Buffer.from(m.password, 'utf8'), meta: { confirmed } });
|
|
617
|
+
else if (kind === 'APIKEY')
|
|
618
|
+
secretRows.push({ account, kind, ident: String(m.prefix ?? ''), material: Buffer.from(String(c.identifier), 'hex'), meta: { ...(m.name ? { name: m.name } : {}), confirmed: true } });
|
|
619
|
+
else if (kind === 'KEYSECRET')
|
|
620
|
+
secretRows.push({ account, kind, ident: c.identifier, material: Buffer.from(String(m.secret ?? ''), 'hex'), meta: { ...(m.name ? { name: m.name } : {}), confirmed: true } });
|
|
621
|
+
else if (kind === 'TOTP')
|
|
622
|
+
secretRows.push({ account, kind, ident: account, material: base32Decode(String(m.secret ?? '')), meta: { confirmed, last_step: Number(m.lastStep ?? 0) } });
|
|
623
|
+
else
|
|
624
|
+
secretRows.push({ account, kind, ident: c.identifier, material: null, meta: { ...m, confirmed } });
|
|
625
|
+
}
|
|
626
|
+
if (secretRows.length) {
|
|
627
|
+
await x.begin(async (tx) => {
|
|
628
|
+
for (const s of secretRows) {
|
|
629
|
+
const [dup] = await tx.unsafe(`select account::text as account from ${S}.secret
|
|
630
|
+
where kind = $1 and casefold(ident) collate pg_unicode_fast = casefold($2) collate pg_unicode_fast and deleted_at is null`, [s.kind, s.ident]);
|
|
631
|
+
if (dup) {
|
|
632
|
+
if (dup.account === s.account)
|
|
633
|
+
report.credentials.skipped++;
|
|
634
|
+
else
|
|
635
|
+
report.credentials.conflicts.push({ account: s.account, kind: s.kind, ident: s.ident });
|
|
698
636
|
continue;
|
|
699
637
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const links = {};
|
|
703
|
-
for (const [k21, target] of Object.entries(v.links)) {
|
|
704
|
-
const role = c.roleOf.get(k21) ?? b.classes.get(k21)?.name ?? k21;
|
|
705
|
-
links[role] = newId(k21, target) ?? v5Id(tenant, b.classes.get(k21)?.name ?? k21, [`missing:${target}`]);
|
|
706
|
-
}
|
|
707
|
-
let ownerId = tenant;
|
|
708
|
-
if (v.owner && v.owner !== o.account) {
|
|
709
|
-
const mapped = systemOld.has(v.owner) ? SYSTEM_ID : accId(v.owner);
|
|
710
|
-
if (accountIds.has(mapped))
|
|
711
|
-
ownerId = mapped;
|
|
712
|
-
else if (k === o.versions.length - 1)
|
|
713
|
-
report.ownerLost++;
|
|
714
|
-
}
|
|
715
|
-
const op = v.del ? 'delete' : k === 0 || afterDelete ? 'create' : 'update';
|
|
716
|
-
afterDelete = v.del;
|
|
717
|
-
const row = { $class: c.name, $rev: k + 1, $at: v.at, $op: op, ...(keyed ? {} : { $ext: o.id }),
|
|
718
|
-
...(v.tags.length ? { $tags: v.tags } : {}), ...(ownerId !== tenant ? { $owner: ownerId } : {}), ...data, ...links };
|
|
719
|
-
rows.push(row);
|
|
720
|
-
origin.push({ class: o.cls, id: o.id, rev: k + 1 });
|
|
721
|
-
if (k === o.versions.length - 1 && !v.del) {
|
|
722
|
-
const { $rev: _r, $at: _a, $op: _o, ...cur } = row;
|
|
723
|
-
lastRows.push(cur);
|
|
724
|
-
scanUuids(data, `${c.name}`, oldIds, report.uuidFields);
|
|
638
|
+
await tx.unsafe(`insert into ${S}.secret (account, kind, ident, material, meta) values ($1::uuid, $2, $3, $4, $5::jsonb)`, [s.account, s.kind, s.ident, s.material, s.meta]);
|
|
639
|
+
report.credentials.imported++;
|
|
725
640
|
}
|
|
726
641
|
});
|
|
727
642
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
if (
|
|
733
|
-
const
|
|
734
|
-
|
|
643
|
+
// --- шаг 4: права — Resource и rule в System ---
|
|
644
|
+
const resources = await src.unsafe(`select alias, category, pattern, meta from ${F}."Resource" order by alias`);
|
|
645
|
+
const resRows = resources.map((r) => {
|
|
646
|
+
let pattern = r.pattern;
|
|
647
|
+
if (isObj(pattern) && 'account' in pattern) {
|
|
648
|
+
const { account, ...rest } = pattern;
|
|
649
|
+
pattern = { ...rest, tenant: account };
|
|
735
650
|
}
|
|
651
|
+
const meta = (r.meta ?? {});
|
|
652
|
+
return { $class: 'Resource', alias: r.alias, category: r.category, ...(isObj(pattern) ? { pattern } : {}),
|
|
653
|
+
...(typeof meta.description === 'string' ? { description: meta.description } : {}) };
|
|
654
|
+
});
|
|
655
|
+
if (resRows.length) {
|
|
656
|
+
const fresh = resRows.filter((row) => !x.taken(v5Id(SYSTEM_ID, 'Resource', [String(row.alias)])));
|
|
657
|
+
const r = fresh.length ? await x.load(fresh, { tenant: SYSTEM_ID, existing: 'skip', agent: 'letopis import' }) : null;
|
|
658
|
+
report.resources = { loaded: r?.loaded ?? 0, skipped: (r?.skipped ?? 0) + resRows.length - fresh.length };
|
|
736
659
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
660
|
+
const rules = await src.unsafe(`select account, resource, permission, weight, meta, enabled from ${F}."Rule" order by account, resource`);
|
|
661
|
+
const ruleRows = rules.map((r) => {
|
|
662
|
+
const meta = (r.meta ?? {});
|
|
663
|
+
const code = typeof meta.code === 'number' ? meta.code : typeof meta.code === 'string' && /^\d+$/.test(meta.code) ? Number(meta.code) : undefined;
|
|
664
|
+
return { $class: 'rule', Group: { $key: [r.account] }, Object: { $key: [r.resource] }, permission: r.permission, weight: r.weight ?? 0,
|
|
665
|
+
enabled: r.enabled !== false, ...(code !== undefined ? { code } : {}), ...(typeof meta.message === 'string' ? { message: meta.message } : {}),
|
|
666
|
+
...(typeof meta.description === 'string' ? { description: meta.description } : {}) };
|
|
667
|
+
});
|
|
668
|
+
if (ruleRows.length) {
|
|
669
|
+
const res = (alias) => v5Id(SYSTEM_ID, 'Resource', [String(alias)]);
|
|
670
|
+
const fresh = ruleRows.filter((row) => !x.taken(v5Id(SYSTEM_ID, 'rule', [res(row.Group.$key[0]), res(row.Object.$key[0])])));
|
|
671
|
+
const r = fresh.length ? await x.load(fresh, { tenant: SYSTEM_ID, existing: 'skip', agent: 'letopis import' }) : null;
|
|
672
|
+
report.rules = { loaded: r?.loaded ?? 0, skipped: (r?.skipped ?? 0) + ruleRows.length - fresh.length };
|
|
673
|
+
}
|
|
674
|
+
// --- шаг 5: данные по арендаторам — загрузчик в режиме истории ---
|
|
675
|
+
const accountIds = new Set([SYSTEM_ID, ...accounts.map((a) => accId(a.id)), ...(sysTenant ? [sysTenant] : [])]);
|
|
676
|
+
const oldIds = new Set([...objs.values()].map((o) => o.id));
|
|
677
|
+
const byTenant = new Map();
|
|
678
|
+
for (const o of objs.values()) {
|
|
679
|
+
const t = tenantOf(o.account);
|
|
680
|
+
if (!byTenant.has(t))
|
|
681
|
+
byTenant.set(t, []);
|
|
682
|
+
byTenant.get(t).push(o);
|
|
683
|
+
}
|
|
684
|
+
for (const [tenant, list] of byTenant) {
|
|
685
|
+
const old = list[0].account;
|
|
686
|
+
report.tenants.push({ old, id: tenant, name: systemOld.has(old) ? opts.tenant : old });
|
|
687
|
+
const rows = [];
|
|
688
|
+
const origin = [];
|
|
689
|
+
const lastRows = [];
|
|
690
|
+
for (const o of list) {
|
|
691
|
+
const c = b.classes.get(o.cls);
|
|
692
|
+
const keyed = b.keyOf(o.cls).length > 0;
|
|
693
|
+
const src21 = report.source[c.name] ?? (report.source[c.name] = { objects: 0, versions: 0, live: 0 });
|
|
694
|
+
src21.objects++;
|
|
695
|
+
src21.versions += o.versions.length;
|
|
696
|
+
if (!o.versions[o.versions.length - 1].del)
|
|
697
|
+
src21.live++;
|
|
698
|
+
let afterDelete = false;
|
|
699
|
+
o.versions.forEach((v, k) => {
|
|
700
|
+
const data = {};
|
|
701
|
+
for (const [f, x] of Object.entries(v.data)) {
|
|
702
|
+
if (f === 'id')
|
|
703
|
+
continue;
|
|
704
|
+
if (f.startsWith('$')) {
|
|
705
|
+
report.droppedFields[`${c.name}.${f}`] = (report.droppedFields[`${c.name}.${f}`] ?? 0) + 1;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
data[f] = x;
|
|
709
|
+
}
|
|
710
|
+
const links = {};
|
|
711
|
+
for (const [k21, target] of Object.entries(v.links)) {
|
|
712
|
+
const role = c.roleOf.get(k21) ?? b.classes.get(k21)?.name ?? k21;
|
|
713
|
+
links[role] = newId(k21, target) ?? v5Id(tenant, b.classes.get(k21)?.name ?? k21, [`missing:${target}`]);
|
|
714
|
+
}
|
|
715
|
+
let ownerId = tenant;
|
|
716
|
+
if (v.owner && v.owner !== o.account) {
|
|
717
|
+
const mapped = systemOld.has(v.owner) ? SYSTEM_ID : accId(v.owner);
|
|
718
|
+
if (accountIds.has(mapped))
|
|
719
|
+
ownerId = mapped;
|
|
720
|
+
else if (k === o.versions.length - 1)
|
|
721
|
+
report.ownerLost++;
|
|
722
|
+
}
|
|
723
|
+
const op = v.del ? 'delete' : k === 0 || afterDelete ? 'create' : 'update';
|
|
724
|
+
afterDelete = v.del;
|
|
725
|
+
const row = { $class: c.name, $rev: k + 1, $at: v.at, $op: op, ...(keyed ? {} : { $ext: o.id }),
|
|
726
|
+
...(v.tags.length ? { $tags: v.tags } : {}), ...(ownerId !== tenant ? { $owner: ownerId } : {}), ...data, ...links };
|
|
727
|
+
rows.push(row);
|
|
728
|
+
origin.push({ class: o.cls, id: o.id, rev: k + 1 });
|
|
729
|
+
if (k === o.versions.length - 1 && !v.del) {
|
|
730
|
+
const { $rev: _r, $at: _a, $op: _o, ...cur } = row;
|
|
731
|
+
lastRows.push(cur);
|
|
732
|
+
scanUuids(data, `${c.name}`, oldIds, report.uuidFields);
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
try {
|
|
737
|
+
const r = await x.load(rows, { tenant, withHistory: true, existing: 'skip', agent: 'letopis import', ...(opts.commitEvery ? { commitEvery: opts.commitEvery } : {}) });
|
|
738
|
+
report.objects.loaded += r.loaded;
|
|
739
|
+
report.objects.skipped += r.skipped;
|
|
740
|
+
if (opts.update && lastRows.length) {
|
|
741
|
+
const u = await x.load(lastRows, { tenant, existing: 'update', agent: 'letopis import' });
|
|
742
|
+
report.objects.updated += u.updated;
|
|
743
|
+
}
|
|
744
744
|
}
|
|
745
|
-
|
|
745
|
+
catch (e) {
|
|
746
|
+
// строки загрузчика → объекты 0.21; total, truncated, limit и complete отчёта загрузчика — как есть
|
|
747
|
+
const err = e;
|
|
748
|
+
const detail = err.detail;
|
|
749
|
+
if (detail?.rows) {
|
|
750
|
+
const mapped = detail.rows.map((x) => ({ ...origin[x.line - 1], code: x.code, message: x.message }));
|
|
751
|
+
throw new LetopisError(err.code ?? 'invalid_data', `import: арендатор ${tenant}: ${err.message}`, { ...detail, rows: mapped, notes: b.notes });
|
|
752
|
+
}
|
|
753
|
+
throw e;
|
|
754
|
+
}
|
|
755
|
+
log(`арендатор ${tenant}: объектов ${list.length}, версий ${rows.length}`);
|
|
746
756
|
}
|
|
747
|
-
|
|
748
|
-
}
|
|
757
|
+
return byTenant;
|
|
758
|
+
};
|
|
759
|
+
const byTenant = opts.initial
|
|
760
|
+
// existing в первой заливке — только 'error': строки сида отфильтрованы taken(), прочих объектов в схеме нет
|
|
761
|
+
? await initialSession({ sql: dst, schema: opts.schema, tenant: null, admin: true, ownerRole: owner }, (sess) => steps({ begin: (fn) => fn(sess.sql), load: (rows, o) => sess.load(rows, { ...o, existing: undefined }), taken: (id) => sess.taken(id) }))
|
|
762
|
+
: await steps({
|
|
763
|
+
begin: (fn) => dst.begin(async (tx) => {
|
|
764
|
+
await tx.unsafe(`set local role ${quote(owner)}`);
|
|
765
|
+
return fn(tx);
|
|
766
|
+
}),
|
|
767
|
+
load: (rows, o) => db.load(rows, o),
|
|
768
|
+
taken: () => false,
|
|
769
|
+
});
|
|
749
770
|
// --- шаг 6: проверка — счётчики и verify() ---
|
|
750
771
|
const tenants = [...byTenant.keys()];
|
|
751
772
|
if (tenants.length) {
|
package/dist/index.d.ts
CHANGED
|
@@ -17,12 +17,13 @@ import { type Batch, type Chain, type StepFn } from './chain.js';
|
|
|
17
17
|
import type { QueryEvent, Row } from './types.js';
|
|
18
18
|
import { type CacheOptions, type CacheStats } from './cache.js';
|
|
19
19
|
import { Subscription, type WatchOptions } from './watch.js';
|
|
20
|
+
import { Projector, type ProjectorOptions } from './project.js';
|
|
20
21
|
import { type MaintainOptions, type MaintainReport, type ModelDescription, type ResetOptions, type ResetResult, type VerifyResult } from './admin.js';
|
|
21
22
|
import type { AnyModelLike, StepsOf } from './typed.js';
|
|
22
23
|
export { DDL_REVISION } from './types.js';
|
|
23
24
|
export { uuidv5, v5Name, v5Id, idOf, type KeyedClass } from './uuid.js';
|
|
24
25
|
export { LetopisError, ValidationError, fromDbError, type Issue } from './errors.js';
|
|
25
|
-
export { Registry, type ClassInfo, type EndInfo, type LeafType } from './registry.js';
|
|
26
|
+
export { Registry, type AggregateInfo, type ClassInfo, type EndInfo, type LeafType } from './registry.js';
|
|
26
27
|
export { compileValidator, applyDefaults, type Validator } from './validate.js';
|
|
27
28
|
export { syncModels, generateTypes, schemaToTs, type SyncResult } from './sync.js';
|
|
28
29
|
export { ne, gt, gte, lt, lte, between, inList, like, ilike, starts, ends, has, hasAny, hasAll, exists, isNull, not, or, isOp } from './ops.js';
|
|
@@ -34,8 +35,11 @@ export type { LoadOptions, LoadReport, LoadIssue, LoadInput } from './load.js';
|
|
|
34
35
|
export type { Chain, ChainCore, StepFn, BatchEntry } from './chain.js';
|
|
35
36
|
export type { ModelLike, AnyModelLike, RowOf, FilterOf, FieldFilterOf, TypedChain, StepsOf, PatchOf } from './typed.js';
|
|
36
37
|
export type { Row, Path, Filter, FieldFilter, Cursor, ChainMods, QueryEvent, Op, Scalar } from './types.js';
|
|
38
|
+
export type { DiffChange, DiffResult, DiffOptions, BlameField, BlameResult, BlameOptions } from './types.js';
|
|
37
39
|
export type { CacheOptions, CacheStats } from './cache.js';
|
|
40
|
+
export type { SignalMode } from './signals.js';
|
|
38
41
|
export type { WatchOptions, WatchEvent, Subscription } from './watch.js';
|
|
42
|
+
export type { Projector, ProjectorOptions, ProjectorChange, ProjectorOut, ProjectorRow, ProjectorReport, ProjectorStatus, ProjectorFailure } from './project.js';
|
|
39
43
|
export type { VerifyResult, VerifyIssue, MaintainOptions, MaintainReport, ResetOptions, ResetResult, ModelDescription } from './admin.js';
|
|
40
44
|
export { describeText } from './admin.js';
|
|
41
45
|
export { up, createTenant, type UpOptions, type UpResult, type HistoryPolicy, type TenantOptions, type TenantResult } from './up.js';
|
|
@@ -73,12 +77,13 @@ export interface ConnectOptions {
|
|
|
73
77
|
/**
|
|
74
78
|
* Слушать сигналы базы (LISTEN, по умолчанию да). false — пул в режиме транзакций (PgBouncer):
|
|
75
79
|
* реестр перечитывается через db.refresh(), подписка опрашивает журнал, кэш результатов выключен
|
|
76
|
-
* (с cache: { ttlOnly: true } — работает только по сроку).
|
|
80
|
+
* (с cache: { ttlOnly: true } — работает только по сроку). В установке с up({ signals: 'none' })
|
|
81
|
+
* слушателя нет всегда, а реестр раз в 5 с сверяет отпечаток классов.
|
|
77
82
|
*/
|
|
78
83
|
listen?: boolean;
|
|
79
84
|
/** Кэш результатов (§2.12): срок по умолчанию, размер, ttlOnly; false — выключен. */
|
|
80
85
|
cache?: CacheOptions | false;
|
|
81
|
-
/** Запускать maintain() по расписанию ('1h', '15m', мс) под
|
|
86
|
+
/** Запускать maintain() по расписанию ('1h', '15m', мс) под блокировкой обслуживания: работает один процесс. */
|
|
82
87
|
maintain?: string | number;
|
|
83
88
|
}
|
|
84
89
|
export interface DbCore {
|
|
@@ -139,17 +144,34 @@ export interface DbCore {
|
|
|
139
144
|
tenant?: string;
|
|
140
145
|
}): Promise<Db>;
|
|
141
146
|
/** Явная транзакция read committed: те же цепочки, commit(), rollback(), lock(), forUpdate(). */
|
|
142
|
-
|
|
147
|
+
/** Явная транзакция; reason — причина изменения для всех операций блока (план 1.1). */
|
|
148
|
+
begin(opts?: {
|
|
149
|
+
reason?: string;
|
|
150
|
+
}): Promise<DbTx>;
|
|
143
151
|
/**
|
|
144
152
|
* Загрузчик (§2.6): строки любых классов — проверки в коде, COPY от роли владельца, один запрос по
|
|
145
153
|
* целям вне загрузки. Только на администраторском подключении (иначе admin_required).
|
|
146
154
|
*/
|
|
147
155
|
load(input: LoadInput, opts?: LoadOptions): Promise<LoadReport>;
|
|
156
|
+
/**
|
|
157
|
+
* Проектор из журнала (план 1.1, §2.4): строит строки производных классов (derivedBy: имя) из изменений
|
|
158
|
+
* классов from; run() — догнать журнал и следовать за ним, rebuild() — перестроить с текущего состояния.
|
|
159
|
+
* Только на администраторском подключении (иначе admin_required).
|
|
160
|
+
*/
|
|
161
|
+
projector(name: string, opts: ProjectorOptions): Projector;
|
|
162
|
+
/**
|
|
163
|
+
* Ремонт агрегатов в базе (план 1.1, §2.4): поля класса (с потомками) во всех арендаторах — заново из живых строк
|
|
164
|
+
* источников, под исключительными блокировками; версия — только у исправленных. Возвращает их число. Только на
|
|
165
|
+
* администраторском подключении (иначе admin_required).
|
|
166
|
+
*/
|
|
167
|
+
rebuildAggregates(cls: string): Promise<number>;
|
|
148
168
|
/**
|
|
149
169
|
* Именованная очередь планов фасада: batch.Класс(…).create(…); batch.run() — одной транзакцией. У корня,
|
|
150
170
|
* у каждого db.as() и у каждой db.begin() очереди свои: план исполняется от имени фасада, который его поставил.
|
|
151
171
|
*/
|
|
152
|
-
batch(name: string
|
|
172
|
+
batch(name: string, opts?: {
|
|
173
|
+
reason?: string;
|
|
174
|
+
}): Batch;
|
|
153
175
|
/** Advisory-блокировка до конца транзакции — только в db.begin() (иначе tx_required). */
|
|
154
176
|
lock(...keys: (string | number)[]): Promise<void>;
|
|
155
177
|
/** Зафиксировать транзакцию tr (то же, что tr.commit()). */
|