letopis 1.4.1 → 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 -278
- package/CHANGELOG.md +558 -1
- package/README.md +606 -1037
- 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 -175
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +5 -15
- package/dist/cache.d.ts +8 -1
- package/dist/cache.js +33 -10
- package/dist/chain.d.ts +19 -41
- package/dist/chain.js +169 -201
- package/dist/cli.js +51 -88
- 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 -71
- package/dist/index.js +245 -130
- package/dist/internal.d.ts +12 -0
- package/dist/internal.js +5 -0
- package/dist/jsonschema.js +18 -17
- package/dist/load.d.ts +20 -12
- package/dist/load.js +225 -186
- package/dist/mcp.d.ts +5 -7
- package/dist/mcp.js +171 -215
- 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 +26 -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 +5 -9
- package/dist/signals.js +5 -9
- package/dist/sql.d.ts +13 -6
- package/dist/sql.js +176 -83
- package/dist/sync.d.ts +9 -3
- package/dist/sync.js +51 -21
- package/dist/tx.d.ts +26 -1
- package/dist/tx.js +44 -14
- package/dist/typed.d.ts +240 -45
- package/dist/types.d.ts +23 -91
- package/dist/types.js +12 -14
- package/dist/up.d.ts +53 -32
- package/dist/up.js +184 -73
- package/dist/uuid.js +5 -5
- package/dist/validate.d.ts +13 -1
- package/dist/validate.js +61 -29
- package/dist/watch.d.ts +11 -10
- package/dist/watch.js +33 -70
- package/dist/write.d.ts +9 -16
- package/dist/write.js +118 -187
- package/llms.txt +22 -25
- package/package.json +8 -11
- 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 -667
- package/sql/70-read.sql +20 -16
- package/sql/80-auth.sql +24 -27
- package/sql/90-time.sql +70 -859
- 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 -485
- package/MIGRATION.md +0 -190
- package/README.en.md +0 -2859
- package/dist/import.d.ts +0 -185
- package/dist/import.js +0 -820
- package/dist/project.d.ts +0 -350
- package/dist/project.js +0 -1224
- package/sql/75-history.sql +0 -358
- package/sql/88-derived.sql +0 -1235
- package/sql/migrate/0006-changed-paths.sql +0 -3
- package/sql/migrate/0007-secret-account-fk.sql +0 -12
package/dist/cli.js
CHANGED
|
@@ -1,41 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* CLI letopis (план, этап 2, п. 7; этап 4, п. 11; этап 6, п.
|
|
4
|
-
*
|
|
3
|
+
* CLI letopis (план, этап 2, п. 7; этап 4, п. 11; этап 6, п. 8): `letopis sync`, `types`, `load`, `maintain`; план 1.1,
|
|
4
|
+
* §2.5 — `mcp`; план 2.0, этап 3, п. 8 — `sync --check`.
|
|
5
5
|
*
|
|
6
|
-
* letopis sync <модуль моделей> --dsn … --schema v2.x (--token … | --api-key …) [--apply]
|
|
7
|
-
*
|
|
6
|
+
* letopis sync <модуль моделей> --dsn … --schema v2.x (--token … | --api-key …) [--apply | --check]
|
|
7
|
+
* --check — сухой прогон с кодом 1, если он нашёл изменения (создать или изменить); вместе с --apply — USAGE, код 2
|
|
8
|
+
* letopis types --dsn … --schema v2.x (--token … | --api-key …) [--out types.d.ts] [--system]
|
|
9
|
+
* --system — и классы арендатора System (системные, сиды пакета), без него — только классы арендатора сессии
|
|
8
10
|
* letopis load <файлы NDJSON…> --dsn … --schema v2.x (--token … | --api-key …) [--class C]
|
|
9
|
-
* [--duplicates error|first|last] [--existing error|skip|update] [--commit-every N] [--
|
|
10
|
-
*
|
|
11
|
-
* letopis maintain --dsn … --schema v2.x [--token … | --api-key …]
|
|
12
|
-
* letopis describe --dsn … --schema v2.x (--token … | --api-key …) [--format json|text] [--out файл]
|
|
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] [--initial] — код 1, если verify нашёл расхождения
|
|
15
|
-
* --initial — первая заливка (план 1.1, §2.3): пустая схема, одна транзакция, индексы и триггеры — после загрузки
|
|
11
|
+
* [--duplicates error|first|last] [--existing error|skip|update] [--commit-every N] [--initial]
|
|
12
|
+
* --initial — первая заливка (план 1.1, §2.3): пустая схема, одна транзакция, индексы и триггеры — после загрузки
|
|
13
|
+
* letopis maintain --dsn … --schema v2.x [--token … | --api-key …]
|
|
16
14
|
* letopis mcp --dsn … --schema v2.x --token … [--write] — сервер MCP по stdio (план 1.1, §2.5); dsn, schema и
|
|
17
15
|
* token — ещё из LETOPIS_DSN, LETOPIS_SCHEMA, LETOPIS_TOKEN; только токен пользователя: --api-key — отказ
|
|
16
|
+
* справка — USAGE (её же читает справочник сайта, guide/scripts/gen-api.ts)
|
|
18
17
|
*/
|
|
19
18
|
import { parseArgs } from 'node:util';
|
|
20
19
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
21
20
|
import { realpathSync } from 'node:fs';
|
|
22
21
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
23
22
|
import { resolve } from 'node:path';
|
|
24
|
-
import { connect
|
|
23
|
+
import { connect } from './index.js';
|
|
25
24
|
import { syncModels, generateTypes } from './sync.js';
|
|
26
|
-
import {
|
|
27
|
-
const USAGE = `letopis sync <модуль> --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--apply]
|
|
28
|
-
letopis types --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--out <файл>]
|
|
25
|
+
import { lerr, lmsg } from './errors.js';
|
|
26
|
+
const USAGE = `letopis sync <модуль> --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--apply | --check]
|
|
27
|
+
letopis types --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--out <файл>] [--system]
|
|
29
28
|
letopis load <файлы…> --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--class <Класс>]
|
|
30
|
-
[--duplicates error|first|last] [--existing error|skip|update] [--commit-every <N>] [--
|
|
31
|
-
letopis
|
|
32
|
-
letopis maintain --dsn <строка> --schema <v2.имя> [--token <токен> | --api-key <ключ>] [--batches <N>]
|
|
33
|
-
letopis describe --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--format json|text] [--out <файл>]
|
|
34
|
-
letopis import --from <строка 0.21> --from-schema <v1.имя> --dsn <строка> --schema <v2.имя> [--tenant <имя>] [--keyless <Класс>]…
|
|
35
|
-
[--rename <Старое>=<Новое>]… [--update] [--commit-every <N>] [--partition <имя>] [--initial]
|
|
29
|
+
[--duplicates error|first|last] [--existing error|skip|update] [--commit-every <N>] [--initial]
|
|
30
|
+
letopis maintain --dsn <строка> --schema <v2.имя> [--token <токен> | --api-key <ключ>]
|
|
36
31
|
letopis mcp --dsn <строка> --schema <v2.имя> --token <токен пользователя> [--write]
|
|
37
32
|
(или LETOPIS_DSN, LETOPIS_SCHEMA, LETOPIS_TOKEN; ключ сервиса --api-key не принимается)`;
|
|
33
|
+
/** Команды CLI: остальное — USAGE с кодом 2 ещё до подключения. */
|
|
34
|
+
const COMMANDS = new Set(['sync', 'types', 'load', 'maintain', 'mcp']);
|
|
38
35
|
const isModel = (v) => typeof v === 'object' && v !== null && typeof v.describe === 'function' && typeof v.name === 'string';
|
|
36
|
+
/** Модели из модуля (letopis sync): экспорты-модели и массивы моделей, без повторов. */
|
|
37
|
+
async function loadModels(path) {
|
|
38
|
+
const mod = await import(pathToFileURL(resolve(path)).href);
|
|
39
|
+
return [...new Set(Object.values(mod).flatMap((v) => (Array.isArray(v) ? v : [v])).filter(isModel))];
|
|
40
|
+
}
|
|
39
41
|
export async function main(argv) {
|
|
40
42
|
const { values, positionals } = parseArgs({
|
|
41
43
|
args: argv,
|
|
@@ -44,11 +46,7 @@ export async function main(argv) {
|
|
|
44
46
|
dsn: { type: 'string' }, schema: { type: 'string' }, token: { type: 'string' }, 'api-key': { type: 'string' },
|
|
45
47
|
apply: { type: 'boolean' }, out: { type: 'string' }, help: { type: 'boolean' },
|
|
46
48
|
class: { type: 'string' }, duplicates: { type: 'string' }, existing: { type: 'string' }, 'commit-every': { type: 'string' },
|
|
47
|
-
|
|
48
|
-
format: { type: 'string' },
|
|
49
|
-
from: { type: 'string' }, 'from-schema': { type: 'string' }, tenant: { type: 'string' }, partition: { type: 'string' },
|
|
50
|
-
keyless: { type: 'string', multiple: true }, rename: { type: 'string', multiple: true }, update: { type: 'boolean' },
|
|
51
|
-
initial: { type: 'boolean' }, write: { type: 'boolean' },
|
|
49
|
+
initial: { type: 'boolean' }, write: { type: 'boolean' }, system: { type: 'boolean' }, check: { type: 'boolean' },
|
|
52
50
|
},
|
|
53
51
|
});
|
|
54
52
|
const [cmd, arg] = positionals;
|
|
@@ -58,57 +56,40 @@ export async function main(argv) {
|
|
|
58
56
|
values.schema ??= process.env.LETOPIS_SCHEMA || undefined;
|
|
59
57
|
values.token ??= process.env.LETOPIS_TOKEN || undefined;
|
|
60
58
|
}
|
|
61
|
-
|
|
59
|
+
// --help — всегда код 0; неизвестная команда — USAGE до подключения к базе (план 2.0, 7.5 №4)
|
|
60
|
+
if (values.help) {
|
|
62
61
|
console.log(USAGE);
|
|
63
|
-
return
|
|
62
|
+
return 0;
|
|
64
63
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return 2;
|
|
70
|
-
}
|
|
71
|
-
const rename = {};
|
|
72
|
-
for (const x of values.rename ?? []) {
|
|
73
|
-
const [from, to] = x.split('=');
|
|
74
|
-
if (!from || !to) {
|
|
75
|
-
console.log(USAGE);
|
|
76
|
-
return 2;
|
|
77
|
-
}
|
|
78
|
-
rename[from] = to;
|
|
79
|
-
}
|
|
80
|
-
const r = await import021({
|
|
81
|
-
from: values.from, fromSchema: values['from-schema'], dsn: values.dsn, schema: values.schema, tenant: values.tenant,
|
|
82
|
-
partition: values.partition, keyless: values.keyless, rename, update: values.update,
|
|
83
|
-
commitEvery: values['commit-every'] ? Number(values['commit-every']) : undefined, initial: values.initial,
|
|
84
|
-
});
|
|
85
|
-
console.log(JSON.stringify(r, null, 2));
|
|
86
|
-
return r.verify.ok ? 0 : 1;
|
|
64
|
+
// --check — сухой прогон: вместе с --apply смысла нет
|
|
65
|
+
if (!cmd || !COMMANDS.has(cmd) || !values.dsn || !values.schema || (cmd === 'sync' && values.check && values.apply)) {
|
|
66
|
+
console.log(USAGE);
|
|
67
|
+
return cmd ? 2 : 0;
|
|
87
68
|
}
|
|
88
69
|
if (cmd === 'mcp')
|
|
89
70
|
return mcp(values);
|
|
90
71
|
// загрузчику нужно администраторское подключение (§2.6, п. 1)
|
|
91
72
|
const db = await connect({ dsn: values.dsn, schema: values.schema, token: values.token, apiKey: values['api-key'], max: 2,
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
...(cmd === 'load' || cmd === '
|
|
73
|
+
// maintain работает и на администраторском подключении без сессии; классы со строками синхронизирует
|
|
74
|
+
// администраторское подключение с сессией арендатора
|
|
75
|
+
...(cmd === 'load' || cmd === 'maintain' || cmd === 'sync' ? { allowBypassRls: true } : {}) });
|
|
95
76
|
try {
|
|
96
77
|
if (cmd === 'sync') {
|
|
97
78
|
if (!arg) {
|
|
98
79
|
console.log(USAGE);
|
|
99
80
|
return 2;
|
|
100
81
|
}
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
for (const [k, list] of [['создать', r.created], ['изменить', r.updated], ['системные (меняет установка)', r.skipped]]) {
|
|
82
|
+
const r = await syncModels(db, await loadModels(arg), { apply: values.apply });
|
|
83
|
+
const counts = { created: r.created.length, updated: r.updated.length, unchanged: r.unchanged.length, skipped: r.skipped.length };
|
|
84
|
+
console.log(lmsg(r.applied ? 'model.cli_sync_applied' : 'model.cli_sync_dry_run', counts));
|
|
85
|
+
for (const [key, list] of [['model.cli_sync_list_create', r.created], ['model.cli_sync_list_update', r.updated], ['model.cli_sync_list_system', r.skipped]]) {
|
|
106
86
|
if (list.length)
|
|
107
|
-
console.log(` ${
|
|
87
|
+
console.log(` ${lmsg(key, { names: list.join(', ') })}`);
|
|
108
88
|
}
|
|
109
89
|
for (const l of r.lost)
|
|
110
|
-
console.log(`
|
|
111
|
-
|
|
90
|
+
console.log(` ${lmsg('model.cli_sync_lost', { kind: l.kind, path: l.path })}`);
|
|
91
|
+
// --check (план 2.0, этап 3, п. 8): модели расходятся с базой — код 1, например для шага CI перед --apply
|
|
92
|
+
return values.check && (r.created.length || r.updated.length) ? 1 : 0;
|
|
112
93
|
}
|
|
113
94
|
if (cmd === 'load') {
|
|
114
95
|
const files = positionals.slice(1);
|
|
@@ -116,8 +97,6 @@ export async function main(argv) {
|
|
|
116
97
|
console.log(USAGE);
|
|
117
98
|
return 2;
|
|
118
99
|
}
|
|
119
|
-
if (values.trust)
|
|
120
|
-
throw new Error('letopis: removed: --trust снят в 1.0.0 — запрос по целям вне загрузки стоит 0,1–1,3 % её времени');
|
|
121
100
|
// несколько файлов — одна загрузка и одна транзакция; в файле — объект на строку
|
|
122
101
|
const rows = [];
|
|
123
102
|
for (const f of files) {
|
|
@@ -130,39 +109,23 @@ export async function main(argv) {
|
|
|
130
109
|
duplicates: values.duplicates,
|
|
131
110
|
existing: values.existing,
|
|
132
111
|
commitEvery: values['commit-every'] ? Number(values['commit-every']) : undefined,
|
|
133
|
-
verify: values.verify,
|
|
134
112
|
initial: values.initial,
|
|
135
113
|
});
|
|
136
114
|
console.log(JSON.stringify(r));
|
|
137
115
|
return 0;
|
|
138
116
|
}
|
|
139
|
-
if (cmd === 'verify') {
|
|
140
|
-
const r = await db.verify({ data: values.data });
|
|
141
|
-
console.log(JSON.stringify(r, null, 2));
|
|
142
|
-
return r.ok ? 0 : 1;
|
|
143
|
-
}
|
|
144
117
|
if (cmd === 'maintain') {
|
|
145
|
-
const r = await db.maintain(
|
|
118
|
+
const r = await db.maintain();
|
|
146
119
|
console.log(JSON.stringify(r));
|
|
147
120
|
return 0;
|
|
148
121
|
}
|
|
149
|
-
if (cmd === 'describe' || cmd === 'types') {
|
|
150
|
-
// обе команды читают реестр сессии: без действующей сессии (токен истёк, погашен, неверен или не задан) он пуст,
|
|
151
|
-
// и команда выдала бы пустой результат с кодом 0, а с --out затёрла бы им файл. acl.check ничего не пишет, а без
|
|
152
|
-
// сессии отказывает no_session — тогда выход с кодом 1, файл не тронут
|
|
153
|
-
await db.acl.check('letopis.cli');
|
|
154
|
-
}
|
|
155
|
-
if (cmd === 'describe') {
|
|
156
|
-
const d = db.describe();
|
|
157
|
-
const text = values.format === 'text' ? describeText(d) : JSON.stringify(d, null, 2);
|
|
158
|
-
if (values.out)
|
|
159
|
-
await writeFile(values.out, text);
|
|
160
|
-
else
|
|
161
|
-
console.log(text);
|
|
162
|
-
return 0;
|
|
163
|
-
}
|
|
164
122
|
if (cmd === 'types') {
|
|
165
|
-
|
|
123
|
+
// реестр сессии без действующей сессии (токен истёк, погашен, неверен или не задан) пуст, и команда выдала бы
|
|
124
|
+
// пустой результат с кодом 0, а с --out затёрла бы им файл. acl.check ничего не пишет, а без сессии отказывает
|
|
125
|
+
// no_session — тогда выход с кодом 1, файл не тронут
|
|
126
|
+
await db.acl.check('letopis.cli');
|
|
127
|
+
// --system — и классы арендатора System: системные и классы сидов пакета (например, booking)
|
|
128
|
+
const text = generateTypes(db.registry, { includeSystem: values.system === true });
|
|
166
129
|
if (values.out)
|
|
167
130
|
await writeFile(values.out, text);
|
|
168
131
|
else
|
|
@@ -184,11 +147,11 @@ export async function main(argv) {
|
|
|
184
147
|
*/
|
|
185
148
|
async function mcp(v) {
|
|
186
149
|
if (v['api-key'] !== undefined) {
|
|
187
|
-
console.error(
|
|
150
|
+
console.error(lerr('model.mcp_api_key_refused').message);
|
|
188
151
|
return 2;
|
|
189
152
|
}
|
|
190
153
|
if (!v.token) {
|
|
191
|
-
console.error('
|
|
154
|
+
console.error(lerr('model.mcp_token_required').message);
|
|
192
155
|
return 2;
|
|
193
156
|
}
|
|
194
157
|
const { createMcpServer } = await import('./mcp.js');
|