letopis 1.4.0 → 1.4.2

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 (62) hide show
  1. package/AGENT-CHEATSHEET.en.md +15 -7
  2. package/AGENT-CHEATSHEET.md +19 -11
  3. package/CHANGELOG.md +299 -1
  4. package/README.en.md +180 -62
  5. package/README.md +165 -55
  6. package/dist/acl.d.ts +11 -5
  7. package/dist/admin.d.ts +30 -14
  8. package/dist/admin.js +53 -18
  9. package/dist/auth.d.ts +61 -17
  10. package/dist/auth.js +16 -2
  11. package/dist/cache.d.ts +5 -1
  12. package/dist/cache.js +3 -2
  13. package/dist/chain.d.ts +92 -15
  14. package/dist/chain.js +56 -7
  15. package/dist/cli.js +16 -2
  16. package/dist/errors.d.ts +2 -2
  17. package/dist/errors.js +5 -5
  18. package/dist/import.js +7 -0
  19. package/dist/index.d.ts +73 -45
  20. package/dist/index.js +69 -14
  21. package/dist/internal.d.ts +7 -0
  22. package/dist/internal.js +5 -0
  23. package/dist/load.d.ts +10 -3
  24. package/dist/load.js +39 -11
  25. package/dist/mcp.d.ts +20 -11
  26. package/dist/mcp.js +8 -6
  27. package/dist/model.d.ts +4 -1
  28. package/dist/model.js +7 -1
  29. package/dist/ops.d.ts +58 -18
  30. package/dist/ops.js +58 -18
  31. package/dist/project.d.ts +39 -11
  32. package/dist/project.js +75 -15
  33. package/dist/registry.d.ts +4 -1
  34. package/dist/registry.js +4 -1
  35. package/dist/signals.d.ts +4 -4
  36. package/dist/signals.js +4 -4
  37. package/dist/sql.d.ts +8 -4
  38. package/dist/sql.js +110 -25
  39. package/dist/sync.js +11 -4
  40. package/dist/tx.d.ts +15 -0
  41. package/dist/tx.js +22 -4
  42. package/dist/types.d.ts +21 -7
  43. package/dist/types.js +13 -6
  44. package/dist/up.d.ts +48 -25
  45. package/dist/up.js +94 -41
  46. package/dist/validate.d.ts +9 -2
  47. package/dist/validate.js +19 -3
  48. package/dist/watch.d.ts +29 -5
  49. package/dist/watch.js +62 -13
  50. package/dist/write.d.ts +3 -2
  51. package/dist/write.js +52 -19
  52. package/package.json +8 -4
  53. package/sql/10-core.sql +6 -1
  54. package/sql/60-write.sql +47 -18
  55. package/sql/70-read.sql +13 -7
  56. package/sql/75-history.sql +106 -28
  57. package/sql/80-auth.sql +41 -3
  58. package/sql/88-derived.sql +3 -1
  59. package/sql/90-time.sql +118 -30
  60. package/sql/95-seed.system.sql +6 -3
  61. package/sql/99-revision.sql +1 -1
  62. package/sql/migrate/0007-secret-account-fk.sql +12 -0
