letopis 1.4.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/AGENT-CHEATSHEET.md +329 -278
  2. package/CHANGELOG.md +558 -1
  3. package/README.md +606 -1037
  4. package/dist/acl.d.ts +78 -1
  5. package/dist/acl.js +264 -13
  6. package/dist/admin.d.ts +10 -141
  7. package/dist/admin.js +24 -175
  8. package/dist/auth.d.ts +2 -2
  9. package/dist/auth.js +5 -15
  10. package/dist/cache.d.ts +8 -1
  11. package/dist/cache.js +33 -10
  12. package/dist/chain.d.ts +19 -41
  13. package/dist/chain.js +169 -201
  14. package/dist/cli.js +51 -88
  15. package/dist/demo/booking.d.ts +352 -36
  16. package/dist/describe.d.ts +47 -0
  17. package/dist/describe.js +195 -0
  18. package/dist/errors.d.ts +59 -13
  19. package/dist/errors.js +190 -29
  20. package/dist/index.d.ts +52 -71
  21. package/dist/index.js +245 -130
  22. package/dist/internal.d.ts +12 -0
  23. package/dist/internal.js +5 -0
  24. package/dist/jsonschema.js +18 -17
  25. package/dist/load.d.ts +20 -12
  26. package/dist/load.js +225 -186
  27. package/dist/mcp.d.ts +5 -7
  28. package/dist/mcp.js +171 -215
  29. package/dist/messages/access.d.ts +90 -0
  30. package/dist/messages/access.js +113 -0
  31. package/dist/messages/api.d.ts +171 -0
  32. package/dist/messages/api.js +215 -0
  33. package/dist/messages/boot.d.ts +201 -0
  34. package/dist/messages/boot.js +256 -0
  35. package/dist/messages/codes.d.ts +172 -0
  36. package/dist/messages/codes.js +173 -0
  37. package/dist/messages/index.d.ts +95 -0
  38. package/dist/messages/index.js +118 -0
  39. package/dist/messages/mcp.d.ts +306 -0
  40. package/dist/messages/mcp.js +387 -0
  41. package/dist/messages/model.d.ts +233 -0
  42. package/dist/messages/model.js +283 -0
  43. package/dist/messages/query.d.ts +131 -0
  44. package/dist/messages/query.js +158 -0
  45. package/dist/messages/sql-access.d.ts +70 -0
  46. package/dist/messages/sql-access.js +87 -0
  47. package/dist/messages/sql-core.d.ts +150 -0
  48. package/dist/messages/sql-core.js +181 -0
  49. package/dist/messages/sql-time.d.ts +71 -0
  50. package/dist/messages/sql-time.js +88 -0
  51. package/dist/messages/sql-write.d.ts +246 -0
  52. package/dist/messages/sql-write.js +270 -0
  53. package/dist/messages/time.d.ts +23 -0
  54. package/dist/messages/time.js +29 -0
  55. package/dist/messages/types.d.ts +27 -0
  56. package/dist/messages/types.js +5 -0
  57. package/dist/messages/write.d.ts +405 -0
  58. package/dist/messages/write.js +509 -0
  59. package/dist/model.d.ts +73 -60
  60. package/dist/model.js +26 -29
  61. package/dist/ops.d.ts +35 -0
  62. package/dist/ops.js +101 -0
  63. package/dist/pglite.d.ts +0 -9
  64. package/dist/pglite.js +3 -1
  65. package/dist/registry.d.ts +3 -19
  66. package/dist/registry.js +3 -16
  67. package/dist/signals.d.ts +5 -9
  68. package/dist/signals.js +5 -9
  69. package/dist/sql.d.ts +13 -6
  70. package/dist/sql.js +176 -83
  71. package/dist/sync.d.ts +9 -3
  72. package/dist/sync.js +51 -21
  73. package/dist/tx.d.ts +26 -1
  74. package/dist/tx.js +44 -14
  75. package/dist/typed.d.ts +240 -45
  76. package/dist/types.d.ts +23 -91
  77. package/dist/types.js +12 -14
  78. package/dist/up.d.ts +53 -32
  79. package/dist/up.js +184 -73
  80. package/dist/uuid.js +5 -5
  81. package/dist/validate.d.ts +13 -1
  82. package/dist/validate.js +61 -29
  83. package/dist/watch.d.ts +11 -10
  84. package/dist/watch.js +33 -70
  85. package/dist/write.d.ts +9 -16
  86. package/dist/write.js +118 -187
  87. package/llms.txt +22 -25
  88. package/package.json +8 -11
  89. package/sql/10-core.sql +7 -50
  90. package/sql/15-errors.sql +64 -10
  91. package/sql/20-context.sql +7 -6
  92. package/sql/30-validate.sql +35 -27
  93. package/sql/40-class.sql +20 -114
  94. package/sql/50-acl.sql +8 -13
  95. package/sql/60-write.sql +220 -667
  96. package/sql/70-read.sql +20 -16
  97. package/sql/80-auth.sql +24 -27
  98. package/sql/90-time.sql +70 -859
  99. package/sql/95-seed.system.sql +7 -14
  100. package/sql/99-revision.sql +2 -1
  101. package/sql/README.md +32 -24
  102. package/sql/migrate/README.md +14 -0
  103. package/AGENT-CHEATSHEET.en.md +0 -485
  104. package/MIGRATION.md +0 -190
  105. package/README.en.md +0 -2859
  106. package/dist/import.d.ts +0 -185
  107. package/dist/import.js +0 -820
  108. package/dist/project.d.ts +0 -350
  109. package/dist/project.js +0 -1224
  110. package/sql/75-history.sql +0 -358
  111. package/sql/88-derived.sql +0 -1235
  112. package/sql/migrate/0006-changed-paths.sql +0 -3
  113. package/sql/migrate/0007-secret-account-fk.sql +0 -12
