letopis 1.4.2 → 3.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.
Files changed (123) hide show
  1. package/AGENT-CHEATSHEET.md +516 -398
  2. package/CHANGELOG.md +739 -0
  3. package/LICENSE +75 -21
  4. package/LICENSE-COMMERCIAL.md +49 -0
  5. package/README.md +1312 -1190
  6. package/api-contract.json +381 -0
  7. package/dist/acl.d.ts +81 -1
  8. package/dist/acl.js +281 -13
  9. package/dist/admin.d.ts +11 -141
  10. package/dist/admin.js +28 -186
  11. package/dist/auth.d.ts +20 -5
  12. package/dist/auth.js +80 -21
  13. package/dist/cache.d.ts +8 -1
  14. package/dist/cache.js +30 -8
  15. package/dist/chain.d.ts +46 -48
  16. package/dist/chain.js +556 -223
  17. package/dist/cli.js +73 -96
  18. package/dist/demo/booking.d.ts +352 -36
  19. package/dist/describe.d.ts +47 -0
  20. package/dist/describe.js +195 -0
  21. package/dist/errors.d.ts +77 -13
  22. package/dist/errors.js +231 -30
  23. package/dist/index.d.ts +62 -74
  24. package/dist/index.js +312 -155
  25. package/dist/internal.d.ts +7 -0
  26. package/dist/jsonschema.js +18 -17
  27. package/dist/load.d.ts +29 -17
  28. package/dist/load.js +235 -190
  29. package/dist/log.d.ts +121 -0
  30. package/dist/log.js +633 -0
  31. package/dist/mcp.d.ts +5 -7
  32. package/dist/mcp.js +194 -225
  33. package/dist/messages/access.d.ts +90 -0
  34. package/dist/messages/access.js +113 -0
  35. package/dist/messages/api.d.ts +237 -0
  36. package/dist/messages/api.js +304 -0
  37. package/dist/messages/boot.d.ts +201 -0
  38. package/dist/messages/boot.js +256 -0
  39. package/dist/messages/codes.d.ts +172 -0
  40. package/dist/messages/codes.js +173 -0
  41. package/dist/messages/index.d.ts +98 -0
  42. package/dist/messages/index.js +121 -0
  43. package/dist/messages/log.d.ts +229 -0
  44. package/dist/messages/log.js +102 -0
  45. package/dist/messages/mcp.d.ts +306 -0
  46. package/dist/messages/mcp.js +387 -0
  47. package/dist/messages/model.d.ts +256 -0
  48. package/dist/messages/model.js +314 -0
  49. package/dist/messages/query.d.ts +141 -0
  50. package/dist/messages/query.js +171 -0
  51. package/dist/messages/sql-access.d.ts +70 -0
  52. package/dist/messages/sql-access.js +87 -0
  53. package/dist/messages/sql-core.d.ts +164 -0
  54. package/dist/messages/sql-core.js +198 -0
  55. package/dist/messages/sql-time.d.ts +86 -0
  56. package/dist/messages/sql-time.js +111 -0
  57. package/dist/messages/sql-write.d.ts +246 -0
  58. package/dist/messages/sql-write.js +270 -0
  59. package/dist/messages/time.d.ts +23 -0
  60. package/dist/messages/time.js +29 -0
  61. package/dist/messages/types.d.ts +27 -0
  62. package/dist/messages/types.js +5 -0
  63. package/dist/messages/write.d.ts +452 -0
  64. package/dist/messages/write.js +572 -0
  65. package/dist/model.d.ts +77 -60
  66. package/dist/model.js +43 -34
  67. package/dist/ops.d.ts +35 -0
  68. package/dist/ops.js +101 -0
  69. package/dist/pglite.d.ts +0 -9
  70. package/dist/pglite.js +3 -1
  71. package/dist/registry.d.ts +3 -19
  72. package/dist/registry.js +3 -16
  73. package/dist/signals.d.ts +1 -5
  74. package/dist/signals.js +1 -5
  75. package/dist/sql.d.ts +9 -1
  76. package/dist/sql.js +175 -107
  77. package/dist/sync.d.ts +10 -4
  78. package/dist/sync.js +90 -17
  79. package/dist/tx.d.ts +21 -2
  80. package/dist/tx.js +76 -13
  81. package/dist/typed.d.ts +252 -53
  82. package/dist/types.d.ts +93 -90
  83. package/dist/types.js +17 -14
  84. package/dist/up.d.ts +56 -35
  85. package/dist/up.js +229 -83
  86. package/dist/uuid.d.ts +8 -0
  87. package/dist/uuid.js +14 -6
  88. package/dist/validate.d.ts +13 -1
  89. package/dist/validate.js +82 -33
  90. package/dist/watch.d.ts +10 -16
  91. package/dist/watch.js +36 -77
  92. package/dist/write.d.ts +87 -17
  93. package/dist/write.js +441 -206
  94. package/llms.txt +48 -31
  95. package/package.json +13 -14
  96. package/sql/10-core.sql +7 -50
  97. package/sql/15-errors.sql +64 -10
  98. package/sql/20-context.sql +18 -9
  99. package/sql/30-validate.sql +79 -30
  100. package/sql/40-class.sql +41 -115
  101. package/sql/50-acl.sql +8 -13
  102. package/sql/60-write.sql +220 -676
  103. package/sql/70-read.sql +11 -13
  104. package/sql/80-auth.sql +109 -42
  105. package/sql/90-time.sql +108 -947
  106. package/sql/95-seed.system.sql +7 -14
  107. package/sql/99-revision.sql +6 -1
  108. package/sql/README.md +32 -24
  109. package/sql/migrate/0009-restore-rev.tenants-name.sql +10 -0
  110. package/sql/migrate/0010-impersonation-no-account.sql +5 -0
  111. package/sql/migrate/0011-key-lock.otp-enabled.sql +7 -0
  112. package/sql/migrate/README.md +18 -0
  113. package/AGENT-CHEATSHEET.en.md +0 -488
  114. package/MIGRATION.md +0 -190
  115. package/README.en.md +0 -2910
  116. package/dist/import.d.ts +0 -185
  117. package/dist/import.js +0 -820
  118. package/dist/project.d.ts +0 -359
  119. package/dist/project.js +0 -1258
  120. package/sql/75-history.sql +0 -436
  121. package/sql/88-derived.sql +0 -1236
  122. package/sql/migrate/0006-changed-paths.sql +0 -3
  123. package/sql/migrate/0007-secret-account-fk.sql +0 -12
