letopis 1.4.2 → 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 (112) hide show
  1. package/AGENT-CHEATSHEET.md +329 -281
  2. package/CHANGELOG.md +418 -0
  3. package/README.md +572 -1053
  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 -184
  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 +30 -8
  12. package/dist/chain.d.ts +18 -41
  13. package/dist/chain.js +165 -203
  14. package/dist/cli.js +45 -90
  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 -70
  21. package/dist/index.js +189 -122
  22. package/dist/internal.d.ts +5 -0
  23. package/dist/jsonschema.js +18 -17
  24. package/dist/load.d.ts +20 -12
  25. package/dist/load.js +207 -181
  26. package/dist/mcp.d.ts +5 -7
  27. package/dist/mcp.js +168 -214
  28. package/dist/messages/access.d.ts +90 -0
  29. package/dist/messages/access.js +113 -0
  30. package/dist/messages/api.d.ts +171 -0
  31. package/dist/messages/api.js +215 -0
  32. package/dist/messages/boot.d.ts +201 -0
  33. package/dist/messages/boot.js +256 -0
  34. package/dist/messages/codes.d.ts +172 -0
  35. package/dist/messages/codes.js +173 -0
  36. package/dist/messages/index.d.ts +95 -0
  37. package/dist/messages/index.js +118 -0
  38. package/dist/messages/mcp.d.ts +306 -0
  39. package/dist/messages/mcp.js +387 -0
  40. package/dist/messages/model.d.ts +233 -0
  41. package/dist/messages/model.js +283 -0
  42. package/dist/messages/query.d.ts +131 -0
  43. package/dist/messages/query.js +158 -0
  44. package/dist/messages/sql-access.d.ts +70 -0
  45. package/dist/messages/sql-access.js +87 -0
  46. package/dist/messages/sql-core.d.ts +150 -0
  47. package/dist/messages/sql-core.js +181 -0
  48. package/dist/messages/sql-time.d.ts +71 -0
  49. package/dist/messages/sql-time.js +88 -0
  50. package/dist/messages/sql-write.d.ts +246 -0
  51. package/dist/messages/sql-write.js +270 -0
  52. package/dist/messages/time.d.ts +23 -0
  53. package/dist/messages/time.js +29 -0
  54. package/dist/messages/types.d.ts +27 -0
  55. package/dist/messages/types.js +5 -0
  56. package/dist/messages/write.d.ts +405 -0
  57. package/dist/messages/write.js +509 -0
  58. package/dist/model.d.ts +73 -60
  59. package/dist/model.js +20 -29
  60. package/dist/ops.d.ts +35 -0
  61. package/dist/ops.js +101 -0
  62. package/dist/pglite.d.ts +0 -9
  63. package/dist/pglite.js +3 -1
  64. package/dist/registry.d.ts +3 -19
  65. package/dist/registry.js +3 -16
  66. package/dist/signals.d.ts +1 -5
  67. package/dist/signals.js +1 -5
  68. package/dist/sql.d.ts +7 -1
  69. package/dist/sql.js +101 -85
  70. package/dist/sync.d.ts +9 -3
  71. package/dist/sync.js +40 -17
  72. package/dist/tx.d.ts +11 -1
  73. package/dist/tx.js +32 -11
  74. package/dist/typed.d.ts +240 -45
  75. package/dist/types.d.ts +15 -90
  76. package/dist/types.js +12 -14
  77. package/dist/up.d.ts +53 -32
  78. package/dist/up.js +180 -74
  79. package/dist/uuid.js +5 -5
  80. package/dist/validate.d.ts +13 -1
  81. package/dist/validate.js +61 -29
  82. package/dist/watch.d.ts +5 -16
  83. package/dist/watch.js +23 -77
  84. package/dist/write.d.ts +9 -16
  85. package/dist/write.js +102 -185
  86. package/llms.txt +22 -25
  87. package/package.json +2 -9
  88. package/sql/10-core.sql +7 -50
  89. package/sql/15-errors.sql +64 -10
  90. package/sql/20-context.sql +7 -6
  91. package/sql/30-validate.sql +35 -27
  92. package/sql/40-class.sql +20 -114
  93. package/sql/50-acl.sql +8 -13
  94. package/sql/60-write.sql +220 -676
  95. package/sql/70-read.sql +11 -13
  96. package/sql/80-auth.sql +23 -27
  97. package/sql/90-time.sql +77 -944
  98. package/sql/95-seed.system.sql +7 -14
  99. package/sql/99-revision.sql +2 -1
  100. package/sql/README.md +32 -24
  101. package/sql/migrate/README.md +14 -0
  102. package/AGENT-CHEATSHEET.en.md +0 -488
  103. package/MIGRATION.md +0 -190
  104. package/README.en.md +0 -2910
  105. package/dist/import.d.ts +0 -185
  106. package/dist/import.js +0 -820
  107. package/dist/project.d.ts +0 -359
  108. package/dist/project.js +0 -1258
  109. package/sql/75-history.sql +0 -436
  110. package/sql/88-derived.sql +0 -1236
  111. package/sql/migrate/0006-changed-paths.sql +0 -3
  112. 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,24 +231,8 @@ 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, строка ответа или ленивая цепочка. */
