letopis 2.0.0 → 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 (75) hide show
  1. package/AGENT-CHEATSHEET.md +484 -414
  2. package/CHANGELOG.md +321 -0
  3. package/LICENSE +75 -21
  4. package/LICENSE-COMMERCIAL.md +49 -0
  5. package/README.md +786 -183
  6. package/api-contract.json +381 -0
  7. package/dist/acl.d.ts +3 -0
  8. package/dist/acl.js +19 -2
  9. package/dist/admin.d.ts +2 -1
  10. package/dist/admin.js +6 -4
  11. package/dist/auth.d.ts +18 -3
  12. package/dist/auth.js +75 -6
  13. package/dist/chain.d.ts +38 -17
  14. package/dist/chain.js +434 -63
  15. package/dist/cli.js +28 -6
  16. package/dist/errors.d.ts +19 -1
  17. package/dist/errors.js +46 -6
  18. package/dist/index.d.ts +10 -4
  19. package/dist/index.js +136 -46
  20. package/dist/internal.d.ts +2 -0
  21. package/dist/load.d.ts +9 -5
  22. package/dist/load.js +30 -11
  23. package/dist/log.d.ts +121 -0
  24. package/dist/log.js +633 -0
  25. package/dist/mcp.js +26 -11
  26. package/dist/messages/api.d.ts +66 -0
  27. package/dist/messages/api.js +90 -1
  28. package/dist/messages/boot.js +4 -4
  29. package/dist/messages/codes.js +4 -4
  30. package/dist/messages/index.d.ts +5 -2
  31. package/dist/messages/index.js +5 -2
  32. package/dist/messages/log.d.ts +229 -0
  33. package/dist/messages/log.js +102 -0
  34. package/dist/messages/model.d.ts +24 -1
  35. package/dist/messages/model.js +34 -3
  36. package/dist/messages/query.d.ts +10 -0
  37. package/dist/messages/query.js +13 -0
  38. package/dist/messages/sql-core.d.ts +15 -1
  39. package/dist/messages/sql-core.js +20 -3
  40. package/dist/messages/sql-time.d.ts +15 -0
  41. package/dist/messages/sql-time.js +23 -0
  42. package/dist/messages/write.d.ts +47 -0
  43. package/dist/messages/write.js +63 -0
  44. package/dist/model.d.ts +4 -0
  45. package/dist/model.js +23 -5
  46. package/dist/sql.d.ts +2 -0
  47. package/dist/sql.js +77 -25
  48. package/dist/sync.d.ts +1 -1
  49. package/dist/sync.js +50 -0
  50. package/dist/tx.d.ts +10 -1
  51. package/dist/tx.js +44 -2
  52. package/dist/typed.d.ts +14 -10
  53. package/dist/types.d.ts +82 -4
  54. package/dist/types.js +6 -1
  55. package/dist/up.d.ts +3 -3
  56. package/dist/up.js +57 -17
  57. package/dist/uuid.d.ts +8 -0
  58. package/dist/uuid.js +9 -1
  59. package/dist/validate.js +24 -7
  60. package/dist/watch.d.ts +5 -0
  61. package/dist/watch.js +13 -0
  62. package/dist/write.d.ts +78 -1
  63. package/dist/write.js +357 -39
  64. package/llms.txt +37 -17
  65. package/package.json +12 -6
  66. package/sql/20-context.sql +11 -3
  67. package/sql/30-validate.sql +51 -10
  68. package/sql/40-class.sql +21 -1
  69. package/sql/80-auth.sql +88 -17
  70. package/sql/90-time.sql +34 -6
  71. package/sql/99-revision.sql +5 -1
  72. package/sql/migrate/0009-restore-rev.tenants-name.sql +10 -0
  73. package/sql/migrate/0010-impersonation-no-account.sql +5 -0
  74. package/sql/migrate/0011-key-lock.otp-enabled.sql +7 -0
  75. package/sql/migrate/README.md +5 -1
package/dist/mcp.js CHANGED
@@ -27,9 +27,11 @@ import { createRequire } from 'node:module';
27
27
  import { describeModel } from './describe.js';
28
28
  import { LetopisError, checkOptions, lerr, lmsg, similar } from './errors.js';
29
29
  import { internalsOf } from './internal.js';
30
+ import { NOLOG } from './log.js';
30
31
  import { OPERATORS, opHint, or } from './ops.js';
31
32
  import { cursorOf } from './sql.js';
