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/load.js
CHANGED
|
@@ -18,16 +18,25 @@
|
|
|
18
18
|
* полиморфного конца — с $class цели. Ключи на $ внутри данных — обычные данные.
|
|
19
19
|
*/
|
|
20
20
|
import { randomUUID } from 'node:crypto';
|
|
21
|
-
import { LetopisError, fromDbError,
|
|
21
|
+
import { LetopisError, checkOptions, fromDbError, issuesFromDb, lerr, lmsg } from './errors.js';
|
|
22
22
|
import { isOp } from './ops.js';
|
|
23
|
-
import { Registry
|
|
23
|
+
import { Registry } from './registry.js';
|
|
24
24
|
import { accepts } from './sql.js';
|
|
25
25
|
import { v5Id } from './uuid.js';
|
|
26
|
-
import { applyDefaults } from './validate.js';
|
|
26
|
+
import { applyDefaults, leavesToDb } from './validate.js';
|
|
27
27
|
import { isInc } from './write.js';
|
|
28
28
|
import { ACL_CLASS_NAMES } from './types.js';
|
|
29
|
+
import { isConnectionError, release, rollbackAfter } from './tx.js';
|
|
29
30
|
const SYSTEM_ID = '2eba6d0a-1edd-4bb6-a85a-a703dab49035';
|
|
30
31
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
32
|
+
/**
|
|
33
|
+
* Все опции load(): неизвестная — invalid_data с похожими (checkOptions; план 2.0, 7.10 №4). Фасад db.load() сверяет их
|
|
34
|
+
* до пула администратора (index.ts), load() — ещё раз до проверки прав.
|
|
35
|
+
*/
|
|
36
|
+
export const LOAD_OPTIONS = {
|
|
37
|
+
duplicates: true, existing: true, batch: true, commitEvery: true, withHistory: true, class: true, tenant: true,
|
|
38
|
+
agent: true, as: true, initial: true,
|
|
39
|
+
};
|
|
31
40
|
/** Классы, строки которых первая заливка сохраняет и возвращает: описания классов, аккаунты, членства, права. */
|
|
32
41
|
const SYSTEM_CLASSES = ['Class', 'Account', 'member', 'Resource', 'rule'];
|
|
33
42
|
// значения без обратной косой, табуляции и переводов строки (почти все: uuid, числа, время) — как есть, без четырёх
|
|
@@ -39,9 +48,8 @@ const esc = (v) => {
|
|
|
39
48
|
const s = String(v);
|
|
40
49
|
return ESCAPED.test(s) ? s.replace(/\\/g, '\\\\').replace(/\t/g, '\\t').replace(/\n/g, '\\n').replace(/\r/g, '\\r') : s;
|
|
41
50
|
};
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const LOG_COLS = 'id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason, moved, prev_hash, tx';
|
|
51
|
+
const ENTITY_COLS = 'id, rev, class, cv, tenant, owner, links, data, tags, at, created_at, author, agent, op, reason, validator';
|
|
52
|
+
const LOG_COLS = 'id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason, tx';
|
|
45
53
|
const asList = (v) => (Array.isArray(v) ? v : [v]);
|
|
46
54
|
/** Теги → литерал text[] для COPY (экранирование COPY — потом, в esc). */
|
|
47
55
|
const pgArray = (tags) => `{${tags.map((t) => `"${t.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`).join(',')}}`;
|
|
@@ -71,8 +79,11 @@ class Issues {
|
|
|
71
79
|
const first = this.list[0];
|
|
72
80
|
const rest = this.total - 1;
|
|
73
81
|
const why = [...this.unchecked].join('; ');
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
// форма текста — по полноте проверки и числу остальных нарушений: четыре записи каталога, а не склейка
|
|
83
|
+
const key = why ? (rest ? 'write.load_cancelled_at_least' : 'write.load_cancelled_maybe_more') : rest ? 'write.load_cancelled_more' : 'write.load_cancelled';
|
|
84
|
+
const params = { line: first.line, message: first.message, rest, why };
|
|
85
|
+
// код ошибки — код первого нарушения, а не записи каталога (поэтому не lerr): ключ и параметры — как у fromDbError
|
|
86
|
+
throw new LetopisError(first.code, lmsg(key, params), { rows: this.list, total: this.total, truncated: this.total > this.list.length, limit: ISSUES_LIMIT, complete: !why }, { key, params });
|
|
76
87
|
}
|
|
77
88
|
}
|
|
78
89
|
/** Вход → доступ к строке по номеру (массивы и объект «класс → строки» — без копии). */
|
|
@@ -91,32 +102,32 @@ async function toList(input, cls) {
|
|
|
91
102
|
}
|
|
92
103
|
if (isObj(input))
|
|
93
104
|
return Object.entries(input).flatMap(([c, rows]) => tag(rows, c));
|
|
94
|
-
throw
|
|
105
|
+
throw lerr('write.load_bad_input');
|
|
95
106
|
}
|
|
96
107
|
/** Ссылка на цель: id, { $key: […] } или { $ext } (у полиморфного конца — с $class). */
|
|
97
108
|
function refOf(reg, tenant, end, v) {
|
|
98
109
|
if (typeof v === 'string')
|
|
99
|
-
return UUID.test(v) ? { id: v.toLowerCase() } : { error: '
|
|
110
|
+
return UUID.test(v) ? { id: v.toLowerCase() } : { error: lmsg('write.load_end_not_uuid') };
|
|
100
111
|
if (!isObj(v) || (!('$key' in v) && !('$ext' in v)))
|
|
101
|
-
return { error: '
|
|
112
|
+
return { error: lmsg('write.load_end_value') };
|
|
102
113
|
const classes = asList(end.class);
|
|
103
114
|
const name = typeof v.$class === 'string' ? v.$class : classes.length === 1 ? classes[0] : null;
|
|
104
115
|
if (!name)
|
|
105
|
-
return { error: '
|
|
116
|
+
return { error: lmsg('write.load_end_polymorphic') };
|
|
106
117
|
const c = reg.find(name);
|
|
107
118
|
if (!c || c.abstract)
|
|
108
|
-
return { error:
|
|
119
|
+
return { error: lmsg('write.load_target_class', { name }) };
|
|
109
120
|
if ('$key' in v) {
|
|
110
121
|
const vals = v.$key;
|
|
111
122
|
if (!Array.isArray(vals) || vals.length !== c.key.length || !vals.every((x) => typeof x === 'string')) {
|
|
112
|
-
return { error:
|
|
123
|
+
return { error: lmsg('write.load_target_key', { cls: c.name, key: c.key.join(', ') }) };
|
|
113
124
|
}
|
|
114
125
|
return { id: v5Id(tenant, c.name, vals.map((x, i) => (c.ends[c.key[i]] ? x.toLowerCase() : x))) };
|
|
115
126
|
}
|
|
116
127
|
if (c.key.length)
|
|
117
|
-
return { error:
|
|
128
|
+
return { error: lmsg('write.load_ext_keyed', { cls: c.name, key: c.key.join(', ') }) };
|
|
118
129
|
if (typeof v.$ext !== 'string')
|
|
119
|
-
return { error: '
|
|
130
|
+
return { error: lmsg('write.load_ext_string') };
|
|
120
131
|
return { id: v5Id(tenant, c.name, [v.$ext]) };
|
|
121
132
|
}
|
|
122
133
|
/** Строка → данные и концы: поля верхнего уровня, роли, пометки на $; проверки по описанию класса. */
|
|
@@ -132,14 +143,14 @@ function shape(reg, tenant, cls, raw, check) {
|
|
|
132
143
|
const end = cls.ends[k];
|
|
133
144
|
if (end.many) {
|
|
134
145
|
if (!Array.isArray(v))
|
|
135
|
-
return { data, links, error: { code: 'invalid_data', message:
|
|
146
|
+
return { data, links, error: { code: 'invalid_data', message: lmsg('write.load_end_list', { role: k }) } };
|
|
136
147
|
const ids = [];
|
|
137
148
|
for (const x of v) {
|
|
138
149
|
const r = refOf(reg, tenant, end, x);
|
|
139
150
|
if ('error' in r)
|
|
140
|
-
return { data, links, error: { code: 'invalid_data', message:
|
|
151
|
+
return { data, links, error: { code: 'invalid_data', message: lmsg('write.load_end_ref', { role: k, error: r.error }) } };
|
|
141
152
|
if (ids.includes(r.id))
|
|
142
|
-
return { data, links, error: { code: 'invalid_data', message:
|
|
153
|
+
return { data, links, error: { code: 'invalid_data', message: lmsg('write.load_end_repeat', { role: k }) } };
|
|
143
154
|
ids.push(r.id);
|
|
144
155
|
}
|
|
145
156
|
links[k] = ids;
|
|
@@ -147,7 +158,7 @@ function shape(reg, tenant, cls, raw, check) {
|
|
|
147
158
|
else {
|
|
148
159
|
const r = refOf(reg, tenant, end, v);
|
|
149
160
|
if ('error' in r)
|
|
150
|
-
return { data, links, error: { code: 'invalid_data', message:
|
|
161
|
+
return { data, links, error: { code: 'invalid_data', message: lmsg('write.load_end_ref', { role: k, error: r.error }) } };
|
|
151
162
|
links[k] = r.id;
|
|
152
163
|
}
|
|
153
164
|
}
|
|
@@ -155,17 +166,13 @@ function shape(reg, tenant, cls, raw, check) {
|
|
|
155
166
|
data[k] = v;
|
|
156
167
|
}
|
|
157
168
|
}
|
|
158
|
-
// агрегатные поля считает база (план 1.1, §2.4): переданное значение не пишется — 0, итог посчитает триггер
|
|
159
|
-
// после оператора, а первая заливка — agg_fill
|
|
160
|
-
for (const f of aggregateFields(cls))
|
|
161
|
-
data[f] = 0;
|
|
162
169
|
if (!check)
|
|
163
170
|
return { data, links };
|
|
164
171
|
const scan = (v, path) => {
|
|
165
172
|
if (isInc(v))
|
|
166
|
-
return
|
|
173
|
+
return lmsg('write.load_inc', { path });
|
|
167
174
|
if (isOp(v))
|
|
168
|
-
return
|
|
175
|
+
return lmsg('write.load_filter_op', { path });
|
|
169
176
|
if (Array.isArray(v)) {
|
|
170
177
|
for (let i = 0; i < v.length; i++) {
|
|
171
178
|
const m = scan(v[i], `${path}.${i}`);
|
|
@@ -188,14 +195,14 @@ function shape(reg, tenant, cls, raw, check) {
|
|
|
188
195
|
const full = applyDefaults(cls.schema, data);
|
|
189
196
|
const issues = cls.validator()(full);
|
|
190
197
|
if (issues.length)
|
|
191
|
-
return { data: full, links, error: { code: 'invalid_data', message:
|
|
198
|
+
return { data: full, links, error: { code: 'invalid_data', message: lmsg('write.load_invalid_data', { cls: cls.name }), issues } };
|
|
192
199
|
for (const [role, end] of Object.entries(cls.ends)) {
|
|
193
200
|
const n = end.many ? (links[role]?.length ?? 0) : links[role] ? 1 : 0;
|
|
194
201
|
const min = Math.max(end.min ?? 0, end.required ? 1 : 0);
|
|
195
202
|
if (end.many ? n < min : end.required && !n)
|
|
196
|
-
return { data: full, links, error: { code: 'invalid_data', message: end.many ?
|
|
203
|
+
return { data: full, links, error: { code: 'invalid_data', message: lmsg(end.many ? 'write.load_end_too_few' : 'write.load_end_required', { role }) } };
|
|
197
204
|
if (end.many && end.max !== undefined && n > end.max)
|
|
198
|
-
return { data: full, links, error: { code: 'invalid_data', message:
|
|
205
|
+
return { data: full, links, error: { code: 'invalid_data', message: lmsg('write.load_end_too_many', { role }) } };
|
|
199
206
|
}
|
|
200
207
|
return { data: full, links };
|
|
201
208
|
}
|
|
@@ -203,7 +210,7 @@ function shape(reg, tenant, cls, raw, check) {
|
|
|
203
210
|
function idOfRow(reg, tenant, cls, raw) {
|
|
204
211
|
if (!cls.key.length) {
|
|
205
212
|
if (raw.$ext !== undefined)
|
|
206
|
-
return typeof raw.$ext === 'string' ? v5Id(tenant, cls.name, [raw.$ext]) : { error: '
|
|
213
|
+
return typeof raw.$ext === 'string' ? v5Id(tenant, cls.name, [raw.$ext]) : { error: lmsg('write.load_ext_string') };
|
|
207
214
|
return v5Id(tenant, cls.name, [randomUUID()]);
|
|
208
215
|
}
|
|
209
216
|
const vals = [];
|
|
@@ -211,13 +218,13 @@ function idOfRow(reg, tenant, cls, raw) {
|
|
|
211
218
|
if (cls.ends[k]) {
|
|
212
219
|
const r = refOf(reg, tenant, cls.ends[k], raw[k]);
|
|
213
220
|
if ('error' in r)
|
|
214
|
-
return { error:
|
|
221
|
+
return { error: lmsg('write.load_key_part_ref', { part: k, error: r.error }) };
|
|
215
222
|
vals.push(r.id);
|
|
216
223
|
}
|
|
217
224
|
else {
|
|
218
225
|
const v = raw[k] ?? cls.schema.properties?.[k]?.default;
|
|
219
226
|
if (typeof v !== 'string')
|
|
220
|
-
return { error:
|
|
227
|
+
return { error: lmsg('write.load_key_part_string', { part: k }) };
|
|
221
228
|
vals.push(v);
|
|
222
229
|
}
|
|
223
230
|
}
|
|
@@ -319,23 +326,6 @@ async function copyLines(conn, text, lines) {
|
|
|
319
326
|
pump();
|
|
320
327
|
});
|
|
321
328
|
}
|
|
322
|
-
/** verify: true — загруженные строки перепроверяет проверка базы (данные и цели концов); нарушения — в to. */
|
|
323
|
-
async function verifyIds(conn, S, ids, to) {
|
|
324
|
-
const bad = await conn.unsafe(`select e.id, e.class, ${S}.validate_data(c.data->'resolved'->'rules', c.validator, c.data->'resolved'->'vars', e.data) as issues
|
|
325
|
-
from ${S}.entity e cross join lateral ${S}.class_row(e.class, e.tenant) c
|
|
326
|
-
where e.id = any ($1::uuid[]) and ${S}.validate_data(c.data->'resolved'->'rules', c.validator, c.data->'resolved'->'vars', e.data) <> '[]'::jsonb limit ${ISSUES_LIMIT}`, [ids]);
|
|
327
|
-
for (const r of bad)
|
|
328
|
-
to.add({ line: 0, code: 'invalid_data', message: `проверка базы: строка ${r.id} не проходит описание класса ${r.class}`, id: r.id, issues: r.issues });
|
|
329
|
-
if (bad.length >= ISSUES_LIMIT)
|
|
330
|
-
to.unchecked.add(`проверка базы называет не больше ${ISSUES_LIMIT} строк`);
|
|
331
|
-
const [t] = await conn.unsafe(`select ${S}.targets_bad((select jsonb_agg(jsonb_build_object('class', e.class, 'tenant', e.tenant, 'links', e.links))
|
|
332
|
-
from ${S}.entity e where e.id = any ($1::uuid[]) and e.links <> '{}'::jsonb), false) as role`, [ids]);
|
|
333
|
-
const role = t?.role;
|
|
334
|
-
if (role?.startsWith('!'))
|
|
335
|
-
to.add({ line: 0, code: 'derived_class', message: `проверка базы: конец ${role.slice(1)} ведёт на строку производного класса` });
|
|
336
|
-
else if (role)
|
|
337
|
-
to.add({ line: 0, code: 'target_not_found', message: `проверка базы: цель конца ${role} не найдена` });
|
|
338
|
-
}
|
|
339
329
|
/**
|
|
340
330
|
* Загрузить строки (§2.6). Одна транзакция (или порции commitEvery): описания классов под
|
|
341
331
|
* разделяемыми блокировками семейств, проверки в коде, COPY пачками, запрос по целям вне загрузки,
|
|
@@ -343,30 +333,29 @@ async function verifyIds(conn, S, ids, to) {
|
|
|
343
333
|
* называет строки и нарушения.
|
|
344
334
|
*/
|
|
345
335
|
export async function load(env, input, opts = {}) {
|
|
346
|
-
|
|
347
|
-
throw removed('load({ trust })', 'запрос по целям вне загрузки стоит 0,1–1,3 % её времени — режим доверия убран (решение 2 точки А)');
|
|
336
|
+
checkOptions('load', opts, LOAD_OPTIONS);
|
|
348
337
|
if (!env.admin)
|
|
349
|
-
throw
|
|
338
|
+
throw lerr('write.load_admin_required');
|
|
350
339
|
const asAcc = opts.as;
|
|
351
340
|
if (asAcc !== undefined && (typeof asAcc !== 'string' || !UUID.test(asAcc)))
|
|
352
|
-
throw
|
|
341
|
+
throw lerr('write.load_as_not_uuid');
|
|
353
342
|
if (asAcc !== undefined && opts.withHistory)
|
|
354
|
-
throw
|
|
343
|
+
throw lerr('write.load_as_history');
|
|
355
344
|
if (opts.withHistory && opts.existing === 'update')
|
|
356
|
-
throw
|
|
345
|
+
throw lerr('write.load_history_update');
|
|
357
346
|
const ini = env.session ?? null;
|
|
358
347
|
if (opts.initial || ini) {
|
|
359
348
|
// снятые индексы и TRUNCATE — одна транзакция; в пустой схеме пропускать и обновлять нечего
|
|
360
349
|
if (opts.commitEvery)
|
|
361
|
-
throw
|
|
350
|
+
throw lerr('write.load_initial_commit_every');
|
|
362
351
|
if (opts.existing !== undefined && opts.existing !== 'error')
|
|
363
|
-
throw
|
|
352
|
+
throw lerr('write.load_initial_existing');
|
|
364
353
|
if (asAcc !== undefined)
|
|
365
|
-
throw
|
|
354
|
+
throw lerr('write.load_initial_as');
|
|
366
355
|
}
|
|
367
356
|
const tenant = opts.tenant ?? asAcc ?? env.tenant;
|
|
368
357
|
if (!tenant)
|
|
369
|
-
throw
|
|
358
|
+
throw lerr('write.load_no_tenant');
|
|
370
359
|
const dupMode = opts.duplicates ?? 'error';
|
|
371
360
|
const existing = opts.existing ?? 'error';
|
|
372
361
|
const batch = opts.batch ?? 10000;
|
|
@@ -412,23 +401,17 @@ export async function load(env, input, opts = {}) {
|
|
|
412
401
|
const name = raw?.$class;
|
|
413
402
|
const cls = typeof name === 'string' ? reg0.find(name) : undefined;
|
|
414
403
|
if (!cls) {
|
|
415
|
-
issues.add({ line, code: 'invalid_class', message: typeof name === 'string' ?
|
|
404
|
+
issues.add({ line, code: 'invalid_class', message: typeof name === 'string' ? lmsg('write.load_class_not_found', { name }) : lmsg('write.load_no_class') });
|
|
416
405
|
p1[i] = null;
|
|
417
406
|
continue;
|
|
418
407
|
}
|
|
419
408
|
if (cls.name === 'Class') {
|
|
420
|
-
issues.add({ line, code: 'class_not_loadable', message: '
|
|
409
|
+
issues.add({ line, code: 'class_not_loadable', message: lmsg('write.load_class_rows'), class: cls.name });
|
|
421
410
|
p1[i] = null;
|
|
422
411
|
continue;
|
|
423
412
|
}
|
|
424
413
|
if (cls.abstract) {
|
|
425
|
-
issues.add({ line, code: 'invalid_class', message:
|
|
426
|
-
p1[i] = null;
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
// строки производного класса пишет только его проектор (план 1.1, §2.4)
|
|
430
|
-
if (cls.derivedBy) {
|
|
431
|
-
issues.add({ line, code: 'derived_class', message: `класс ${cls.name} производный: его строки пишет проектор ${cls.derivedBy}`, class: cls.name });
|
|
414
|
+
issues.add({ line, code: 'invalid_class', message: lmsg('write.load_class_abstract', { cls: cls.name }), class: cls.name });
|
|
432
415
|
p1[i] = null;
|
|
433
416
|
continue;
|
|
434
417
|
}
|
|
@@ -440,7 +423,7 @@ export async function load(env, input, opts = {}) {
|
|
|
440
423
|
}
|
|
441
424
|
// первая заливка: первичного ключа нет — повтор id со строками системных классов и прошлых загрузок сессии ловит проход 1
|
|
442
425
|
if (ini?.ids.has(id)) {
|
|
443
|
-
issues.add({ line, code: 'exists', message: '
|
|
426
|
+
issues.add({ line, code: 'exists', message: lmsg('write.load_taken'), id, class: cls.name });
|
|
444
427
|
p1[i] = null;
|
|
445
428
|
continue;
|
|
446
429
|
}
|
|
@@ -458,7 +441,7 @@ export async function load(env, input, opts = {}) {
|
|
|
458
441
|
chosen.add(i);
|
|
459
442
|
}
|
|
460
443
|
else if (dupMode === 'error') {
|
|
461
|
-
issues.add({ line, code: 'duplicate', message:
|
|
444
|
+
issues.add({ line, code: 'duplicate', message: lmsg('write.load_duplicate', { id, prev: prev + 1 }), id, class: cls.name });
|
|
462
445
|
}
|
|
463
446
|
else {
|
|
464
447
|
report.duplicates++;
|
|
@@ -527,7 +510,6 @@ export async function load(env, input, opts = {}) {
|
|
|
527
510
|
laterUnchecked(portion, issues);
|
|
528
511
|
// первая заливка: транзакцию, роль и пояс поставила сессия, фиксирует и откатывает тоже она
|
|
529
512
|
const conn = ini ? ini.conn : await env.sql.reserve();
|
|
530
|
-
const written = [];
|
|
531
513
|
let failed = null;
|
|
532
514
|
try {
|
|
533
515
|
if (!ini) {
|
|
@@ -551,7 +533,7 @@ export async function load(env, input, opts = {}) {
|
|
|
551
533
|
const n = p1[i].cls.name;
|
|
552
534
|
if (!(n in acl.write) && !denied.has(n)) {
|
|
553
535
|
denied.add(n);
|
|
554
|
-
issues.add({ line: i + 1, code: 'acl_denied', message:
|
|
536
|
+
issues.add({ line: i + 1, code: 'acl_denied', message: lmsg('write.load_no_write', { cls: n }), class: n });
|
|
555
537
|
}
|
|
556
538
|
}
|
|
557
539
|
if (issues.any)
|
|
@@ -568,15 +550,16 @@ export async function load(env, input, opts = {}) {
|
|
|
568
550
|
const classesTouched = new Set();
|
|
569
551
|
// существующие объекты — только в режимах skip и update, одним запросом на пачку и до проверок:
|
|
570
552
|
// пропущенный объект для ссылок на него — вне загрузки (его сверит запрос в конце)
|
|
571
|
-
|
|
553
|
+
// живые строки пачки: id → класс (existing: 'update' правит только объект того же класса)
|
|
554
|
+
const live = new Map();
|
|
572
555
|
const past = new Map();
|
|
573
556
|
if (existing !== 'error' && !opts.withHistory) {
|
|
574
557
|
for (let b = 0; b < portion.length; b += batch) {
|
|
575
558
|
const ids = portion.slice(b, b + batch).map((i) => p1[i].id);
|
|
576
559
|
const lock = existing === 'update' ? ' order by e.id for no key update' : '';
|
|
577
|
-
for (const r of await conn.unsafe(`select e.id from ${S}.entity e where e.id = any ($1::uuid[])${lock}`, [ids]))
|
|
578
|
-
live.
|
|
579
|
-
for (const r of await conn.unsafe(`select distinct on (l.id) l.id, l.rev, l.
|
|
560
|
+
for (const r of await conn.unsafe(`select e.id, e.class from ${S}.entity e where e.id = any ($1::uuid[])${lock}`, [ids]))
|
|
561
|
+
live.set(r.id, r.class);
|
|
562
|
+
for (const r of await conn.unsafe(`select distinct on (l.id) l.id, l.rev, l.at::text as at from ${S}.log l
|
|
580
563
|
where l.id = any ($1::uuid[]) order by l.id, l.rev desc`, [ids]))
|
|
581
564
|
past.set(r.id, r);
|
|
582
565
|
}
|
|
@@ -602,23 +585,23 @@ export async function load(env, input, opts = {}) {
|
|
|
602
585
|
todo = portion.filter((i) => !taken.has(p1[i].id));
|
|
603
586
|
}
|
|
604
587
|
if (opts.withHistory) {
|
|
605
|
-
await historyWrite(conn, reg, todo, inLoad, external, xid, classesTouched
|
|
588
|
+
await historyWrite(conn, reg, todo, inLoad, external, xid, classesTouched);
|
|
606
589
|
}
|
|
607
590
|
else if (ini) {
|
|
608
|
-
await pipeline(conn, reg, portion, inLoad, external, ts, xid, classesTouched,
|
|
591
|
+
await pipeline(conn, reg, portion, inLoad, external, ts, xid, classesTouched, live, past);
|
|
609
592
|
}
|
|
610
593
|
else {
|
|
611
594
|
for (let b = 0; b < portion.length; b += batch) {
|
|
612
595
|
const part = portion.slice(b, b + batch);
|
|
613
|
-
await writeBatch(conn, reg, part, inLoad, external, ts, xid, classesTouched,
|
|
596
|
+
await writeBatch(conn, reg, part, inLoad, external, ts, xid, classesTouched, live, past);
|
|
614
597
|
}
|
|
615
598
|
}
|
|
616
599
|
if (issues.any)
|
|
617
600
|
issues.fail();
|
|
618
601
|
if (ini) {
|
|
619
|
-
// цели,
|
|
602
|
+
// цели, владельцы и арендатор — в конце сессии, по перестроенным индексам
|
|
620
603
|
report.external += external.size;
|
|
621
|
-
ini.pending.push({ reg, tenant: tenant, external, owners: new Map(owners)
|
|
604
|
+
ini.pending.push({ reg, tenant: tenant, external, owners: new Map(owners) });
|
|
622
605
|
for (const id of inLoad.keys())
|
|
623
606
|
ini.ids.add(id);
|
|
624
607
|
let set = ini.touched.get(tenant);
|
|
@@ -638,10 +621,7 @@ export async function load(env, input, opts = {}) {
|
|
|
638
621
|
const ref = external.get(id);
|
|
639
622
|
const f = found.get(id);
|
|
640
623
|
if (!f || (f.tenant !== tenant && f.tenant !== SYSTEM_ID) || !accepts(reg, ref.allowed, f.class)) {
|
|
641
|
-
issues.add({ line: ref.line, code: 'target_not_found', message:
|
|
642
|
-
}
|
|
643
|
-
else if (reg.find(f.class)?.derivedBy) {
|
|
644
|
-
issues.add({ line: ref.line, code: 'derived_class', message: `цель конца ${ref.role} — строка производного класса ${f.class}`, id });
|
|
624
|
+
issues.add({ line: ref.line, code: 'target_not_found', message: lmsg('write.load_target_not_found', { role: ref.role }), id });
|
|
645
625
|
}
|
|
646
626
|
}
|
|
647
627
|
}
|
|
@@ -653,7 +633,7 @@ export async function load(env, input, opts = {}) {
|
|
|
653
633
|
: []);
|
|
654
634
|
for (const id of ids)
|
|
655
635
|
if (!found.has(id))
|
|
656
|
-
issues.add({ line: owners.get(id), code: 'target_not_found', message:
|
|
636
|
+
issues.add({ line: owners.get(id), code: 'target_not_found', message: lmsg('write.load_owner_not_found', { id }), id });
|
|
657
637
|
}
|
|
658
638
|
// от имени пользователя цели ссылок должны быть ему видны (право READ): и из загрузки, и вне её
|
|
659
639
|
if (acl && targets.size) {
|
|
@@ -662,20 +642,15 @@ export async function load(env, input, opts = {}) {
|
|
|
662
642
|
and not ${S}.row_visible(e.tenant, e.class, e.id, e.owner, e.tags, e.data, e.links, $2::uuid, $3::uuid, $4::jsonb)`, [ids, tenant, asAcc, acl.read]);
|
|
663
643
|
for (const h of hidden) {
|
|
664
644
|
const t = targets.get(h.id);
|
|
665
|
-
issues.add({ line: t.line, code: 'target_not_found', message:
|
|
645
|
+
issues.add({ line: t.line, code: 'target_not_found', message: lmsg('write.load_target_not_found', { role: t.role }), id: h.id });
|
|
666
646
|
}
|
|
667
647
|
}
|
|
668
648
|
// арендатор существует и включён
|
|
669
649
|
const [acc] = await conn.unsafe(`select coalesce((data->>'enabled')::boolean, true) as enabled from ${S}.entity where id = $1::uuid and class = 'Account'`, [tenant]);
|
|
670
650
|
if (!acc || !acc.enabled)
|
|
671
|
-
issues.add({ line: portion.length ? portion[0] + 1 : 0, code: 'tenant_denied', message:
|
|
651
|
+
issues.add({ line: portion.length ? portion[0] + 1 : 0, code: 'tenant_denied', message: lmsg('write.load_tenant_denied', { tenant }) });
|
|
672
652
|
if (issues.any)
|
|
673
653
|
issues.fail();
|
|
674
|
-
if (opts.verify && written.length) {
|
|
675
|
-
await verifyIds(conn, S, written, issues);
|
|
676
|
-
if (issues.any)
|
|
677
|
-
issues.fail();
|
|
678
|
-
}
|
|
679
654
|
// сигнал — функцией базы, как у триггеров: нагрузка по режиму сигналов установки (план 1.1)
|
|
680
655
|
if (classesTouched.size) {
|
|
681
656
|
const touched = [...classesTouched];
|
|
@@ -690,12 +665,13 @@ export async function load(env, input, opts = {}) {
|
|
|
690
665
|
}
|
|
691
666
|
catch (e) {
|
|
692
667
|
failed = e;
|
|
668
|
+
// обрыв: в мёртвое соединение не пишут и в пул его не возвращают (план 2.0, 7.4 №7)
|
|
693
669
|
if (!ini)
|
|
694
|
-
await conn
|
|
670
|
+
await rollbackAfter(conn, e);
|
|
695
671
|
}
|
|
696
672
|
finally {
|
|
697
673
|
if (!ini)
|
|
698
|
-
|
|
674
|
+
release(conn, failed);
|
|
699
675
|
}
|
|
700
676
|
if (failed) {
|
|
701
677
|
const err = failed;
|
|
@@ -708,7 +684,7 @@ export async function load(env, input, opts = {}) {
|
|
|
708
684
|
/** Порция не последняя (commitEvery): следующие порции загрузчик уже не проверит — нарушений может быть больше. */
|
|
709
685
|
function laterUnchecked(portion, to) {
|
|
710
686
|
if (portion !== portions[portions.length - 1])
|
|
711
|
-
to.unchecked.add('
|
|
687
|
+
to.unchecked.add(lmsg('write.load_later_portions'));
|
|
712
688
|
}
|
|
713
689
|
function isLastVersion(i) {
|
|
714
690
|
const list = versions.get(p1[i].id);
|
|
@@ -728,11 +704,11 @@ export async function load(env, input, opts = {}) {
|
|
|
728
704
|
for (const i of portion) {
|
|
729
705
|
const c = taken.get(p1[i].id);
|
|
730
706
|
if (c)
|
|
731
|
-
out.add({ line: i + 1, code: c, message: c === 'exists' ? '
|
|
707
|
+
out.add({ line: i + 1, code: c, message: lmsg(c === 'exists' ? 'write.load_exists' : 'write.load_deleted'), id: p1[i].id, class: p1[i].cls.name });
|
|
732
708
|
}
|
|
733
709
|
if (out.any)
|
|
734
710
|
out.fail();
|
|
735
|
-
throw
|
|
711
|
+
throw lerr('write.load_objects_exist');
|
|
736
712
|
}
|
|
737
713
|
/** Проверки строки и ссылок; null — строка не пишется (нарушение уже в отчёте). */
|
|
738
714
|
function check(reg, i, inLoad, external, lastVersion) {
|
|
@@ -745,16 +721,16 @@ export async function load(env, input, opts = {}) {
|
|
|
745
721
|
}
|
|
746
722
|
// аккаунты — только в System; членство лежит в арендаторе, в которого вступают (как в триггерах)
|
|
747
723
|
if (cls.name === 'Account' && tenant !== SYSTEM_ID) {
|
|
748
|
-
issues.add({ line: i + 1, code: 'tenant_denied', message: '
|
|
724
|
+
issues.add({ line: i + 1, code: 'tenant_denied', message: lmsg('write.load_account_tenant'), id: p1[i].id, class: cls.name });
|
|
749
725
|
return null;
|
|
750
726
|
}
|
|
751
727
|
if (cls.name === 'member' && sh.links.Tenant !== tenant) {
|
|
752
|
-
issues.add({ line: i + 1, code: 'invalid_data', message: '
|
|
728
|
+
issues.add({ line: i + 1, code: 'invalid_data', message: lmsg('write.load_member_tenant'), id: p1[i].id, class: cls.name });
|
|
753
729
|
return null;
|
|
754
730
|
}
|
|
755
731
|
const tags = raw.$tags;
|
|
756
732
|
if (tags !== undefined && (!Array.isArray(tags) || !tags.every((x) => typeof x === 'string'))) {
|
|
757
|
-
issues.add({ line: i + 1, code: 'invalid_data', message: '
|
|
733
|
+
issues.add({ line: i + 1, code: 'invalid_data', message: lmsg('write.load_tags'), id: p1[i].id, class: cls.name });
|
|
758
734
|
return null;
|
|
759
735
|
}
|
|
760
736
|
if (tags)
|
|
@@ -762,7 +738,7 @@ export async function load(env, input, opts = {}) {
|
|
|
762
738
|
const own = raw.$owner;
|
|
763
739
|
if (own !== undefined && own !== null) {
|
|
764
740
|
if (asAcc || typeof own !== 'string' || !UUID.test(own)) {
|
|
765
|
-
issues.add({ line: i + 1, code: 'invalid_data', message: asAcc ? '
|
|
741
|
+
issues.add({ line: i + 1, code: 'invalid_data', message: lmsg(asAcc ? 'write.load_as_owner' : 'write.load_owner_id'), id: p1[i].id, class: cls.name });
|
|
766
742
|
return null;
|
|
767
743
|
}
|
|
768
744
|
sh.owner = own.toLowerCase();
|
|
@@ -780,7 +756,7 @@ export async function load(env, input, opts = {}) {
|
|
|
780
756
|
const t = inLoad.get(id);
|
|
781
757
|
if (t) {
|
|
782
758
|
if (!t.alive || !accepts(reg, allowed, t.cls))
|
|
783
|
-
issues.add({ line: i + 1, code: 'target_not_found', message:
|
|
759
|
+
issues.add({ line: i + 1, code: 'target_not_found', message: lmsg('write.load_target_not_found', { role }), id: p1[i].id, class: cls.name });
|
|
784
760
|
}
|
|
785
761
|
else if (!external.has(id)) {
|
|
786
762
|
external.set(id, { line: i + 1, role, allowed });
|
|
@@ -789,11 +765,63 @@ export async function load(env, input, opts = {}) {
|
|
|
789
765
|
}
|
|
790
766
|
return sh;
|
|
791
767
|
}
|
|
768
|
+
function leftToDb(reg, cls) {
|
|
769
|
+
return leavesToDb((reg.find(cls.name) ?? cls).validator());
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Правила, которые валидатор на TypeScript оставляет базе (`pattern`: шаблон арендатора процесс приложения не
|
|
773
|
+
* исполняет — ревью, ReDoS), — запросом к базе на пачку строк классов с ними и до записи: COPY идёт мимо триггеров.
|
|
774
|
+
* Нарушения — в отчёт с номером строки, как у проверки в коде.
|
|
775
|
+
*
|
|
776
|
+
* Цена — на каждую строку (в режиме истории — на каждую версию), поэтому (замер против 1.0.1 — засевка полигона была на 42 %
|
|
777
|
+
* медленнее, 40 % времени — здесь): одинаковые класс и данные проверяются один раз, строки классов читаются один
|
|
778
|
+
* раз на пачку, а полный разбор `validate_data` идёт только у строк, которых не пропустил общий `validator` класса.
|
|
779
|
+
* Правил `extra` в `validator` нет, но сюда доходят строки, прошедшие валидатор на TypeScript, — у него те же правила,
|
|
780
|
+
* кроме `pattern` (равенство — тест `validator-parity`); `pattern` в `validator` есть.
|
|
781
|
+
*/
|
|
782
|
+
async function checkInDb(conn, list) {
|
|
783
|
+
// порядок — по первой строке: первые ISSUES_LIMIT нарушений — те же строки, что без схлопывания
|
|
784
|
+
const uniq = new Map();
|
|
785
|
+
for (const r of [...list].sort((a, b) => a.i - b.i)) {
|
|
786
|
+
const key = `${r.class}\u0000${JSON.stringify(r.data)}`;
|
|
787
|
+
const u = uniq.get(key);
|
|
788
|
+
if (u)
|
|
789
|
+
u.lines.push(r.i);
|
|
790
|
+
else
|
|
791
|
+
uniq.set(key, { class: r.class, data: r.data, lines: [r.i] });
|
|
792
|
+
}
|
|
793
|
+
const all = [...uniq.values()];
|
|
794
|
+
const found = [];
|
|
795
|
+
let capped = false;
|
|
796
|
+
for (let b = 0; b < all.length; b += batch) {
|
|
797
|
+
const part = all.slice(b, b + batch);
|
|
798
|
+
// materialized: без него планировщик возвращает class_row в цикл по строкам
|
|
799
|
+
const bad = await conn.unsafe(`with c as materialized (select n as name, r.validator, r.data->'resolved'->'rules' as rules,
|
|
800
|
+
r.data->'resolved'->'vars' as vars from unnest($3::text[]) n cross join lateral ${S}.class_row(n, $2::uuid) r)
|
|
801
|
+
select k, issues from (select (x.ord - 1)::int as k,
|
|
802
|
+
case when jsonb_path_match(x.v->'data', c.validator, c.vars, true) is true then '[]'::jsonb
|
|
803
|
+
else ${S}.validate_data(c.rules, c.validator, c.vars, x.v->'data') end as issues
|
|
804
|
+
from jsonb_array_elements($1::jsonb) with ordinality x(v, ord) join c on c.name = x.v->>'class') s
|
|
805
|
+
where issues <> '[]'::jsonb order by k limit ${ISSUES_LIMIT}`, [part.map((u) => ({ class: u.class, data: u.data })), tenant, [...new Set(part.map((u) => u.class))]]);
|
|
806
|
+
if (bad.length >= ISSUES_LIMIT)
|
|
807
|
+
capped = true;
|
|
808
|
+
for (const r of bad)
|
|
809
|
+
for (const line of part[r.k].lines)
|
|
810
|
+
found.push({ line, raw: r.issues });
|
|
811
|
+
}
|
|
812
|
+
for (const f of found.sort((a, b) => a.line - b.line)) {
|
|
813
|
+
const { id, cls } = p1[f.line];
|
|
814
|
+
// нарушения базы — с ключом каталога: key и params наружу не идут
|
|
815
|
+
issues.add({ line: f.line + 1, code: 'invalid_data', message: lmsg('write.load_invalid_data', { cls: cls.name }), id, class: cls.name, issues: issuesFromDb(f.raw) });
|
|
816
|
+
}
|
|
817
|
+
if (capped)
|
|
818
|
+
issues.unchecked.add(lmsg('write.load_db_check_limit', { limit: ISSUES_LIMIT }));
|
|
819
|
+
}
|
|
792
820
|
/** Пачка — подготовка и запись по очереди. */
|
|
793
|
-
async function writeBatch(conn, reg, part, inLoad, external, ts, xid, touched,
|
|
794
|
-
const p = await prepareBatch(reg, part, inLoad, external, ts, xid, touched,
|
|
821
|
+
async function writeBatch(conn, reg, part, inLoad, external, ts, xid, touched, live, past) {
|
|
822
|
+
const p = await prepareBatch(reg, part, inLoad, external, ts, xid, touched, live, past, 0);
|
|
795
823
|
if (p)
|
|
796
|
-
await flushBatch(conn, p, ts, touched
|
|
824
|
+
await flushBatch(conn, p, ts, touched);
|
|
797
825
|
}
|
|
798
826
|
/**
|
|
799
827
|
* Первая заливка — конвейером (решение точки А плана 1.1): следующая пачка готовится, пока база пишет текущую.
|
|
@@ -801,18 +829,22 @@ export async function load(env, input, opts = {}) {
|
|
|
801
829
|
* идёт синхронная работа. Отказ записи до её await ловит пустой обработчик (иначе — необработанный отказ
|
|
802
830
|
* процесса); при ошибке подготовки запись в полёте доходит до конца, и откат идёт по готовому соединению.
|
|
803
831
|
*/
|
|
804
|
-
async function pipeline(conn, reg, portion, inLoad, external, ts, xid, touched,
|
|
832
|
+
async function pipeline(conn, reg, portion, inLoad, external, ts, xid, touched, live, past) {
|
|
805
833
|
let flushing = null;
|
|
806
834
|
try {
|
|
807
835
|
for (let b = 0; b < portion.length; b += batch) {
|
|
808
|
-
const p = await prepareBatch(reg, portion.slice(b, b + batch), inLoad, external, ts, xid, touched,
|
|
836
|
+
const p = await prepareBatch(reg, portion.slice(b, b + batch), inLoad, external, ts, xid, touched, live, past, 256);
|
|
809
837
|
const prev = flushing;
|
|
810
838
|
flushing = null;
|
|
811
839
|
if (prev)
|
|
812
840
|
await prev;
|
|
813
|
-
if (!p || issues.any)
|
|
841
|
+
if (!p || issues.any) {
|
|
842
|
+
// нарушение нашла проверка базы прошлой пачки, когда эта была уже готова: её строки база не проверила
|
|
843
|
+
if (p?.inDb.length)
|
|
844
|
+
issues.unchecked.add(lmsg('write.load_db_rules_unchecked'));
|
|
814
845
|
return;
|
|
815
|
-
|
|
846
|
+
}
|
|
847
|
+
flushing = flushBatch(conn, p, ts, touched);
|
|
816
848
|
flushing.catch(() => { });
|
|
817
849
|
}
|
|
818
850
|
const last = flushing;
|
|
@@ -830,11 +862,13 @@ export async function load(env, input, opts = {}) {
|
|
|
830
862
|
* Подготовка пачки без запросов к базе: проверки, id, концы, строки COPY. yieldEvery — уступать циклу событий
|
|
831
863
|
* каждые столько строк (конвейер первой заливки). Нарушения — null.
|
|
832
864
|
*/
|
|
833
|
-
async function prepareBatch(reg, part, inLoad, external, ts, xid, touched,
|
|
865
|
+
async function prepareBatch(reg, part, inLoad, external, ts, xid, touched, live, past, yieldEvery) {
|
|
834
866
|
const ent = [];
|
|
835
867
|
const log = [];
|
|
836
868
|
const updates = [];
|
|
837
869
|
const rowsAcl = [];
|
|
870
|
+
const inDb = [];
|
|
871
|
+
let dbLeft = false;
|
|
838
872
|
let n = 0;
|
|
839
873
|
for (const i of part) {
|
|
840
874
|
if (yieldEvery && ++n % yieldEvery === 0)
|
|
@@ -845,9 +879,22 @@ export async function load(env, input, opts = {}) {
|
|
|
845
879
|
continue;
|
|
846
880
|
}
|
|
847
881
|
const sh = check(reg, i, inLoad, external, true);
|
|
882
|
+
if (sh && leftToDb(reg, cls)) {
|
|
883
|
+
if (issues.any)
|
|
884
|
+
dbLeft = true;
|
|
885
|
+
else
|
|
886
|
+
inDb.push({ i, class: cls.name, data: sh.data });
|
|
887
|
+
}
|
|
848
888
|
if (!sh || issues.any)
|
|
849
889
|
continue;
|
|
850
890
|
if (live.has(id)) {
|
|
891
|
+
// строка с тем же id, но другого класса (класс строки сменили мимо триггеров): правка
|
|
892
|
+
// прошла бы мимо триггеров с данными, проверенными по чужой схеме, — exists (план 2.0, 7.2 №4)
|
|
893
|
+
const was = live.get(id);
|
|
894
|
+
if (was !== cls.name) {
|
|
895
|
+
issues.add({ line: i + 1, code: 'exists', message: lmsg('write.load_other_class', { was, cls: cls.name }), id, class: cls.name });
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
851
898
|
updates.push({ id, data: sh.data, links: sh.links, tags: sh.tags ?? null, line: i + 1 });
|
|
852
899
|
continue;
|
|
853
900
|
}
|
|
@@ -863,25 +910,27 @@ export async function load(env, input, opts = {}) {
|
|
|
863
910
|
rowsAcl.push({ i, class: cls.name, id, owner, tags, data: sh.data, links: sh.links });
|
|
864
911
|
// общая голова строк entity и log — одной сборкой и одним экранированием
|
|
865
912
|
const head = [id, rev, cls.name, cur.rev, tenant, owner, JSON.stringify(sh.links), JSON.stringify(sh.data), pgArray(tags), atv].map(esc).join('\t');
|
|
866
|
-
|
|
867
|
-
ent.push(`${head}\t${esc(atv)}\t${esc(author)}\t${esc(agent)}\tcreate\t\\N\t\\N\t${ph}\t\\N\n`);
|
|
913
|
+
ent.push(`${head}\t${esc(atv)}\t${esc(author)}\t${esc(agent)}\tcreate\t\\N\t\\N\n`);
|
|
868
914
|
if (cur.history !== false)
|
|
869
|
-
log.push(`${head}\t${esc(author)}\t${esc(agent)}\tcreate\t\\N\t
|
|
915
|
+
log.push(`${head}\t${esc(author)}\t${esc(agent)}\tcreate\t\\N\t${esc(xid)}\n`);
|
|
870
916
|
report.loaded++;
|
|
871
917
|
report.byClass[cls.name] = (report.byClass[cls.name] ?? 0) + 1;
|
|
872
918
|
touched.add(cls.name);
|
|
873
|
-
written.push(id);
|
|
874
919
|
}
|
|
875
920
|
if (issues.any) {
|
|
876
921
|
if (aclRan)
|
|
877
|
-
issues.unchecked.add('
|
|
922
|
+
issues.unchecked.add(lmsg('write.load_later_batches'));
|
|
923
|
+
if (dbLeft || inDb.length)
|
|
924
|
+
issues.unchecked.add(lmsg('write.load_db_rules_unchecked'));
|
|
878
925
|
return null;
|
|
879
926
|
}
|
|
880
|
-
return { ent, log, updates, rowsAcl };
|
|
927
|
+
return { ent, log, updates, rowsAcl, inDb };
|
|
881
928
|
}
|
|
882
929
|
/** Запись подготовленной пачки: условия ACL ({ as }) — запросом на пачку, COPY в entity и log, правки существующих. */
|
|
883
|
-
async function flushBatch(conn, p, ts, touched
|
|
884
|
-
const { ent, log, updates, rowsAcl } = p;
|
|
930
|
+
async function flushBatch(conn, p, ts, touched) {
|
|
931
|
+
const { ent, log, updates, rowsAcl, inDb } = p;
|
|
932
|
+
if (inDb.length)
|
|
933
|
+
await checkInDb(conn, inDb);
|
|
885
934
|
if (acl) {
|
|
886
935
|
aclRan = true;
|
|
887
936
|
// условия ACL на строки — одним запросом на пачку: файл пользователя — не доверенные данные
|
|
@@ -889,7 +938,7 @@ export async function load(env, input, opts = {}) {
|
|
|
889
938
|
where not ${S}.acl_row_ok($2::jsonb, x->>'class', (x->>'id')::uuid, (x->>'owner')::uuid, $3::uuid,
|
|
890
939
|
array(select jsonb_array_elements_text(x->'tags')), x->'data', x->'links')`, [rowsAcl, acl.write, tenant]);
|
|
891
940
|
for (const b of bad)
|
|
892
|
-
issues.add({ line: b.i + 1, code: 'acl_denied', message: '
|
|
941
|
+
issues.add({ line: b.i + 1, code: 'acl_denied', message: lmsg('write.load_acl_row'), id: p1[b.i].id });
|
|
893
942
|
if (updates.length) {
|
|
894
943
|
// existing: 'update' — условие на прежние значения и на новые (как политика правки)
|
|
895
944
|
const ids = updates.map((u) => u.id);
|
|
@@ -900,42 +949,39 @@ export async function load(env, input, opts = {}) {
|
|
|
900
949
|
where not ${S}.acl_row_ok($2::jsonb, e.class, e.id, e.owner, e.tenant, coalesce(u.tags, e.tags), u.data, u.links)`, [updates.map((u) => ({ id: u.id, data: u.data, links: u.links, tags: u.tags })), acl.write]);
|
|
901
950
|
const lineOfId = new Map(updates.map((u) => [u.id, u.line]));
|
|
902
951
|
for (const r of [...old, ...fresh])
|
|
903
|
-
issues.add({ line: lineOfId.get(r.id) ?? 0, code: 'acl_denied', message: '
|
|
952
|
+
issues.add({ line: lineOfId.get(r.id) ?? 0, code: 'acl_denied', message: lmsg('write.load_acl_update'), id: r.id });
|
|
904
953
|
}
|
|
905
954
|
if (issues.any)
|
|
906
955
|
return;
|
|
907
956
|
}
|
|
908
|
-
|
|
909
|
-
|
|
957
|
+
if (issues.any)
|
|
958
|
+
return;
|
|
959
|
+
// первая заливка: FREEZE — таблицы опустошены этой же транзакцией
|
|
910
960
|
await copyLines(conn, `copy ${S}.log (${LOG_COLS}) from stdin${ini?.freeze ? ' with (freeze)' : ''}`, log);
|
|
911
961
|
await copyLines(conn, `copy ${S}.entity (${ENTITY_COLS}) from stdin${ini?.freeze ? ' with (freeze)' : ''}`, ent);
|
|
912
962
|
if (updates.length) {
|
|
913
|
-
// новая версия из заблокированных значений — одним оператором; без изменений — без
|
|
914
|
-
|
|
915
|
-
const res = await conn.unsafe(`with u as (select x.id, ${S}.agg_keep(e.class, e.tenant, e.data, x.data) as data, x.links, x.tags
|
|
916
|
-
from jsonb_to_recordset($1::jsonb) as x(id uuid, data jsonb, links jsonb, tags text[]) join ${S}.entity e on e.id = x.id),
|
|
963
|
+
// новая версия из заблокированных значений — одним оператором; без изменений — без версии
|
|
964
|
+
const res = await conn.unsafe(`with u as (select * from jsonb_to_recordset($1::jsonb) as x(id uuid, data jsonb, links jsonb, tags text[])),
|
|
917
965
|
up as (
|
|
918
|
-
update ${S}.entity e set data = u.data, links = u.links, tags = coalesce(u.tags, e.tags), rev = e.rev + 1,
|
|
966
|
+
update ${S}.entity e set data = u.data, links = u.links, tags = coalesce(u.tags, e.tags), rev = e.rev + 1,
|
|
919
967
|
at = greatest($2::timestamptz, e.at + interval '1 microsecond'), author = $3::uuid, agent = $4::text, op = 'update',
|
|
920
|
-
reason = null,
|
|
968
|
+
reason = null, cv = coalesce((${S}.class_row(e.class, e.tenant)).rev, e.cv)
|
|
921
969
|
from u where e.id = u.id and (e.data is distinct from u.data or e.links is distinct from u.links
|
|
922
970
|
or e.tags is distinct from coalesce(u.tags, e.tags))
|
|
923
971
|
returning e.*),
|
|
924
972
|
lg as (
|
|
925
|
-
insert into ${S}.log (id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason
|
|
926
|
-
select id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason
|
|
973
|
+
insert into ${S}.log (id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason)
|
|
974
|
+
select id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason from up
|
|
927
975
|
where ${S}.history_on(up.class, up.tenant) order by up.id)
|
|
928
976
|
select up.id, up.class from up`, [updates.map((u) => ({ id: u.id, data: u.data, links: u.links, tags: u.tags })), ts, author, agent]);
|
|
929
977
|
report.updated += res.length;
|
|
930
978
|
report.skipped += updates.length - res.length;
|
|
931
|
-
for (const r of res)
|
|
979
|
+
for (const r of res)
|
|
932
980
|
touched.add(r.class);
|
|
933
|
-
written.push(r.id);
|
|
934
|
-
}
|
|
935
981
|
}
|
|
936
982
|
}
|
|
937
|
-
/** Режим истории: версии
|
|
938
|
-
async function historyWrite(conn, reg, portion, inLoad, external, xid, touched
|
|
983
|
+
/** Режим истории: версии в журнал — волнами по номеру (первые версии всех объектов, затем вторые…), вставками по 5000. */
|
|
984
|
+
async function historyWrite(conn, reg, portion, inLoad, external, xid, touched) {
|
|
939
985
|
const byId = new Map();
|
|
940
986
|
for (const i of portion) {
|
|
941
987
|
const id = p1[i].id;
|
|
@@ -948,7 +994,7 @@ export async function load(env, input, opts = {}) {
|
|
|
948
994
|
for (let k = 0; k < l.length; k++) {
|
|
949
995
|
const r = rows[l[k]];
|
|
950
996
|
if (Number(r.$rev) !== k + 1 || typeof r.$at !== 'string') {
|
|
951
|
-
issues.add({ line: l[k] + 1, code: 'invalid_data', message: '
|
|
997
|
+
issues.add({ line: l[k] + 1, code: 'invalid_data', message: lmsg('write.load_history_versions'), id: p1[l[k]].id });
|
|
952
998
|
}
|
|
953
999
|
}
|
|
954
1000
|
}
|
|
@@ -959,16 +1005,31 @@ export async function load(env, input, opts = {}) {
|
|
|
959
1005
|
if (sh)
|
|
960
1006
|
shaped.set(i, sh);
|
|
961
1007
|
});
|
|
1008
|
+
// правила, которые проверяет только база (pattern), — у всех версий, до записи
|
|
1009
|
+
const inDb = [];
|
|
1010
|
+
for (const [i, sh] of shaped)
|
|
1011
|
+
if (leftToDb(reg, p1[i].cls))
|
|
1012
|
+
inDb.push({ i, class: p1[i].cls.name, data: sh.data });
|
|
1013
|
+
if (issues.any) {
|
|
1014
|
+
if (inDb.length)
|
|
1015
|
+
issues.unchecked.add(lmsg('write.load_db_rules_unchecked'));
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
await checkInDb(conn, inDb);
|
|
962
1019
|
if (issues.any)
|
|
963
1020
|
return;
|
|
964
1021
|
// без разворота в аргументы: на сотнях тысяч объектов он переполняет стек
|
|
965
1022
|
const waves = lists.reduce((m, l) => Math.max(m, l.length), 0);
|
|
966
|
-
const
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
1023
|
+
const items = [];
|
|
1024
|
+
const flush = async () => {
|
|
1025
|
+
if (!items.length)
|
|
1026
|
+
return;
|
|
1027
|
+
await conn.unsafe(`insert into ${S}.log (${LOG_COLS})
|
|
1028
|
+
select x.id, x.rev, x.class, x.cv, x.tenant, x.owner, x.links, x.data, x.tags, x.at, x.author, x.agent, x.op, null, x.tx::xid8
|
|
1029
|
+
from jsonb_to_recordset($1::jsonb) as x(id uuid, rev int, class text, cv int, tenant uuid, owner uuid, tags text[], links jsonb,
|
|
1030
|
+
data jsonb, at timestamptz, author uuid, agent text, op text, tx text)`, [items.splice(0)]);
|
|
1031
|
+
};
|
|
970
1032
|
for (let w = 0; w < waves; w++) {
|
|
971
|
-
const items = [];
|
|
972
1033
|
for (const l of lists) {
|
|
973
1034
|
const i = l[w];
|
|
974
1035
|
if (i === undefined)
|
|
@@ -980,25 +1041,14 @@ export async function load(env, input, opts = {}) {
|
|
|
980
1041
|
const r = rows[i];
|
|
981
1042
|
const sh = shaped.get(i);
|
|
982
1043
|
const op = typeof r.$op === 'string' ? r.$op : w === 0 ? 'create' : 'update';
|
|
983
|
-
const ph = prevHash.get(id) ?? null;
|
|
984
|
-
if (w === l.length - 1)
|
|
985
|
-
lastPrev.set(id, ph);
|
|
986
1044
|
items.push({ id, rev: w + 1, class: cls.name, cv: cur.rev, tenant, owner: sh.owner ?? tenant, tags: sh.tags ?? [], links: sh.links,
|
|
987
|
-
data: sh.data, at: r.$at, author: SYSTEM_ID, agent, op,
|
|
1045
|
+
data: sh.data, at: r.$at, author: SYSTEM_ID, agent, op, tx: xid });
|
|
988
1046
|
touched.add(cls.name);
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
const res = await conn.unsafe(`insert into ${S}.log (${LOG_COLS})
|
|
992
|
-
select x.id, x.rev, x.class, x.cv, x.tenant, x.owner, x.links, x.data, x.tags, x.at, x.author, x.agent, x.op, null, null,
|
|
993
|
-
decode(x.ph, 'hex'), x.tx::xid8
|
|
994
|
-
from jsonb_to_recordset($1::jsonb) as x(id uuid, rev int, class text, cv int, tenant uuid, owner uuid, tags text[], links jsonb,
|
|
995
|
-
data jsonb, at timestamptz, author uuid, agent text, op text, ph text, tx text)
|
|
996
|
-
returning id, hash`, [items.slice(k, k + 5000)]);
|
|
997
|
-
if (w < waves - 1)
|
|
998
|
-
for (const h of res)
|
|
999
|
-
prevHash.set(h.id, h.hash);
|
|
1047
|
+
if (items.length >= 5000)
|
|
1048
|
+
await flush();
|
|
1000
1049
|
}
|
|
1001
1050
|
}
|
|
1051
|
+
await flush();
|
|
1002
1052
|
// текущее состояние — последняя версия, если она не надгробие; created_at — время первой версии
|
|
1003
1053
|
const ent = [];
|
|
1004
1054
|
for (const l of lists) {
|
|
@@ -1011,11 +1061,9 @@ export async function load(env, input, opts = {}) {
|
|
|
1011
1061
|
const sh = shaped.get(last);
|
|
1012
1062
|
const op = typeof r.$op === 'string' ? r.$op : l.length === 1 ? 'create' : 'update';
|
|
1013
1063
|
ent.push([id, l.length, cls.name, cur.rev, tenant, sh.owner ?? tenant, JSON.stringify(sh.links), JSON.stringify(sh.data), pgArray(sh.tags ?? []), r.$at,
|
|
1014
|
-
rows[l[0]].$at, SYSTEM_ID, agent, op, null, null
|
|
1064
|
+
rows[l[0]].$at, SYSTEM_ID, agent, op, null, null].map(esc).join('\t') + '\n');
|
|
1015
1065
|
report.loaded++;
|
|
1016
1066
|
report.byClass[cls.name] = (report.byClass[cls.name] ?? 0) + 1;
|
|
1017
|
-
// verify: true проверяет и строки режима истории (в 1.0.1 список был пуст — проверка не шла)
|
|
1018
|
-
written.push(id);
|
|
1019
1067
|
}
|
|
1020
1068
|
await copyLines(conn, `copy ${S}.entity (${ENTITY_COLS}) from stdin${ini?.freeze ? ' with (freeze)' : ''}`, ent);
|
|
1021
1069
|
}
|
|
@@ -1051,7 +1099,7 @@ async function catalogOf(conn, S) {
|
|
|
1051
1099
|
}
|
|
1052
1100
|
/**
|
|
1053
1101
|
* Первая заливка (план 1.1, §2.3): загрузки fn — одной транзакцией в пустую схему. Блокировка таблиц до
|
|
1054
|
-
* проверки; в схеме только строки системных классов
|
|
1102
|
+
* проверки; в схеме только строки системных классов (иначе not_empty); они (с seq и tx
|
|
1055
1103
|
* журнала) и определения ограничений, индексов и триггеров из каталога (с режимом включения tgenabled:
|
|
1056
1104
|
* pg_get_triggerdef его не передаёт) сохраняются в памяти; внешний ключ secret.account снимается до TRUNCATE и
|
|
1057
1105
|
* возвращается после первичного ключа entity; TRUNCATE; триггеры, ограничения и индексы
|
|
@@ -1063,22 +1111,23 @@ async function catalogOf(conn, S) {
|
|
|
1063
1111
|
*/
|
|
1064
1112
|
export async function initialSession(env, fn, tune = {}) {
|
|
1065
1113
|
if (!env.admin)
|
|
1066
|
-
throw
|
|
1114
|
+
throw lerr('write.load_admin_required');
|
|
1067
1115
|
const S = `"${env.schema.replace(/"/g, '""')}"`;
|
|
1068
1116
|
const ident = (n) => `"${n.replace(/"/g, '""')}"`;
|
|
1069
1117
|
const conn = await env.sql.reserve();
|
|
1070
1118
|
const q = (text, params = []) => conn.unsafe(text, params);
|
|
1119
|
+
let initialFailure = null;
|
|
1071
1120
|
try {
|
|
1072
1121
|
await q('begin isolation level read committed');
|
|
1073
1122
|
await q(`set local role ${ident(env.ownerRole)}`);
|
|
1074
1123
|
await q(`select set_config('TimeZone', 'UTC', true)`);
|
|
1075
1124
|
// 1. блокировка — до проверки: строка, вставленная между проверкой и TRUNCATE, пропала бы молча
|
|
1076
1125
|
await q(`lock table ${S}.entity, ${S}.log in access exclusive mode`);
|
|
1077
|
-
// 2. пустая схема: кроме строк системных классов
|
|
1126
|
+
// 2. пустая схема: кроме строк системных классов — ничего
|
|
1078
1127
|
const [busy] = await q(`select exists (select 1 from ${S}.entity where class <> all ($1::text[]))
|
|
1079
|
-
or exists (select 1 from ${S}.log where class <> all ($
|
|
1128
|
+
or exists (select 1 from ${S}.log where class <> all ($1::text[])) as b`, [SYSTEM_CLASSES]);
|
|
1080
1129
|
if (busy.b) {
|
|
1081
|
-
throw
|
|
1130
|
+
throw lerr('write.load_not_empty');
|
|
1082
1131
|
}
|
|
1083
1132
|
// 3. системные строки и определения из каталога — в память процесса (так режим не повторяет 10-core.sql)
|
|
1084
1133
|
const [sys] = await q(`select coalesce((select jsonb_agg(to_jsonb(e)) from ${S}.entity e), '[]') as ent,
|
|
@@ -1104,15 +1153,8 @@ export async function initialSession(env, fn, tune = {}) {
|
|
|
1104
1153
|
await q(`insert into ${S}.entity (${cat.entityCols}) select ${cat.entityCols} from jsonb_populate_recordset(null::${S}.entity, $1::jsonb)`, [sys.ent]);
|
|
1105
1154
|
await q(`insert into ${S}.log (${cat.logCols}) overriding system value select ${cat.logCols} from jsonb_populate_recordset(null::${S}.log, $1::jsonb)`, [sys.log]);
|
|
1106
1155
|
const state = { conn: conn, ids, pending: [], touched: new Map(), freeze: tune.freeze !== false };
|
|
1107
|
-
const out = await fn({
|
|
1108
|
-
|
|
1109
|
-
load: (input, opts = {}) => load({ ...env, session: state }, input, opts),
|
|
1110
|
-
taken: (id) => state.ids.has(id.toLowerCase()),
|
|
1111
|
-
});
|
|
1112
|
-
// 6. агрегаты в базе (план 1.1, §2.4): триггеров нет — поле целей считает agg_fill, одним оператором на агрегат, и
|
|
1113
|
-
// пишет его в ту же (последнюю) версию в entity и log: строки ещё никто не видел, хэши — генерируемые колонки
|
|
1114
|
-
await q(`select ${S}.agg_fill()`);
|
|
1115
|
-
// 7. ограничения, затем индексы; память и параллельные исполнители сборки — не меньше серверных
|
|
1156
|
+
const out = await fn({ load: (input, opts = {}) => load({ ...env, session: state }, input, opts) });
|
|
1157
|
+
// 6. ограничения, затем индексы; память и параллельные исполнители сборки — не меньше серверных
|
|
1116
1158
|
await q(`select set_config('maintenance_work_mem', (greatest(pg_size_bytes(current_setting('maintenance_work_mem')), 268435456) / 1024)::bigint || 'kB', true),
|
|
1117
1159
|
set_config('max_parallel_maintenance_workers', greatest(current_setting('max_parallel_maintenance_workers')::int,
|
|
1118
1160
|
least(current_setting('max_parallel_workers')::int, 4))::text, true)`);
|
|
@@ -1124,14 +1166,14 @@ export async function initialSession(env, fn, tune = {}) {
|
|
|
1124
1166
|
await q(`alter table ${f.tbl} add constraint ${ident(f.name)} ${f.def}`);
|
|
1125
1167
|
for (const i of indexes)
|
|
1126
1168
|
await q(i.def);
|
|
1127
|
-
//
|
|
1169
|
+
// 7. триггеры — с прежним режимом включения (ENABLE ALWAYS работает и при session_replication_role = replica)
|
|
1128
1170
|
for (const t of triggers) {
|
|
1129
1171
|
await q(t.def);
|
|
1130
1172
|
const mode = t.enabled === 'A' ? 'enable always' : t.enabled === 'R' ? 'enable replica' : t.enabled === 'D' ? 'disable' : null;
|
|
1131
1173
|
if (mode)
|
|
1132
1174
|
await q(`alter table ${t.tbl} ${mode} trigger ${ident(t.name)}`);
|
|
1133
1175
|
}
|
|
1134
|
-
//
|
|
1176
|
+
// 8. отложенные проверки загрузок, сигнал, счётчик прав
|
|
1135
1177
|
const issues = new Issues();
|
|
1136
1178
|
for (const p of state.pending) {
|
|
1137
1179
|
const ext = [...p.external.keys()].sort();
|
|
@@ -1142,10 +1184,7 @@ export async function initialSession(env, fn, tune = {}) {
|
|
|
1142
1184
|
const ref = p.external.get(id);
|
|
1143
1185
|
const f = found.get(id);
|
|
1144
1186
|
if (!f || (f.tenant !== p.tenant && f.tenant !== SYSTEM_ID) || !accepts(p.reg, ref.allowed, f.class)) {
|
|
1145
|
-
issues.add({ line: ref.line, code: 'target_not_found', message:
|
|
1146
|
-
}
|
|
1147
|
-
else if (p.reg.find(f.class)?.derivedBy) {
|
|
1148
|
-
issues.add({ line: ref.line, code: 'derived_class', message: `цель конца ${ref.role} — строка производного класса ${f.class}`, id });
|
|
1187
|
+
issues.add({ line: ref.line, code: 'target_not_found', message: lmsg('write.load_target_not_found', { role: ref.role }), id });
|
|
1149
1188
|
}
|
|
1150
1189
|
}
|
|
1151
1190
|
}
|
|
@@ -1154,13 +1193,11 @@ export async function initialSession(env, fn, tune = {}) {
|
|
|
1154
1193
|
const found = new Set((await q(`select e.id from ${S}.entity e where e.id = any ($1::uuid[]) and e.class = 'Account'`, [own])).map((r) => r.id));
|
|
1155
1194
|
for (const id of own)
|
|
1156
1195
|
if (!found.has(id))
|
|
1157
|
-
issues.add({ line: p.owners.get(id), code: 'target_not_found', message:
|
|
1196
|
+
issues.add({ line: p.owners.get(id), code: 'target_not_found', message: lmsg('write.load_owner_not_found', { id }), id });
|
|
1158
1197
|
}
|
|
1159
1198
|
const [acc] = await q(`select coalesce((data->>'enabled')::boolean, true) as enabled from ${S}.entity where id = $1::uuid and class = 'Account'`, [p.tenant]);
|
|
1160
1199
|
if (!acc || !acc.enabled)
|
|
1161
|
-
issues.add({ line: 0, code: 'tenant_denied', message:
|
|
1162
|
-
if (p.verify?.length)
|
|
1163
|
-
await verifyIds(conn, S, p.verify, issues);
|
|
1200
|
+
issues.add({ line: 0, code: 'tenant_denied', message: lmsg('write.load_tenant_denied', { tenant: p.tenant }) });
|
|
1164
1201
|
}
|
|
1165
1202
|
if (issues.any)
|
|
1166
1203
|
issues.fail();
|
|
@@ -1177,10 +1214,12 @@ export async function initialSession(env, fn, tune = {}) {
|
|
|
1177
1214
|
return out;
|
|
1178
1215
|
}
|
|
1179
1216
|
catch (e) {
|
|
1180
|
-
|
|
1217
|
+
initialFailure = e;
|
|
1218
|
+
if (!isConnectionError(e))
|
|
1219
|
+
await q('rollback').catch(() => { });
|
|
1181
1220
|
throw e instanceof LetopisError ? e : fromDbError(e);
|
|
1182
1221
|
}
|
|
1183
1222
|
finally {
|
|
1184
|
-
|
|
1223
|
+
release(conn, initialFailure);
|
|
1185
1224
|
}
|
|
1186
1225
|
}
|