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.
- package/AGENT-CHEATSHEET.md +329 -278
- package/CHANGELOG.md +558 -1
- package/README.md +606 -1037
- 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 -175
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +5 -15
- package/dist/cache.d.ts +8 -1
- package/dist/cache.js +33 -10
- package/dist/chain.d.ts +19 -41
- package/dist/chain.js +169 -201
- package/dist/cli.js +51 -88
- 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 -71
- package/dist/index.js +245 -130
- package/dist/internal.d.ts +12 -0
- package/dist/internal.js +5 -0
- package/dist/jsonschema.js +18 -17
- package/dist/load.d.ts +20 -12
- package/dist/load.js +225 -186
- package/dist/mcp.d.ts +5 -7
- package/dist/mcp.js +171 -215
- 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 +26 -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 +5 -9
- package/dist/signals.js +5 -9
- package/dist/sql.d.ts +13 -6
- package/dist/sql.js +176 -83
- package/dist/sync.d.ts +9 -3
- package/dist/sync.js +51 -21
- package/dist/tx.d.ts +26 -1
- package/dist/tx.js +44 -14
- package/dist/typed.d.ts +240 -45
- package/dist/types.d.ts +23 -91
- package/dist/types.js +12 -14
- package/dist/up.d.ts +53 -32
- package/dist/up.js +184 -73
- package/dist/uuid.js +5 -5
- package/dist/validate.d.ts +13 -1
- package/dist/validate.js +61 -29
- package/dist/watch.d.ts +11 -10
- package/dist/watch.js +33 -70
- package/dist/write.d.ts +9 -16
- package/dist/write.js +118 -187
- package/llms.txt +22 -25
- package/package.json +8 -11
- 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 -667
- package/sql/70-read.sql +20 -16
- package/sql/80-auth.sql +24 -27
- package/sql/90-time.sql +70 -859
- 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 -485
- package/MIGRATION.md +0 -190
- package/README.en.md +0 -2859
- package/dist/import.d.ts +0 -185
- package/dist/import.js +0 -820
- package/dist/project.d.ts +0 -350
- package/dist/project.js +0 -1224
- package/sql/75-history.sql +0 -358
- package/sql/88-derived.sql +0 -1235
- 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,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
|
-
/**
|
|
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, строка ответа или ленивая цепочка. */
|
|
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
|
|
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
|
|
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
|
|
278
|
+
throw lerr('api.slot_single', { name, op, role });
|
|
282
279
|
if (op === 'unset' && (end.required || (end.min ?? 0) > 0))
|
|
283
|
-
throw
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 | цепочка). */
|