letopis 3.0.0 → 3.0.1

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 (50) hide show
  1. package/AGENT-CHEATSHEET.md +12 -5
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +202 -46
  4. package/api-contract.json +4 -0
  5. package/dist/acl.d.ts +4 -2
  6. package/dist/admin.d.ts +5 -0
  7. package/dist/admin.js +10 -5
  8. package/dist/auth.d.ts +29 -8
  9. package/dist/auth.js +54 -9
  10. package/dist/chain.d.ts +19 -13
  11. package/dist/chain.js +17 -7
  12. package/dist/cli.js +46 -16
  13. package/dist/errors.d.ts +4 -5
  14. package/dist/errors.js +6 -7
  15. package/dist/index.d.ts +24 -13
  16. package/dist/index.js +75 -24
  17. package/dist/load.d.ts +1 -1
  18. package/dist/log.js +23 -2
  19. package/dist/mcp.d.ts +1 -1
  20. package/dist/mcp.js +8 -5
  21. package/dist/messages/access.d.ts +10 -0
  22. package/dist/messages/access.js +13 -0
  23. package/dist/messages/api.d.ts +10 -0
  24. package/dist/messages/api.js +23 -10
  25. package/dist/messages/boot.d.ts +5 -0
  26. package/dist/messages/boot.js +7 -0
  27. package/dist/messages/codes.js +2 -2
  28. package/dist/messages/log.d.ts +8 -5
  29. package/dist/messages/log.js +8 -5
  30. package/dist/messages/mcp.js +7 -7
  31. package/dist/messages/model.d.ts +16 -0
  32. package/dist/messages/model.js +20 -2
  33. package/dist/messages/write.d.ts +5 -0
  34. package/dist/messages/write.js +22 -14
  35. package/dist/model.d.ts +6 -2
  36. package/dist/model.js +5 -1
  37. package/dist/registry.d.ts +6 -0
  38. package/dist/registry.js +6 -0
  39. package/dist/sync.d.ts +3 -4
  40. package/dist/sync.js +3 -4
  41. package/dist/typed.d.ts +6 -6
  42. package/dist/types.d.ts +14 -9
  43. package/dist/types.js +2 -2
  44. package/dist/up.d.ts +5 -2
  45. package/dist/up.js +14 -6
  46. package/dist/write.d.ts +13 -9
  47. package/dist/write.js +81 -12
  48. package/llms.txt +2 -1
  49. package/package.json +1 -1
  50. package/sql/60-write.sql +18 -6
@@ -1,7 +1,7 @@
1
1
  /** Неизвестное имя класса, алиаса или роли: агенту доступна не db.registry, а letopis_model. */
2
2
  const NAME_HINT = 'проверить имя: похожие названы в тексте и в similar, классы, алиасы и концы — letopis_model';
3
- /** Удаление сорвалось после превью: превью — заново. */
4
- const PREVIEW_HINT = 'посмотреть превью заново (delete без confirm) и повторить удаление с confirm: true';
3
+ /** Удаление сорвалось после предпросмотра: предпросмотр — заново. */
4
+ const PREVIEW_HINT = 'посмотреть предпросмотр заново (delete без confirm) и повторить удаление с confirm: true';
5
5
  /** Неверная опция createMcpServer. */
6
6
  const OPTION_HINT = 'исправить опцию createMcpServer: допустимые значения названы в тексте';
