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.
- package/AGENT-CHEATSHEET.md +329 -281
- package/CHANGELOG.md +418 -0
- package/README.md +572 -1053
- package/dist/acl.d.ts +78 -1
- package/dist/acl.js +264 -13
- package/dist/admin.d.ts +10 -141
- package/dist/admin.js +24 -184
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +5 -15
- package/dist/cache.d.ts +8 -1
- package/dist/cache.js +30 -8
- package/dist/chain.d.ts +18 -41
- package/dist/chain.js +165 -203
- package/dist/cli.js +45 -90
- package/dist/demo/booking.d.ts +352 -36
- package/dist/describe.d.ts +47 -0
- package/dist/describe.js +195 -0
- package/dist/errors.d.ts +59 -13
- package/dist/errors.js +190 -29
- package/dist/index.d.ts +52 -70
- package/dist/index.js +189 -122
- package/dist/internal.d.ts +5 -0
- package/dist/jsonschema.js +18 -17
- package/dist/load.d.ts +20 -12
- package/dist/load.js +207 -181
- package/dist/mcp.d.ts +5 -7
- package/dist/mcp.js +168 -214
- package/dist/messages/access.d.ts +90 -0
- package/dist/messages/access.js +113 -0
- package/dist/messages/api.d.ts +171 -0
- package/dist/messages/api.js +215 -0
- package/dist/messages/boot.d.ts +201 -0
- package/dist/messages/boot.js +256 -0
- package/dist/messages/codes.d.ts +172 -0
- package/dist/messages/codes.js +173 -0
- package/dist/messages/index.d.ts +95 -0
- package/dist/messages/index.js +118 -0
- package/dist/messages/mcp.d.ts +306 -0
- package/dist/messages/mcp.js +387 -0
- package/dist/messages/model.d.ts +233 -0
- package/dist/messages/model.js +283 -0
- package/dist/messages/query.d.ts +131 -0
- package/dist/messages/query.js +158 -0
- package/dist/messages/sql-access.d.ts +70 -0
- package/dist/messages/sql-access.js +87 -0
- package/dist/messages/sql-core.d.ts +150 -0
- package/dist/messages/sql-core.js +181 -0
- package/dist/messages/sql-time.d.ts +71 -0
- package/dist/messages/sql-time.js +88 -0
- package/dist/messages/sql-write.d.ts +246 -0
- package/dist/messages/sql-write.js +270 -0
- package/dist/messages/time.d.ts +23 -0
- package/dist/messages/time.js +29 -0
- package/dist/messages/types.d.ts +27 -0
- package/dist/messages/types.js +5 -0
- package/dist/messages/write.d.ts +405 -0
- package/dist/messages/write.js +509 -0
- package/dist/model.d.ts +73 -60
- package/dist/model.js +20 -29
- package/dist/ops.d.ts +35 -0
- package/dist/ops.js +101 -0
- package/dist/pglite.d.ts +0 -9
- package/dist/pglite.js +3 -1
- package/dist/registry.d.ts +3 -19
- package/dist/registry.js +3 -16
- package/dist/signals.d.ts +1 -5
- package/dist/signals.js +1 -5
- package/dist/sql.d.ts +7 -1
- package/dist/sql.js +101 -85
- package/dist/sync.d.ts +9 -3
- package/dist/sync.js +40 -17
- package/dist/tx.d.ts +11 -1
- package/dist/tx.js +32 -11
- package/dist/typed.d.ts +240 -45
- package/dist/types.d.ts +15 -90
- package/dist/types.js +12 -14
- package/dist/up.d.ts +53 -32
- package/dist/up.js +180 -74
- package/dist/uuid.js +5 -5
- package/dist/validate.d.ts +13 -1
- package/dist/validate.js +61 -29
- package/dist/watch.d.ts +5 -16
- package/dist/watch.js +23 -77
- package/dist/write.d.ts +9 -16
- package/dist/write.js +102 -185
- package/llms.txt +22 -25
- package/package.json +2 -9
- package/sql/10-core.sql +7 -50
- package/sql/15-errors.sql +64 -10
- package/sql/20-context.sql +7 -6
- package/sql/30-validate.sql +35 -27
- package/sql/40-class.sql +20 -114
- package/sql/50-acl.sql +8 -13
- package/sql/60-write.sql +220 -676
- package/sql/70-read.sql +11 -13
- package/sql/80-auth.sql +23 -27
- package/sql/90-time.sql +77 -944
- package/sql/95-seed.system.sql +7 -14
- package/sql/99-revision.sql +2 -1
- package/sql/README.md +32 -24
- package/sql/migrate/README.md +14 -0
- package/AGENT-CHEATSHEET.en.md +0 -488
- package/MIGRATION.md +0 -190
- package/README.en.md +0 -2910
- package/dist/import.d.ts +0 -185
- package/dist/import.js +0 -820
- package/dist/project.d.ts +0 -359
- package/dist/project.js +0 -1258
- package/sql/75-history.sql +0 -436
- package/sql/88-derived.sql +0 -1236
- package/sql/migrate/0006-changed-paths.sql +0 -3
- 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
|
|
10
|
+
* Запись (этап 4): глаголы create, update, upsert, delete — звенья плана; слоты
|
|
11
11
|
* `.Роль.set/unset/add/remove` правят концы записываемой версии. Терминал цепочки с глаголами
|
|
12
12
|
* исполняет весь план одной транзакцией (write.ts): продолжение идёт от результата глагола.
|
|
13
|
-
* Этап 6: restore
|
|
13
|
+
* Этап 6: restore; versions(); кэш результатов .cache() (cache.ts).
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
142
|
-
|
|
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(),
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
225
|
-
|
|
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
|
|
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
|
|
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
|
|
278
|
+
throw lerr('api.slot_single', { name, op, role });
|
|
285
279
|
if (op === 'unset' && (end.required || (end.min ?? 0) > 0))
|
|
286
|
-
throw
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 | цепочка). */
|