package/dist/chain.js CHANGED
@@ -7,12 +7,12 @@
7
7
  * узлу (pivot): от него ветвится следующий шаг. Модификаторы возвращают новую цепочку; терминалы
8
8
  * строят один запрос (sql.ts) и исполняют его транзакцией под сессией (tx.ts).
9
9
  *
10
- * Запись (этап 4): глаголы create, update, upsert, delete, anonymize, reclass — звенья плана; слоты
10
+ * Запись (этап 4): глаголы create, update, upsert, delete — звенья плана; слоты
11
11
  * `.Роль.set/unset/add/remove` правят концы записываемой версии. Терминал цепочки с глаголами
12
12
  * исполняет весь план одной транзакцией (write.ts): продолжение идёт от результата глагола.
13
- * Этап 6: restore, purge, rekey; versions({ follow }); кэш результатов .cache() (cache.ts).
13
+ * Этап 6: restore; versions(); кэш результатов .cache() (cache.ts).
14
14
  */
15
- import { LetopisError, removed } from './errors.js';
15
+ import { lerr, lwarn, similar } from './errors.js';
16
16
  import { sessionTag } from './cache.js';
17
17
  import { buildRead, nextNodeKey, resolveHop, roleTargets, stepClasses, toRow, } from './sql.js';
18
18
  import { execBatch, execPlan, precheckPlan, slotRole } from './write.js';
@@ -38,8 +38,11 @@ export function lookupStep(reg, name) {
38
38
  const t = targets.map((n) => reg.find(n)).find((c) => !!c);
39
39
  return t ? { cls: t, role: name } : undefined;
40
40
  }