package/dist/cli.js CHANGED
@@ -1,57 +1,74 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * CLI letopis (план, этап 2, п. 7; этап 4, п. 11; этап 6, п. 4, 8, 12): `letopis sync`, `types`, `load`,
4
- * `verify`, `maintain`, `describe`; этап 7 — `import` (из 0.21).
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
- * letopis types --dsn … --schema v2.x (--token … | --api-key …) [--out types.d.ts]
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] [--verify] [--initial]
10
- * letopis verify --dsn … --schema v2.x [--token … | --api-key …] [--data] — код 1 при находках
11
- * letopis maintain --dsn … --schema v2.x [--token … | --api-key …] [--batches N]
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
+ * --log[=уровень] — у любой команды (логирование 3.0): события JSON-строками в stderr, уровень debug|info|warn|error|off,
17
+ * без значения — info; без флага действует LETOPIS_LOG
18
+ * справка — USAGE (её же читает справочник сайта, guide/scripts/gen-api.ts)
18
19
  */
19
20
  import { parseArgs } from 'node:util';
20
21
  import { readFile, writeFile } from 'node:fs/promises';
21
22
  import { realpathSync } from 'node:fs';
22
23
  import { fileURLToPath, pathToFileURL } from 'node:url';
23
24
  import { resolve } from 'node:path';
24
- import { connect, describeText } from './index.js';
25
+ import { connect } from './index.js';
25
26
  import { syncModels, generateTypes } from './sync.js';
26
- import { import021 } from './import.js';
27
- import { LetopisError } from './errors.js';
28
- const USAGE = `letopis sync <модуль> --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--apply]
29
- letopis types --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--out <файл>]
27
+ import { lerr, lmsg } from './errors.js';
28
+ import { stderrLog } from './log.js';
29
+ const USAGE = `letopis sync <модуль> --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--apply | --check]
30
+ letopis types --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--out <файл>] [--system]
30
31
  letopis load <файлы…> --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--class <Класс>]