@@ -255,7 +249,7 @@ function slotValue(v) {
255
249
  return v.map((x) => (typeof x === 'string' ? x : x.id));
256
250
  if (typeof v === 'object' && typeof v.id === 'string')
257
251
  return { id: v.id };
258
- throw new LetopisError('invalid_query', 'значение слота — id, список id, строка ответа или цепочка');
252
+ throw lerr('api.slot_value');
259
253
  }
260
254
  function classProp(ctx, name, found, ownerSteps, mods, nodes) {
261
255
  const { cls, role } = found;
@@ -276,16 +270,16 @@ function classProp(ctx, name, found, ownerSteps, mods, nodes) {
276
270
  const last = ownerSteps?.[ownerSteps.length - 1];
277
271
  const kind = last?.op?.kind;
278
272
  if (!last || (kind !== 'create' && kind !== 'update' && kind !== 'upsert')) {
279
- throw new LetopisError('invalid_query', `слот .${name}.${op}() — после create(), update() или upsert(): db.Класс(…).update(…).${name}.${op}(…)`);
273
+ throw lerr('api.slot_without_write', { name, op });
280
274
  }
281
275
  const role = slotRole(ctx.reg, last.cls, name);
282
276
  const end = last.cls.ends[role];
283
277
  if ((op === 'add' || op === 'remove') && !end.many)
284
- throw new LetopisError('invalid_query', `слот .${name}.${op}(): конец ${role} одиночный — set() или unset()`);
278
+ throw lerr('api.slot_single', { name, op, role });
285
279
  if (op === 'unset' && (end.required || (end.min ?? 0) > 0))
286
- throw new LetopisError('invalid_query', `слот .${name}.unset(): конец ${role} обязателен`);
280
+ throw lerr('api.slot_required', { name, role });
287
281
  if ((op === 'set' || op === 'unset') && last.slots?.some((x) => x.role === role && (x.op === 'set' || x.op === 'unset'))) {
288
- throw new LetopisError('invalid_query', `слот ${role} задан дважды в одной записи`);
282
+ throw lerr('api.slot_twice', { role });
289
283
  }
290
284
  const value = op === 'unset' ? null : slotValue(v);
291
285
  const next = ownerSteps.slice();
@@ -313,7 +307,7 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
313
307
  const withLast = (patch) => {
314
308
  const lastOp = steps[steps.length - 1]?.op;
315
309
  if (lastOp)
316
- throw new LetopisError('invalid_query', `модификатор шага после ${lastOp.kind}() — сначала шаг класса`);
310
+ throw lerr('api.step_mod_after_verb', { verb: lastOp.kind });
317
311
  const next = steps.slice();
318
312
  next[next.length - 1] = { ...steps[steps.length - 1], ...patch };
319
313
  return makeChain(ctx, next, mods, nodes);
@@ -335,7 +329,7 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
335
329
  const withOp = (op) => {
336
330
  const last = steps[steps.length - 1];
337
331
  if (!last)
338
- throw new LetopisError('invalid_query', `${op.kind}() — нужен шаг класса: db.Класс(…).${op.kind}(…)`);
332
+ throw lerr('api.verb_without_step', { verb: op.kind });
339
333
  // батч: причина db.batch(имя, { reason }) — при постановке в очередь, а не при run(): очередь общая по имени, и
340
334
  // run() может позвать фасад без причины или с другой (ревью №1 точки А плана 1.1)
341
335
  if (ctx.batch && op.reason === undefined && ctx.reason !== undefined)
@@ -350,7 +344,7 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
350
344
  const plan = peekPlan(x);
351
345
  const prev = steps.length ? nodeOf(steps, steps[steps.length - 1]) : undefined;
352
346
  if (plan?.steps.some((s) => s.op))
353
- throw new LetopisError('invalid_query', 'entity() принимает читающий паттерн — цепочку с глаголами вклеить нельзя');
347
+ throw lerr('api.entity_with_verbs');
354
348
  if (plan) {
355
349
  const known = nodes.get(x);
356
350
  if (known !== undefined) {
@@ -372,9 +366,10 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
372
366
  resolveHop(ctx.reg, prev, next);
373
367
  return makeChain(ctx, [...steps, next], mods, nodes);
374
368
  }
375
- throw new LetopisError('invalid_query', 'entity() принимает ленивую цепочку или строку ответа');
369
+ throw lerr('api.entity_arg');
376
370
  };
377
371
  const agg = (fn) => async (field) => {
372
+ afterNotPaged(mods, fn);
378
373
  const { q, res } = await read(ctx, steps, mods, 'agg', { fn, field });
379
374
  if (fn === 'countBy')
380
375
  return Object.fromEntries(res.map((r) => [r.k ?? 'null', r.n]));
@@ -387,150 +382,116 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
387
382
  return Number(v);
388
383
  return v;
389
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
+ };
390
488
  const handler = {
391
489
  get(_t, prop) {
392
490
  if (prop === PLAN)
393
491
  return { steps, mods };
394
492
  if (typeof prop === 'symbol')
395
493
  return undefined;
396
- switch (prop) {
397
- case 'then':
398
- return undefined;
399
- case 'entity':
400
- return entityStep;
401
- case 'paths':
402
- return async () => {
403
- checkAfterVerb(steps, mods);
404
- // q === null — пути из результата глагола: строки уже готовы. Ключи — в порядке шагов (jsonb хранит их
405
- // по длине), узел последнего шага — последним: по нему cursorOf(путь, поле) берёт курсор (план 2.0, 7.1 №3)
406
- const { q, res } = await read(ctx, steps, mods, 'paths');
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])])));
410
- };
411
- case 'rows':
412
- return async () => {
413
- checkAfterVerb(steps, mods);
414
- return (await read(ctx, steps, mods, 'rows')).res.map(rowOf);
415
- };
416
- case 'first':
417
- return async () => {
418
- checkAfterVerb(steps, mods);
419
- const { res } = await read(ctx, steps, { ...mods, limit: 1 }, 'rows');
420
- return res[0] ? rowOf(res[0]) : null;
421
- };
422
- case 'ids':
423
- return async () => {
424
- checkAfterVerb(steps, mods);
425
- return (await read(ctx, steps, mods, 'ids')).res.map((r) => r.id);
426
- };
427
- case 'count':
428
- return async (opts) => {
429
- checkAfterVerb(steps, mods);
430
- return (await read(ctx, steps, mods, opts?.paths ? 'countPaths' : 'count')).res[0].n;
431
- };
432
- case 'versions':
433
- return async (opts) => {
434
- warnNoHistory(steps, 'versions');
435
- const m = opts?.follow ? { ...mods, follow: true } : mods;
436
- return (await read(ctx, steps, m, 'versions')).res.map((r) => toRow(r.row));
437
- };
438
- case 'diff':
439
- return async (opts = {}) => {
440
- warnNoHistory(steps, 'diff');
441
- const hist = {
442
- ...(opts.from !== undefined ? { from: versionRef(opts.from, 'diff({ from })') } : {}),
443
- ...(opts.to !== undefined ? { to: versionRef(opts.to, 'diff({ to })') } : {}),
444
- ...(opts.follow ? { follow: true } : {}),
445
- };
446
- return (await read(ctx, steps, { ...mods, hist }, 'diff')).res.map((r) => r.row);
447
- };
448
- case 'blame':
449
- return async (opts = {}) => {
450
- warnNoHistory(steps, 'blame');
451
- if (opts.limit !== undefined)
452
- versionRef(opts.limit, 'blame({ limit })', false);
453
- const hist = {
454
- ...(opts.at !== undefined ? { at: momentOf(opts.at, 'blame({ at })') } : {}),
455
- ...(opts.limit !== undefined ? { limit: opts.limit } : {}),
456
- };
457
- // null — объекта на момент at не было (или он не виден)
458
- return (await read(ctx, steps, { ...mods, hist }, 'blame')).res
459
- .map((r) => r.row).filter((x) => x !== null);
460
- };
461
- case 'sum':
462
- case 'avg':
463
- case 'min':
464
- case 'max':
465
- case 'countBy':
466
- return agg(prop);
467
- case 'limit':
468
- return (n) => withMods({ limit: n });
469
- case 'offset':
470
- return (n) => withMods({ offset: n });
471
- case 'sort':
472
- return (field, dir) => withMods({ order: field, desc: dir === true || dir === 'desc' });
473
- case 'asOf':
474
- return (t) => {
475
- warnNoHistory(steps, 'asOf');
476
- return withMods({ asOf: t instanceof Date ? t.toISOString() : t });
477
- };
478
- case 'after':
479
- return (cursor) => withMods({ after: cursor });
480
- case 'withDeleted':
481
- return () => withMods({ withDeleted: true });
482
- case 'deep':
483
- return (max = 32) => withLast({ deepMax: max });
484
- case 'exact':
485
- return () => withLast({ exact: true });
486
- case 'alias':
487
- return (name) => withLast({ aliasKey: name });
488
- case 'tags':
489
- return (v) => withLast({ tagsFilter: v });
490
- case 'owner':
491
- return (v) => withLast({ ownerFilter: typeof v === 'object' ? v.id : v });
492
- // охранники снесённого API (этап 3, п. 11)
493
- case 'run':
494
- throw removed('run()', 'paths()');
495
- case 'execute':
496
- throw removed('execute()', 'paths()');
497
- case 'account':
498
- throw removed('account()', 'арендатор сессии и auth.switch() (этап 5)');
499
- // глаголы записи — звенья плана (этап 4)
500
- case 'create':
501
- return (data = {}, opts) => withOp({ kind: 'create', data, ...reasonOf(opts) });
502
- case 'update':
503
- return (data = {}, opts) => withOp({ kind: 'update', data, ...(opts?.rev !== undefined ? { rev: opts.rev } : {}), ...reasonOf(opts) });
504
- case 'upsert':
505
- return (data = {}, opts) => withOp({ kind: 'upsert', data, ...reasonOf(opts) });
506
- case 'delete':
507
- return (opts) => withOp({ kind: 'delete', confirm: opts?.confirm === true, ...reasonOf(opts) });
508
- case 'anonymize':
509
- return (fields, opts) => withOp({ kind: 'anonymize', fields, ...reasonOf(opts) });
510
- case 'reclass':
511
- return (cls, data, opts) => withOp({ kind: 'reclass', cls, ...(data !== undefined ? { data } : {}), ...reasonOf(opts) });
512
- case 'set':
513
- case 'unset':
514
- case 'add':
515
- case 'remove':
516
- throw new LetopisError('invalid_query', `${prop}() — метод слота: db.Класс(…).update(…).Роль.${prop}(…)`);
517
- case 'forUpdate':
518
- return () => withMods({ forUpdate: true });
519
- case 'restore':
520
- return (opts) => withOp({ kind: 'restore', ...reasonOf(opts) });
521
- case 'purge':
522
- return (opts) => withOp({ kind: 'purge', confirm: opts?.confirm === true, ...reasonOf(opts) });
523
- case 'rekey':
524
- return (patch, opts) => withOp({ kind: 'rekey', data: patch ?? {}, ...reasonOf(opts) });
525
- case 'cache':
526
- return (opts) => {
527
- if (opts?.ttl !== undefined && !(opts.ttl > 0))
528
- throw new LetopisError('invalid_query', 'cache({ ttl }): срок — положительное число секунд');
529
- return withMods({ cache: opts ?? {} });
530
- };
531
- }
532
- const found = lookupStep(ctx.reg, prop) ?? unknownName(prop);
533
- return classProp(ctx, prop, found, steps, mods, nodes);
494
+ return member(prop);
534
495
  },