package/dist/chain.d.ts CHANGED
@@ -32,6 +32,11 @@ export interface ChainCtx {
32
32
  /** План в очереди батча; steps обновляется по мере роста цепочки. */
33
33
  export interface BatchEntry {
34
34
  steps: Step[];
35
+ /** Модификатор выборки, поставленный сразу после последнего глагола плана: run() откажет (`afterVerbError`). */
36
+ loose?: {
37
+ name: string;
38
+ verb: string;
39
+ };
35
40
  }
36
41
  /** Ссылка цепочки на очередь: первый глагол регистрирует план, следующие — обновляют. */
37
42
  export interface BatchRef {
@@ -47,9 +52,16 @@ export type Batch = {
47
52
  [className: string]: StepFn;
48
53
  };
49
54
  export interface ChainCore {
50
- /** Пути: [{ шаг1: Row, шаг2: Row, … }] — узлы каждого варианта. */
55
+ /**
56
+ * Все пути цепочки. Путь — строки всех шагов, по одной на шаг, связанные ссылками; приходит объектом, где ключ — имя
57
+ * шага (или имя из `alias()`), а значение — строка этого шага: `{ Shop: …, Product: … }`. Если к объекту последнего
58
+ * шага ведут несколько путей, придут все.
59
+ */
51
60
  paths(): Promise<Path[]>;
52
- /** Уникальные сущности последнего шага. */
61
+ /**
62
+ * Строки объектов последнего шага — каждый объект один раз, сколько бы путей к нему ни вело. Если цепочка кончается
63
+ * глаголом записи — строки этого глагола.
64
+ */
53
65
  rows(): Promise<Row[]>;
54
66
  first(): Promise<Row | null>;
55
67
  ids(): Promise<string[]>;
@@ -68,41 +80,93 @@ export interface ChainCore {
68
80
  diff(opts?: DiffOptions): Promise<DiffResult[]>;
69
81
  /** Кто, когда и почему последним изменил каждое поле версии (по умолчанию текущей), план 1.1. */
70
82
  blame(opts?: BlameOptions): Promise<BlameResult[]>;
71
- /** Агрегаты по полю последнего шага ('data.<путь>') — по сущностям, считает база. */
83
+ /**
84
+ * Сумма поля `'data.<путь>'` по объектам последнего шага: значения приводятся к числу, складывает база. `limit()` и
85
+ * `offset()` на итог не влияют; нет ни одного значения — `null`.
86
+ */
72
87
  sum(field: string): Promise<number | null>;
88
+ /**
89
+ * Среднее значение поля `'data.<путь>'` по объектам последнего шага: значения приводятся к числу, считает база.
90
+ * `limit()` и `offset()` на итог не влияют; нет ни одного значения — `null`.
91
+ */
73
92
  avg(field: string): Promise<number | null>;
93
+ /**
94
+ * Наименьшее значение поля `'data.<путь>'` среди объектов последнего шага; считает база. Числа сравниваются как числа
95
+ * и приходят числом, даты — как даты, поле без описания в классе — как текст. `limit()` и `offset()` на итог не
96
+ * влияют; нет ни одного значения — `null`.
97
+ */
74
98
  min(field: string): Promise<unknown>;
99
+ /**
100
+ * Наибольшее значение поля `'data.<путь>'` среди объектов последнего шага; считает база. Числа сравниваются как числа
101
+ * и приходят числом, даты — как даты, поле без описания в классе — как текст. `limit()` и `offset()` на итог не
102
+ * влияют; нет ни одного значения — `null`.
103
+ */
75
104
  max(field: string): Promise<unknown>;
105
+ /**
106
+ * Сколько объектов последнего шага с каждым значением поля `'data.<путь>'`: объект «значение → число», объекты без
107
+ * значения — под ключом `'null'`. Перед `countBy()` `limit()` и `offset()` отбирают группы — от самых частых значений.
108
+ */
76
109
  countBy(field: string): Promise<Record<string, number>>;
110
+ /**
111
+ * Не больше n строк ответа: объектов последнего шага, у `paths()` — путей, у `versions()` — версий, перед `countBy()` —
112
+ * групп. До глагола сужает его цели. Сразу после глагола перед `rows`, `first`, `ids`, `count` и `paths` —
113
+ * `invalid_query`: глагол уже прошёл по всем целям, сужать нечего.
114
+ */
77
115
  limit(n: number): Chain;
116
+ /** Пропустить первые n строк ответа — тех же, что считает `limit()`; где действует — как у `limit()`. */
78
117
  offset(n: number): Chain;
79
118
  /** Сортировка по полю последнего шага: 'at' | 'rev' | 'id' | 'data.<путь>'. */
80
119
  sort(field: string, dir?: 'asc' | 'desc' | boolean): Chain;
81
120
  /** Срез «как было на момент T»: версии журнала. */
82
121
  asOf(t: string | Date): Chain;
83
- /** Keyset-пагинация: строго после курсора (cursorOf). Требует sort(). */
122
+ /**
123
+ * Постраничный вывод по курсору: ответ начинается строго после строки, на которой кончилась прошлая страница, в
124
+ * порядке `sort()` (без `sort()` — ошибка `invalid_query`). Курсор этой строки даёт `cursorOf(строка, поле)` с тем же
125
+ * полем, что в `sort()`. Листает `rows()`, `ids()`, `versions()` и `paths()` (курсор пути — `cursorOf(путь, поле)`);
126
+ * `count()`, агрегаты, `diff()` и `blame()` курсор не учитывают.
127
+ */
84
128
  after(cursor: Cursor): Chain;
85
- /** Последний шаг включает удалённые (надгробия, $deleted). */
129
+ /**
130
+ * Последний шаг находит и удалённые объекты. Удалённый объект приходит надгробием — своей последней версией, версией
131
+ * удаления, — с `$deleted: true`.
132
+ */
86
133
  withDeleted(): Chain;
87
134
  /** Дети любой глубины на переходе к тому же классу; $depth в строке. */
88
135
  deep(max?: number): Chain;
89
- /** Только сам класс шага, без потомков. */
136
+ /**
137
+ * Последний шаг находит объекты только своего класса — без классов-наследников (`extends` в модели), которых без
138
+ * `exact()` находит тоже.
139
+ */
90
140
  exact(): Chain;
91
141
  /** Ключ текущего шага в путях. */
92
142
  alias(name: string): Chain;
93
- /** Колонка tags: строка | список (все) | has / hasAny / hasAll. */
143
+ /**
144
+ * Теги строки. В чтении и перед глаголами — отбор: строка или список — строки со всеми этими тегами (пустой список
145
+ * подходит любой строке), `has`, `hasAny`, `hasAll` — оператором. Перед `create()` и `upsert()` — теги новой строки
146
+ * (строка или список). Перед `update()` строка или список и отбирают, и заменяют теги целиком, а оператор только
147
+ * отбирает.
148
+ */
94
149
  tags(v: string | string[] | object): Chain;
95
- /** Колонка owner. */
150
+ /**
151
+ * Владелец строк последнего шага (поле `owner`). В чтении и перед `update()`, `delete()` и другими глаголами — отбор:
152
+ * только строки этого владельца; перед `create()` и `upsert()` — владелец новой строки (существующей строке `upsert()`
153
+ * владельца не меняет).
154
+ */
96
155
  owner(v: string | {
97
156
  id: string;
98
157
  }): Chain;
99
- /** Вставить узел: ленивая цепочка-паттерн или строка ответа; та же переменная повторно — возврат к её узлу. */
158
+ /**
159
+ * Добавляет в цепочку шаги из строки ответа (шаг её класса с её id) или из ещё не исполненной цепочки без глаголов
160
+ * записи (все её шаги как есть); `limit()`, `sort()`, `asOf()` и другие модификаторы всей цепочки не переносятся. Та
161
+ * же цепочка (та же переменная) второй раз в одном пути шагов не добавляет, а возвращает к своему последнему шагу:
162
+ * следующий шаг пойдёт от него.
163
+ */
100
164
  entity(x: Chain | Row): Chain;
101
165
  /** Создать объект: концы — из пути и слотов; занятый ключ — ошибка exists с id. */
102
166
  create(data?: Record<string, unknown>, opts?: {
103
167
  reason?: string;
104
168
  }): Chain;
105
- /** Новая версия каждой цели шага: слияние патча в базе; inc(n) — приращение; { rev } — строгий режим. */
169
+ /** Новая версия каждой цели шага: слияние патча в базе; inc(n) — приращение; { rev } — с проверкой версии. */
106
170
  update(patch?: Record<string, unknown>, opts?: {
107
171
  rev?: number;
108
172
  reason?: string;
@@ -111,7 +175,11 @@ export interface ChainCore {
111
175
  upsert(data: Record<string, unknown>, opts?: {
112
176
  reason?: string;
113
177
  }): Chain;
114
- /** Удалить цели шага с каскадом; без { confirm: true } — превью замыкания ($action). */
178
+ /**
179
+ * Удаляет цели шага; строки, которые на них ссылаются, база удаляет вместе с ними, отвязывает или не даёт удалить цель —
180
+ * как задано в `onDelete` их ссылок. Без `{ confirm: true }` — предпросмотр удаления: ничего не удаляется, терминал
181
+ * вернёт все строки, которых оно коснулось бы, у каждой `$action` — что с ней будет.
182
+ */
115
183
  delete(opts?: {
116
184
  confirm?: boolean;
117
185
  reason?: string;
@@ -130,12 +198,19 @@ export interface ChainCore {
130
198
  restore(opts?: {
131
199
  reason?: string;
132
200
  }): Chain;
133
- /** Стереть историю удалённых объектов шага; без { confirm: true } — превью ($versions). */
201
+ /**
202
+ * Стирает историю удалённых объектов шага: все их версии в журнале, от каждого остаётся одна запись `purge`. Удалённые
203
+ * объекты шаг находит сам, без `withDeleted()`. Без `{ confirm: true }` — предпросмотр: ничего не стирается, терминал
204
+ * вернёт надгробия с `$versions` — сколько версий сотрётся.
205
+ */
134
206
  purge(opts?: {
135
207
  confirm?: boolean;
136
208
  reason?: string;
137
209
  }): Chain;
138
- /** Новое значение ключа: строка с новым id, ссылки переведены (поля — данные, роли — концы). */
210
+ /**
211
+ * Новое значение ключа объектов шага. id вычисляется из ключа, поэтому объект получает новый id: ссылки других строк
212
+ * на него переводятся на новый id, а журнал связывает старый id с новым.
213
+ */
139
214
  rekey(patch: Record<string, unknown>, opts?: {
140
215
  reason?: string;
141
216
  }): Chain;
@@ -145,8 +220,10 @@ export interface ChainCore {
145
220
  }): Chain;
146
221
  }
147
222
  /**
148
- * Шаг цепочки: вызов — переход (фильтр); после глагола записи без вызова — слот конца версии:
149
- * `.Роль.set(id | [id] | строка | цепочка)`, `.unset()`, у множественного конца `.add()` и `.remove()`.
223
+ * Шаг цепочки — имя класса, его алиаса или роли с необязательным фильтром: `db.Shop(id).Product({ price: gt(1000) })`;
224
+ * каждый следующий шаг переходит по ссылкам от объектов предыдущего. После `create()`, `update()` или `upsert()` имя
225
+ * роли без вызова задаёт ссылку записываемой строки: `.Роль.set(…)`, `.Роль.unset()`, у множественной ссылки ещё
226
+ * `.Роль.add(…)` и `.Роль.remove(…)`.
150
227
  */
151
228
  export type StepFn = ((filter?: Filter) => Chain) & {
152
229
  set(value: unknown): Chain;
package/dist/chain.js CHANGED
@@ -132,6 +132,27 @@ async function read(ctx, steps, mods, mode, agg) {
132
132
  const ev = mode === 'countPaths' ? 'count' : mode;
133
133
  return exec(ctx, q, ev, steps, mods).then((res) => ({ q, res }));
134
134
  }
135
+ /** Модификатор выборки, заданный в mods, — именем метода цепочки; forUpdate() отказывает сам (lock_unsupported). */
136
+ function modName(mods) {
137
+ const key = Object.keys(mods).find((k) => mods[k] !== undefined && k !== 'forUpdate');
138
+ return key === 'order' || key === 'desc' ? 'sort' : key;
139
+ }
140
+ function afterVerbError(name, verb) {
141
+ return new LetopisError('invalid_query', `${name}() после ${verb}() ни на что не действует: глагол проходит по всем целям шага, а ответ — его строки. ` +
142
+ `Цели сужают limit(), offset() и sort() до глагола: db.Класс(…).limit(n).${verb}(…)`);
143
+ }
144
+ /**
145
+ * Терминалы rows, first, ids, count и paths цепочки, которая кончается глаголом, отдают строки самой правки.
146
+ * Модификатор выборки между глаголом и терминалом ни на что не подействовал бы: limit резал бы только ответ, хотя
147
+ * правка прошла по всем целям, остальные молча пропускались. Поэтому — отказ до записи. После глагола модификатор
148
+ * относится к чтению за ним (шаг класса, versions(), diff(), blame(), агрегаты) или сужает цели следующего глагола.
149
+ */
150
+ function checkAfterVerb(steps, mods) {
151
+ const op = steps[steps.length - 1]?.op;
152
+ const name = op ? modName(mods) : undefined;
153
+ if (op && name)
154
+ throw afterVerbError(name, op.kind);
155
+ }
135
156
  /**
136
157
  * Цепочка с глаголами: весь план одной транзакцией (§2.5). Результат приводится к виду ответа
137
158
  * чтения (строки rows/paths, id, n, агрегаты), чтобы терминалы разбирали его одинаково.
@@ -223,8 +244,11 @@ const rowOf = (r) => (r.done ? r.row : toRow(r.row));
223
244
  /** Значение слота: id, список id, строка ответа или ленивая цепочка. */
224
245
  function slotValue(v) {
225
246
  const peek = peekPlan(v);
226
- if (peek)
247
+ if (peek) {
248
+ // модификатор сразу после глагола вложенной цепочки — тот же отказ, что у терминала (план 2.0, 7.2 №3)
249
+ checkAfterVerb(peek.steps, peek.mods);
227
250
  return { plan: peek.steps, mods: peek.mods };
251
+ }
228
252
  if (v === null || typeof v === 'string')
229
253
  return v;
230
254
  if (Array.isArray(v))
@@ -279,6 +303,13 @@ function classProp(ctx, name, found, ownerSteps, mods, nodes) {
279
303
  });
280
304
  }
281
305
  export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
306
+ // батч: терминала нет, план исполнит run() — он и откажет, если цепочка кончилась модификатором после глагола
307
+ // (как checkAfterVerb вне батча); следующий шаг или глагол забирает модификатор себе, и отметка снимается
308
+ if (ctx.batch?.entry) {
309
+ const op = steps[steps.length - 1]?.op;
310
+ const name = op ? modName(mods) : undefined;
311
+ ctx.batch.entry.loose = op && name ? { name, verb: op.kind } : undefined;
312
+ }
282
313
  const withLast = (patch) => {
283
314
  const lastOp = steps[steps.length - 1]?.op;
284
315
  if (lastOp)
@@ -369,21 +400,35 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
369
400
  return entityStep;
370
401
  case 'paths':
371
402
  return async () => {
372
- // q === null — пути из результата глагола: строки уже готовы
403
+ checkAfterVerb(steps, mods);
404
+ // q === null — пути из результата глагола: строки уже готовы. Ключи — в порядке шагов (jsonb хранит их
405
+ // по длине), узел последнего шага — последним: по нему cursorOf(путь, поле) берёт курсор (план 2.0, 7.1 №3)
373
406
  const { q, res } = await read(ctx, steps, mods, 'paths');
374
- return res.map((r) => Object.fromEntries(Object.entries(r.path).map(([k, v]) => [k, q === null ? v : toRow(v)])));
407
+ if (q === null)
408
+ return res.map((r) => r.path);
409
+ return res.map((r) => Object.fromEntries(q.keys.map((k) => [k, toRow(r.path[k])])));
375
410
  };
376
411
  case 'rows':
377
- return async () => (await read(ctx, steps, mods, 'rows')).res.map(rowOf);
412
+ return async () => {
413
+ checkAfterVerb(steps, mods);
414
+ return (await read(ctx, steps, mods, 'rows')).res.map(rowOf);
415
+ };
378
416
  case 'first':
379
417
  return async () => {
418
+ checkAfterVerb(steps, mods);
380
419
  const { res } = await read(ctx, steps, { ...mods, limit: 1 }, 'rows');
381
420
  return res[0] ? rowOf(res[0]) : null;
382
421
  };
383
422
  case 'ids':
384
- return async () => (await read(ctx, steps, mods, 'ids')).res.map((r) => r.id);
423
+ return async () => {
424
+ checkAfterVerb(steps, mods);
425
+ return (await read(ctx, steps, mods, 'ids')).res.map((r) => r.id);
426
+ };
385
427
  case 'count':
386
- return async (opts) => (await read(ctx, steps, mods, opts?.paths ? 'countPaths' : 'count')).res[0].n;
428
+ return async (opts) => {
429
+ checkAfterVerb(steps, mods);
430
+ return (await read(ctx, steps, mods, opts?.paths ? 'countPaths' : 'count')).res[0].n;
431
+ };
387
432
  case 'versions':
388
433
  return async (opts) => {
389
434
  warnNoHistory(steps, 'versions');
@@ -503,7 +548,11 @@ export function makeBatch(ctx, queue) {
503
548
  switch (prop) {
504
549
  case 'run':
505
550
  return async () => {
506
- const plans = queue.splice(0, queue.length).map((x) => x.steps);
551
+ const entries = queue.splice(0, queue.length);
552
+ const loose = entries.find((x) => x.loose)?.loose;
553
+ if (loose)
554
+ throw afterVerbError(loose.name, loose.verb);
555
+ const plans = entries.map((x) => x.steps);
507
556
  for (const p of plans)
508
557
  precheckPlan(ctx.reg, p);
509
558
  if (!plans.length)
package/dist/cli.js CHANGED
@@ -24,6 +24,7 @@ import { resolve } from 'node:path';
24
24
  import { connect, describeText } from './index.js';
25
25
  import { syncModels, generateTypes } from './sync.js';
26
26
  import { import021 } from './import.js';
27
+ import { LetopisError } from './errors.js';
27
28
  const USAGE = `letopis sync <модуль> --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--apply]
28
29
  letopis types --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--out <файл>]
29
30
  letopis load <файлы…> --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--class <Класс>]
@@ -35,6 +36,8 @@ letopis import --from <строка 0.21> --from-schema <v1.имя> --dsn <ст
35
36
  [--rename <Старое>=<Новое>]… [--update] [--commit-every <N>] [--partition <имя>] [--initial]
36
37
  letopis mcp --dsn <строка> --schema <v2.имя> --token <токен пользователя> [--write]
37
38
  (или LETOPIS_DSN, LETOPIS_SCHEMA, LETOPIS_TOKEN; ключ сервиса --api-key не принимается)`;
39
+ /** Команды CLI: остальное — USAGE с кодом 2 ещё до подключения. */
40
+ const COMMANDS = new Set(['sync', 'types', 'load', 'verify', 'maintain', 'describe', 'import', 'mcp']);
38
41
  const isModel = (v) => typeof v === 'object' && v !== null && typeof v.describe === 'function' && typeof v.name === 'string';
39
42
  export async function main(argv) {
40
43
  const { values, positionals } = parseArgs({
@@ -58,7 +61,12 @@ export async function main(argv) {
58
61
  values.schema ??= process.env.LETOPIS_SCHEMA || undefined;
59
62
  values.token ??= process.env.LETOPIS_TOKEN || undefined;
60
63
  }
61
- if (values.help || !cmd || !values.dsn || !values.schema) {
64
+ // --help — всегда код 0; неизвестная команда — USAGE до подключения к базе (план 2.0, 7.5 №4)
65
+ if (values.help) {
66
+ console.log(USAGE);
67
+ return 0;
68
+ }
69
+ if (!cmd || !COMMANDS.has(cmd) || !values.dsn || !values.schema) {
62
70
  console.log(USAGE);
63
71
  return cmd ? 2 : 0;
64
72
  }
@@ -117,7 +125,7 @@ export async function main(argv) {
117
125
  return 2;
118
126
  }
119
127
  if (values.trust)
120
- throw new Error('letopis: removed: --trust снят в 1.0.0 — запрос по целям вне загрузки стоит 0,1–1,3 % её времени');
128
+ throw new LetopisError('removed', '--trust снят в 1.0.0 — запрос по целям вне загрузки стоит 0,1–1,3 % её времени');
121
129
  // несколько файлов — одна загрузка и одна транзакция; в файле — объект на строку
122
130
  const rows = [];
123
131
  for (const f of files) {
@@ -146,6 +154,12 @@ export async function main(argv) {
146
154
  console.log(JSON.stringify(r));
147
155
  return 0;
148
156
  }
157
+ if (cmd === 'describe' || cmd === 'types') {
158
+ // обе команды читают реестр сессии: без действующей сессии (токен истёк, погашен, неверен или не задан) он пуст,
159
+ // и команда выдала бы пустой результат с кодом 0, а с --out затёрла бы им файл. acl.check ничего не пишет, а без
160
+ // сессии отказывает no_session — тогда выход с кодом 1, файл не тронут
161
+ await db.acl.check('letopis.cli');
162
+ }
149
163
  if (cmd === 'describe') {
150
164
  const d = db.describe();
151
165
  const text = values.format === 'text' ? describeText(d) : JSON.stringify(d, null, 2);
package/dist/errors.d.ts CHANGED
@@ -13,10 +13,10 @@ export declare class LetopisError extends Error {
13
13
  readonly detail: unknown;
14
14
  constructor(code: string, message: string, detail?: unknown);
15
15
  }
16
+ /** Версия, в которой снято старое API (её берёт и контракт API — scripts/gen-api-contract.mjs). */
17
+ export declare const REMOVED_IN = "1.0.0";
16
18
  /** Охранник снесённого API (план, этап 3, п. 11): понятная ошибка с версией и заменой. */
17
19
  export declare function removed(name: string, replacement: string): LetopisError;
18
- /** Метод зарезервирован, но появится на следующем этапе плана. */
19
- export declare function notYet(name: string, stage: number): LetopisError;
20
20
  /** Данные не проходят описание класса (`invalid_data`). */
21
21
  export declare class ValidationError extends LetopisError {
22
22
  readonly issues: Issue[];
package/dist/errors.js CHANGED
@@ -12,13 +12,13 @@ export class LetopisError extends Error {
12
12
  this.detail = detail;
13
13
  }
14
14
  }
15
+ /** Версия, в которой снято старое API (её берёт и контракт API — scripts/gen-api-contract.mjs). */
16
+ export const REMOVED_IN = '1.0.0';
15
17
  /** Охранник снесённого API (план, этап 3, п. 11): понятная ошибка с версией и заменой. */
16
18
  export function removed(name, replacement) {
17
- return new LetopisError('removed', `${name} снят в 1.0.0 — ${replacement}`);
18
- }
19
- /** Метод зарезервирован, но появится на следующем этапе плана. */
20
- export function notYet(name, stage) {
21
- return new LetopisError('not_implemented', `${name} появится на этапе ${stage} плана 1.0`);
19
+ // «опция …» — женского рода; методы и свойства — мужского
20
+ const verb = name.startsWith('опция ') ? 'снята' : / и /.test(name) ? 'сняты' : 'снят';
21
+ return new LetopisError('removed', `${name} ${verb} в ${REMOVED_IN} — ${replacement}`);
22
22
  }
23
23
  /** Данные не проходят описание класса (`invalid_data`). */
24
24
  export class ValidationError extends LetopisError {
package/dist/import.js CHANGED
@@ -625,7 +625,14 @@ export async function import021(opts) {
625
625
  }
626
626
  if (secretRows.length) {
627
627
  await x.begin(async (tx) => {
628
+ // кред аккаунта, которого в установке нет (его нет и в Account 0.21), привязать не к чему: строка secret ссылается
629
+ // на аккаунт внешним ключом (ревизия 7) — такой кред пропускается и считается в skipped
630
+ const known = new Set((await tx.unsafe(`select id::text as id from ${S}.entity where id = any ($1::uuid[])`, [[...new Set(secretRows.map((s) => s.account))]])).map((r) => r.id));
628
631
  for (const s of secretRows) {
632
+ if (!known.has(s.account)) {
633
+ report.credentials.skipped++;
634
+ continue;
635
+ }
629
636
  const [dup] = await tx.unsafe(`select account::text as account from ${S}.secret
630
637
  where kind = $1 and casefold(ident) collate pg_unicode_fast = casefold($2) collate pg_unicode_fast and deleted_at is null`, [s.kind, s.ident]);
631
638
  if (dup) {