31
- [--duplicates error|first|last] [--existing error|skip|update] [--commit-every <N>] [--verify] [--initial]
32
- letopis verify --dsn <строка> --schema <v2.имя> [--token <токен> | --api-key <ключ>] [--data]
33
- letopis maintain --dsn <строка> --schema <v2.имя> [--token <токен> | --api-key <ключ>] [--batches <N>]
34
- letopis describe --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--format json|text] [--out <файл>]
35
- letopis import --from <строка 0.21> --from-schema <v1.имя> --dsn <строка> --schema <v2.имя> [--tenant <имя>] [--keyless <Класс>]…
36
- [--rename <Старое>=<Новое>]… [--update] [--commit-every <N>] [--partition <имя>] [--initial]
32
+ [--duplicates error|first|last] [--existing error|skip|update] [--commit-every <N>] [--initial]
33
+ letopis maintain --dsn <строка> --schema <v2.имя> [--token <токен> | --api-key <ключ>]
37
34
  letopis mcp --dsn <строка> --schema <v2.имя> --token <токен пользователя> [--write]
38
- (или LETOPIS_DSN, LETOPIS_SCHEMA, LETOPIS_TOKEN; ключ сервиса --api-key не принимается)`;
35
+ (или LETOPIS_DSN, LETOPIS_SCHEMA, LETOPIS_TOKEN; ключ сервиса --api-key не принимается)
36
+ у любой команды: --log[=debug|info|warn|error|off] — события JSON-строками в stderr (без значения — info)`;
39
37
  /** Команды CLI: остальное — USAGE с кодом 2 ещё до подключения. */
40
- const COMMANDS = new Set(['sync', 'types', 'load', 'verify', 'maintain', 'describe', 'import', 'mcp']);
38
+ const COMMANDS = new Set(['sync', 'types', 'load', 'maintain', 'mcp']);
41
39
  const isModel = (v) => typeof v === 'object' && v !== null && typeof v.describe === 'function' && typeof v.name === 'string';
