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