letopis 2.0.0 → 3.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 +484 -414
- package/CHANGELOG.md +321 -0
- package/LICENSE +75 -21
- package/LICENSE-COMMERCIAL.md +49 -0
- package/README.md +786 -183
- package/api-contract.json +381 -0
- package/dist/acl.d.ts +3 -0
- package/dist/acl.js +19 -2
- package/dist/admin.d.ts +2 -1
- package/dist/admin.js +6 -4
- package/dist/auth.d.ts +18 -3
- package/dist/auth.js +75 -6
- package/dist/chain.d.ts +38 -17
- package/dist/chain.js +434 -63
- package/dist/cli.js +28 -6
- package/dist/errors.d.ts +19 -1
- package/dist/errors.js +46 -6
- package/dist/index.d.ts +10 -4
- package/dist/index.js +136 -46
- package/dist/internal.d.ts +2 -0
- package/dist/load.d.ts +9 -5
- package/dist/load.js +30 -11
- package/dist/log.d.ts +121 -0
- package/dist/log.js +633 -0
- package/dist/mcp.js +26 -11
- package/dist/messages/api.d.ts +66 -0
- package/dist/messages/api.js +90 -1
- package/dist/messages/boot.js +4 -4
- package/dist/messages/codes.js +4 -4
- package/dist/messages/index.d.ts +5 -2
- package/dist/messages/index.js +5 -2
- package/dist/messages/log.d.ts +229 -0
- package/dist/messages/log.js +102 -0
- package/dist/messages/model.d.ts +24 -1
- package/dist/messages/model.js +34 -3
- package/dist/messages/query.d.ts +10 -0
- package/dist/messages/query.js +13 -0
- package/dist/messages/sql-core.d.ts +15 -1
- package/dist/messages/sql-core.js +20 -3
- package/dist/messages/sql-time.d.ts +15 -0
- package/dist/messages/sql-time.js +23 -0
- package/dist/messages/write.d.ts +47 -0
- package/dist/messages/write.js +63 -0
- package/dist/model.d.ts +4 -0
- package/dist/model.js +23 -5
- package/dist/sql.d.ts +2 -0
- package/dist/sql.js +77 -25
- package/dist/sync.d.ts +1 -1
- package/dist/sync.js +50 -0
- package/dist/tx.d.ts +10 -1
- package/dist/tx.js +44 -2
- package/dist/typed.d.ts +14 -10
- package/dist/types.d.ts +82 -4
- package/dist/types.js +6 -1
- package/dist/up.d.ts +3 -3
- package/dist/up.js +57 -17
- package/dist/uuid.d.ts +8 -0
- package/dist/uuid.js +9 -1
- package/dist/validate.js +24 -7
- package/dist/watch.d.ts +5 -0
- package/dist/watch.js +13 -0
- package/dist/write.d.ts +78 -1
- package/dist/write.js +357 -39
- package/llms.txt +37 -17
- package/package.json +12 -6
- package/sql/20-context.sql +11 -3
- package/sql/30-validate.sql +51 -10
- package/sql/40-class.sql +21 -1
- package/sql/80-auth.sql +88 -17
- package/sql/90-time.sql +34 -6
- package/sql/99-revision.sql +5 -1
- package/sql/migrate/0009-restore-rev.tenants-name.sql +10 -0
- package/sql/migrate/0010-impersonation-no-account.sql +5 -0
- package/sql/migrate/0011-key-lock.otp-enabled.sql +7 -0
- package/sql/migrate/README.md +5 -1
package/dist/chain.js
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
15
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
|
-
import {
|
|
18
|
+
import { CHAIN_METHODS, OP } from './types.js';
|
|
19
|
+
import { NOLOG, guarded, traced } from './log.js';
|
|
20
|
+
import { completeIssues, dropUndefined, execBatch, execPlan, lockIds, precheckPlan, slotRole, } from './write.js';
|
|
19
21
|
/** Внутренний канал: план чужой ленивой цепочки (entity()). */
|
|
20
22
|
export const PLAN = Symbol('letopis.plan');
|
|
21
23
|
const peekPlan = (x) => x !== null && (typeof x === 'object' || typeof x === 'function') ? x[PLAN] : undefined;
|
|
@@ -38,12 +40,67 @@ export function lookupStep(reg, name) {
|
|
|
38
40
|
const t = targets.map((n) => reg.find(n)).find((c) => !!c);
|
|
39
41
|
return t ? { cls: t, role: name } : undefined;
|
|
40
42
|
}
|
|
41
|
-
/**
|
|
42
|
-
const
|
|
43
|
-
|
|
43
|
+
/** Системные классы (прав, членств, аккаунтов, описаний): в «похожие» неизвестного шага не идут (план 2.1, A6). */
|
|
44
|
+
const SYSTEM_CLASSES = new Set(['Class', 'Account', 'member', 'Resource', 'rule']);
|
|
45
|
+
const SYSTEM_ID = '2eba6d0a-1edd-4bb6-a85a-a703dab49035';
|
|
46
|
+
const isSystem = (c) => c.ancestors.some((n) => SYSTEM_CLASSES.has(n));
|
|
47
|
+
/** Имена, которыми начинается шаг: классы, их алиасы и роли концов реестра, кроме системных классов. */
|
|
48
|
+
const stepNames = (reg) => reg.all().filter((c) => !isSystem(c)).flatMap((c) => [c.name, ...(c.alias ? [c.alias] : []), ...Object.keys(c.ends)]);
|
|
49
|
+
/**
|
|
50
|
+
* Свойства массива, которые спрашивают у цепочки, забыв терминал: `.length`, `.map(…)`, `[0]` — не шаги (план 2.1, A6).
|
|
51
|
+
* Не case-метки member: имена попали бы в зарезервированные (check:docs, reserved) — проверяются после неудачного поиска.
|
|
52
|
+
*/
|
|
53
|
+
const ARRAY_PROPS = new Set(['length', 'map', 'filter', 'forEach', 'reduce', 'find', 'some', 'every', 'slice', 'at', 'toJSON', '0']);
|
|
54
|
+
/**
|
|
55
|
+
* Неизвестный класс или роль — с похожими именами реестра (план 2.0, 7.9 №6). Свойство массива — api.chain_not_rows;
|
|
56
|
+
* в реестре только классы System — сессия, похоже, в личном арендаторе (api.unknown_step_no_classes).
|
|
57
|
+
*/
|
|
44
58
|
const unknownName = (reg, name) => {
|
|
45
|
-
|
|
59
|
+
if (ARRAY_PROPS.has(name))
|
|
60
|
+
throw lerr('api.chain_not_rows', { name });
|
|
61
|
+
const key = reg.all().every((c) => c.tenant === SYSTEM_ID) ? 'api.unknown_step_no_classes' : 'api.unknown_step';
|
|
62
|
+
throw lerr(key, { name, similar: similar(name, stepNames(reg)) });
|
|
63
|
+
};
|
|
64
|
+
/** Терминалы и агрегаты цепочки: у цепочки без сессии они отдают отказ промисом (sessionless). */
|
|
65
|
+
const TERMINALS = new Set(['rows', 'first', 'ids', 'count', 'paths', 'versions', 'sum', 'avg', 'min', 'max', 'countBy']);
|
|
66
|
+
/**
|
|
67
|
+
* Неизвестный шаг у фасада без сессии (токен погашен или истёк после connect(), второй раунд 2.1, A5): не синхронный
|
|
68
|
+
* выброс про «личный арендатор», а цепочка, чьи терминалы отдают api.token_not_active (no_session) отклонённым
|
|
69
|
+
* промисом — как у любой другой ошибки запроса. Шаги, модификаторы и глаголы такой цепочки ничего не проверяют.
|
|
70
|
+
*/
|
|
71
|
+
function sessionless() {
|
|
72
|
+
const fail = () => Promise.reject(lerr('api.token_not_active'));
|
|
73
|
+
const dead = new Proxy(function () { }, {
|
|
74
|
+
get(_t, p) {
|
|
75
|
+
if (typeof p === 'symbol' || p === 'then')
|
|
76
|
+
return undefined;
|
|
77
|
+
return TERMINALS.has(p) ? fail : dead;
|
|
78
|
+
},
|
|
79
|
+
apply: () => dead,
|
|
80
|
+
});
|
|
81
|
+
return dead;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Фасад с токеном без сессии: шага нет среди классов, а арендатора у сессии нет — шаг из sessionless(); батч и
|
|
85
|
+
* подключение без токена (администраторское) — как прежде.
|
|
86
|
+
*/
|
|
87
|
+
const noSession = (ctx, name) => !ctx.batch && !ARRAY_PROPS.has(name) && ctx.hasToken?.() === true && ctx.tenant() === null;
|
|
88
|
+
/** Перебор цепочки (for…of, spread, Array.from) — понятный отказ вместо «is not iterable». */
|
|
89
|
+
const notIterable = () => {
|
|
90
|
+
throw lerr('api.chain_not_rows', { name: '[Symbol.iterator]' });
|
|
46
91
|
};
|
|
92
|
+
/**
|
|
93
|
+
* withDeleted() — модификатор своего шага (план 2.1, A9): у шага не последнего, когда у последнего его нет, — один раз
|
|
94
|
+
* предупреждение (до 2.1 флаг относился к последнему шагу).
|
|
95
|
+
*/
|
|
96
|
+
function warnWithDeleted(steps, log) {
|
|
97
|
+
const last = steps[steps.length - 1];
|
|
98
|
+
if (!last || last.withDeleted)
|
|
99
|
+
return;
|
|
100
|
+
const at = steps.slice(0, -1).find((s) => s.withDeleted && s.pivotKey === undefined);
|
|
101
|
+
if (at)
|
|
102
|
+
lwarn('api.with_deleted_step', { step: at.name, last: last.name }, { once: `with-deleted|${at.name}|${last.name}`, log });
|
|
103
|
+
}
|
|
47
104
|
const nodeOf = (steps, s) => s.pivotKey === undefined ? s : steps.find((x) => x.nodeKey === s.pivotKey) ?? s;
|
|
48
105
|
/** Узел для возврата: последний реальный шаг того же класса-связи в текущем читающем сегменте. */
|
|
49
106
|
function pivotNodeOf(steps, cls) {
|
|
@@ -57,7 +114,7 @@ function pivotNodeOf(steps, cls) {
|
|
|
57
114
|
return undefined;
|
|
58
115
|
}
|
|
59
116
|
const noHistoryWarned = new Set();
|
|
60
|
-
function warnNoHistory(steps, method) {
|
|
117
|
+
function warnNoHistory(steps, method, log) {
|
|
61
118
|
const cls = steps[steps.length - 1]?.cls;
|
|
62
119
|
if (!cls || cls.history !== false)
|
|
63
120
|
return;
|
|
@@ -65,7 +122,7 @@ function warnNoHistory(steps, method) {
|
|
|
65
122
|
if (noHistoryWarned.has(key))
|
|
66
123
|
return;
|
|
67
124
|
noHistoryWarned.add(key);
|
|
68
|
-
lwarn('api.no_history', { method, cls: cls.name });
|
|
125
|
+
lwarn('api.no_history', { method, cls: cls.name }, { log });
|
|
69
126
|
}
|
|
70
127
|
function report(ctx, mode, steps, t0, rows) {
|
|
71
128
|
if (!ctx.onQuery && ctx.slowMs === undefined)
|
|
@@ -76,7 +133,233 @@ function report(ctx, mode, steps, t0, rows) {
|
|
|
76
133
|
if (ctx.onQuery)
|
|
77
134
|
ctx.onQuery({ mode, classes, ms, rows, slow });
|
|
78
135
|
else if (slow)
|
|
79
|
-
lwarn('api.slow_query', { ms: ms.toFixed(0), mode, classes: classes.join('→') });
|
|
136
|
+
lwarn('api.slow_query', { ms: ms.toFixed(0), mode, classes: classes.join('→') }, { log: ctx.log });
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Свойства цепочки, вызов которых может отказать синхронно (модификаторы и глаголы): ошибка — событие error. Не
|
|
140
|
+
* case-метки: check-docs принимает их за зарезервированные имена.
|
|
141
|
+
*/
|
|
142
|
+
const GUARDED = new Set(['entity', 'limit', 'offset', 'sort', 'asOf', 'after', 'withDeleted', 'deep', 'exact', 'alias', 'tags',
|
|
143
|
+
'owner', 'create', 'update', 'upsert', 'delete', 'forUpdate', 'restore', 'cache']);
|
|
144
|
+
const LIST_MAX = 50;
|
|
145
|
+
/** Сколько строк каждого класса. */
|
|
146
|
+
function byClass(rows) {
|
|
147
|
+
const m = {};
|
|
148
|
+
for (const r of rows)
|
|
149
|
+
m[r.class] = (m[r.class] ?? 0) + 1;
|
|
150
|
+
return m;
|
|
151
|
+
}
|
|
152
|
+
/** id строк — не больше LIST_MAX. */
|
|
153
|
+
const idsOf = (rows) => rows.slice(0, LIST_MAX).map((r) => r.id);
|
|
154
|
+
/** Значение фильтра или аргумента для текста цепочки: поля-секреты — '***', длинное — обрезано. */
|
|
155
|
+
function fmtVal(log, x, depth = 0) {
|
|
156
|
+
if (x === undefined)
|
|
157
|
+
return 'undefined';
|
|
158
|
+
if (x === null || typeof x === 'number' || typeof x === 'boolean')
|
|
159
|
+
return String(x);
|
|
160
|
+
if (typeof x === 'string')
|
|
161
|
+
return JSON.stringify(x.length > 80 ? `${x.slice(0, 80)}…` : x);
|
|
162
|
+
if (typeof x === 'bigint')
|
|
163
|
+
return `${x}n`;
|
|
164
|
+
if (typeof x !== 'object')
|
|
165
|
+
return typeof x;
|
|
166
|
+
if (depth > 3)
|
|
167
|
+
return '…';
|
|
168
|
+
if (x instanceof Date)
|
|
169
|
+
return JSON.stringify(x.toISOString());
|
|
170
|
+
const op = x[OP];
|
|
171
|
+
if (typeof op === 'string') {
|
|
172
|
+
const args = x.args ?? [];
|
|
173
|
+
return `${op}(${args.map((a) => fmtVal(log, a, depth + 1)).join(', ')})`;
|
|
174
|
+
}
|
|
175
|
+
const peek = peekPlan(x);
|
|
176
|
+
if (peek)
|
|
177
|
+
return `(${chainText(log, peek.steps, peek.mods)})`;
|
|
178
|
+
if (Array.isArray(x)) {
|
|
179
|
+
const head = x.slice(0, 10).map((a) => fmtVal(log, a, depth + 1)).join(',');
|
|
180
|
+
return `[${head}${x.length > 10 ? `,…+${x.length - 10}` : ''}]`;
|
|
181
|
+
}
|
|
182
|
+
const entries = Object.entries(x);
|
|
183
|
+
const shown = entries.slice(0, 20).map(([k, v]) => `${JSON.stringify(k)}:${log.secret(k) ? '"***"' : fmtVal(log, v, depth + 1)}`);
|
|
184
|
+
return `{${shown.join(',')}${entries.length > 20 ? `,…+${entries.length - 20}` : ''}}`;
|
|
185
|
+
}
|
|
186
|
+
/** Данные записи — только имена полей (значения в лог не идут). */
|
|
187
|
+
const keysOf = (data) => `{${Object.keys(data ?? {}).join(',')}}`;
|
|
188
|
+
/** Модификаторы выборки текстом: .sort(…).limit(…)… */
|
|
189
|
+
function modsText(m) {
|
|
190
|
+
if (!m)
|
|
191
|
+
return '';
|
|
192
|
+
let t = '';
|
|
193
|
+
if (m.order !== undefined)
|
|
194
|
+
t += `.sort(${JSON.stringify(m.order)}${m.desc ? ", 'desc'" : ''})`;
|
|
195
|
+
if (m.limit !== undefined)
|
|
196
|
+
t += `.limit(${m.limit})`;
|
|
197
|
+
if (m.offset !== undefined)
|
|
198
|
+
t += `.offset(${m.offset})`;
|
|
199
|
+
if (m.asOf !== undefined)
|
|
200
|
+
t += `.asOf(${JSON.stringify(m.asOf)})`;
|
|
201
|
+
if (m.after !== undefined)
|
|
202
|
+
t += '.after(…)';
|
|
203
|
+
if (m.forUpdate)
|
|
204
|
+
t += '.forUpdate()';
|
|
205
|
+
if (m.cache)
|
|
206
|
+
t += `.cache(${m.cache.ttl !== undefined ? `{ttl:${m.cache.ttl}}` : ''})`;
|
|
207
|
+
return t;
|
|
208
|
+
}
|
|
209
|
+
/** Глагол текстом: данные — именами полей, expect — числом строк. */
|
|
210
|
+
function opText(op) {
|
|
211
|
+
const o = [];
|
|
212
|
+
if (op.kind === 'create' || op.kind === 'upsert')
|
|
213
|
+
return `${op.kind}(${keysOf(op.data)})`;
|
|
214
|
+
if (op.kind === 'update') {
|
|
215
|
+
if (op.rev !== undefined)
|
|
216
|
+
o.push(`rev:${op.rev}`);
|
|
217
|
+
if (op.all)
|
|
218
|
+
o.push('all:true');
|
|
219
|
+
return `update(${keysOf(op.data)}${o.length ? `, {${o.join(',')}}` : ''})`;
|
|
220
|
+
}
|
|
221
|
+
if (op.kind === 'delete') {
|
|
222
|
+
if (op.confirm)
|
|
223
|
+
o.push('confirm:true');
|
|
224
|
+
if (op.all)
|
|
225
|
+
o.push('all:true');
|
|
226
|
+
}
|
|
227
|
+
if (op.kind === 'restore' && op.partial)
|
|
228
|
+
o.push('partial:true');
|
|
229
|
+
if (op.expect !== undefined)
|
|
230
|
+
o.push(`expect:[${op.expect.length}]`);
|
|
231
|
+
return `${op.kind}(${o.length ? `{${o.join(',')}}` : ''})`;
|
|
232
|
+
}
|
|
233
|
+
/** Значение слота текстом. */
|
|
234
|
+
function slotText(v) {
|
|
235
|
+
if (v === null)
|
|
236
|
+
return 'null';
|
|
237
|
+
if (typeof v === 'string')
|
|
238
|
+
return JSON.stringify(v);
|
|
239
|
+
if (Array.isArray(v))
|
|
240
|
+
return `[${v.length}]`;
|
|
241
|
+
return 'plan' in v ? '(цепочка)' : JSON.stringify(v.id);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Цепочка в читаемом виде: `Shop({"name":"A"}).Product.limit(10).rows()`. Фильтры — значениями (поля-секреты — '***'),
|
|
245
|
+
* данные записи — только именами полей.
|
|
246
|
+
*/
|
|
247
|
+
export function chainText(log, steps, mods, term, args = []) {
|
|
248
|
+
let out = '';
|
|
249
|
+
steps.forEach((s, i) => {
|
|
250
|
+
if (!s.self) {
|
|
251
|
+
out += `${i ? '.' : ''}${s.name}(${s.filter !== undefined ? fmtVal(log, s.filter) : ''})`;
|
|
252
|
+
if (s.exact)
|
|
253
|
+
out += '.exact()';
|
|
254
|
+
if (s.withDeleted)
|
|
255
|
+
out += '.withDeleted()';
|
|
256
|
+
if (s.deepMax !== undefined)
|
|
257
|
+
out += `.deep(${s.deepMax})`;
|
|
258
|
+
if (s.aliasKey !== undefined)
|
|
259
|
+
out += `.alias(${JSON.stringify(s.aliasKey)})`;
|
|
260
|
+
if (s.tagsFilter !== undefined)
|
|
261
|
+
out += `.tags(${fmtVal(log, s.tagsFilter)})`;
|
|
262
|
+
if (s.ownerFilter !== undefined)
|
|
263
|
+
out += `.owner(${JSON.stringify(s.ownerFilter)})`;
|
|
264
|
+
}
|
|
265
|
+
if (s.op) {
|
|
266
|
+
out += `${modsText(s.opMods)}.${opText(s.op)}`;
|
|
267
|
+
for (const sl of s.slots ?? [])
|
|
268
|
+
out += `.${sl.role}.${sl.op}(${sl.op === 'unset' ? '' : slotText(sl.value)})`;
|
|
269
|
+
}
|
|
270
|
+
if (out.length > 4000)
|
|
271
|
+
return;
|
|
272
|
+
});
|
|
273
|
+
out += modsText(mods);
|
|
274
|
+
if (term)
|
|
275
|
+
out += `.${term}(${args.filter((a) => a !== undefined).map((a) => fmtVal(log, a)).join(', ')})`;
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
/** Подробности события записи: состав по классам и id; у удаления — сверка с expect, у restore — пропуски. */
|
|
279
|
+
function writeDetail(op, rows) {
|
|
280
|
+
const d = {};
|
|
281
|
+
if (op.kind === 'delete') {
|
|
282
|
+
d.confirm = op.confirm;
|
|
283
|
+
d.expect = op.expect !== undefined ? op.expect.length : false;
|
|
284
|
+
if (op.all)
|
|
285
|
+
d.all = true;
|
|
286
|
+
// с expect замыкание каскада блокируется for update и превью сверяется заново (write.ts, deleteOp)
|
|
287
|
+
if (op.confirm && op.expect !== undefined)
|
|
288
|
+
d.locked = true;
|
|
289
|
+
if (rows && !op.confirm) {
|
|
290
|
+
const actions = {};
|
|
291
|
+
for (const r of rows)
|
|
292
|
+
if (r.$action)
|
|
293
|
+
actions[r.$action] = (actions[r.$action] ?? 0) + 1;
|
|
294
|
+
d.actions = actions;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else if (op.kind === 'restore') {
|
|
298
|
+
d.expect = op.expect !== undefined ? op.expect.length : false;
|
|
299
|
+
if (op.partial)
|
|
300
|
+
d.partial = true;
|
|
301
|
+
if (rows) {
|
|
302
|
+
const skipped = rows.filter((r) => r.$action === 'skip');
|
|
303
|
+
d.restored = rows.length - skipped.length;
|
|
304
|
+
d.skipped = skipped.slice(0, LIST_MAX).map((r) => ({ id: r.id, why: r.$why }));
|
|
305
|
+
rows = rows.filter((r) => r.$action !== 'skip');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else if (rows && op.kind === 'upsert') {
|
|
309
|
+
const how = {};
|
|
310
|
+
for (const r of rows)
|
|
311
|
+
if (r.$upsert)
|
|
312
|
+
how[r.$upsert] = (how[r.$upsert] ?? 0) + 1;
|
|
313
|
+
d.upsert = how;
|
|
314
|
+
}
|
|
315
|
+
if (rows) {
|
|
316
|
+
d.classes = byClass(rows);
|
|
317
|
+
d.ids = idsOf(rows);
|
|
318
|
+
}
|
|
319
|
+
return d;
|
|
320
|
+
}
|
|
321
|
+
/** Событие терминала: read — без глаголов, иначе по последнему глаголу write, delete.preview, delete, restore. */
|
|
322
|
+
function termEvent(log, steps, mods, name, args, st, r, ms) {
|
|
323
|
+
const ops = steps.flatMap((s) => (s.op ? [s.op] : []));
|
|
324
|
+
const rows = name === 'count' && typeof r === 'number' ? r : st.n;
|
|
325
|
+
if (!ops.length) {
|
|
326
|
+
if (!log.wants('read'))
|
|
327
|
+
return;
|
|
328
|
+
log.emit('read', { op: name, ms, ...(rows !== undefined ? { rows } : {}), chain: chainText(log, steps, mods, name, args),
|
|
329
|
+
...(st.cache ? { detail: { cache: st.cache } } : {}) });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const last = ops[ops.length - 1];
|
|
333
|
+
const event = last.kind === 'delete' ? (last.confirm ? 'delete' : 'delete.preview') : last.kind === 'restore' ? 'restore' : 'write';
|
|
334
|
+
// запись в db.begin() — в счётчик транзакции: по нему её tx.commit идёт уровнем info
|
|
335
|
+
log.mark(event);
|
|
336
|
+
if (!log.wants(event))
|
|
337
|
+
return;
|
|
338
|
+
log.emit(event, { op: [...new Set(ops.map((o) => o.kind))].join(','), ms, rows: st.rows ? st.rows.length : rows,
|
|
339
|
+
chain: chainText(log, steps, mods, name, args), detail: writeDetail(last, st.rows) });
|
|
340
|
+
}
|
|
341
|
+
/** Терминал под логгером: время, событие успеха и error с цепочкой; выключенный логгер — сам вызов. */
|
|
342
|
+
async function traceTerm(ctx, steps, mods, name, args, fn) {
|
|
343
|
+
const log = ctx.log;
|
|
344
|
+
if (!log?.enabled)
|
|
345
|
+
return fn({});
|
|
346
|
+
const st = {};
|
|
347
|
+
const t0 = performance.now();
|
|
348
|
+
let r;
|
|
349
|
+
try {
|
|
350
|
+
r = await fn(st);
|
|
351
|
+
}
|
|
352
|
+
catch (e) {
|
|
353
|
+
log.error(e, { op: name, ms: performance.now() - t0, chain: chainText(log, steps, mods, name, args) });
|
|
354
|
+
throw e;
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
termEvent(log, steps, mods, name, args, st, r, performance.now() - t0);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// событие не собралось — терминал всё равно отработал
|
|
361
|
+
}
|
|
362
|
+
return r;
|
|
80
363
|
}
|
|
81
364
|
/** Классы, которые читает цепочка (семейства шагов): по ним сбрасывается её ответ в кэше. */
|
|
82
365
|
function readClasses(reg, steps) {
|
|
@@ -94,7 +377,7 @@ function cacheTtl(ctx, steps, mods, classes) {
|
|
|
94
377
|
return null;
|
|
95
378
|
return Math.min(...marks.map((m) => (m.ttl !== undefined ? m.ttl * 1000 : c.store.ttlMs)));
|
|
96
379
|
}
|
|
97
|
-
async function exec(ctx, q, mode, steps, mods = {}) {
|
|
380
|
+
async function exec(ctx, q, mode, steps, mods = {}, st = {}) {
|
|
98
381
|
const classes = ctx.cache ? readClasses(ctx.reg, steps) : [];
|
|
99
382
|
const ttl = ctx.cache ? cacheTtl(ctx, steps, mods, classes) : null;
|
|
100
383
|
if (ttl !== null) {
|
|
@@ -102,8 +385,16 @@ async function exec(ctx, q, mode, steps, mods = {}) {
|
|
|
102
385
|
const who = ctx.cache.who();
|
|
103
386
|
const key = store.key(q.text, q.params, who);
|
|
104
387
|
const hit = store.get(key);
|
|
105
|
-
|
|
388
|
+
const log = ctx.log;
|
|
389
|
+
if (hit) {
|
|
390
|
+
st.cache = 'hit';
|
|
391
|
+
if (log?.wants('cache.hit'))
|
|
392
|
+
log.emit('cache.hit', { op: mode, rows: hit.length, chain: chainText(log, steps, mods, mode) });
|
|
106
393
|
return hit;
|
|
394
|
+
}
|
|
395
|
+
st.cache = 'miss';
|
|
396
|
+
if (log?.wants('cache.miss'))
|
|
397
|
+
log.emit('cache.miss', { op: mode, chain: chainText(log, steps, mods, mode), detail: { ttl: ttl / 1000 } });
|
|
107
398
|
const tag = sessionTag(who.token);
|
|
108
399
|
const tok = store.token(classes, tag, who.tenant);
|
|
109
400
|
const t0 = performance.now();
|
|
@@ -122,18 +413,24 @@ async function exec(ctx, q, mode, steps, mods = {}) {
|
|
|
122
413
|
report(ctx, mode, steps, t0, res.length);
|
|
123
414
|
return res;
|
|
124
415
|
}
|
|
125
|
-
async function read(ctx, steps, mods, mode, agg) {
|
|
416
|
+
async function read(ctx, steps, mods, mode, agg, st = {}) {
|
|
126
417
|
const hasOps = steps.some((s) => s.op);
|
|
127
418
|
if (ctx.batch && hasOps)
|
|
128
419
|
throw lerr('api.batch_queued');
|
|
420
|
+
if (!hasOps)
|
|
421
|
+
warnWithDeleted(steps, ctx.log);
|
|
422
|
+
const counted = (r) => {
|
|
423
|
+
st.n = r.res.length;
|
|
424
|
+
return r;
|
|
425
|
+
};
|
|
129
426
|
if (mods.forUpdate)
|
|
130
|
-
return lockedRead(ctx, steps, mods, mode);
|
|
427
|
+
return lockedRead(ctx, steps, mods, mode, agg).then(counted);
|
|
131
428
|
if (hasOps)
|
|
132
|
-
return plan(ctx, steps, mods, mode, agg);
|
|
429
|
+
return plan(ctx, steps, mods, mode, agg, st).then(counted);
|
|
133
430
|
const { cache: _c, ...plain } = mods;
|
|
134
431
|
const q = buildRead(ctx.reg, ctx.schema, steps, plain, mode, agg);
|
|
135
432
|
const ev = mode === 'countPaths' ? 'count' : mode;
|
|
136
|
-
return exec(ctx, q, ev, steps, mods).then((res) => ({ q, res }));
|
|
433
|
+
return exec(ctx, q, ev, steps, mods, st).then((res) => counted({ q, res }));
|
|
137
434
|
}
|
|
138
435
|
/** Модификатор выборки, заданный в mods, — именем метода цепочки; forUpdate() отказывает сам (lock_unsupported). */
|
|
139
436
|
function modName(mods) {
|
|
@@ -167,8 +464,13 @@ function checkAfterVerb(steps, mods) {
|
|
|
167
464
|
* Цепочка с глаголами: весь план одной транзакцией (§2.5). Результат приводится к виду ответа
|
|
168
465
|
* чтения (строки rows/paths, id, n, агрегаты), чтобы терминалы разбирали его одинаково.
|
|
169
466
|
*/
|
|
170
|
-
async function plan(ctx, steps, mods, mode, agg) {
|
|
171
|
-
|
|
467
|
+
async function plan(ctx, steps, mods, mode, agg, st = {}) {
|
|
468
|
+
try {
|
|
469
|
+
precheckPlan(ctx.reg, steps, mods.order, agg);
|
|
470
|
+
}
|
|
471
|
+
catch (e) {
|
|
472
|
+
throw await completeIssues(ctx, e);
|
|
473
|
+
}
|
|
172
474
|
const t0 = performance.now();
|
|
173
475
|
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));
|
|
174
476
|
if (r.kind === 'read') {
|
|
@@ -182,6 +484,7 @@ async function plan(ctx, steps, mods, mode, agg) {
|
|
|
182
484
|
return r;
|
|
183
485
|
}
|
|
184
486
|
const rows = r.rows;
|
|
487
|
+
st.rows = rows;
|
|
185
488
|
report(ctx, mode === 'countPaths' ? 'count' : mode === 'paths' ? 'paths' : mode === 'ids' ? 'ids' : 'rows', steps, t0, rows.length);
|
|
186
489
|
// не switch: check-docs принимает case-метки этого файла за имена, которые Proxy разбирает раньше классов
|
|
187
490
|
if (mode === 'paths')
|
|
@@ -199,12 +502,18 @@ async function plan(ctx, steps, mods, mode, agg) {
|
|
|
199
502
|
* которая перестала подходить под условия шага, в ответ не попадает; подходящая, но не
|
|
200
503
|
* заблокированная — acl_denied.
|
|
201
504
|
*/
|
|
202
|
-
async function lockedRead(ctx, steps, mods, mode) {
|
|
505
|
+
async function lockedRead(ctx, steps, mods, mode, agg) {
|
|
203
506
|
if (!ctx.exec.explicit)
|
|
204
507
|
throw lerr('api.for_update_outside_tx');
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
508
|
+
// что именно мешает блокировке — в тексте, что допустимо — в подсказке (второй раунд 2.1, A7)
|
|
509
|
+
const op = steps.find((s) => s.op)?.op;
|
|
510
|
+
const what = op ? `${op.kind}()`
|
|
511
|
+
: mode !== 'rows' && mode !== 'ids' ? `${mode === 'agg' ? agg?.fn ?? 'agg' : mode === 'countPaths' ? 'count' : mode}()`
|
|
512
|
+
: mods.asOf !== undefined ? 'asOf()'
|
|
513
|
+
: steps.some((s) => s.withDeleted) ? 'withDeleted()'
|
|
514
|
+
: steps.some((s) => s.deepMax !== undefined) ? 'deep()' : undefined;
|
|
515
|
+
if (what)
|
|
516
|
+
throw lerr('api.for_update_unsupported', { what });
|
|
208
517
|
const S = `"${ctx.schema.replace(/"/g, '""')}"`;
|
|
209
518
|
const { forUpdate: _f, ...plain } = mods;
|
|
210
519
|
const idsQ = buildRead(ctx.reg, ctx.schema, steps, plain, 'ids');
|
|
@@ -215,7 +524,7 @@ async function lockedRead(ctx, steps, mods, mode) {
|
|
|
215
524
|
const ids = (await q.unsafe(idsQ.text, idsQ.params, { prepare: true })).map((r) => r.id);
|
|
216
525
|
if (!ids.length)
|
|
217
526
|
return [];
|
|
218
|
-
const locked =
|
|
527
|
+
const locked = await lockIds(q, S, ids);
|
|
219
528
|
const want = new Set(ids);
|
|
220
529
|
const fresh = (await q.unsafe(rowsQ.text, rowsQ.params, { prepare: true }))
|
|
221
530
|
.filter((r) => want.has(r.row.id));
|
|
@@ -224,15 +533,37 @@ async function lockedRead(ctx, steps, mods, mode) {
|
|
|
224
533
|
throw lerr('api.lock_denied', { ids: denied.join(', ') }, { detail: { ids: denied } });
|
|
225
534
|
return fresh;
|
|
226
535
|
});
|
|
227
|
-
report(ctx, mode, steps, t0, res.length);
|
|
536
|
+
report(ctx, mode === 'ids' ? 'ids' : 'rows', steps, t0, res.length);
|
|
228
537
|
return { q: rowsQ, res: mode === 'ids' ? res.map((r) => ({ id: r.row.id })) : res };
|
|
229
538
|
}
|
|
539
|
+
/**
|
|
540
|
+
* Промис versions(): методы цепочки на нём (versions().rows(), versions().sort()) — ошибка letopis с подсказкой, а не
|
|
541
|
+
* TypeError «… is not a function» (3.0). Сам промис уже идёт: его отказ гасится, иначе он ушёл бы в unhandledRejection.
|
|
542
|
+
*/
|
|
543
|
+
function versionsPromise(p) {
|
|
544
|
+
for (const method of CHAIN_METHODS) {
|
|
545
|
+
if (method === 'then')
|
|
546
|
+
continue;
|
|
547
|
+
Object.defineProperty(p, method, {
|
|
548
|
+
value: () => {
|
|
549
|
+
p.catch(() => { });
|
|
550
|
+
throw lerr('api.versions_terminal', { method });
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return p;
|
|
555
|
+
}
|
|
230
556
|
/** Причины для плана: по умолчанию у глаголов (у батча — своя) и та, что стоит в транзакции. */
|
|
231
557
|
const reasons = (ctx) => ({ reason: ctx.reason ?? ctx.txReason ?? '', txReason: ctx.txReason ?? '' });
|
|
232
558
|
/** Опция reason глагола (план 1.1, §2.1): проверяет precheckPlan. */
|
|
233
559
|
const reasonOf = (o) => (o?.reason !== undefined ? { reason: o.reason } : {});
|
|
234
|
-
/**
|
|
560
|
+
/**
|
|
561
|
+
* Опция all у update() и delete({ confirm: true }): правка всех целей без фильтра (план 2.0, этап 4, п. 2); у
|
|
562
|
+
* delete({ confirm: true }) и restore() ещё — без сверки с expect (3.0).
|
|
563
|
+
*/
|
|
235
564
|
const allOf = (o) => (o?.all === true ? { all: true } : {});
|
|
565
|
+
/** Опция expect у restore() и delete({ confirm: true }) (план 2.1, A1, A2): форму проверяет precheckPlan. */
|
|
566
|
+
const expectOf = (o) => (o?.expect !== undefined ? { expect: o.expect } : {});
|
|
236
567
|
/** Строка ответа: из чтения — to_jsonb узла, из плана — готовая строка записи. */
|
|
237
568
|
const rowOf = (r) => (r.done ? r.row : toRow(r.row));
|
|
238
569
|
/** Значение слота: id, список id, строка ответа или ленивая цепочка. */
|
|
@@ -253,7 +584,15 @@ function slotValue(v) {
|
|
|
253
584
|
}
|
|
254
585
|
function classProp(ctx, name, found, ownerSteps, mods, nodes) {
|
|
255
586
|
const { cls, role } = found;
|
|
256
|
-
const step = (
|
|
587
|
+
const step = (...args) => {
|
|
588
|
+
const filter = args[0];
|
|
589
|
+
// A13: X(undefined) — фильтра нет, шаг найдёт все строки (как X()); чаще всего это пустая переменная с id
|
|
590
|
+
if (args.length && filter === undefined)
|
|
591
|
+
lwarn('api.step_undefined_filter', { name }, { once: `undefined-filter|${name}`, log: ctx.log });
|
|
592
|
+
// A13: sort() относится к последнему шагу цепочки — шаг после него забирает сортировку себе
|
|
593
|
+
if (ownerSteps && mods.order !== undefined) {
|
|
594
|
+
lwarn('api.sort_not_last', { field: mods.order, step: name }, { once: `sort-not-last|${mods.order}|${name}`, log: ctx.log });
|
|
595
|
+
}
|
|
257
596
|
const prev = ownerSteps?.[ownerSteps.length - 1];
|
|
258
597
|
// повтор связи в пути — возврат к её узлу
|
|
259
598
|
const node = prev && cls.kind === 'link' && !role ? pivotNodeOf(ownerSteps, cls.name) : undefined;
|
|
@@ -288,10 +627,16 @@ function classProp(ctx, name, found, ownerSteps, mods, nodes) {
|
|
|
288
627
|
ctx.batch.entry.steps = next;
|
|
289
628
|
return makeChain(ctx, next, mods, nodes);
|
|
290
629
|
};
|
|
291
|
-
|
|
630
|
+
// ошибка шага или слота (переход, слот без глагола, …) — событие error с цепочкой до него
|
|
631
|
+
const log = ctx.log;
|
|
632
|
+
const g = (op, fn) => (log?.enabled
|
|
633
|
+
? (...a) => guarded(log, () => ({ op, chain: chainText(log, ownerSteps ?? [], mods) }), () => fn(...a))
|
|
634
|
+
: fn);
|
|
635
|
+
const stepFn = g(name, step);
|
|
636
|
+
return new Proxy(stepFn, {
|
|
292
637
|
get(t, p, r) {
|
|
293
638
|
if (p === 'set' || p === 'unset' || p === 'add' || p === 'remove')
|
|
294
|
-
return slot(p);
|
|
639
|
+
return g(`${name}.${p}`, slot(p));
|
|
295
640
|
return Reflect.get(t, p, r);
|
|
296
641
|
},
|
|
297
642
|
});
|
|
@@ -368,9 +713,11 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
|
|
|
368
713
|
}
|
|
369
714
|
throw lerr('api.entity_arg');
|
|
370
715
|
};
|
|
371
|
-
|
|
716
|
+
/** Терминал: время, событие (read, write, delete, restore) и error — traceTerm. */
|
|
717
|
+
const term = (name, fn) => (...a) => traceTerm(ctx, steps, mods, name, a, (st) => fn(st, ...a));
|
|
718
|
+
const agg = (fn) => term(fn, async (st, field) => {
|
|
372
719
|
afterNotPaged(mods, fn);
|
|
373
|
-
const { q, res } = await read(ctx, steps, mods, 'agg', { fn, field });
|
|
720
|
+
const { q, res } = await read(ctx, steps, mods, 'agg', { fn, field }, st);
|
|
374
721
|
if (fn === 'countBy')
|
|
375
722
|
return Object.fromEntries(res.map((r) => [r.k ?? 'null', r.n]));
|
|
376
723
|
const v = res[0]?.v;
|
|
@@ -381,7 +728,7 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
|
|
|
381
728
|
if (fn === 'sum' || fn === 'avg' || q?.aggLeaf === 'numeric')
|
|
382
729
|
return Number(v);
|
|
383
730
|
return v;
|
|
384
|
-
};
|
|
731
|
+
});
|
|
385
732
|
/** Свойство цепочки: метод или модификатор — функция, иначе шаг класса, алиаса или роли. */
|
|
386
733
|
const member = (prop) => {
|
|
387
734
|
switch (prop) {
|
|
@@ -390,42 +737,42 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
|
|
|
390
737
|
case 'entity':
|
|
391
738
|
return entityStep;
|
|
392
739
|
case 'paths':
|
|
393
|
-
return async () => {
|
|
740
|
+
return term('paths', async (st) => {
|
|
394
741
|
checkAfterVerb(steps, mods);
|
|
395
742
|
// q === null — пути из результата глагола: строки уже готовы. Ключи — в порядке шагов (jsonb хранит их
|
|
396
743
|
// по длине), узел последнего шага — последним: по нему cursorOf(путь, поле) берёт курсор (план 2.0, 7.1 №3)
|
|
397
|
-
const { q, res } = await read(ctx, steps, mods, 'paths');
|
|
744
|
+
const { q, res } = await read(ctx, steps, mods, 'paths', undefined, st);
|
|
398
745
|
if (q === null)
|
|
399
746
|
return res.map((r) => r.path);
|
|
400
747
|
return res.map((r) => Object.fromEntries(q.keys.map((k) => [k, toRow(r.path[k])])));
|
|
401
|
-
};
|
|
748
|
+
});
|
|
402
749
|
case 'rows':
|
|
403
|
-
return async () => {
|
|
750
|
+
return term('rows', async (st) => {
|
|
404
751
|
checkAfterVerb(steps, mods);
|
|
405
|
-
return (await read(ctx, steps, mods, 'rows')).res.map(rowOf);
|
|
406
|
-
};
|
|
752
|
+
return (await read(ctx, steps, mods, 'rows', undefined, st)).res.map(rowOf);
|
|
753
|
+
});
|
|
407
754
|
case 'first':
|
|
408
|
-
return async () => {
|
|
755
|
+
return term('first', async (st) => {
|
|
409
756
|
checkAfterVerb(steps, mods);
|
|
410
|
-
const { res } = await read(ctx, steps, { ...mods, limit: 1 }, 'rows');
|
|
757
|
+
const { res } = await read(ctx, steps, { ...mods, limit: 1 }, 'rows', undefined, st);
|
|
411
758
|
return res[0] ? rowOf(res[0]) : null;
|
|
412
|
-
};
|
|
759
|
+
});
|
|
413
760
|
case 'ids':
|
|
414
|
-
return async () => {
|
|
761
|
+
return term('ids', async (st) => {
|
|
415
762
|
checkAfterVerb(steps, mods);
|
|
416
|
-
return (await read(ctx, steps, mods, 'ids')).res.map((r) => r.id);
|
|
417
|
-
};
|
|
763
|
+
return (await read(ctx, steps, mods, 'ids', undefined, st)).res.map((r) => r.id);
|
|
764
|
+
});
|
|
418
765
|
case 'count':
|
|
419
|
-
return async (opts) => {
|
|
766
|
+
return term('count', async (st, opts) => {
|
|
420
767
|
checkAfterVerb(steps, mods);
|
|
421
768
|
afterNotPaged(mods, 'count');
|
|
422
|
-
return (await read(ctx, steps, mods, opts?.paths ? 'countPaths' : 'count')).res[0].n;
|
|
423
|
-
};
|
|
769
|
+
return (await read(ctx, steps, mods, opts?.paths ? 'countPaths' : 'count', undefined, st)).res[0].n;
|
|
770
|
+
});
|
|
424
771
|
case 'versions':
|
|
425
|
-
return async () => {
|
|
426
|
-
warnNoHistory(steps, 'versions');
|
|
427
|
-
return (await read(ctx, steps, mods, 'versions')).res.map((r) => toRow(r.row));
|
|
428
|
-
};
|
|
772
|
+
return (...a) => versionsPromise(term('versions', async (st) => {
|
|
773
|
+
warnNoHistory(steps, 'versions', ctx.log);
|
|
774
|
+
return (await read(ctx, steps, mods, 'versions', undefined, st)).res.map((r) => toRow(r.row));
|
|
775
|
+
})(...a));
|
|
429
776
|
case 'sum':
|
|
430
777
|
case 'avg':
|
|
431
778
|
case 'min':
|
|
@@ -440,13 +787,14 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
|
|
|
440
787
|
return (field, dir) => withMods({ order: field, desc: dir === true || dir === 'desc' });
|
|
441
788
|
case 'asOf':
|
|
442
789
|
return (t) => {
|
|
443
|
-
warnNoHistory(steps, 'asOf');
|
|
790
|
+
warnNoHistory(steps, 'asOf', ctx.log);
|
|
444
791
|
return withMods({ asOf: t instanceof Date ? t.toISOString() : t });
|
|
445
792
|
};
|
|
446
793
|
case 'after':
|
|
447
794
|
return (cursor) => withMods({ after: cursor });
|
|
448
795
|
case 'withDeleted':
|
|
449
|
-
|
|
796
|
+
// модификатор своего шага (план 2.1, A9): X(id).withDeleted().Y().withDeleted() — удалённые Y удалённого X
|
|
797
|
+
return () => withLast({ withDeleted: true });
|
|
450
798
|
case 'deep':
|
|
451
799
|
return (max = 32) => withLast({ deepMax: max });
|
|
452
800
|
case 'exact':
|
|
@@ -459,13 +807,13 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
|
|
|
459
807
|
return (v) => withLast({ ownerFilter: typeof v === 'object' ? v.id : v });
|
|
460
808
|
// глаголы записи — звенья плана (этап 4)
|
|
461
809
|
case 'create':
|
|
462
|
-
return (data = {}, opts) => withOp({ kind: 'create', data, ...reasonOf(opts) });
|
|
810
|
+
return (data = {}, opts) => withOp({ kind: 'create', data: dropUndefined(data), ...reasonOf(opts) });
|
|
463
811
|
case 'update':
|
|
464
812
|
return (data = {}, opts) => withOp({ kind: 'update', data, ...(opts?.rev !== undefined ? { rev: opts.rev } : {}), ...allOf(opts), ...reasonOf(opts) });
|
|
465
813
|
case 'upsert':
|
|
466
|
-
return (data = {}, opts) => withOp({ kind: 'upsert', data, ...reasonOf(opts) });
|
|
814
|
+
return (data = {}, opts) => withOp({ kind: 'upsert', data: dropUndefined(data), ...reasonOf(opts) });
|
|
467
815
|
case 'delete':
|
|
468
|
-
return (opts) => withOp({ kind: 'delete', confirm: opts?.confirm === true, ...allOf(opts), ...reasonOf(opts) });
|
|
816
|
+
return (opts) => withOp({ kind: 'delete', confirm: opts?.confirm === true, ...allOf(opts), ...reasonOf(opts), ...expectOf(opts) });
|
|
469
817
|
case 'set':
|
|
470
818
|
case 'unset':
|
|
471
819
|
case 'add':
|
|
@@ -474,7 +822,7 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
|
|
|
474
822
|
case 'forUpdate':
|
|
475
823
|
return () => withMods({ forUpdate: true });
|
|
476
824
|
case 'restore':
|
|
477
|
-
return (opts) => withOp({ kind: 'restore', ...reasonOf(opts) });
|
|
825
|
+
return (opts) => withOp({ kind: 'restore', ...allOf(opts), ...reasonOf(opts), ...expectOf(opts), ...(opts?.partial === true ? { partial: true } : {}) });
|
|
478
826
|
case 'cache':
|
|
479
827
|
return (opts) => {
|
|
480
828
|
if (opts?.ttl !== undefined && !(opts.ttl > 0))
|
|
@@ -482,16 +830,29 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
|
|
|
482
830
|
return withMods({ cache: opts ?? {} });
|
|
483
831
|
};
|
|
484
832
|
}
|
|
485
|
-
const found = lookupStep(ctx.reg, prop)
|
|
486
|
-
|
|
833
|
+
const found = lookupStep(ctx.reg, prop);
|
|
834
|
+
if (!found && noSession(ctx, prop))
|
|
835
|
+
return sessionless();
|
|
836
|
+
return classProp(ctx, prop, found ?? unknownName(ctx.reg, prop), steps, mods, nodes);
|
|
487
837
|
};
|
|
838
|
+
const log = ctx.log;
|
|
488
839
|
const handler = {
|
|
489
840
|
get(_t, prop) {
|
|
490
841
|
if (prop === PLAN)
|
|
491
842
|
return { steps, mods };
|
|
843
|
+
if (prop === Symbol.iterator)
|
|
844
|
+
return notIterable;
|
|
492
845
|
if (typeof prop === 'symbol')
|
|
493
846
|
return undefined;
|
|
494
|
-
|
|
847
|
+
if (!log?.enabled)
|
|
848
|
+
return member(prop);
|
|
849
|
+
// ошибка имени (неизвестный шаг, слот как метод) и модификатора или глагола (после глагола, неверный аргумент) —
|
|
850
|
+
// событие error с цепочкой до него; терминалы пишут свои события сами (traceTerm)
|
|
851
|
+
const where = () => ({ op: prop, chain: chainText(log, steps, mods) });
|
|
852
|
+
const got = guarded(log, where, () => member(prop));
|
|
853
|
+
return typeof got === 'function' && GUARDED.has(prop)
|
|
854
|
+
? (...a) => guarded(log, where, () => got(...a))
|
|
855
|
+
: got;
|
|
495
856
|
},
|
|
496
857
|
};
|
|
497
858
|
return new Proxy({}, handler);
|
|
@@ -505,18 +866,26 @@ export function makeBatch(ctx, queue) {
|
|
|
505
866
|
const member = (prop) => {
|
|
506
867
|
switch (prop) {
|
|
507
868
|
case 'run':
|
|
508
|
-
return async () => {
|
|
869
|
+
return () => traced(ctx.log ?? NOLOG, 'write', { op: 'batch' }, async () => {
|
|
509
870
|
const entries = queue.splice(0, queue.length);
|
|
510
871
|
const loose = entries.find((x) => x.loose)?.loose;
|
|
511
872
|
if (loose)
|
|
512
873
|
throw afterVerbError(loose.name, loose.verb);
|
|
513
874
|
const plans = entries.map((x) => x.steps);
|
|
514
|
-
|
|
515
|
-
|
|
875
|
+
try {
|
|
876
|
+
for (const p of plans)
|
|
877
|
+
precheckPlan(ctx.reg, p);
|
|
878
|
+
}
|
|
879
|
+
catch (e) {
|
|
880
|
+
throw await completeIssues(ctx, e);
|
|
881
|
+
}
|
|
516
882
|
if (!plans.length)
|
|
517
883
|
return [];
|
|
518
884
|
return ctx.exec.run((q) => execBatch({ reg: ctx.reg, schema: ctx.schema, q, tenant: ctx.tenant(), owner: ctx.owner, ...reasons(ctx) }, plans));
|
|
519
|
-
}
|
|
885
|
+
}, (out) => {
|
|
886
|
+
const rows = out.flat();
|
|
887
|
+
return { rows: rows.length, detail: { plans: out.length, classes: byClass(rows), ids: idsOf(rows) } };
|
|
888
|
+
});
|
|
520
889
|
case 'discard':
|
|
521
890
|
return () => void queue.splice(0, queue.length);
|
|
522
891
|
case 'size':
|
|
@@ -534,8 +903,10 @@ export function makeBatch(ctx, queue) {
|
|
|
534
903
|
}
|
|
535
904
|
/** Старт пути от корня db: класс, алиас или роль. */
|
|
536
905
|
export function startStep(ctx, name) {
|
|
537
|
-
const found = lookupStep(ctx.reg, name)
|
|
538
|
-
|
|
906
|
+
const found = lookupStep(ctx.reg, name);
|
|
907
|
+
if (!found && noSession(ctx, name))
|
|
908
|
+
return sessionless();
|
|
909
|
+
return classProp(ctx, name, found ?? unknownName(ctx.reg, name), undefined, {}, NO_NODES);
|
|
539
910
|
}
|
|
540
911
|
/** Старт пути с готового узла: db.entity(row | цепочка). */
|
|
541
912
|
export function startEntity(ctx, x) {
|