32
33
  import { ACL_CLASS_NAMES, RESERVED_CLASS_NAMES } from './types.js';
34
+ import { UUID_RE } from './uuid.js';
33
35
  /** Все опции createMcpServer: неизвестная — invalid_data с похожими именами (checkOptions; план 2.0, 7.10 №4). */
34
36
  const MCP_OPTIONS = {
35
37
  write: true, maxRows: true, maxBytes: true, allowAclWrite: true, allowSystemAdmin: true,
@@ -43,7 +45,7 @@ const STATE_KEY = randomBytes(32);
43
45
  const CONFIRM_TTL_MS = 10 * 60_000;
44
46
  const RESERVED = new Set(RESERVED_CLASS_NAMES);
45
47
  const SYSTEM_ID = '2eba6d0a-1edd-4bb6-a85a-a703dab49035';
46
- const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
48
+ const UUID = UUID_RE;
47
49
  const TERMINALS = ['rows', 'first', 'ids', 'count', 'sum', 'avg', 'min', 'max', 'countBy'];
48
50
  const PAGED = new Set(['rows', 'first', 'ids']);
49
51
  /** Шагов цепочки и условий $or в одном фильтре не больше (ревью безопасности №2): запрос растёт с каждым. */
@@ -375,6 +377,8 @@ function stepFilter(a, where) {
375
377
  return undefined;
376
378
  }
377
379
  const step = (x, name) => x[name];
380
+ /** Шаг с фильтром или без: без фильтра — вызов без аргумента (X(undefined) цепочка отмечает предупреждением, A13). */
381
+ const stepWith = (fn, f) => (f === undefined ? fn() : fn(f));
378
382
  const encodeCursor = (c) => Buffer.from(JSON.stringify(c)).toString('base64url');
379
383
  function decodeCursor(s) {
380
384
  let c;
@@ -425,7 +429,7 @@ async function query(db, a, o) {
425
429
  if (typeof terminal !== 'string' || !TERMINALS.includes(terminal)) {
426
430
  throw lerr('mcp.unknown_terminal', { terminal: String(terminal), similar: similar(String(terminal), TERMINALS) });
427
431
  }
428
- let chain = step(db, stepName(reg, a.class, 'letopis_query.class'))(stepFilter(a, 'letopis_query'));
432
+ let chain = stepWith(step(db, stepName(reg, a.class, 'letopis_query.class')), stepFilter(a, 'letopis_query'));
429
433
  if (a.steps !== undefined) {
430
434
  if (!Array.isArray(a.steps))
431
435
  throw lerr('mcp.steps_not_list');
@@ -436,7 +440,7 @@ async function query(db, a, o) {
436
440
  if (!isObj(s))
437
441
  throw lerr('mcp.step_not_object', { where });
438
442
  checkKeys(s, ['step', 'id', 'ids', 'filter'], where);
439
- chain = step(chain, stepName(reg, s.step, `${where}.step`))(stepFilter(s, where));
443
+ chain = stepWith(step(chain, stepName(reg, s.step, `${where}.step`)), stepFilter(s, where));
440
444
  });
441
445
  }
442
446
  if (a.asOf !== undefined) {
@@ -674,13 +678,14 @@ async function write(db, a, ctx, sdk, server, o, who) {
674
678
  return c.ask;
675
679
  if (!c.granted)
676
680
  throw lerr(c.reason);
677
- // превью и удаление — одной транзакцией: подтверждено ровно то, что удаляется
681
+ // превью и удаление — одной транзакцией: подтверждено ровно то, что удаляется; delete сверяет превью сам (expect,
682
+ // 3.0) — под блокировкой замыкания каскада
678
683
  const tx = await db.begin();
679
684
  try {
680
685
  const again = await step(tx, cls)(targets).delete(opts).rows();
681
686
  if (operationOf(db, cls, targets, again) !== op)
682
687
  throw lerr('mcp.delete_data_changed');
683
- const rows = await step(tx, cls)(targets).delete({ confirm: true, ...opts }).rows();
688
+ const rows = await step(tx, cls)(targets).delete({ confirm: true, expect: again, ...opts }).rows();
684
689
  await tx.commit();
685
690
  return ok({ deleted: rows, operation: op });
686
691
  }
@@ -748,19 +753,29 @@ async function serve(db, opts) {
748
753
  const who = await checkCredentials(db, o);
749
754
  const sdk = await loadSdk();
750
755
  const server = new sdk.McpServer({ name: 'letopis', version: VERSION }, { instructions: instructions(o), capabilities: { tools: {} } });
751
- const guard = (fn) => async (args, ctx) => {
756
+ // логирование 3.0: вызов инструмента — mcp.call (имя, время, исход), отказ — ещё error; логгер — фасада db
757
+ const log = internalsOf(db)?.log ?? NOLOG;
758
+ const guard = (tool, fn) => async (args, ctx) => {
759
+ const t0 = performance.now();
752
760
  try {
753
- return await fn(isObj(args) ? args : {}, ctx);
761
+ const r = await fn(isObj(args) ? args : {}, ctx);
762
+ if (log.wants('mcp.call')) {
763
+ log.emit('mcp.call', { op: tool, ms: performance.now() - t0, detail: { ok: !r.isError } });
764
+ }
765
+ return r;
754
766
  }
755
767
  catch (e) {
768
+ log.error(e, { op: tool, ms: performance.now() - t0 });
769
+ if (log.wants('mcp.call'))
770
+ log.emit('mcp.call', { op: tool, ms: performance.now() - t0, detail: { ok: false } });
756
771
  return failure(e);
757
772
  }
758
773
  };
759
- server.registerTool('letopis_model', { title: 'Модель letopis', description: DESCRIBE_MODEL, inputSchema: sdk.fromJsonSchema(MODEL_SCHEMA), annotations: READ_ONLY }, guard((a) => model(db, a, o)));
760
- server.registerTool('letopis_query', { title: 'Чтение letopis', description: describeQuery(o), inputSchema: sdk.fromJsonSchema(QUERY_SCHEMA), annotations: READ_ONLY }, guard((a) => query(db, a, o)));
761
- server.registerTool('letopis_history', { title: 'История letopis', description: describeHistory(o), inputSchema: sdk.fromJsonSchema(HISTORY_SCHEMA), annotations: READ_ONLY }, guard((a) => history(db, a, o)));
774
+ server.registerTool('letopis_model', { title: 'Модель letopis', description: DESCRIBE_MODEL, inputSchema: sdk.fromJsonSchema(MODEL_SCHEMA), annotations: READ_ONLY }, guard('letopis_model', (a) => model(db, a, o)));
775
+ server.registerTool('letopis_query', { title: 'Чтение letopis', description: describeQuery(o), inputSchema: sdk.fromJsonSchema(QUERY_SCHEMA), annotations: READ_ONLY }, guard('letopis_query', (a) => query(db, a, o)));
776
+ server.registerTool('letopis_history', { title: 'История letopis', description: describeHistory(o), inputSchema: sdk.fromJsonSchema(HISTORY_SCHEMA), annotations: READ_ONLY }, guard('letopis_history', (a) => history(db, a, o)));
762
777
  if (o.write) {
763
- server.registerTool('letopis_write', { title: 'Запись letopis', description: describeWrite(o), inputSchema: sdk.fromJsonSchema(WRITE_SCHEMA), annotations: DESTRUCTIVE }, guard((a, ctx) => write(db, a, ctx, sdk, server, o, who)));
778
+ server.registerTool('letopis_write', { title: 'Запись letopis', description: describeWrite(o), inputSchema: sdk.fromJsonSchema(WRITE_SCHEMA), annotations: DESTRUCTIVE }, guard('letopis_write', (a, ctx) => write(db, a, ctx, sdk, server, o, who)));
764
779
  }
765
780
  return server;
766
781
  }
@@ -6,6 +6,51 @@ export declare const api: {
6
6
  'api.unknown_step': {
7
7
  code: "invalid_class";
8
8
  ru: string;
9
+ hint: string;
10
+ };
11
+ 'api.token_not_active': {
12
+ code: "no_session";
13
+ ru: string;
14
+ hint: string;
15
+ };
16
+ 'api.unknown_step_no_classes': {
17
+ code: "invalid_class";
18
+ ru: string;
19
+ hint: string;
20
+ };
21
+ 'api.chain_not_rows': {
22
+ code: "invalid_query";
23
+ ru: string;
24
+ hint: string;
25
+ };
26
+ 'api.step_undefined_filter': {
27
+ code: "invalid_query";
28
+ ru: string;
29
+ };
30
+ 'api.sort_not_last': {
31
+ code: "invalid_query";
32
+ ru: string;
33
+ };
34
+ 'api.with_deleted_step': {
35
+ code: "invalid_query";
36
+ ru: string;
37
+ };
38
+ 'api.db_bad_value': {
39
+ code: "invalid_query";
40
+ ru: string;
41
+ hint: string;
42
+ };
43
+ 'api.validation_issue': {
44
+ code: "invalid_data";
45
+ ru: string;
46
+ };
47
+ 'api.validation_root': {
48
+ code: "invalid_data";
49
+ ru: string;
50
+ };
51
+ 'api.validation_more': {
52
+ code: "invalid_data";
53
+ ru: string;
9
54
  };
10
55
  'api.connect_schema': {
11
56
  code: "invalid_data";
@@ -49,6 +94,7 @@ export declare const api: {
49
94
  'api.for_update_unsupported': {
50
95
  code: "lock_unsupported";
51
96
  ru: string;
97
+ hint: string;
52
98
  };
53
99
  'api.lock_denied': {
54
100
  code: "acl_denied";
@@ -119,6 +165,11 @@ export declare const api: {
119
165
  ru: string;
120
166
  hint: string;
121
167
  };
168
+ 'api.begin_arg': {
169
+ code: "invalid_query";
170
+ ru: string;
171
+ hint: string;
172
+ };
122
173
  'api.nested_begin': {
123
174
  code: "invalid_query";
124
175
  ru: string;
@@ -132,6 +183,11 @@ export declare const api: {
132
183
  ru: string;
133
184
  hint: string;
134
185
  };
186
+ 'api.connect_empty_credential': {
187
+ code: "invalid_data";
188
+ ru: string;
189
+ hint: string;
190
+ };
135
191
  'api.connect_admin_type': {
136
192
  code: "invalid_data";
137
193
  ru: string;
@@ -168,4 +224,14 @@ export declare const api: {
168
224
  code: "invalid_data";
169
225
  ru: string;
170
226
  };
227
+ 'api.create_tenant_admin': {
228
+ code: "invalid_query";
229
+ ru: string;
230
+ hint: string;
231
+ };
232
+ 'api.versions_terminal': {
233
+ code: "invalid_query";
234
+ ru: string;
235
+ hint: string;
236
+ };
171
237
  };
@@ -8,6 +8,67 @@ export const api = {
8
8
  'api.unknown_step': {
9
9
  code: 'invalid_class',
10
10
  ru: 'класс или роль {name} не найдены',
11
+ hint: 'классы арендатора видны только в его сессии: арендаторы пользователя — db.auth.tenants(), переход — db.auth.switch(tenant) или db.as(аккаунт, { tenant }); имя — как в модели (классы записывает syncModels)',
12
+ },
13
+ // chain.ts: шаг не найден, а в реестре сессии нет ни одного класса, кроме классов System — сессия, похоже, в
14
+ // личном арендаторе (вход без switch); похожие имена — params.similar
15
+ // index.ts: connect({ token }) — токен не даёт сессии (погашен, истёк, неверен); chain.ts: шаг корня без сессии
16
+ // (сессия погасла после connect) — отказ терминала, а не синхронный
17
+ 'api.token_not_active': {
18
+ code: 'no_session',
19
+ ru: 'нет действующей сессии: токен погашен, истёк или неверен',
20
+ hint: 'войти заново и подключиться с новым токеном; веб-сервису — одно подключение сервиса и svc.as(account, { tenant }) на запрос, а не connect({ token }) на каждый вход',
21
+ },
22
+ 'api.unknown_step_no_classes': {
23
+ code: 'invalid_class',
24
+ ru: 'класс или роль {name} не найдены: в реестре сессии нет ни одного класса арендатора — сессия, похоже, в личном арендаторе',
25
+ hint: 'после входа сессия работает в личном арендаторе пользователя: найти нужного — db.auth.tenants(), перейти — db.auth.switch(tenant) или db.as(аккаунт, { tenant }); классы в арендатор записывает syncModels',
26
+ },
27
+ // chain.ts: у цепочки спросили свойство массива (length, map, filter, 0 …) или перебрали её — она не массив строк
28
+ 'api.chain_not_rows': {
29
+ code: 'invalid_query',
30
+ ru: 'цепочка — не массив: {name} есть у строк ответа, а не у цепочки',
31
+ hint: 'получить строки терминалом с await: const rows = await db.Класс(…).rows(), одну строку — await ….first()',
32
+ },
33
+ // chain.ts, A13: шаг вызван с аргументом undefined — фильтра нет, шаг находит все строки класса (как без аргумента);
34
+ // предупреждение (lwarn), один раз на имя шага
35
+ 'api.step_undefined_filter': {
36
+ code: 'invalid_query',
37
+ ru: '{name}(undefined) — фильтра нет: шаг находит все строки, как {name}(). Проверьте переменную с id',
38
+ },
39
+ // chain.ts, A13: шаг после sort() — сортировка относится к последнему шагу цепочки, а не к тому, после которого
40
+ // стоит; предупреждение (lwarn), один раз на пару
41
+ 'api.sort_not_last': {
42
+ code: 'invalid_query',
43
+ ru: "sort('{field}') перед шагом {step} сортирует строки последнего шага ({step}), а не прежнего: ставьте sort() в конец цепочки",
44
+ },
45
+ // chain.ts, A9: withDeleted() не у последнего шага, а у последнего — нет; предупреждение (lwarn), один раз на пару:
46
+ // с 2.1 withDeleted() — модификатор своего шага
47
+ 'api.with_deleted_step': {
48
+ code: 'invalid_query',
49
+ ru: 'withDeleted() у шага {step} относится к нему, а не к последнему шагу {last}: удалённые {last} — .{last}().withDeleted()',
50
+ },
51
+ // errors.ts, fromDbError: подстраховка — значение не того типа (SQLSTATE 22P02), которое не поймала проверка до
52
+ // запроса; text — текст сервера
53
+ 'api.db_bad_value': {
54
+ code: 'invalid_query',
55
+ ru: 'значение не того типа: {text}',
56
+ hint: 'id — строка uuid, числа — числа, даты — строки ISO; проверьте значения фильтра, курсора и опций',
57
+ },
58
+ // errors.ts, ValidationError: первое нарушение в тексте ошибки — путь и сообщение; текст не для ошибки (lmsg)
59
+ 'api.validation_issue': {
60
+ code: 'invalid_data',
61
+ ru: '{path} — {issue}',
62
+ },
63
+ // errors.ts, ValidationError: путь нарушения на верхнем уровне данных (пустой путь); текст не для ошибки (lmsg)
64
+ 'api.validation_root': {
65
+ code: 'invalid_data',
66
+ ru: 'данные',
67
+ },
68
+ // errors.ts, ValidationError: число нарушений, если их больше одного; текст не для ошибки (lmsg)
69
+ 'api.validation_more': {
70
+ code: 'invalid_data',
71
+ ru: ' (всего нарушений: {n}, все — err.issues)',
11
72
  },
12
73
  // index.ts: connect({ schema })
13
74
  'api.connect_schema': {
@@ -61,9 +122,11 @@ export const api = {
61
122
  ru: 'forUpdate() — только внутри db.begin(): вне явной транзакции блокировка снялась бы сразу',
62
123
  },
63
124
  // chain.ts, lockedRead: forUpdate() с другим терминалом, агрегатом, срезом, глаголом
125
+ // chain.ts, lockedRead: what — что мешает блокировке (терминал, asOf(), withDeleted(), deep(), глагол)
64
126
  'api.for_update_unsupported': {
65
127
  code: 'lock_unsupported',
66
- ru: 'forUpdate() — с терминалами rows, first, ids, без агрегатов, asOf, versions, withDeleted, deep и глаголов',
128
+ ru: 'forUpdate() нельзя с {what}: блокировка — только у чтения rows(), first(), ids()',
129
+ hint: 'forUpdate() — в db.begin(), перед rows(), first() или ids(); без asOf(), versions(), withDeleted(), deep(), count(), paths(), агрегатов и глаголов. Правка — отдельной цепочкой после чтения: await tr.X(id).forUpdate().first(); await tr.X(id).update(…).rows()',
67
130
  },
68
131
  // chain.ts, lockedRead: строки видны, но политика правки не даёт их заблокировать; ids — через запятую (detail.ids — списком)
69
132
  'api.lock_denied': {
@@ -152,6 +215,13 @@ export const api = {
152
215
  hint: 'повторить вызов: между запросами db.as() сессия сменила арендатора или кончилась',
153
216
  },
154
217
  // index.ts: db.begin() внутри db.begin()
218
+ // index.ts: db.begin(аргумент) — не объект опций (функция, строка, число); функцию begin() не вызывает; type — typeof
219
+ // аргумента (array — массив)
220
+ 'api.begin_arg': {
221
+ code: 'invalid_query',
222
+ ru: 'db.begin() принимает только опции { reason }, а не {type}: транзакцию открывает begin(), закрывают commit() и rollback()',
223
+ hint: 'const tr = await db.begin(); try { …; await tr.commit() } catch (e) { await tr.rollback(); throw e }',
224
+ },
155
225
  'api.nested_begin': {
156
226
  code: 'invalid_query',
157
227
  ru: 'вложенных транзакций нет: db.begin() внутри db.begin()',
@@ -167,6 +237,12 @@ export const api = {
167
237
  ru: 'connect: нужен dsn или sql',
168
238
  hint: 'передать dsn — строку подключения — или sql — готовый пул postgres.js',
169
239
  },
240
+ // index.ts: connect({ apiKey | service | token }) передан, но пуст (null, '', не строка) — опция есть, а значения нет
241
+ 'api.connect_empty_credential': {
242
+ code: 'invalid_data',
243
+ ru: 'connect: {name} передан, но пуст ({value}) — подключение без сессии не открывается',
244
+ hint: 'передать ключ сервиса или токен строкой (process.env.SERVICE_KEY не задана?) — либо убрать опцию, если сессия не нужна; ключ сервиса up() выдаёт один раз — храните его сами',
245
+ },
170
246
  // index.ts: connect({ admin }) — не строка и не пул
171
247
  'api.connect_admin_type': {
172
248
  code: 'invalid_data',
@@ -212,4 +288,17 @@ export const api = {
212
288
  code: 'invalid_data',
213
289
  ru: 'движок схемы {schema} ревизии {rev} новее библиотеки (она знает {known}) — обновите пакет letopis',
214
290
  },
291
+ // up.ts, createTenant(): первый аргумент — не пул postgres() (строка DSN, подключение connect(), объект без begin и
292
+ // unsafe); type — typeof значения или null
293
+ 'api.create_tenant_admin': {
294
+ code: 'invalid_query',
295
+ ru: 'createTenant(admin, …): admin — пул postgres() с правами администратора, а не {type}',
296
+ hint: "const admin = postgres(dsnАдминистратора); await createTenant(admin, 'salon', { name }); await admin.end(). DSN-строку и подключение connect() letopis createTenant не принимает",
297
+ },
298
+ // chain.ts, versions(): у результата versions() вызвали метод цепочки (rows, first, sort…); method — его имя
299
+ 'api.versions_terminal': {
300
+ code: 'invalid_query',
301
+ ru: 'versions() — терминал: отдаёт строки версий, у результата нет .{method}()',
302
+ hint: "const v = await db.Класс(id).versions(); модификаторы — до versions(): db.Класс(id).sort('at', 'desc').limit(10).versions()",
303
+ },
215
304
  };
@@ -3,7 +3,7 @@ const LOGIN = 'передать login: { user, password } — имя обычн
3
3
  /** Подсказка отказов user.roles в createTenant() и up({ tenant }). */
4
4
  const ROLES = "передать user.roles — список разных имён ролей членства, например ['staff'], или не передавать: по умолчанию ['owner']";
5
5
  /** Подсказка отказов createTenant() об удалённой строке. */
6
- const GONE = 'вернуть удалённое restore() (текст называет, кто это может) или выбрать другое имя или логин: id вычисляется из них';
6
+ const GONE = 'вернуть удалённое restore({ expect }) — надгробие из withDeleted().rows() (текст называет, кто это может) или выбрать другое имя или логин: id вычисляется из них';
7
7
  export const boot = {
8
8
  // up.ts, substitute(): в тексте SQL (ядро, миграция, свой сид) маркер, которого установщик не знает
9
9
  'boot.unknown_marker': {
@@ -123,19 +123,19 @@ export const boot = {
123
123
  // up.ts, createTenant(): строки арендатора нет, а его история в журнале есть
124
124
  'boot.tenant_deleted': {
125
125
  code: 'invalid_data',
126
- ru: 'createTenant: арендатор «{name}» ({id}) удалён, а его история осталась в журнале — повтор удалённое не воскрешает: верните его db.Account(id).restore() от имени системного администратора или выберите другое имя',
126
+ ru: 'createTenant: арендатор «{name}» ({id}) удалён, а его история осталась в журнале — повтор удалённое не воскрешает: верните его db.Account(id).restore({ expect: await db.Account(id).withDeleted().rows() }) от имени системного администратора или выберите другое имя',
127
127
  hint: GONE,
128
128
  },
129
129
  // up.ts, createTenant(): строки аккаунта пользователя нет, а его история в журнале есть
130
130
  'boot.tenant_user_deleted': {
131
131
  code: 'invalid_data',
132
- ru: 'createTenant: аккаунт пользователя {login} ({id}) удалён, а его история осталась в журнале — повтор удалённое не воскрешает: верните его db.Account(id).restore() от имени системного администратора или выберите другой логин',
132
+ ru: 'createTenant: аккаунт пользователя {login} ({id}) удалён, а его история осталась в журнале — повтор удалённое не воскрешает: верните его db.Account(id).restore({ expect: await db.Account(id).withDeleted().rows() }) от имени системного администратора или выберите другой логин',
133
133
  hint: GONE,
134
134
  },
135
135
  // up.ts, createTenant(): строки членства нет, а его история в журнале есть
136
136
  'boot.tenant_member_deleted': {
137
137
  code: 'invalid_data',
138
- ru: 'createTenant: членство {login} в арендаторе «{name}» ({id}) удалено, а его история осталась в журнале — повтор удалённое не воскрешает: верните его db.member(id).restore() в этом арендаторе или выберите другой логин',
138
+ ru: 'createTenant: членство {login} в арендаторе «{name}» ({id}) удалено, а его история осталась в журнале — повтор удалённое не воскрешает: верните его db.member(id).restore({ expect: await db.member(id).withDeleted().rows() }) в этом арендаторе или выберите другой логин',
139
139
  hint: GONE,
140
140
  },
141
141
  // up.ts, refuse1x(): схема letopis 1.x — ревизия из комментария схемы ниже первой ревизии 2.0 (FIRST_REVISION) или
@@ -27,7 +27,7 @@ export const codes = {
27
27
  'codes.acl_denied': {
28
28
  code: 'acl_denied',
29
29
  ru: 'правила доступа запрещают это действие',
30
- hint: 'добавить правило доступа (`Rule`) или выполнить действие от пользователя, которому оно разрешено; проверить заранее — `db.acl.checkData`, `db.acl.check`',
30
+ hint: 'выдать право `db.acl.grant` (правила — класс `rule`) или выполнить действие от пользователя, которому оно разрешено; проверить заранее — `db.acl.checkData`, `db.acl.check`',
31
31
  },
32
32
  'codes.id_mismatch': {
33
33
  code: 'id_mismatch',
@@ -72,7 +72,7 @@ export const codes = {
72
72
  'codes.not_deleted': {
73
73
  code: 'not_deleted',
74
74
  ru: '`restore` работает только для удалённых объектов',
75
- hint: 'сначала `delete()`, потом `restore()`',
75
+ hint: 'сначала `delete({ confirm: true, expect })`, потом `restore({ expect })` с надгробиями из ответа `delete()` или `withDeleted().rows()`',
76
76
  },
77
77
  'codes.exists': {
78
78
  code: 'exists',
@@ -91,8 +91,8 @@ export const codes = {
91
91
  },
92
92
  'codes.conflict': {
93
93
  code: 'conflict',
94
- ru: '`update` с `{ rev }`: строку уже изменили после того, как вы её прочли',
95
- hint: 'перечитать строку и повторить правку с новым `rev`; для чисел — `inc` вместо записи итога',
94
+ ru: 'то, что вы видели, уже изменилось — ничего не записано: `update` с `{ rev }` (строку изменили после чтения, `write.rev_conflict`), `delete({ confirm: true, expect })` (превью изменилось, `write.preview_changed`; в том числе цели уже удалены или не найдены — `detail.preview: []`), `restore({ expect })` (объект меняли после удаления, `write.restore_stale`)',
95
+ hint: 'перечитать и повторить с новыми данными: `update` — с новым `rev` (для чисел — `inc` вместо записи итога); `delete` — показать новое превью `err.detail.preview` и удалить с ним; `restore` — сообщить пользователю (вещь уже у другого) или `restore({ expect, partial: true })`',
96
96
  },
97
97
  'codes.rev_ambiguous': {
98
98
  code: 'rev_ambiguous',
@@ -9,7 +9,7 @@
9
9
  * 1. Раздел — по файлу, где брошена ошибка:
10
10
  * api — index, chain, typed, errors, internal; write — write, tx, load; query — sql, ops;
11
11
  * time — watch, admin, cache, signals; access — auth, acl; model — model, registry, sync, cli, validate,
12
- * jsonschema, uuid; boot — up, pglite; mcp — mcp, describe;
12
+ * jsonschema, uuid; boot — up, pglite; mcp — mcp, describe; log — log (и тексты логирования из index, tx);
13
13
  * sql-core — sql/10, 15, 20, 30, 40; sql-write — sql/60; sql-access — sql/50, 80;
14
14
  * sql-time — sql/70, 90, 95, 99 и sql/migrate.
15
15
  * codes.ts — запись на каждый код: заголовок и подсказка. Новый код — ещё ErrorCode (types.ts), для кода базы —
@@ -56,6 +56,7 @@ import { access } from './access.js';
56
56
  import { api } from './api.js';
57
57
  import { boot } from './boot.js';
58
58
  import { codes } from './codes.js';
59
+ import { log } from './log.js';
59
60
  import { mcp } from './mcp.js';
60
61
  import { model } from './model.js';
61
62
  import { query } from './query.js';
@@ -67,9 +68,11 @@ import { time } from './time.js';
67
68
  import { write } from './write.js';
68
69
  import type { ErrorCode, Msg, Params } from './types.js';
69
70
  export type { ErrorCode, Msg, Params, Section } from './types.js';
71
+ /** Каталог событий логирования (не сообщения об ошибках — в MESSAGES не входит). */
72
+ export { LOG_EVENTS, type LogEventInfo, type LogEventLevel, type LogEventName } from './log.js';
70
73
  /** Разделы, которые установщик кладёт в базу (message_catalog() в lib/sql/15-errors.sql). */
71
74
  export declare const SQL_SECTIONS: readonly ["sql-core", "sql-write", "sql-access", "sql-time"];
72
- type All = typeof codes & typeof api & typeof write & typeof query & typeof time & typeof access & typeof model & typeof boot & typeof mcp & typeof sqlCore & typeof sqlWrite & typeof sqlAccess & typeof sqlTime;
75
+ type All = typeof codes & typeof api & typeof write & typeof query & typeof time & typeof access & typeof model & typeof boot & typeof mcp & typeof log & typeof sqlCore & typeof sqlWrite & typeof sqlAccess & typeof sqlTime;
73
76
  /** Ключ каталога: опечатку в lerr('…') ловит проверка типов. */
74
77
  export type MsgKey = keyof All;
75
78
  /** Все записи каталога по ключу. */
@@ -9,7 +9,7 @@
9
9
  * 1. Раздел — по файлу, где брошена ошибка:
10
10
  * api — index, chain, typed, errors, internal; write — write, tx, load; query — sql, ops;
11
11
  * time — watch, admin, cache, signals; access — auth, acl; model — model, registry, sync, cli, validate,
12
- * jsonschema, uuid; boot — up, pglite; mcp — mcp, describe;
12
+ * jsonschema, uuid; boot — up, pglite; mcp — mcp, describe; log — log (и тексты логирования из index, tx);
13
13
  * sql-core — sql/10, 15, 20, 30, 40; sql-write — sql/60; sql-access — sql/50, 80;
14
14
  * sql-time — sql/70, 90, 95, 99 и sql/migrate.
15
15
  * codes.ts — запись на каждый код: заголовок и подсказка. Новый код — ещё ErrorCode (types.ts), для кода базы —
@@ -56,6 +56,7 @@ import { access } from './access.js';
56
56
  import { api } from './api.js';
57
57
  import { boot } from './boot.js';
58
58
  import { codes } from './codes.js';
59
+ import { log } from './log.js';
59
60
  import { mcp } from './mcp.js';
60
61
  import { model } from './model.js';
61
62
  import { query } from './query.js';
@@ -65,9 +66,11 @@ import { sqlTime } from './sql-time.js';
65
66
  import { sqlWrite } from './sql-write.js';
66
67
  import { time } from './time.js';
67
68
  import { write } from './write.js';
69
+ /** Каталог событий логирования (не сообщения об ошибках — в MESSAGES не входит). */
70
+ export { LOG_EVENTS } from './log.js';
68
71
  /** Разделы каталога: имя — префикс ключа и имя файла. */
69
72
  const SECTIONS = {
70
- codes, api, write, query, time, access, model, boot, mcp,
73
+ codes, api, write, query, time, access, model, boot, mcp, log,
71
74
  'sql-core': sqlCore, 'sql-write': sqlWrite, 'sql-access': sqlAccess, 'sql-time': sqlTime,
72
75
  };
73
76
  /** Разделы, которые установщик кладёт в базу (message_catalog() в lib/sql/15-errors.sql). */