41
- const unknownName = (name) => {
42
- throw new LetopisError('invalid_class', `класс или роль ${name} не найдены`);
41
+ /** Имена, которыми начинается шаг: классы, их алиасы и роли концов реестра. */
42
+ const stepNames = (reg) => reg.all().flatMap((c) => [c.name, ...(c.alias ? [c.alias] : []), ...Object.keys(c.ends)]);
43
+ /** Неизвестный класс или роль — с похожими именами реестра (план 2.0, 7.9 №6). */
44
+ const unknownName = (reg, name) => {
45
+ throw lerr('api.unknown_step', { name, similar: similar(name, stepNames(reg)) });
43
46
  };
44
47
  const nodeOf = (steps, s) => s.pivotKey === undefined ? s : steps.find((x) => x.nodeKey === s.pivotKey) ?? s;
45
48
  /** Узел для возврата: последний реальный шаг того же класса-связи в текущем читающем сегменте. */
@@ -62,7 +65,7 @@ function warnNoHistory(steps, method) {
62
65
  if (noHistoryWarned.has(key))
63
66
  return;
64
67
  noHistoryWarned.add(key);
65
- console.warn(`letopis: .${method}() у класса ${cls.name} без журнала (history: false) — истории нет`);
68
+ lwarn('api.no_history', { method, cls: cls.name });
66
69
  }
67
70
  function report(ctx, mode, steps, t0, rows) {
68
71
  if (!ctx.onQuery && ctx.slowMs === undefined)
@@ -73,7 +76,7 @@ function report(ctx, mode, steps, t0, rows) {
73
76
  if (ctx.onQuery)
74
77
  ctx.onQuery({ mode, classes, ms, rows, slow });
75
78
  else if (slow)
76
- console.warn(`letopis: медленный запрос ${ms.toFixed(0)} мс — ${mode} ${classes.join('→')}`);
79
+ lwarn('api.slow_query', { ms: ms.toFixed(0), mode, classes: classes.join('→') });
77
80
  }
78
81
  /** Классы, которые читает цепочка (семейства шагов): по ним сбрасывается её ответ в кэше. */
79
82
  function readClasses(reg, steps) {
@@ -122,7 +125,7 @@ async function exec(ctx, q, mode, steps, mods = {}) {
122
125
  async function read(ctx, steps, mods, mode, agg) {
123
126
  const hasOps = steps.some((s) => s.op);
124
127
  if (ctx.batch && hasOps)
125
- throw new LetopisError('invalid_query', 'план стоит в очереди батча — его исполнит batch.run()');
128
+ throw lerr('api.batch_queued');
126
129
  if (mods.forUpdate)
127
130
  return lockedRead(ctx, steps, mods, mode);
128
131
  if (hasOps)
@@ -138,14 +141,21 @@ function modName(mods) {
138
141
  return key === 'order' || key === 'desc' ? 'sort' : key;
139
142
  }
140
143
  function afterVerbError(name, verb) {
141
- return new LetopisError('invalid_query', `${name}() после ${verb}() ни на что не действует: глагол проходит по всем целям шага, а ответ — его строки. ` +
142
- `Цели сужают limit(), offset() и sort() до глагола: db.Класс(…).limit(n).${verb}(…)`);
144
+ return lerr('api.mod_after_verb', { name, verb });
145
+ }
146
+ /**
147
+ * Курсор after() листает строки, а count() и агрегаты чтения считают по всем объектам шага: раньше курсор там молча не
148
+ * учитывался, с 2.0 — invalid_query до запроса (план 2.0, этап 4, п. 2; 7.10 №7).
149
+ */
150
+ function afterNotPaged(mods, fn) {
151
+ if (mods.after !== undefined)
152
+ throw lerr('api.after_not_paged', { fn });
143
153
  }
144
154
  /**
145
155
  * Терминалы rows, first, ids, count и paths цепочки, которая кончается глаголом, отдают строки самой правки.
146
156
  * Модификатор выборки между глаголом и терминалом ни на что не подействовал бы: limit резал бы только ответ, хотя
147
157
  * правка прошла по всем целям, остальные молча пропускались. Поэтому — отказ до записи. После глагола модификатор
148
- * относится к чтению за ним (шаг класса, versions(), diff(), blame(), агрегаты) или сужает цели следующего глагола.
158
+ * относится к чтению за ним (шаг класса, versions(), агрегаты) или сужает цели следующего глагола.
149
159
  */
150
160
  function checkAfterVerb(steps, mods) {
151
161
  const op = steps[steps.length - 1]?.op;
@@ -158,7 +168,7 @@ function checkAfterVerb(steps, mods) {
158
168
  * чтения (строки rows/paths, id, n, агрегаты), чтобы терминалы разбирали его одинаково.
159
169
  */
160
170
  async function plan(ctx, steps, mods, mode, agg) {
161
- precheckPlan(ctx.reg, steps);
171
+ precheckPlan(ctx.reg, steps, mods.order, agg);
162
172
  const t0 = performance.now();
163
173
  const r = await ctx.exec.run((q) => execPlan({ reg: ctx.reg, schema: ctx.schema, q, tenant: ctx.tenant(), owner: ctx.owner, ...reasons(ctx) }, steps, mods, mode, agg));
164
174
  if (r.kind === 'read') {
@@ -191,9 +201,9 @@ async function plan(ctx, steps, mods, mode, agg) {
191
201
  */
192
202
  async function lockedRead(ctx, steps, mods, mode) {
193
203
  if (!ctx.exec.explicit)
194
- throw new LetopisError('tx_required', 'forUpdate() — только внутри db.begin(): вне явной транзакции блокировка снялась бы сразу');
204
+ throw lerr('api.for_update_outside_tx');
195
205
  if ((mode !== 'rows' && mode !== 'ids') || mods.asOf !== undefined || mods.withDeleted || steps.some((s) => s.deepMax !== undefined || s.op)) {
196
- throw new LetopisError('lock_unsupported', 'forUpdate() — с терминалами rows, first, ids, без агрегатов, asOf, versions, withDeleted, deep и глаголов');
206
+ throw lerr('api.for_update_unsupported');
197
207
  }
198
208
  const S = `"${ctx.schema.replace(/"/g, '""')}"`;
199
209
  const { forUpdate: _f, ...plain } = mods;
@@ -211,7 +221,7 @@ async function lockedRead(ctx, steps, mods, mode) {
211
221
  .filter((r) => want.has(r.row.id));
212
222
  const denied = fresh.filter((r) => !locked.has(r.row.id)).map((r) => r.row.id);
213
223
  if (denied.length)
214
- throw new LetopisError('acl_denied', `строки видны, но заблокировать их нельзя: ${denied.join(', ')}`, { ids: denied });
224
+ throw lerr('api.lock_denied', { ids: denied.join(', ') }, { detail: { ids: denied } });
215
225
  return fresh;
216
226
  });
217
227
  report(ctx, mode, steps, t0, res.length);
@@ -221,38 +231,25 @@ async function lockedRead(ctx, steps, mods, mode) {
221
231
  const reasons = (ctx) => ({ reason: ctx.reason ?? ctx.txReason ?? '', txReason: ctx.txReason ?? '' });
222
232
  /** Опция reason глагола (план 1.1, §2.1): проверяет precheckPlan. */
223
233
  const reasonOf = (o) => (o?.reason !== undefined ? { reason: o.reason } : {});
224
- /** diff({ from, to }): номер версии (целое ≥ 1) или момент (ISO-строка, Date). */
225
- function versionRef(v, what, moments = true) {
226
- if (typeof v === 'number' && Number.isInteger(v) && v >= 1 && v <= 2147483647)
227
- return v;
228
- if (moments && v instanceof Date && !Number.isNaN(+v))
229
- return v.toISOString();
230
- if (moments && typeof v === 'string' && !Number.isNaN(Date.parse(v)))
231
- return v;
232
- throw new LetopisError('invalid_query', `${what}: номер версии (целое ≥ 1)${moments ? ' или момент (ISO-строка, Date)' : ''}`);
233
- }
234
- /** blame({ at }): момент (ISO-строка, Date). */
235
- function momentOf(v, what) {
236
- if (v instanceof Date && !Number.isNaN(+v))
237
- return v.toISOString();
238
- if (typeof v === 'string' && !Number.isNaN(Date.parse(v)))
239
- return v;
240
- throw new LetopisError('invalid_query', `${what}: момент — ISO-строка или Date`);
241
- }
234
+ /** Опция all у update() и delete({ confirm: true }): правка всех целей без фильтра (план 2.0, этап 4, п. 2). */
235
+ const allOf = (o) => (o?.all === true ? { all: true } : {});
242
236
  /** Строка ответа: из чтения — to_jsonb узла, из плана — готовая строка записи. */
243
237
  const rowOf = (r) => (r.done ? r.row : toRow(r.row));
244
238
  /** Значение слота: id, список id, строка ответа или ленивая цепочка. */
245
239
  function slotValue(v) {
246
240
  const peek = peekPlan(v);
247
- if (peek)
241
+ if (peek) {
242
+ // модификатор сразу после глагола вложенной цепочки — тот же отказ, что у терминала (план 2.0, 7.2 №3)
243
+ checkAfterVerb(peek.steps, peek.mods);
248
244
  return { plan: peek.steps, mods: peek.mods };
245
+ }
249
246
  if (v === null || typeof v === 'string')
250
247
  return v;
251
248
  if (Array.isArray(v))
252
249
  return v.map((x) => (typeof x === 'string' ? x : x.id));
253
250
  if (typeof v === 'object' && typeof v.id === 'string')
254
251
  return { id: v.id };
255
- throw new LetopisError('invalid_query', 'значение слота — id, список id, строка ответа или цепочка');
252
+ throw lerr('api.slot_value');
256
253
  }
257
254
  function classProp(ctx, name, found, ownerSteps, mods, nodes) {
258
255
  const { cls, role } = found;
@@ -273,16 +270,16 @@ function classProp(ctx, name, found, ownerSteps, mods, nodes) {
273
270
  const last = ownerSteps?.[ownerSteps.length - 1];
274
271
  const kind = last?.op?.kind;
275
272
  if (!last || (kind !== 'create' && kind !== 'update' && kind !== 'upsert')) {
276
- throw new LetopisError('invalid_query', `слот .${name}.${op}() — после create(), update() или upsert(): db.Класс(…).update(…).${name}.${op}(…)`);
273
+ throw lerr('api.slot_without_write', { name, op });
277
274
  }
278
275
  const role = slotRole(ctx.reg, last.cls, name);
279
276
  const end = last.cls.ends[role];
280
277
  if ((op === 'add' || op === 'remove') && !end.many)
281
- throw new LetopisError('invalid_query', `слот .${name}.${op}(): конец ${role} одиночный — set() или unset()`);
278
+ throw lerr('api.slot_single', { name, op, role });
282
279
  if (op === 'unset' && (end.required || (end.min ?? 0) > 0))
283
- throw new LetopisError('invalid_query', `слот .${name}.unset(): конец ${role} обязателен`);
280
+ throw lerr('api.slot_required', { name, role });
284
281
  if ((op === 'set' || op === 'unset') && last.slots?.some((x) => x.role === role && (x.op === 'set' || x.op === 'unset'))) {
285
- throw new LetopisError('invalid_query', `слот ${role} задан дважды в одной записи`);
282
+ throw lerr('api.slot_twice', { role });
286
283
  }
287
284
  const value = op === 'unset' ? null : slotValue(v);
288
285
  const next = ownerSteps.slice();
@@ -310,7 +307,7 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
310
307
  const withLast = (patch) => {
311
308
  const lastOp = steps[steps.length - 1]?.op;
312
309
  if (lastOp)
313
- throw new LetopisError('invalid_query', `модификатор шага после ${lastOp.kind}() — сначала шаг класса`);
310
+ throw lerr('api.step_mod_after_verb', { verb: lastOp.kind });
314
311
  const next = steps.slice();
315
312
  next[next.length - 1] = { ...steps[steps.length - 1], ...patch };
316
313
  return makeChain(ctx, next, mods, nodes);
@@ -332,7 +329,7 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
332
329
  const withOp = (op) => {
333
330
  const last = steps[steps.length - 1];
334
331
  if (!last)
335
- throw new LetopisError('invalid_query', `${op.kind}() — нужен шаг класса: db.Класс(…).${op.kind}(…)`);
332
+ throw lerr('api.verb_without_step', { verb: op.kind });
336
333
  // батч: причина db.batch(имя, { reason }) — при постановке в очередь, а не при run(): очередь общая по имени, и
337
334
  // run() может позвать фасад без причины или с другой (ревью №1 точки А плана 1.1)
338
335
  if (ctx.batch && op.reason === undefined && ctx.reason !== undefined)
@@ -347,7 +344,7 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
347
344
  const plan = peekPlan(x);
348
345
  const prev = steps.length ? nodeOf(steps, steps[steps.length - 1]) : undefined;
349
346
  if (plan?.steps.some((s) => s.op))
350
- throw new LetopisError('invalid_query', 'entity() принимает читающий паттерн — цепочку с глаголами вклеить нельзя');
347
+ throw lerr('api.entity_with_verbs');
351
348
  if (plan) {
352
349
  const known = nodes.get(x);
353
350
  if (known !== undefined) {
@@ -369,9 +366,10 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
369
366
  resolveHop(ctx.reg, prev, next);
370
367
  return makeChain(ctx, [...steps, next], mods, nodes);
371
368
  }
372
- throw new LetopisError('invalid_query', 'entity() принимает ленивую цепочку или строку ответа');
369
+ throw lerr('api.entity_arg');
373
370
  };
374
371
  const agg = (fn) => async (field) => {
372
+ afterNotPaged(mods, fn);
375
373
  const { q, res } = await read(ctx, steps, mods, 'agg', { fn, field });
376
374
  if (fn === 'countBy')
377
375
  return Object.fromEntries(res.map((r) => [r.k ?? 'null', r.n]));
@@ -384,147 +382,116 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
384
382
  return Number(v);
385
383
  return v;
386
384
  };
385
+ /** Свойство цепочки: метод или модификатор — функция, иначе шаг класса, алиаса или роли. */
386
+ const member = (prop) => {
387
+ switch (prop) {
388
+ case 'then':
389
+ return undefined;
390
+ case 'entity':
391
+ return entityStep;
392
+ case 'paths':
393
+ return async () => {
394
+ checkAfterVerb(steps, mods);
395
+ // q === null — пути из результата глагола: строки уже готовы. Ключи — в порядке шагов (jsonb хранит их
396
+ // по длине), узел последнего шага — последним: по нему cursorOf(путь, поле) берёт курсор (план 2.0, 7.1 №3)
397
+ const { q, res } = await read(ctx, steps, mods, 'paths');
398
+ if (q === null)
399
+ return res.map((r) => r.path);
400
+ return res.map((r) => Object.fromEntries(q.keys.map((k) => [k, toRow(r.path[k])])));
401
+ };
402
+ case 'rows':
403
+ return async () => {
404
+ checkAfterVerb(steps, mods);
405
+ return (await read(ctx, steps, mods, 'rows')).res.map(rowOf);
406
+ };
407
+ case 'first':
408
+ return async () => {
409
+ checkAfterVerb(steps, mods);
410
+ const { res } = await read(ctx, steps, { ...mods, limit: 1 }, 'rows');
411
+ return res[0] ? rowOf(res[0]) : null;
412
+ };
413
+ case 'ids':
414
+ return async () => {
415
+ checkAfterVerb(steps, mods);
416
+ return (await read(ctx, steps, mods, 'ids')).res.map((r) => r.id);
417
+ };
418
+ case 'count':
419
+ return async (opts) => {
420
+ checkAfterVerb(steps, mods);
421
+ afterNotPaged(mods, 'count');
422
+ return (await read(ctx, steps, mods, opts?.paths ? 'countPaths' : 'count')).res[0].n;
423
+ };
424
+ case 'versions':
425
+ return async () => {
426
+ warnNoHistory(steps, 'versions');
427
+ return (await read(ctx, steps, mods, 'versions')).res.map((r) => toRow(r.row));
428
+ };
429
+ case 'sum':
430
+ case 'avg':
431
+ case 'min':
432
+ case 'max':
433
+ case 'countBy':
434
+ return agg(prop);
435
+ case 'limit':
436
+ return (n) => withMods({ limit: n });
437
+ case 'offset':
438
+ return (n) => withMods({ offset: n });
439
+ case 'sort':
440
+ return (field, dir) => withMods({ order: field, desc: dir === true || dir === 'desc' });
441
+ case 'asOf':
442
+ return (t) => {
443
+ warnNoHistory(steps, 'asOf');
444
+ return withMods({ asOf: t instanceof Date ? t.toISOString() : t });
445
+ };
446
+ case 'after':
447
+ return (cursor) => withMods({ after: cursor });
448
+ case 'withDeleted':
449
+ return () => withMods({ withDeleted: true });
450
+ case 'deep':
451
+ return (max = 32) => withLast({ deepMax: max });
452
+ case 'exact':
453
+ return () => withLast({ exact: true });
454
+ case 'alias':
455
+ return (name) => withLast({ aliasKey: name });
456
+ case 'tags':
457
+ return (v) => withLast({ tagsFilter: v });
458
+ case 'owner':
459
+ return (v) => withLast({ ownerFilter: typeof v === 'object' ? v.id : v });
460
+ // глаголы записи — звенья плана (этап 4)
461
+ case 'create':
462
+ return (data = {}, opts) => withOp({ kind: 'create', data, ...reasonOf(opts) });
463
+ case 'update':
464
+ return (data = {}, opts) => withOp({ kind: 'update', data, ...(opts?.rev !== undefined ? { rev: opts.rev } : {}), ...allOf(opts), ...reasonOf(opts) });
465
+ case 'upsert':
466
+ return (data = {}, opts) => withOp({ kind: 'upsert', data, ...reasonOf(opts) });
467
+ case 'delete':
468
+ return (opts) => withOp({ kind: 'delete', confirm: opts?.confirm === true, ...allOf(opts), ...reasonOf(opts) });
469
+ case 'set':
470
+ case 'unset':
471
+ case 'add':
472
+ case 'remove':
473
+ throw lerr('api.slot_method', { method: prop });
474
+ case 'forUpdate':
475
+ return () => withMods({ forUpdate: true });
476
+ case 'restore':
477
+ return (opts) => withOp({ kind: 'restore', ...reasonOf(opts) });
478
+ case 'cache':
479
+ return (opts) => {
480
+ if (opts?.ttl !== undefined && !(opts.ttl > 0))
481
+ throw lerr('api.cache_ttl');
482
+ return withMods({ cache: opts ?? {} });
483
+ };
484
+ }
485
+ const found = lookupStep(ctx.reg, prop) ?? unknownName(ctx.reg, prop);
486
+ return classProp(ctx, prop, found, steps, mods, nodes);
487
+ };
387
488
  const handler = {
388
489
  get(_t, prop) {
389
490
  if (prop === PLAN)
390
491
  return { steps, mods };
391
492
  if (typeof prop === 'symbol')
392
493
  return undefined;
393
- switch (prop) {
394
- case 'then':
395
- return undefined;
396
- case 'entity':
397
- return entityStep;
398
- case 'paths':
399
- return async () => {
400
- checkAfterVerb(steps, mods);
401
- // q === null — пути из результата глагола: строки уже готовы
402
- const { q, res } = await read(ctx, steps, mods, 'paths');
403
- return res.map((r) => Object.fromEntries(Object.entries(r.path).map(([k, v]) => [k, q === null ? v : toRow(v)])));
404
- };
405
- case 'rows':
406
- return async () => {
407
- checkAfterVerb(steps, mods);
408
- return (await read(ctx, steps, mods, 'rows')).res.map(rowOf);
409
- };
410
- case 'first':
411
- return async () => {
412
- checkAfterVerb(steps, mods);
413
- const { res } = await read(ctx, steps, { ...mods, limit: 1 }, 'rows');
414
- return res[0] ? rowOf(res[0]) : null;
415
- };
416
- case 'ids':
417
- return async () => {
418
- checkAfterVerb(steps, mods);
419
- return (await read(ctx, steps, mods, 'ids')).res.map((r) => r.id);
420
- };
421
- case 'count':
422
- return async (opts) => {
423
- checkAfterVerb(steps, mods);
424
- return (await read(ctx, steps, mods, opts?.paths ? 'countPaths' : 'count')).res[0].n;
425
- };
426
- case 'versions':
427
- return async (opts) => {
428
- warnNoHistory(steps, 'versions');
429
- const m = opts?.follow ? { ...mods, follow: true } : mods;
430
- return (await read(ctx, steps, m, 'versions')).res.map((r) => toRow(r.row));
431
- };
432
- case 'diff':
433
- return async (opts = {}) => {
434
- warnNoHistory(steps, 'diff');
435
- const hist = {
436
- ...(opts.from !== undefined ? { from: versionRef(opts.from, 'diff({ from })') } : {}),
437
- ...(opts.to !== undefined ? { to: versionRef(opts.to, 'diff({ to })') } : {}),
438
- ...(opts.follow ? { follow: true } : {}),
439
- };
440
- return (await read(ctx, steps, { ...mods, hist }, 'diff')).res.map((r) => r.row);
441
- };
442
- case 'blame':
443
- return async (opts = {}) => {
444
- warnNoHistory(steps, 'blame');
445
- if (opts.limit !== undefined)
446
- versionRef(opts.limit, 'blame({ limit })', false);
447
- const hist = {
448
- ...(opts.at !== undefined ? { at: momentOf(opts.at, 'blame({ at })') } : {}),
449
- ...(opts.limit !== undefined ? { limit: opts.limit } : {}),
450
- };
451
- // null — объекта на момент at не было (или он не виден)
452
- return (await read(ctx, steps, { ...mods, hist }, 'blame')).res
453
- .map((r) => r.row).filter((x) => x !== null);
454
- };
455
- case 'sum':
456
- case 'avg':
457
- case 'min':
458
- case 'max':
459
- case 'countBy':
460
- return agg(prop);
461
- case 'limit':
462
- return (n) => withMods({ limit: n });
463
- case 'offset':
464
- return (n) => withMods({ offset: n });
465
- case 'sort':
466
- return (field, dir) => withMods({ order: field, desc: dir === true || dir === 'desc' });
467
- case 'asOf':
468
- return (t) => {
469
- warnNoHistory(steps, 'asOf');
470
- return withMods({ asOf: t instanceof Date ? t.toISOString() : t });
471
- };
472
- case 'after':
473
- return (cursor) => withMods({ after: cursor });
474
- case 'withDeleted':
475
- return () => withMods({ withDeleted: true });
476
- case 'deep':
477
- return (max = 32) => withLast({ deepMax: max });
478
- case 'exact':
479
- return () => withLast({ exact: true });
480
- case 'alias':
481
- return (name) => withLast({ aliasKey: name });
482
- case 'tags':
483
- return (v) => withLast({ tagsFilter: v });
484
- case 'owner':
485
- return (v) => withLast({ ownerFilter: typeof v === 'object' ? v.id : v });
486
- // охранники снесённого API (этап 3, п. 11)
487
- case 'run':
488
- throw removed('run()', 'paths()');
489
- case 'execute':
490
- throw removed('execute()', 'paths()');
491
- case 'account':
492
- throw removed('account()', 'арендатор сессии и auth.switch() (этап 5)');
493
- // глаголы записи — звенья плана (этап 4)
494
- case 'create':
495
- return (data = {}, opts) => withOp({ kind: 'create', data, ...reasonOf(opts) });
496
- case 'update':
497
- return (data = {}, opts) => withOp({ kind: 'update', data, ...(opts?.rev !== undefined ? { rev: opts.rev } : {}), ...reasonOf(opts) });
498
- case 'upsert':
499
- return (data = {}, opts) => withOp({ kind: 'upsert', data, ...reasonOf(opts) });
500
- case 'delete':
501
- return (opts) => withOp({ kind: 'delete', confirm: opts?.confirm === true, ...reasonOf(opts) });
502
- case 'anonymize':
503
- return (fields, opts) => withOp({ kind: 'anonymize', fields, ...reasonOf(opts) });
504
- case 'reclass':
505
- return (cls, data, opts) => withOp({ kind: 'reclass', cls, ...(data !== undefined ? { data } : {}), ...reasonOf(opts) });
506
- case 'set':
507
- case 'unset':
508
- case 'add':
509
- case 'remove':
510
- throw new LetopisError('invalid_query', `${prop}() — метод слота: db.Класс(…).update(…).Роль.${prop}(…)`);
511
- case 'forUpdate':
512
- return () => withMods({ forUpdate: true });
513
- case 'restore':
514
- return (opts) => withOp({ kind: 'restore', ...reasonOf(opts) });
515
- case 'purge':
516
- return (opts) => withOp({ kind: 'purge', confirm: opts?.confirm === true, ...reasonOf(opts) });
517
- case 'rekey':
518
- return (patch, opts) => withOp({ kind: 'rekey', data: patch ?? {}, ...reasonOf(opts) });
519
- case 'cache':
520
- return (opts) => {
521
- if (opts?.ttl !== undefined && !(opts.ttl > 0))
522
- throw new LetopisError('invalid_query', 'cache({ ttl }): срок — положительное число секунд');
523
- return withMods({ cache: opts ?? {} });
524
- };
525
- }
526
- const found = lookupStep(ctx.reg, prop) ?? unknownName(prop);
527
- return classProp(ctx, prop, found, steps, mods, nodes);
494
+ return member(prop);
528
495
  },
529
496
  };
530
497
  return new Proxy({}, handler);
@@ -535,38 +502,39 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
535
502
  * и общей с другими фасадами быть не может: иначе план исполнился бы от имени чужого актора.
536
503
  */
537
504
  export function makeBatch(ctx, queue) {
505
+ const member = (prop) => {
506
+ switch (prop) {
507
+ case 'run':
508
+ return async () => {
509
+ const entries = queue.splice(0, queue.length);
510
+ const loose = entries.find((x) => x.loose)?.loose;
511
+ if (loose)
512
+ throw afterVerbError(loose.name, loose.verb);
513
+ const plans = entries.map((x) => x.steps);
514
+ for (const p of plans)
515
+ precheckPlan(ctx.reg, p);
516
+ if (!plans.length)
517
+ return [];
518
+ return ctx.exec.run((q) => execBatch({ reg: ctx.reg, schema: ctx.schema, q, tenant: ctx.tenant(), owner: ctx.owner, ...reasons(ctx) }, plans));
519
+ };
520
+ case 'discard':
521
+ return () => void queue.splice(0, queue.length);
522
+ case 'size':
523
+ return () => queue.length;
524
+ }
525
+ return startStep({ ...ctx, batch: { queue } }, prop);
526
+ };
538
527
  return new Proxy({}, {
539
528
  get(_t, prop) {
540
529
  if (typeof prop === 'symbol' || prop === 'then')
541
530
  return undefined;
542
- switch (prop) {
543
- case 'run':
544
- return async () => {
545
- const entries = queue.splice(0, queue.length);
546
- const loose = entries.find((x) => x.loose)?.loose;
547
- if (loose)
548
- throw afterVerbError(loose.name, loose.verb);
549
- const plans = entries.map((x) => x.steps);
550
- for (const p of plans)
551
- precheckPlan(ctx.reg, p);
552
- if (!plans.length)
553
- return [];
554
- return ctx.exec.run((q) => execBatch({ reg: ctx.reg, schema: ctx.schema, q, tenant: ctx.tenant(), owner: ctx.owner, ...reasons(ctx) }, plans));
555
- };
556
- case 'execute':
557
- throw removed('batch.execute()', 'batch.run()');
558
- case 'discard':
559
- return () => void queue.splice(0, queue.length);
560
- case 'size':
561
- return () => queue.length;
562
- }
563
- return startStep({ ...ctx, batch: { queue } }, prop);
531
+ return member(prop);
564
532
  },
565
533
  });
566
534
  }
567
535
  /** Старт пути от корня db: класс, алиас или роль. */
568
536
  export function startStep(ctx, name) {
569
- const found = lookupStep(ctx.reg, name) ?? unknownName(name);
537
+ const found = lookupStep(ctx.reg, name) ?? unknownName(ctx.reg, name);
570
538
  return classProp(ctx, name, found, undefined, {}, NO_NODES);
571
539
  }
572
540
  /** Старт пути с готового узла: db.entity(row | цепочка). */