7
7
  export const mcp = {
@@ -300,7 +300,7 @@ export const mcp = {
300
300
  // mcp.ts, write: код операции агента не совпал с превью (detail.operation — нынешний)
301
301
  'mcp.delete_operation_changed': {
302
302
  code: 'confirmation_required',
303
- ru: 'letopis_write: операция изменилась после превью — посмотрите превью заново',
303
+ ru: 'letopis_write: операция изменилась после предпросмотра — посмотрите предпросмотр заново',
304
304
  hint: PREVIEW_HINT,
305
305
  },
306
306
  // mcp.ts, write: целей удаления не видно
@@ -308,10 +308,10 @@ export const mcp = {
308
308
  code: 'target_not_found',
309
309
  ru: 'letopis_write: удалять нечего — целей не видно',
310
310
  },
311
- // mcp.ts, write: в превью есть $action restrict (detail.preview)
311
+ // mcp.ts, write: в превью есть $action restrict или сводка скрытых строк $hidden restrict (detail.preview)
312
312
  'mcp.delete_restricted': {
313
313
  code: 'delete_restricted',
314
- ru: 'letopis_write: удалению мешают строки с $action: restrict',
314
+ ru: 'letopis_write: удалению мешают строки с $action: restrict (или скрытые правами — сводка $hidden: restrict)',
315
315
  },
316
316
  // mcp.ts, confirmDelete (stateError): requestState не той операции, не того пользователя или подделан
317
317
  'mcp.delete_foreign_answer': {
@@ -321,7 +321,7 @@ export const mcp = {
321
321
  // mcp.ts, confirmDelete (stateError): срок requestState истёк
322
322
  'mcp.delete_answer_expired': {
323
323
  code: 'confirmation_required',
324
- ru: 'letopis_write: удаление не выполнено — подтверждение устарело — посмотрите превью и подтвердите удаление заново',
324
+ ru: 'letopis_write: удаление не выполнено — подтверждение устарело — посмотрите предпросмотр и подтвердите удаление заново',
325
325
  hint: PREVIEW_HINT,
326
326
  },
327
327
  // mcp.ts, confirmDelete: человек отказал или закрыл окно
@@ -337,7 +337,7 @@ export const mcp = {
337
337
  // mcp.ts, write: повторное превью в транзакции разошлось с подтверждённым
338
338
  'mcp.delete_data_changed': {
339
339
  code: 'confirmation_required',
340
- ru: 'letopis_write: данные изменились после подтверждения — посмотрите превью заново',
340
+ ru: 'letopis_write: данные изменились после подтверждения — посмотрите предпросмотр заново',
341
341
  hint: PREVIEW_HINT,
342
342
  },
343
343
  // mcp.ts, limitsOf: maxRows, maxBytes — не целое из диапазона
@@ -244,6 +244,22 @@ export declare const model: {
244
244
  code: "invalid_query";
245
245
  ru: string;
246
246
  };
247
+ 'model.cli_unknown_flag': {
248
+ code: "invalid_query";
249
+ ru: string;
250
+ };
251
+ 'model.cli_flag_needs_value': {
252
+ code: "invalid_query";
253
+ ru: string;
254
+ };
255
+ 'model.cli_flag_no_value': {
256
+ code: "invalid_query";
257
+ ru: string;
258
+ };
259
+ 'model.cli_unexpected_arg': {
260
+ code: "invalid_query";
261
+ ru: string;
262
+ };
247
263
  'model.types_header': {
248
264
  code: "invalid_query";
249
265
  ru: string;
@@ -299,6 +299,24 @@ export const model = {
299
299
  code: 'invalid_query',
300
300
  ru: 'правило {kind} в {path} проверяется только в библиотеке — в базе его нет',
301
301
  },
302
+ // cli.ts, main: ошибка разбора аргументов (node:util parseArgs) — текст по-русски вместо английского текста Node (lmsg,
303
+ // код не печатается; выход с кодом 2). flag — флаг, как его передали (--aply, -x); arg — лишний аргумент
304
+ 'model.cli_unknown_flag': {
305
+ code: 'invalid_query',
306
+ ru: 'неизвестный флаг {flag}; см. letopis --help',
307
+ },
308
+ 'model.cli_flag_needs_value': {
309
+ code: 'invalid_query',
310
+ ru: 'флагу {flag} нужно значение: {flag} <значение> или {flag}=<значение>; см. letopis --help',
311
+ },
312
+ 'model.cli_flag_no_value': {
313
+ code: 'invalid_query',
314
+ ru: 'флаг {flag} не принимает значения — уберите «=…» после него; см. letopis --help',
315
+ },
316
+ 'model.cli_unexpected_arg': {
317
+ code: 'invalid_query',
318
+ ru: 'лишний аргумент {arg}; см. letopis --help',
319
+ },
302
320
  // sync.ts, generateTypes: пометка в первой строке файла типов (после «// ») — текст не для ошибки
303
321
  'model.types_header': {
304
322
  code: 'invalid_query',
@@ -308,7 +326,7 @@ export const model = {
308
326
  // и валидатор letopis его не исполняют, данные записались бы как есть; rules — пути и виды через запятую
309
327
  'model.transform_unsupported': {
310
328
  code: 'invalid_class',
311
- ru: 'преобразования Zod не исполняются при записи: {rules}',
312
- hint: 'привести значение до записи в коде сервиса (например, value.trim()) и убрать преобразование из модели; проверку без изменения значения дают .regex(), .min(), .refine()',
329
+ ru: 'в модели есть преобразования Zod, а при записи их никто не исполняет — значение записалось бы как есть; поля с преобразованием: {rules}',
330
+ hint: 'уберите из модели .trim(), .toLowerCase(), .transform(), z.preprocess() и .pipe() и приводите значение в коде сервиса до записи (например, name.trim()); проверку без изменения значения дают .regex(), .min(), .refine()',
313
331
  },
314
332
  };
@@ -172,6 +172,11 @@ export declare const write: {
172
172
  ru: string;
173
173
  hint: string;
174
174
  };
175
+ 'write.delete_restricted': {
176
+ code: "delete_restricted";
177
+ ru: string;
178
+ hint: string;
179
+ };
175
180
  'write.link_in_data': {
176
181
  code: "invalid_data";
177
182
  ru: string;
@@ -62,7 +62,7 @@ export const write = {
62
62
  // create() или upsert() на возврате к узлу пути
63
63
  'write.create_on_pivot': {
64
64
  code: 'invalid_query',
65
- ru: '{verb}() на возврате к узлу — он ведёт к существующей сущности, используйте update()',
65
+ ru: '{verb}() на возврате к узлу — он ведёт к существующему объекту, используйте update()',
66
66
  },
67
67
  // create() или upsert() после шага с фильтром, а не с id
68
68
  'write.create_with_filter': {
@@ -100,7 +100,7 @@ export const write = {
100
100
  // слот после глагола, который слотов не принимает
101
101
  'write.slot_bad_verb': {
102
102
  code: 'invalid_query',
103
- ru: 'слот .{role} после {verb}() — слоты у create(), update() и upsert()',
103
+ ru: 'шаг роли .{role} после {verb}() — шаги ролей бывают только у create(), update() и upsert()',
104
104
  },
105
105
  // write.ts:slotRole: имя слота — не роль записываемого класса и не класс реестра
106
106
  'write.slot_unknown': {
@@ -115,22 +115,22 @@ export const write = {
115
115
  // write.ts:slotRole: класс слота принимают несколько ролей
116
116
  'write.slot_ambiguous': {
117
117
  code: 'invalid_query',
118
- ru: 'слот {name} у класса {cls} неоднозначен: роли {roles} — назовите роль',
118
+ ru: 'шаг {name} у класса {cls} неоднозначен: роли {roles} — назовите роль',
119
119
  },
120
120
  // конец из пути: шаг подходит нескольким ролям
121
121
  'write.end_ambiguous': {
122
122
  code: 'invalid_query',
123
- ru: 'конец класса {cls} для шага {step} неоднозначен: роли {roles} — назовите шаг ролью или задайте слот',
123
+ ru: 'конец класса {cls} для шага {step} неоднозначен: роли {roles} — назовите шаг ролью или задайте конец шагом роли после глагола',
124
124
  },
125
125
  // конец из пути: шаг дал не одну сущность; count — сколько найдено
126
126
  'write.end_not_single': {
127
127
  code: 'invalid_query',
128
- ru: 'шаг {step} должен дать ровно одну сущность для конца {cls} (найдено {count})',
128
+ ru: 'шаг {step} должен дать ровно один объект для конца {cls} (найдено {count})',
129
129
  },
130
130
  // слот .роль.set() одиночного конца — значение дало несколько сущностей
131
131
  'write.slot_single_many': {
132
132
  code: 'invalid_query',
133
- ru: 'слот .{role}.set(): конец одиночный, а значение дало {count} сущностей',
133
+ ru: 'шаг роли .{role}.set(): конец одиночный, а значение дало объектов: {count}',
134
134
  },
135
135
  // write.ts:existsError: create() — объект уже есть, id не вычислить
136
136
  'write.exists': {
@@ -207,32 +207,40 @@ export const write = {
207
207
  // restore({ expect }), delete({ expect }): expect — не список строк { id, rev }; verb — глагол
208
208
  'write.bad_expect': {
209
209
  code: 'invalid_query',
210
- ru: '{verb}({ expect }): expect — список строк { id, rev } (строки ответа)',
210
+ ru: '{verb}({ expect }): expect — список строк { id, rev } (строки ответа; у delete — и строки-сводки предпросмотра как есть)',
211
211
  },
212
212
  // delete({ expect }) без confirm: превью сравнивать не с чем
213
213
  'write.expect_without_confirm': {
214
214
  code: 'invalid_query',
215
- ru: 'delete({ expect }) — только с { confirm: true }: без него delete() и есть превью',
215
+ ru: 'delete({ expect }) — только с { confirm: true }: без него delete() и есть предпросмотр',
216
216
  },
217
217
  // delete({ confirm: true, expect }): превью в транзакции удаления не совпало с показанным; detail.preview — новое превью
218
218
  'write.preview_changed': {
219
219
  code: 'conflict',
220
- ru: 'delete({ confirm: true, expect }): превью изменилось с тех пор, как его показали — ничего не удалено',
221
- hint: 'показать пользователю новое превью (err.detail.preview) и удалить с ним: delete({ confirm: true, expect: err.detail.preview })',
220
+ ru: 'delete({ confirm: true, expect }): предпросмотр изменился с тех пор, как его показали — ничего не удалено',
221
+ hint: 'показать пользователю новый предпросмотр (err.detail.preview) и удалить с ним: delete({ confirm: true, expect: err.detail.preview })',
222
+ },
223
+ // write.ts, checkRestrict (3.0.1): сверенный предпросмотр delete({ confirm: true, expect }) содержит мешающие строки
224
+ // (restrict) — видимые или сводку скрытых правами; отказ до удаления. visible — число видимых, others — скрытых;
225
+ // detail — { rows: [{ id, class }], others }, как у отказа триггера (sql-write.delete_restricted)
226
+ 'write.delete_restricted': {
227
+ code: 'delete_restricted',
228
+ ru: 'удалению мешают ссылающиеся строки: видимых {visible}, скрытых правами {others} — ничего не удалено',
229
+ hint: 'показать пользователю мешающие строки (err.detail.rows); скрытые правами (err.detail.others) видит только их владелец — обратиться к нему; удалить или перевести мешающие строки (или сменить у их конца onDelete) и заново показать предпросмотр delete()',
222
230
  },
223
231
  // write.ts, checkFullData и checkPatchEnds (3.0, вместо api.validation_failed_end): лишний ключ данных — конец класса
224
232
  // (имя роли или класса на конце); keys — такие ключи, role — роль первого из них. Нарушения ValidationError дописывает
225
233
  // к тексту сам
226
234
  'write.link_in_data': {
227
235
  code: 'invalid_data',
228
- ru: '{verb}() у {cls}: {keys} — связь, а не поле данных; конец задают путь цепочки или слот после глагола: .{role}.set(id)',
229
- hint: 'конец (связь) — не поле данных: путь цепочки (db.Владелец(id).Класс().create(…)) или слот после глагола: db.Класс.create({ … }).Роль.set(id); у конца-списка — .Роль.add(id) / .Роль.remove(id), снять — .Роль.unset()',
236
+ ru: '{verb}() у {cls}: {keys} — связь, а не поле данных; конец задают путь цепочки или шаг роли после глагола: .{role}.set(id)',
237
+ hint: 'конец (связь) — не поле данных: путь цепочки (db.Владелец(id).Класс().create(…)) или шаг роли после глагола: db.Класс.create({ … }).Роль.set(id); у конца-списка — .Роль.add(id) / .Роль.remove(id), снять — .Роль.unset()',
230
238
  },
231
239
  // 3.0: delete({ confirm: true }) без expect и без all — удаление не сверено с показанным превью
232
240
  'write.confirm_without_expect': {
233
241
  code: 'invalid_query',
234
- ru: 'delete({ confirm: true }) без { expect }: удаление не сверено с превью, которое видел пользователь — ничего не удалено',
235
- hint: 'передайте expect из превью: const p = await db.Класс(id).delete().rows(); показать p пользователю; await db.Класс(id).delete({ confirm: true, expect: p }).rows(). Осознанно удалить текущий состав без сверки — delete({ confirm: true, all: true })',
242
+ ru: 'delete({ confirm: true }) без { expect }: удаление не сверено с предпросмотром, который видел пользователь — ничего не удалено',
243
+ hint: 'передайте expect из предпросмотра: const p = await db.Класс(id).delete().rows(); показать p пользователю; await db.Класс(id).delete({ confirm: true, expect: p }).rows(). Осознанно удалить текущий состав без сверки — delete({ confirm: true, all: true })',
236
244
  },
237
245
  // 3.0: restore() без expect и без all — и для явных id: последнее надгробие могло смениться с тех пор, как его видели
238
246
  'write.restore_without_expect': {
package/dist/model.d.ts CHANGED
@@ -29,7 +29,11 @@ type TargetName<T> = T extends string ? T : T extends {
29
29
  type MinRequired<O> = O extends {
30
30
  min: infer N;
31
31
  } ? (N extends 0 | undefined ? false : number extends N ? boolean : true) : false;
32
- /** Конец класса: `one(X)`, `one(A, B)`, `many(X, { min, max })`. `T` — имена классов целей (только для типов). */
32
+ /**
33
+ * Конец класса: `one(X)`, `one(A, B)`, `many(X, { min, max })`. Параметры типа: `Many` — конец-список, `Required` —
34
+ * обязателен, `T` — имена классов целей (только для типов). `min`, `max`, `onDeleteRule`, `metaData` необязательны:
35
+ * не заданы — `undefined`.
36
+ */
33
37
  export declare class End<Many extends boolean = boolean, Required extends boolean = boolean, T extends string = string> {
34
38
  readonly targets: readonly Target[];
35
39
  readonly many: Many;
@@ -38,7 +42,7 @@ export declare class End<Many extends boolean = boolean, Required extends boolea
38
42
  readonly max?: number;
39
43
  readonly onDeleteRule?: OnDelete;
40
44
  readonly metaData?: Record<string, unknown>;
41
- /** Только для вывода типов: имена классов целей — строгие переходы и слоты цепочек (план 2.0, этап 4, п. 1). */
45
+ /** Только для вывода типов: имена классов целей — строгие переходы и шаги ролей цепочек. */
42
46
  readonly __targets?: T;
43
47
  constructor(targets: readonly Target[], opts: {
44
48
  many: Many;
package/dist/model.js CHANGED
@@ -14,7 +14,11 @@ import { z } from 'zod';
14
14
  import { lerr } from './errors.js';
15
15
  import { normalizeSchema } from './jsonschema.js';
16
16
  import { RESERVED_CLASS_NAMES } from './types.js';
17
- /** Конец класса: `one(X)`, `one(A, B)`, `many(X, { min, max })`. `T` — имена классов целей (только для типов). */
17
+ /**
18
+ * Конец класса: `one(X)`, `one(A, B)`, `many(X, { min, max })`. Параметры типа: `Many` — конец-список, `Required` —
19
+ * обязателен, `T` — имена классов целей (только для типов). `min`, `max`, `onDeleteRule`, `metaData` необязательны:
20
+ * не заданы — `undefined`.
21
+ */
18
22
  export class End {
19
23
  targets;
20
24
  many;
@@ -41,6 +41,12 @@ interface ClassRow {
41
41
  resolved: Record<string, unknown>;
42
42
  };
43
43
  }
44
+ /**
45
+ * Реестр классов сессии — `db.registry`: описания классов арендатора и системных классов, как их видит подключение.
46
+ * Методы: `has(имя)` — есть ли класс или алиас; `find(имя)` — описание `ClassInfo` или `undefined`; `resolve(имя)` — то же,
47
+ * а неизвестное имя — `invalid_class`; `all()` — все классы; `family(имя)` — класс и его потомки; `leafType(класс, путь)` —
48
+ * тип поля по пути в данных; `load(строки)` — заменить содержимое (его зовёт само подключение, `db.refresh()`).
49
+ */
44
50
  export declare class Registry {
45
51
  private byName;
46
52
  private byAlias;
package/dist/registry.js CHANGED
@@ -5,6 +5,12 @@
5
5
  */
6
6
  import { lerr } from './errors.js';
7
7
  import { compileValidator } from './validate.js';
8
+ /**
9
+ * Реестр классов сессии — `db.registry`: описания классов арендатора и системных классов, как их видит подключение.
10
+ * Методы: `has(имя)` — есть ли класс или алиас; `find(имя)` — описание `ClassInfo` или `undefined`; `resolve(имя)` — то же,
11
+ * а неизвестное имя — `invalid_class`; `all()` — все классы; `family(имя)` — класс и его потомки; `leafType(класс, путь)` —
12
+ * тип поля по пути в данных; `load(строки)` — заменить содержимое (его зовёт само подключение, `db.refresh()`).
13
+ */
8
14
  export class Registry {
9
15
  byName = new Map();
10
16
  byAlias = new Map();
package/dist/sync.d.ts CHANGED
@@ -27,10 +27,9 @@ export declare function syncModels(db: DbCore, models: readonly AnyModel[], opts
27
27
  export declare function schemaToTs(s: JsonSchema | undefined, indent?: string): string;
28
28
  /**
29
29
  * Объявления `<Класс>Data` и `<Класс>Links` для классов реестра и `<Класс>Model` — имя, алиас, концы с классами целей
30
- * и предки (ModelLike, typed.ts), — а в конце список `Models`: строгие типы цепочек без моделей Zod, `connect<Models>()`
31
- * (план 2.0, этап 4, п. 1). Классы арендатора System (системные и классы сидов пакета) — только с includeSystem
32
- * (`letopis types --system`); без него системные классы даёт встроенный набор SystemModels. Пометка в первой строке —
33
- * запись каталога model.types_header.
30
+ * и предки, — а в конце список `Models`: строгие типы цепочек без моделей Zod, `connect<Models>()`. Классы арендатора
31
+ * System (системные и классы сидов пакета) — только с includeSystem (`letopis types --system`); без него системные
32
+ * классы даёт встроенный набор SystemModels. Первая строка — пометка «Сгенерировано letopis types — не править руками.»
34
33
  */
35
34
  export declare function generateTypes(registry: Registry, opts?: {
36
35
  includeSystem?: boolean;
package/dist/sync.js CHANGED
@@ -166,10 +166,9 @@ export function schemaToTs(s, indent = '') {
166
166
  }
167
167
  /**
168
168
  * Объявления `<Класс>Data` и `<Класс>Links` для классов реестра и `<Класс>Model` — имя, алиас, концы с классами целей
169
- * и предки (ModelLike, typed.ts), — а в конце список `Models`: строгие типы цепочек без моделей Zod, `connect<Models>()`
170
- * (план 2.0, этап 4, п. 1). Классы арендатора System (системные и классы сидов пакета) — только с includeSystem
171
- * (`letopis types --system`); без него системные классы даёт встроенный набор SystemModels. Пометка в первой строке —
172
- * запись каталога model.types_header.
169
+ * и предки, — а в конце список `Models`: строгие типы цепочек без моделей Zod, `connect<Models>()`. Классы арендатора
170
+ * System (системные и классы сидов пакета) — только с includeSystem (`letopis types --system`); без него системные
171
+ * классы даёт встроенный набор SystemModels. Первая строка — пометка «Сгенерировано letopis types — не править руками.»
173
172
  */
174
173
  export function generateTypes(registry, opts = {}) {
175
174
  const out = [`// ${lmsg('model.types_header')}`, ''];
package/dist/typed.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * - переходы цепочки выводятся из концов, как в resolveHop (lib/src/sql.ts): от класса C — роли его семейства (C и
7
7
  * потомков) и классы, связанные с семейством C концом в любую сторону (роль, имя которой совпало с именем класса,
8
8
  * во время выполнения — шаг класса, так же и здесь);
9
- * - слоты — только роли класса C: `set(id | строка | цепочка)`, `add` и `remove` — у множественного конца, `unset` —
9
+ * - шаги ролей после глагола — только роли класса C: `set(id | строка | цепочка)`, `add` и `remove` — у множественного конца, `unset` —
10
10
  * у необязательного;
11
11
  * - поля фильтров, `or()` и агрегатов `sum`, `avg`, `min`, `max`, `countBy` — поля класса.
12
12
  * Чего типы не знают: возврат к узлу связи из пути (pivot) от класса, с которым связь не связана концом, и роль чужого
@@ -214,11 +214,11 @@ export type StepsOf<M extends readonly AnyModelLike[]> = {
214
214
  } & {
215
215
  [K in AllOf<M> as AliasOf<K>]: StepFnOf<K, M>;
216
216
  };
217
- /** Значение слота одиночного конца: id, строка ответа или цепочка. */
217
+ /** Значение шага роли одиночного конца (`.Роль.set(…)`): id, строка ответа или цепочка. */
218
218
  type SlotOne = string | {
219
219
  readonly id: string;
220
220
  } | ChainRef;
221
- /** Значение слота множественного конца: ещё и список id или строк. */
221
+ /** Значение шага роли множественного конца (`.Роль.add(…)`, `.remove(…)`): ещё и список id или строк. */
222
222
  type SlotMany = SlotOne | readonly (string | {
223
223
  readonly id: string;
224
224
  })[];
@@ -248,7 +248,7 @@ type HopsOf<C, M extends readonly AnyModelLike[], All = AllOf<M>> = {
248
248
  type Retyped = 'paths' | 'rows' | 'first' | 'versions' | 'limit' | 'offset' | 'sort' | 'asOf' | 'after' | 'withDeleted' | 'deep' | 'exact' | 'alias' | 'tags' | 'owner' | 'create' | 'update' | 'upsert' | 'delete' | 'forUpdate' | 'restore' | 'cache' | 'sum' | 'avg' | 'min' | 'max' | 'countBy';
249
249
  /**
250
250
  * Цепочка с текущим классом C (у шага-роли с несколькими целями — их союз): терминалы отдают RowOf<C>, шаги — переходы
251
- * из концов, слоты — роли C, агрегаты — поля C.
251
+ * из концов, шаги ролей после глагола (`.Роль.set(…)`) — роли C, агрегаты — поля C.
252
252
  */
253
253
  export type TypedChain<C, M extends readonly AnyModelLike[]> = Omit<ChainCore, Retyped> & {
254
254
  paths(): Promise<Path[]>;
@@ -291,8 +291,8 @@ export type TypedChain<C, M extends readonly AnyModelLike[]> = Omit<ChainCore, R
291
291
  reason?: string;
292
292
  }): TypedChain<C, M>;
293
293
  /**
294
- * Без опций — превью. `{ confirm: true, expect: превью }` — удалить ровно показанное (цели сужены фильтром, id, `tags()`,
295
- * `owner()` или `limit()`); `{ confirm: true }` без `expect` — `invalid_query` (`write.confirm_without_expect`);
294
+ * Без опций — предпросмотр. `{ confirm: true, expect: предпросмотр }` — удалить ровно показанное (цели сужены фильтром, id, `tags()`,
295
+ * `owner()` или `limit()`); `{ confirm: true }` без `expect` — `invalid_query`;
296
296
  * осознанно удалить текущий состав без сверки — `{ confirm: true, all: true }`.
297
297
  */
298
298
  delete(opts?: DeleteOptions): TypedChain<C, M>;
package/dist/types.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  /**
5
5
  * Ревизия схемы, которую знает эта версия пакета, — номер версии SQL-части letopis (таблиц и функций в схеме установки).
6
- * После `up()` у схемы та же ревизия (`r.revision`); схему отставшей ревизии 2.x `up()` обновляет только с
6
+ * После `up()` у схемы та же ревизия (`r.revision`); схему отставшей ревизии (того же поколения `v2`) `up()` обновляет только с
7
7
  * `upgrade: true`, а `connect()` о ней предупреждает.
8
8
  */
9
9
  export declare const DDL_REVISION = 11;
@@ -18,7 +18,7 @@ export declare const SCHEMA_VERSION = "v2";
18
18
  */
19
19
  export declare const ACL_CLASS_NAMES: readonly string[];
20
20
  /**
21
- * Имена методов цепочки: модификаторы, терминалы, глаголы записи и слоты концов, а также `then`. После шага цепочки эти
21
+ * Имена методов цепочки: модификаторы, терминалы, глаголы записи и шаги роли, а также `then`. После шага цепочки эти
22
22
  * имена библиотека не ищет среди классов; все они входят в `RESERVED_CLASS_NAMES`.
23
23
  */
24
24
  export declare const CHAIN_METHODS: readonly ["then", "entity", "paths", "rows", "first", "ids", "count", "limit", "offset", "sort", "asOf", "withDeleted", "deep", "exact", "sum", "avg", "min", "max", "countBy", "after", "versions", "create", "update", "upsert", "set", "unset", "add", "remove", "delete", "restore", "alias", "tags", "owner", "forUpdate", "cache"];
@@ -51,17 +51,22 @@ export interface Row<D = Record<string, unknown>, L = Record<string, string | st
51
51
  /** Вид операции версии: create, update, delete, restore, … */
52
52
  op: string;
53
53
  reason?: string;
54
- /** Надгробие: удалённая сущность (withDeleted) или версия удаления (versions). */
54
+ /** Надгробие: удалённый объект (withDeleted) или версия удаления (versions). */
55
55
  $deleted?: true;
56
- /** Глубина узла при deep() (1 — прямой потомок); в превью удаления — уровень каскада (0 — цель). */
56
+ /** Глубина узла при deep() (1 — прямой потомок); в предпросмотре удаления — уровень каскада (0 — цель). */
57
57
  $depth?: number;
58
58
  /** Что сделал upsert. */
59
59
  $upsert?: 'created' | 'updated' | 'unchanged';
60
60
  /**
61
61
  * Превью удаления (delete() без confirm): удалится, снимется конец или помешает (restrict). В ответе
62
- * `restore({ expect, partial: true })` — `skip`: цель не восстановлена, причина — `$why`.
62
+ * `restore({ expect, partial: true })` — `skip`: цель не восстановлена, причина — `$why`. `hidden` — строка-сводка
63
+ * предпросмотра, а не объект: скрытые правами строки замыкания (что с ними будет — `$hidden`, сколько — `$count`).
63
64
  */
64
- $action?: 'delete' | 'unset' | 'restrict' | 'skip';
65
+ $action?: 'delete' | 'unset' | 'restrict' | 'skip' | 'hidden';
66
+ /** Строка-сводка предпросмотра удаления (`$action: 'hidden'`): что будет со скрытыми правами строками. */
67
+ $hidden?: 'delete' | 'unset' | 'restrict';
68
+ /** Строка-сводка предпросмотра удаления (`$action: 'hidden'`): число скрытых правами строк. */
69
+ $count?: number;
65
70
  /**
66
71
  * Почему цель `restore({ expect, partial: true })` не восстановлена: `restored` — её уже восстановили, `reissued` — id
67
72
  * занял новый объект с тем же ключом, `changed` — после показанного удаления её меняли (удалили снова и т. п.).
@@ -139,7 +144,7 @@ export interface QueryEvent {
139
144
  }
140
145
  /** Порог логирования: события этого уровня и выше; `'off'` — ничего. */
141
146
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off';
142
- /** Событие логирования (каталог событий — lib/src/messages/log.ts). Поля без значения в событии отсутствуют. */
147
+ /** Событие логирования (каталог событий — `LOG_EVENTS`). Поля без значения в событии отсутствуют. */
143
148
  export interface LogEvent {
144
149
  /** Время события, ISO 8601 (UTC). */
145
150
  ts: string;
@@ -150,9 +155,9 @@ export interface LogEvent {
150
155
  op?: string;
151
156
  /** Длительность операции, мс (с точностью до 0,1). */
152
157
  ms?: number;
153
- /** Арендатор сессии фасада. */
158
+ /** Арендатор сессии подключения (db, db.as(), db.begin()). */
154
159
  tenant?: string;
155
- /** Аккаунт фасада: аккаунт сессии или тот, от чьего имени действует db.as(). */
160
+ /** Аккаунт подключения: аккаунт сессии или тот, от чьего имени действует db.as(). */
156
161
  account?: string;
157
162
  /** Метка транзакции db.begin() или db.transaction(). */
158
163
  tx?: string;
package/dist/types.js CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  /**
5
5
  * Ревизия схемы, которую знает эта версия пакета, — номер версии SQL-части letopis (таблиц и функций в схеме установки).
6
- * После `up()` у схемы та же ревизия (`r.revision`); схему отставшей ревизии 2.x `up()` обновляет только с
6
+ * После `up()` у схемы та же ревизия (`r.revision`); схему отставшей ревизии (того же поколения `v2`) `up()` обновляет только с
7
7
  * `upgrade: true`, а `connect()` о ней предупреждает.
8
8
  */
9
9
  export const DDL_REVISION = 11;
@@ -21,7 +21,7 @@ export const ACL_CLASS_NAMES = ['rule', 'Resource', 'member', 'Account', 'Class'
21
21
  // Методы цепочки — единственный источник зарезервированных имён (план, §2.14, этап 3, п. 10)
22
22
  // ---------------------------------------------------------------------------
23
23
  /**
24
- * Имена методов цепочки: модификаторы, терминалы, глаголы записи и слоты концов, а также `then`. После шага цепочки эти
24
+ * Имена методов цепочки: модификаторы, терминалы, глаголы записи и шаги роли, а также `then`. После шага цепочки эти
25
25
  * имена библиотека не ищет среди классов; все они входят в `RESERVED_CLASS_NAMES`.
26
26
  */
27
27
  export const CHAIN_METHODS = [
package/dist/up.d.ts CHANGED
@@ -121,9 +121,11 @@ export interface UpOptions extends Omit<InstallOptions, 'schema' | 'upTo' | 'mig
121
121
  */
122
122
  login?: LoginOptions;
123
123
  }
124
- /** Роль приложения up({ login }). */
124
+ /** Роль приложения up({ login }): `{ user, password }` (поле — `user`, не `name`). */
125
125
  export interface LoginOptions {
126
+ /** Имя роли PostgreSQL для приложения, например `'shop_app'`: нет — up() её создаёт, есть — меняет пароль. */
126
127
  user: string;
128
+ /** Пароль роли: войдёт в строку подключения `r.login.dsn`. */
127
129
  password: string;
128
130
  }
129
131
  export interface UpResult extends InstallResult {
@@ -200,7 +202,8 @@ export interface TenantOptions {
200
202
  * APIKEY, KEYSECRET, OTP, TOTP, KEY, LOCK); иначе invalid_data.
201
203
  */
202
204
  kind?: string;
203
- ttl?: string;
205
+ /** Срок сессии: как ttl у auth (число секунд, '15m', интервал PostgreSQL '15 minutes'); по умолчанию 7 суток. */
206
+ ttl?: string | number;
204
207
  /** Роли членства (по умолчанию ['owner']); роли уже существующего членства повтор не меняет. */
205
208
  roles?: string[];
206
209
  };
package/dist/up.js CHANGED
@@ -19,7 +19,7 @@ import postgres from 'postgres';
19
19
  import { DDL_REVISION, RESERVED_CLASS_NAMES, SCHEMA_VERSION } from './types.js';
20
20
  import { makeLog, maskDsn, traceSql, traced } from './log.js';
21
21
  import { startPglite } from './pglite.js';
22
- import { hashPassword } from './auth.js';
22
+ import { hashPassword, ttlGuard, ttlOf } from './auth.js';
23
23
  import { checkOptions, lerr, lmsg } from './errors.js';
24
24
  import { sqlCatalog } from './messages/index.js';
25
25
  import { v5Id } from './uuid.js';
@@ -572,8 +572,15 @@ async function ensureLogin(sql, login, owner, app, dsn) {
572
572
  const verb = r ? 'alter role %I login password %L' : 'create role %I login password %L';
573
573
  const [{ q }] = await tx `select format(${verb}::text, ${login.user}::text, ${login.password}::text) as q`;
574
574
  await tx.unsafe(q);
575
- const [{ g }] = await tx `select format('grant %I to %I', ${app}::text, ${login.user}::text) as g`;
576
- await tx.unsafe(g);
575
+ const [{ g, installer }] = await tx `select format('grant %I to %I', ${app}::text, ${login.user}::text) as g, current_user::text as installer`;
576
+ // PostgreSQL 16+: выдать роль может только тот, у кого на неё ADMIN OPTION (или суперпользователь) — понятный отказ
577
+ // вместо сырого 42501 «permission denied to grant role» (3.0.1)
578
+ await tx.unsafe(g).catch((e) => {
579
+ if (e?.code === '42501') {
580
+ throw lerr('boot.login_grant_denied', { installer, app, user: login.user }, { cause: e });
581
+ }
582
+ throw e;
583
+ });
577
584
  return !r;
578
585
  });
579
586
  let url;
@@ -642,8 +649,9 @@ export async function createTenant(sql, schema, opts, owner = 'letopis_owner') {
642
649
  // логин без крайних пробелов, как у auth.setPassword() и verifyPassword() (план 2.1, A12)
643
650
  const login = u ? (typeof u.login === 'string' ? u.login.trim() : u.login) : '';
644
651
  const user = u ? v5Id(SYSTEM_ID, 'Account', [`user:${login}`]) : null;
652
+ const ttl = u ? ttlOf(u.ttl, 'createTenant') : null;
645
653
  const hash = u ? await hashPassword(u.password) : null;
646
- return sql.begin(async (tx) => {
654
+ return ttlGuard(sql.begin(async (tx) => {
647
655
  // под блокировкой схемы, как накат: up({ tenant }) нескольких экземпляров сервиса разом заводит арендатора один
648
656
  // раз, остальные находят его по имени (без неё оба вставили бы ту же строку и один упал бы на ключе)
649
657
  await lockSchema(tx, schema);
@@ -676,8 +684,8 @@ export async function createTenant(sql, schema, opts, owner = 'letopis_owner') {
676
684
  await tx.unsafe(`insert into ${S}.secret (account, kind, ident, material, meta)
677
685
  select $1::uuid, $2, $3, convert_to($4, 'UTF8'), '{"confirmed": true}'::jsonb
678
686
  where not exists (select 1 from ${S}.secret where kind = $2 and casefold(ident) collate pg_unicode_fast = casefold($3) collate pg_unicode_fast and deleted_at is null)`, [user, kind, login, hash]);
679
- token = (await tx.unsafe(`select ${S}.new_session($1::uuid, $2::interval, $3::jsonb) as t`, [user, u.ttl ?? '7 days', { tenant }]))[0].t;
687
+ token = (await tx.unsafe(`select ${S}.new_session($1::uuid, $2::interval, $3::jsonb) as t`, [user, ttl ?? '7 days', { tenant }]))[0].t;
680
688
  }
681
689
  return { tenant, user, token, created };
682
- });
690
+ }), u?.ttl, 'createTenant');
683
691
  }
package/dist/write.d.ts CHANGED
@@ -19,19 +19,23 @@ export declare function inc(by: number, opts?: {
19
19
  }): IncMarker;
20
20
  export declare const isInc: (v: unknown) => v is IncMarker;
21
21
  /**
22
- * Ожидаемая строка `expect` у `restore()` и `delete({ confirm: true })` (план 2.1, A1, A2): строка ответа или её `id`,
23
- * `rev` (и `$action` превью удаления). Лишние поля строки не мешают.
22
+ * Ожидаемая строка `expect` у `restore()` и `delete({ confirm: true })`: строка ответа или её `id`, `rev` (и `$action`
23
+ * предпросмотра удаления). Лишние поля строки не мешают.
24
24
  */
25
25
  export interface Expected {
26
26
  id: string;
27
27
  rev: number;
28
- /** У строки превью — что с ней будет; строку ответа (с любым $action) можно передать как есть. */
28
+ /** У строки предпросмотра — что с ней будет; строку ответа (с любым $action) можно передать как есть. */
29
29
  $action?: Row['$action'];
30
+ /** Строка-сводка предпросмотра (`$action: 'hidden'`, 3.0.1): что будет со скрытыми правами строками. */
31
+ $hidden?: Row['$hidden'];
32
+ /** Строка-сводка предпросмотра: число скрытых правами строк. */
33
+ $count?: number;
30
34
  }
31
35
  /**
32
- * Опции delete() (3.0): без confirm — превью; `{ confirm: true, expect }` — удалить ровно показанное превью;
36
+ * Опции delete(): без confirm — предпросмотр; `{ confirm: true, expect }` — удалить ровно показанный предпросмотр;
33
37
  * `{ confirm: true, all: true }` — осознанно удалить текущий состав целей без сверки (и без фильтра). `{ confirm: true }`
34
- * без expect и all — invalid_query (write.confirm_without_expect).
38
+ * без expect и all — invalid_query.
35
39
  */
36
40
  export type DeleteOptions = {
37
41
  confirm?: false;
@@ -50,9 +54,9 @@ export type DeleteOptions = {
50
54
  reason?: string;
51
55
  };
52
56
  /**
53
- * Опции restore() (3.0): `{ expect }` — надгробия, которые видел пользователь (ответ delete() или withDeleted()), с
57
+ * Опции restore(): `{ expect }` — надгробия, которые видел пользователь (ответ delete() или withDeleted()), с
54
58
  * `partial: true` — что можно; `{ all: true }` — осознанно вернуть все удалённые цели шага без сверки. Без expect и all —
55
- * invalid_query (write.restore_without_expect), и для явных id.
59
+ * invalid_query, и для явных id.
56
60
  */
57
61
  export type RestoreOptions = {
58
62
  expect: readonly Expected[];
@@ -98,7 +102,7 @@ export type PlanOp = ({
98
102
  export declare function dropUndefined(data: Record<string, unknown>): Record<string, unknown>;
99
103
  /** Причина изменения: строка до 1000 символов без NUL (её не принимает set_config). */
100
104
  export declare function checkReason(v: unknown, where: string): string;
101
- /** Значение слота: id, список id, строка ответа или ленивая цепочка (в той же транзакции). */
105
+ /** Значение шага роли: id, список id, строка ответа или ленивая цепочка (в той же транзакции). */
102
106
  export type SlotValue = string | string[] | {
103
107
  id: string;
104
108
  } | null | {
@@ -111,7 +115,7 @@ export interface Slot {
111
115
  op: 'set' | 'unset' | 'add' | 'remove';
112
116
  value: SlotValue;
113
117
  }
114
- /** Строка ответа записи (результат upsert — $upsert, превью удаления — $action — поля Row). */
118
+ /** Строка ответа записи (результат upsert — $upsert, предпросмотр удаления — $action — поля Row). */
115
119
  export type WriteRow = Row;
116
120
  export interface IncSpec {
117
121
  path: string[];