40
+ /** Модели из модуля (letopis sync): экспорты-модели и массивы моделей, без повторов. */
41
+ async function loadModels(path) {
42
+ const mod = await import(pathToFileURL(resolve(path)).href);
43
+ return [...new Set(Object.values(mod).flatMap((v) => (Array.isArray(v) ? v : [v])).filter(isModel))];
44
+ }
45
+ /**
46
+ * Флаг --log[=уровень] (логирование 3.0): вынимается из аргументов до parseArgs — у строкового флага parseArgs требует
47
+ * значение, а --log без него значит info.
48
+ */
49
+ function logFlag(argv) {
50
+ let level;
51
+ const args = [];
52
+ for (const a of argv) {
53
+ if (a === '--log')
54
+ level = 'info';
55
+ else if (a.startsWith('--log='))
56
+ level = (a.slice('--log='.length) || 'info');
57
+ else
58
+ args.push(a);
59
+ }
60
+ return { args, log: level === undefined ? {} : { log: stderrLog, logLevel: level } };
61
+ }
42
62
  export async function main(argv) {
63
+ const { args, log } = logFlag(argv);
43
64
  const { values, positionals } = parseArgs({
44
- args: argv,
65
+ args,
45
66
  allowPositionals: true,
46
67
  options: {
47
68
  dsn: { type: 'string' }, schema: { type: 'string' }, token: { type: 'string' }, 'api-key': { type: 'string' },
48
69
  apply: { type: 'boolean' }, out: { type: 'string' }, help: { type: 'boolean' },
49
70
  class: { type: 'string' }, duplicates: { type: 'string' }, existing: { type: 'string' }, 'commit-every': { type: 'string' },
50
- verify: { type: 'boolean' }, trust: { type: 'boolean' }, data: { type: 'boolean' }, batches: { type: 'string' },
51
- format: { type: 'string' },
52
- from: { type: 'string' }, 'from-schema': { type: 'string' }, tenant: { type: 'string' }, partition: { type: 'string' },
53
- keyless: { type: 'string', multiple: true }, rename: { type: 'string', multiple: true }, update: { type: 'boolean' },
54
- initial: { type: 'boolean' }, write: { type: 'boolean' },
71
+ initial: { type: 'boolean' }, write: { type: 'boolean' }, system: { type: 'boolean' }, check: { type: 'boolean' },
55
72
  },
56
73
  });
57
74
  const [cmd, arg] = positionals;
@@ -66,57 +83,35 @@ export async function main(argv) {
66
83
  console.log(USAGE);
67
84
  return 0;
68
85
  }
69
- if (!cmd || !COMMANDS.has(cmd) || !values.dsn || !values.schema) {
86
+ // --check — сухой прогон: вместе с --apply смысла нет
87
+ if (!cmd || !COMMANDS.has(cmd) || !values.dsn || !values.schema || (cmd === 'sync' && values.check && values.apply)) {
70
88
  console.log(USAGE);
71
89
  return cmd ? 2 : 0;
72
90
  }
73
- if (cmd === 'import') {
74
- // импорт сам открывает администраторские подключения к 0.21 и к 1.0 (загрузчик, функции владельца)
75
- if (!values.from || !values['from-schema']) {
76
- console.log(USAGE);
77
- return 2;
78
- }
79
- const rename = {};
80
- for (const x of values.rename ?? []) {
81
- const [from, to] = x.split('=');
82
- if (!from || !to) {
83
- console.log(USAGE);
84
- return 2;
85
- }
86
- rename[from] = to;
87
- }
88
- const r = await import021({
89
- from: values.from, fromSchema: values['from-schema'], dsn: values.dsn, schema: values.schema, tenant: values.tenant,
90
- partition: values.partition, keyless: values.keyless, rename, update: values.update,
91
- commitEvery: values['commit-every'] ? Number(values['commit-every']) : undefined, initial: values.initial,
92
- });
93
- console.log(JSON.stringify(r, null, 2));
94
- return r.verify.ok ? 0 : 1;
95
- }
96
91
  if (cmd === 'mcp')
97
- return mcp(values);
92
+ return mcp(values, log);
98
93
  // загрузчику нужно администраторское подключение (§2.6, п. 1)
99
- const db = await connect({ dsn: values.dsn, schema: values.schema, token: values.token, apiKey: values['api-key'], max: 2,
100
- // verify и maintain работают и на администраторском подключении без сессии; производные классы (derivedBy)
101
- // синхронизирует администраторское подключение с сессией арендатора
102
- ...(cmd === 'load' || cmd === 'verify' || cmd === 'maintain' || cmd === 'sync' ? { allowBypassRls: true } : {}) });
94
+ const db = await connect({ dsn: values.dsn, schema: values.schema, token: values.token, apiKey: values['api-key'], max: 2, ...log,
95
+ // maintain работает и на администраторском подключении без сессии; классы со строками синхронизирует
96
+ // администраторское подключение с сессией арендатора
97
+ ...(cmd === 'load' || cmd === 'maintain' || cmd === 'sync' ? { allowBypassRls: true } : {}) });
103
98
  try {
104
99
  if (cmd === 'sync') {
105
100
  if (!arg) {
106
101
  console.log(USAGE);
107
102
  return 2;
108
103
  }
109
- const mod = await import(pathToFileURL(resolve(arg)).href);
110
- const models = [...new Set(Object.values(mod).flatMap((v) => (Array.isArray(v) ? v : [v])).filter(isModel))];
111
- const r = await syncModels(db, models, { apply: values.apply });
112
- console.log(`${r.applied ? 'применено' : 'сухой прогон'}: создать ${r.created.length}, изменить ${r.updated.length}, без изменений ${r.unchanged.length}, системных ${r.skipped.length}`);
113
- for (const [k, list] of [['создать', r.created], ['изменить', r.updated], ['системные (меняет установка)', r.skipped]]) {
104
+ const r = await syncModels(db, await loadModels(arg), { apply: values.apply });
105
+ const counts = { created: r.created.length, updated: r.updated.length, unchanged: r.unchanged.length, skipped: r.skipped.length };
106
+ console.log(lmsg(r.applied ? 'model.cli_sync_applied' : 'model.cli_sync_dry_run', counts));
107
+ 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]]) {
114
108
  if (list.length)
115
- console.log(` ${k}: ${list.join(', ')}`);
109
+ console.log(` ${lmsg(key, { names: list.join(', ') })}`);
116
110
  }
117
111
  for (const l of r.lost)
118
- console.log(` правило ${l.kind} в ${l.path} проверяется только в библиотеке — в базе его нет`);
119
- return 0;
112
+ console.log(` ${lmsg('model.cli_sync_lost', { kind: l.kind, path: l.path })}`);
113
+ // --check (план 2.0, этап 3, п. 8): модели расходятся с базой — код 1, например для шага CI перед --apply
114
+ return values.check && (r.created.length || r.updated.length) ? 1 : 0;
120
115
  }
121
116
  if (cmd === 'load') {
122
117
  const files = positionals.slice(1);
@@ -124,8 +119,6 @@ export async function main(argv) {
124
119
  console.log(USAGE);
125
120
  return 2;
126
121
  }
127
- if (values.trust)
128
- throw new LetopisError('removed', '--trust снят в 1.0.0 — запрос по целям вне загрузки стоит 0,1–1,3 % её времени');
129
122
  // несколько файлов — одна загрузка и одна транзакция; в файле — объект на строку
130
123
  const rows = [];
131
124
  for (const f of files) {
@@ -138,39 +131,23 @@ export async function main(argv) {
138
131
  duplicates: values.duplicates,
139
132
  existing: values.existing,
140
133
  commitEvery: values['commit-every'] ? Number(values['commit-every']) : undefined,
141
- verify: values.verify,
142
134
  initial: values.initial,
143
135
  });
144
136
  console.log(JSON.stringify(r));
145
137
  return 0;
146
138
  }
147
- if (cmd === 'verify') {
148
- const r = await db.verify({ data: values.data });
149
- console.log(JSON.stringify(r, null, 2));
150
- return r.ok ? 0 : 1;
151
- }
152
139
  if (cmd === 'maintain') {
153
- const r = await db.maintain({ batches: values.batches ? Number(values.batches) : undefined });
140
+ const r = await db.maintain();
154
141
  console.log(JSON.stringify(r));
155
142
  return 0;
156
143
  }
157
- if (cmd === 'describe' || cmd === 'types') {
158
- // обе команды читают реестр сессии: без действующей сессии (токен истёк, погашен, неверен или не задан) он пуст,
159
- // и команда выдала бы пустой результат с кодом 0, а с --out затёрла бы им файл. acl.check ничего не пишет, а без
160
- // сессии отказывает no_session — тогда выход с кодом 1, файл не тронут
161
- await db.acl.check('letopis.cli');
162
- }
163
- if (cmd === 'describe') {
164
- const d = db.describe();
165
- const text = values.format === 'text' ? describeText(d) : JSON.stringify(d, null, 2);
166
- if (values.out)
167
- await writeFile(values.out, text);
168
- else
169
- console.log(text);
170
- return 0;
171
- }
172
144
  if (cmd === 'types') {
173
- const text = generateTypes(db.registry);
145
+ // реестр сессии без действующей сессии (токен истёк, погашен, неверен или не задан) пуст, и команда выдала бы
146
+ // пустой результат с кодом 0, а с --out затёрла бы им файл. acl.check ничего не пишет, а без сессии отказывает
147
+ // no_session — тогда выход с кодом 1, файл не тронут
148
+ await db.acl.check('letopis.cli');
149
+ // --system — и классы арендатора System: системные и классы сидов пакета (например, booking)
150
+ const text = generateTypes(db.registry, { includeSystem: values.system === true });
174
151
  if (values.out)
175
152
  await writeFile(values.out, text);
176
153
  else
@@ -190,17 +167,17 @@ export async function main(argv) {
190
167
  * грузятся только здесь: letopis --help и прочие команды работают без SDK. В stdout — только протокол (предупреждения
191
168
  * connect() идут в stderr); процесс живёт, пока клиент не закроет stdin.
192
169
  */
193
- async function mcp(v) {
170
+ async function mcp(v, log = {}) {
194
171
  if (v['api-key'] !== undefined) {
195
- console.error('letopis: user_session_required: letopis mcp принимает только токен сессии пользователя (--token или LETOPIS_TOKEN): ключ сервиса даёт все права auth.* и не должен лежать на машине агента');
172
+ console.error(lerr('model.mcp_api_key_refused').message);
196
173
  return 2;
197
174
  }
198
175
  if (!v.token) {
199
- console.error('letopis: user_session_required: letopis mcp — нужен токен сессии пользователя: --token или LETOPIS_TOKEN');
176
+ console.error(lerr('model.mcp_token_required').message);
200
177
  return 2;
201
178
  }
202
179
  const { createMcpServer } = await import('./mcp.js');
203
- const db = await connect({ dsn: v.dsn, schema: v.schema, token: v.token, max: 4 });
180
+ const db = await connect({ dsn: v.dsn, schema: v.schema, token: v.token, max: 4, ...log });
204
181
  try {
205
182
  const opts = { write: v.write === true };
206
183
  // учётные данные — до начала протокола: отказ виден кодом выхода и текстом в stderr