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