535
496
  };
536
497
  return new Proxy({}, handler);
@@ -541,38 +502,39 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
541
502
  * и общей с другими фасадами быть не может: иначе план исполнился бы от имени чужого актора.
542
503
  */
543
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
+ };
544
527
  return new Proxy({}, {
545
528
  get(_t, prop) {
546
529
  if (typeof prop === 'symbol' || prop === 'then')
547
530
  return undefined;
548
- switch (prop) {
549
- case 'run':
550
- return async () => {
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);
556
- for (const p of plans)
557
- precheckPlan(ctx.reg, p);
558
- if (!plans.length)
559
- return [];
560
- return ctx.exec.run((q) => execBatch({ reg: ctx.reg, schema: ctx.schema, q, tenant: ctx.tenant(), owner: ctx.owner, ...reasons(ctx) }, plans));
561
- };
562
- case 'execute':
563
- throw removed('batch.execute()', 'batch.run()');
564
- case 'discard':
565
- return () => void queue.splice(0, queue.length);
566
- case 'size':
567
- return () => queue.length;
568
- }
569
- return startStep({ ...ctx, batch: { queue } }, prop);
531
+ return member(prop);
570
532
  },
571
533
  });
572
534
  }
573
535
  /** Старт пути от корня db: класс, алиас или роль. */
574
536
  export function startStep(ctx, name) {
575
- const found = lookupStep(ctx.reg, name) ?? unknownName(name);
537
+ const found = lookupStep(ctx.reg, name) ?? unknownName(ctx.reg, name);
576
538
  return classProp(ctx, name, found, undefined, {}, NO_NODES);
577
539
  }
578
540
  /** Старт пути с готового узла: db.entity(row | цепочка). */