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/chain.js
CHANGED
|
@@ -102,7 +102,7 @@ async function exec(ctx, q, mode, steps, mods = {}) {
|
|
|
102
102
|
if (hit)
|
|
103
103
|
return hit;
|
|
104
104
|
const tag = sessionTag(who.token);
|
|
105
|
-
const tok = store.token(classes, tag);
|
|
105
|
+
const tok = store.token(classes, tag, who.tenant);
|
|
106
106
|
const t0 = performance.now();
|
|
107
107
|
const S = `"${ctx.schema.replace(/"/g, '""')}"`;
|
|
108
108
|
const [res, exp] = await ctx.exec.run((tx) => Promise.all([
|
|
@@ -139,7 +139,7 @@ async function read(ctx, steps, mods, mode, agg) {
|
|
|
139
139
|
async function plan(ctx, steps, mods, mode, agg) {
|
|
140
140
|
precheckPlan(ctx.reg, steps);
|
|
141
141
|
const t0 = performance.now();
|
|
142
|
-
const r = await ctx.exec.run((q) => execPlan({ reg: ctx.reg, schema: ctx.schema, q, tenant: ctx.tenant(), owner: ctx.owner }, steps, mods, mode, agg));
|
|
142
|
+
const r = await ctx.exec.run((q) => execPlan({ reg: ctx.reg, schema: ctx.schema, q, tenant: ctx.tenant(), owner: ctx.owner, ...reasons(ctx) }, steps, mods, mode, agg));
|
|
143
143
|
if (r.kind === 'read') {
|
|
144
144
|
report(ctx, mode === 'countPaths' ? 'count' : mode, steps, t0, r.res.length);
|
|
145
145
|
if (r.q === null) {
|
|
@@ -196,6 +196,28 @@ async function lockedRead(ctx, steps, mods, mode) {
|
|
|
196
196
|
report(ctx, mode, steps, t0, res.length);
|
|
197
197
|
return { q: rowsQ, res: mode === 'ids' ? res.map((r) => ({ id: r.row.id })) : res };
|
|
198
198
|
}
|
|
199
|
+
/** Причины для плана: по умолчанию у глаголов (у батча — своя) и та, что стоит в транзакции. */
|
|
200
|
+
const reasons = (ctx) => ({ reason: ctx.reason ?? ctx.txReason ?? '', txReason: ctx.txReason ?? '' });
|
|
201
|
+
/** Опция reason глагола (план 1.1, §2.1): проверяет precheckPlan. */
|
|
202
|
+
const reasonOf = (o) => (o?.reason !== undefined ? { reason: o.reason } : {});
|
|
203
|
+
/** diff({ from, to }): номер версии (целое ≥ 1) или момент (ISO-строка, Date). */
|
|
204
|
+
function versionRef(v, what, moments = true) {
|
|
205
|
+
if (typeof v === 'number' && Number.isInteger(v) && v >= 1 && v <= 2147483647)
|
|
206
|
+
return v;
|
|
207
|
+
if (moments && v instanceof Date && !Number.isNaN(+v))
|
|
208
|
+
return v.toISOString();
|
|
209
|
+
if (moments && typeof v === 'string' && !Number.isNaN(Date.parse(v)))
|
|
210
|
+
return v;
|
|
211
|
+
throw new LetopisError('invalid_query', `${what}: номер версии (целое ≥ 1)${moments ? ' или момент (ISO-строка, Date)' : ''}`);
|
|
212
|
+
}
|
|
213
|
+
/** blame({ at }): момент (ISO-строка, Date). */
|
|
214
|
+
function momentOf(v, what) {
|
|
215
|
+
if (v instanceof Date && !Number.isNaN(+v))
|
|
216
|
+
return v.toISOString();
|
|
217
|
+
if (typeof v === 'string' && !Number.isNaN(Date.parse(v)))
|
|
218
|
+
return v;
|
|
219
|
+
throw new LetopisError('invalid_query', `${what}: момент — ISO-строка или Date`);
|
|
220
|
+
}
|
|
199
221
|
/** Строка ответа: из чтения — to_jsonb узла, из плана — готовая строка записи. */
|
|
200
222
|
const rowOf = (r) => (r.done ? r.row : toRow(r.row));
|
|
201
223
|
/** Значение слота: id, список id, строка ответа или ленивая цепочка. */
|
|
@@ -283,6 +305,10 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
|
|
|
283
305
|
const last = steps[steps.length - 1];
|
|
284
306
|
if (!last)
|
|
285
307
|
throw new LetopisError('invalid_query', `${op.kind}() — нужен шаг класса: db.Класс(…).${op.kind}(…)`);
|
|
308
|
+
// батч: причина db.batch(имя, { reason }) — при постановке в очередь, а не при run(): очередь общая по имени, и
|
|
309
|
+
// run() может позвать фасад без причины или с другой (ревью №1 точки А плана 1.1)
|
|
310
|
+
if (ctx.batch && op.reason === undefined && ctx.reason !== undefined)
|
|
311
|
+
op = { ...op, reason: ctx.reason };
|
|
286
312
|
const next = last.op
|
|
287
313
|
? [...steps, { name: last.name, cls: last.cls, role: last.role, aliasKey: last.aliasKey, self: true, op, opMods: mods, nodeKey: nextNodeKey() }]
|
|
288
314
|
: [...steps.slice(0, -1), { ...last, op, opMods: mods }];
|
|
@@ -364,6 +390,29 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
|
|
|
364
390
|
const m = opts?.follow ? { ...mods, follow: true } : mods;
|
|
365
391
|
return (await read(ctx, steps, m, 'versions')).res.map((r) => toRow(r.row));
|
|
366
392
|
};
|
|
393
|
+
case 'diff':
|
|
394
|
+
return async (opts = {}) => {
|
|
395
|
+
warnNoHistory(steps, 'diff');
|
|
396
|
+
const hist = {
|
|
397
|
+
...(opts.from !== undefined ? { from: versionRef(opts.from, 'diff({ from })') } : {}),
|
|
398
|
+
...(opts.to !== undefined ? { to: versionRef(opts.to, 'diff({ to })') } : {}),
|
|
399
|
+
...(opts.follow ? { follow: true } : {}),
|
|
400
|
+
};
|
|
401
|
+
return (await read(ctx, steps, { ...mods, hist }, 'diff')).res.map((r) => r.row);
|
|
402
|
+
};
|
|
403
|
+
case 'blame':
|
|
404
|
+
return async (opts = {}) => {
|
|
405
|
+
warnNoHistory(steps, 'blame');
|
|
406
|
+
if (opts.limit !== undefined)
|
|
407
|
+
versionRef(opts.limit, 'blame({ limit })', false);
|
|
408
|
+
const hist = {
|
|
409
|
+
...(opts.at !== undefined ? { at: momentOf(opts.at, 'blame({ at })') } : {}),
|
|
410
|
+
...(opts.limit !== undefined ? { limit: opts.limit } : {}),
|
|
411
|
+
};
|
|
412
|
+
// null — объекта на момент at не было (или он не виден)
|
|
413
|
+
return (await read(ctx, steps, { ...mods, hist }, 'blame')).res
|
|
414
|
+
.map((r) => r.row).filter((x) => x !== null);
|
|
415
|
+
};
|
|
367
416
|
case 'sum':
|
|
368
417
|
case 'avg':
|
|
369
418
|
case 'min':
|
|
@@ -404,17 +453,17 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
|
|
|
404
453
|
throw removed('account()', 'арендатор сессии и auth.switch() (этап 5)');
|
|
405
454
|
// глаголы записи — звенья плана (этап 4)
|
|
406
455
|
case 'create':
|
|
407
|
-
return (data = {}) => withOp({ kind: 'create', data });
|
|
456
|
+
return (data = {}, opts) => withOp({ kind: 'create', data, ...reasonOf(opts) });
|
|
408
457
|
case 'update':
|
|
409
|
-
return (data = {}, opts) => withOp({ kind: 'update', data, ...(opts?.rev !== undefined ? { rev: opts.rev } : {}) });
|
|
458
|
+
return (data = {}, opts) => withOp({ kind: 'update', data, ...(opts?.rev !== undefined ? { rev: opts.rev } : {}), ...reasonOf(opts) });
|
|
410
459
|
case 'upsert':
|
|
411
|
-
return (data = {}) => withOp({ kind: 'upsert', data });
|
|
460
|
+
return (data = {}, opts) => withOp({ kind: 'upsert', data, ...reasonOf(opts) });
|
|
412
461
|
case 'delete':
|
|
413
|
-
return (opts) => withOp({ kind: 'delete', confirm: opts?.confirm === true });
|
|
462
|
+
return (opts) => withOp({ kind: 'delete', confirm: opts?.confirm === true, ...reasonOf(opts) });
|
|
414
463
|
case 'anonymize':
|
|
415
|
-
return (fields) => withOp({ kind: 'anonymize', fields });
|
|
464
|
+
return (fields, opts) => withOp({ kind: 'anonymize', fields, ...reasonOf(opts) });
|
|
416
465
|
case 'reclass':
|
|
417
|
-
return (cls, data) => withOp({ kind: 'reclass', cls, ...(data !== undefined ? { data } : {}) });
|
|
466
|
+
return (cls, data, opts) => withOp({ kind: 'reclass', cls, ...(data !== undefined ? { data } : {}), ...reasonOf(opts) });
|
|
418
467
|
case 'set':
|
|
419
468
|
case 'unset':
|
|
420
469
|
case 'add':
|
|
@@ -423,11 +472,11 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
|
|
|
423
472
|
case 'forUpdate':
|
|
424
473
|
return () => withMods({ forUpdate: true });
|
|
425
474
|
case 'restore':
|
|
426
|
-
return () => withOp({ kind: 'restore' });
|
|
475
|
+
return (opts) => withOp({ kind: 'restore', ...reasonOf(opts) });
|
|
427
476
|
case 'purge':
|
|
428
|
-
return (opts) => withOp({ kind: 'purge', confirm: opts?.confirm === true });
|
|
477
|
+
return (opts) => withOp({ kind: 'purge', confirm: opts?.confirm === true, ...reasonOf(opts) });
|
|
429
478
|
case 'rekey':
|
|
430
|
-
return (patch) => withOp({ kind: 'rekey', data: patch ?? {} });
|
|
479
|
+
return (patch, opts) => withOp({ kind: 'rekey', data: patch ?? {}, ...reasonOf(opts) });
|
|
431
480
|
case 'cache':
|
|
432
481
|
return (opts) => {
|
|
433
482
|
if (opts?.ttl !== undefined && !(opts.ttl > 0))
|
|
@@ -459,7 +508,7 @@ export function makeBatch(ctx, queue) {
|
|
|
459
508
|
precheckPlan(ctx.reg, p);
|
|
460
509
|
if (!plans.length)
|
|
461
510
|
return [];
|
|
462
|
-
return ctx.exec.run((q) => execBatch({ reg: ctx.reg, schema: ctx.schema, q, tenant: ctx.tenant(), owner: ctx.owner }, plans));
|
|
511
|
+
return ctx.exec.run((q) => execBatch({ reg: ctx.reg, schema: ctx.schema, q, tenant: ctx.tenant(), owner: ctx.owner, ...reasons(ctx) }, plans));
|
|
463
512
|
};
|
|
464
513
|
case 'execute':
|
|
465
514
|
throw removed('batch.execute()', 'batch.run()');
|
package/dist/cli.js
CHANGED
|
@@ -6,12 +6,15 @@
|
|
|
6
6
|
* letopis sync <модуль моделей> --dsn … --schema v2.x (--token … | --api-key …) [--apply]
|
|
7
7
|
* letopis types --dsn … --schema v2.x (--token … | --api-key …) [--out types.d.ts]
|
|
8
8
|
* letopis load <файлы NDJSON…> --dsn … --schema v2.x (--token … | --api-key …) [--class C]
|
|
9
|
-
* [--duplicates error|first|last] [--existing error|skip|update] [--commit-every N] [--verify]
|
|
9
|
+
* [--duplicates error|first|last] [--existing error|skip|update] [--commit-every N] [--verify] [--initial]
|
|
10
10
|
* letopis verify --dsn … --schema v2.x [--token … | --api-key …] [--data] — код 1 при находках
|
|
11
11
|
* letopis maintain --dsn … --schema v2.x [--token … | --api-key …] [--batches N]
|
|
12
12
|
* letopis describe --dsn … --schema v2.x (--token … | --api-key …) [--format json|text] [--out файл]
|
|
13
13
|
* letopis import --from <строка 0.21> --from-schema v1.x --dsn … --schema v2.x [--tenant <имя>] [--keyless C]…
|
|
14
|
-
* [--rename Старое=Новое]… [--update] [--commit-every N] [--partition p] — код 1, если verify нашёл расхождения
|
|
14
|
+
* [--rename Старое=Новое]… [--update] [--commit-every N] [--partition p] [--initial] — код 1, если verify нашёл расхождения
|
|
15
|
+
* --initial — первая заливка (план 1.1, §2.3): пустая схема, одна транзакция, индексы и триггеры — после загрузки
|
|
16
|
+
* letopis mcp --dsn … --schema v2.x --token … [--write] — сервер MCP по stdio (план 1.1, §2.5); dsn, schema и
|
|
17
|
+
* token — ещё из LETOPIS_DSN, LETOPIS_SCHEMA, LETOPIS_TOKEN; только токен пользователя: --api-key — отказ
|
|
15
18
|
*/
|
|
16
19
|
import { parseArgs } from 'node:util';
|
|
17
20
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
@@ -24,12 +27,14 @@ import { import021 } from './import.js';
|
|
|
24
27
|
const USAGE = `letopis sync <модуль> --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--apply]
|
|
25
28
|
letopis types --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--out <файл>]
|
|
26
29
|
letopis load <файлы…> --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--class <Класс>]
|
|
27
|
-
[--duplicates error|first|last] [--existing error|skip|update] [--commit-every <N>] [--verify]
|
|
30
|
+
[--duplicates error|first|last] [--existing error|skip|update] [--commit-every <N>] [--verify] [--initial]
|
|
28
31
|
letopis verify --dsn <строка> --schema <v2.имя> [--token <токен> | --api-key <ключ>] [--data]
|
|
29
32
|
letopis maintain --dsn <строка> --schema <v2.имя> [--token <токен> | --api-key <ключ>] [--batches <N>]
|
|
30
33
|
letopis describe --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--format json|text] [--out <файл>]
|
|
31
34
|
letopis import --from <строка 0.21> --from-schema <v1.имя> --dsn <строка> --schema <v2.имя> [--tenant <имя>] [--keyless <Класс>]…
|
|
32
|
-
[--rename <Старое>=<Новое>]… [--update] [--commit-every <N>] [--partition <имя>]
|
|
35
|
+
[--rename <Старое>=<Новое>]… [--update] [--commit-every <N>] [--partition <имя>] [--initial]
|
|
36
|
+
letopis mcp --dsn <строка> --schema <v2.имя> --token <токен пользователя> [--write]
|
|
37
|
+
(или LETOPIS_DSN, LETOPIS_SCHEMA, LETOPIS_TOKEN; ключ сервиса --api-key не принимается)`;
|
|
33
38
|
const isModel = (v) => typeof v === 'object' && v !== null && typeof v.describe === 'function' && typeof v.name === 'string';
|
|
34
39
|
export async function main(argv) {
|
|
35
40
|
const { values, positionals } = parseArgs({
|
|
@@ -43,9 +48,16 @@ export async function main(argv) {
|
|
|
43
48
|
format: { type: 'string' },
|
|
44
49
|
from: { type: 'string' }, 'from-schema': { type: 'string' }, tenant: { type: 'string' }, partition: { type: 'string' },
|
|
45
50
|
keyless: { type: 'string', multiple: true }, rename: { type: 'string', multiple: true }, update: { type: 'boolean' },
|
|
51
|
+
initial: { type: 'boolean' }, write: { type: 'boolean' },
|
|
46
52
|
},
|
|
47
53
|
});
|
|
48
54
|
const [cmd, arg] = positionals;
|
|
55
|
+
// клиенты MCP передают строку подключения и токен окружением (план 1.1, §2.5) — только для этой команды
|
|
56
|
+
if (cmd === 'mcp') {
|
|
57
|
+
values.dsn ??= process.env.LETOPIS_DSN || undefined;
|
|
58
|
+
values.schema ??= process.env.LETOPIS_SCHEMA || undefined;
|
|
59
|
+
values.token ??= process.env.LETOPIS_TOKEN || undefined;
|
|
60
|
+
}
|
|
49
61
|
if (values.help || !cmd || !values.dsn || !values.schema) {
|
|
50
62
|
console.log(USAGE);
|
|
51
63
|
return cmd ? 2 : 0;
|
|
@@ -68,15 +80,18 @@ export async function main(argv) {
|
|
|
68
80
|
const r = await import021({
|
|
69
81
|
from: values.from, fromSchema: values['from-schema'], dsn: values.dsn, schema: values.schema, tenant: values.tenant,
|
|
70
82
|
partition: values.partition, keyless: values.keyless, rename, update: values.update,
|
|
71
|
-
commitEvery: values['commit-every'] ? Number(values['commit-every']) : undefined,
|
|
83
|
+
commitEvery: values['commit-every'] ? Number(values['commit-every']) : undefined, initial: values.initial,
|
|
72
84
|
});
|
|
73
85
|
console.log(JSON.stringify(r, null, 2));
|
|
74
86
|
return r.verify.ok ? 0 : 1;
|
|
75
87
|
}
|
|
88
|
+
if (cmd === 'mcp')
|
|
89
|
+
return mcp(values);
|
|
76
90
|
// загрузчику нужно администраторское подключение (§2.6, п. 1)
|
|
77
91
|
const db = await connect({ dsn: values.dsn, schema: values.schema, token: values.token, apiKey: values['api-key'], max: 2,
|
|
78
|
-
// verify и maintain работают и на администраторском подключении без
|
|
79
|
-
|
|
92
|
+
// verify и maintain работают и на администраторском подключении без сессии; производные классы (derivedBy)
|
|
93
|
+
// синхронизирует администраторское подключение с сессией арендатора
|
|
94
|
+
...(cmd === 'load' || cmd === 'verify' || cmd === 'maintain' || cmd === 'sync' ? { allowBypassRls: true } : {}) });
|
|
80
95
|
try {
|
|
81
96
|
if (cmd === 'sync') {
|
|
82
97
|
if (!arg) {
|
|
@@ -116,6 +131,7 @@ export async function main(argv) {
|
|
|
116
131
|
existing: values.existing,
|
|
117
132
|
commitEvery: values['commit-every'] ? Number(values['commit-every']) : undefined,
|
|
118
133
|
verify: values.verify,
|
|
134
|
+
initial: values.initial,
|
|
119
135
|
});
|
|
120
136
|
console.log(JSON.stringify(r));
|
|
121
137
|
return 0;
|
|
@@ -154,6 +170,42 @@ export async function main(argv) {
|
|
|
154
170
|
await db.close();
|
|
155
171
|
}
|
|
156
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* letopis mcp (план 1.1, §2.5): сервер MCP по stdio от имени одного пользователя. Только токен сессии пользователя —
|
|
175
|
+
* ключ сервиса даёт все права auth.* (имперсонация, выпуск сессий) и лежал бы на машине агента. Модуль mcp.js и SDK
|
|
176
|
+
* грузятся только здесь: letopis --help и прочие команды работают без SDK. В stdout — только протокол (предупреждения
|
|
177
|
+
* connect() идут в stderr); процесс живёт, пока клиент не закроет stdin.
|
|
178
|
+
*/
|
|
179
|
+
async function mcp(v) {
|
|
180
|
+
if (v['api-key'] !== undefined) {
|
|
181
|
+
console.error('letopis: user_session_required: letopis mcp принимает только токен сессии пользователя (--token или LETOPIS_TOKEN): ключ сервиса даёт все права auth.* и не должен лежать на машине агента');
|
|
182
|
+
return 2;
|
|
183
|
+
}
|
|
184
|
+
if (!v.token) {
|
|
185
|
+
console.error('letopis: user_session_required: letopis mcp — нужен токен сессии пользователя: --token или LETOPIS_TOKEN');
|
|
186
|
+
return 2;
|
|
187
|
+
}
|
|
188
|
+
const { createMcpServer } = await import('./mcp.js');
|
|
189
|
+
const db = await connect({ dsn: v.dsn, schema: v.schema, token: v.token, max: 4 });
|
|
190
|
+
try {
|
|
191
|
+
const opts = { write: v.write === true };
|
|
192
|
+
// учётные данные — до начала протокола: отказ виден кодом выхода и текстом в stderr
|
|
193
|
+
await createMcpServer(db, opts);
|
|
194
|
+
const { serveStdio } = await import('@modelcontextprotocol/server/stdio');
|
|
195
|
+
const closed = new Promise((resolve) => {
|
|
196
|
+
process.stdin.once('end', resolve);
|
|
197
|
+
process.stdin.once('close', resolve);
|
|
198
|
+
});
|
|
199
|
+
// экземпляр сервера — на соединение (и на пробу server/discover): ревизию протокола выбирает клиент
|
|
200
|
+
const handle = serveStdio(() => createMcpServer(db, opts), { onerror: (e) => console.error(`letopis mcp: ${e.message}`) });
|
|
201
|
+
await closed;
|
|
202
|
+
await handle.close();
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
await db.close();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
157
209
|
// Запущен ли файл как программа. Сравниваются реальные пути: на Linux и macOS `node_modules/.bin/letopis`
|
|
158
210
|
// (npx, скрипты package.json) — симлинк, `process.argv[1]` указывает на него, а `import.meta.url` — на сам файл.
|
|
159
211
|
function isMain() {
|
package/dist/errors.js
CHANGED
|
@@ -35,7 +35,8 @@ const LT_NAMES = {
|
|
|
35
35
|
LT006: 'acl_denied', LT007: 'id_mismatch', LT008: 'id_from_db', LT009: 'immutable_key', LT010: 'invalid_class',
|
|
36
36
|
LT011: 'use_reclass', LT012: 'reclass_denied', LT013: 'tightening_conflict', LT014: 'isolation_level',
|
|
37
37
|
LT015: 'recreated_in_statement', LT016: 'cursor_expired', LT017: 'delete_restricted', LT018: 'not_deleted',
|
|
38
|
-
LT019: 'reset_disabled', LT020: 'confirm_mismatch',
|
|
38
|
+
LT019: 'reset_disabled', LT020: 'confirm_mismatch', LT021: 'derived_class', LT022: 'aggregate_field',
|
|
39
|
+
LT023: 'projector_pending',
|
|
39
40
|
};
|
|
40
41
|
/**
|
|
41
42
|
* Ошибка базы → LetopisError (ValidationError для invalid_data с нарушениями). Детали из DETAIL
|
|
@@ -64,7 +65,8 @@ export function fromDbError(e) {
|
|
|
64
65
|
if (name === 'invalid_data' && Array.isArray(issues)) {
|
|
65
66
|
const cls = /класса (\S+)/.exec(text)?.[1] ?? '?';
|
|
66
67
|
const v = new ValidationError(cls, issues);
|
|
67
|
-
|
|
68
|
+
// текст базы целиком: в нём бывает больше, чем имя класса (у проектора — арендатор и id строки)
|
|
69
|
+
return Object.assign(v, { cause: e, ...(text ? { message: `letopis: invalid_data: ${text}` } : {}) });
|
|
68
70
|
}
|
|
69
71
|
return Object.assign(new LetopisError(name, text, detail), { cause: e });
|
|
70
72
|
}
|
package/dist/import.d.ts
CHANGED
|
@@ -46,6 +46,12 @@ export interface ImportOptions {
|
|
|
46
46
|
update?: boolean;
|
|
47
47
|
/** Порции загрузчика (§2.6, п. 8). */
|
|
48
48
|
commitEvery?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Первая заливка (план 1.1, §2.3): весь импорт — одной транзакцией в пустую схему, индексы и триггеры —
|
|
51
|
+
* после загрузки; несовместимо с update и commitEvery. Строки, уже лежащие в схеме (системный сид:
|
|
52
|
+
* ресурсы, правила), пропускаются, как без флага.
|
|
53
|
+
*/
|
|
54
|
+
initial?: boolean;
|
|
49
55
|
/** Без сообщений в консоль. */
|
|
50
56
|
quiet?: boolean;
|
|
51
57
|
}
|