letopis 1.0.1 → 1.2.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.en.md +117 -10
- package/AGENT-CHEATSHEET.md +110 -8
- package/CHANGELOG.md +739 -0
- package/README.en.md +873 -68
- package/README.md +830 -66
- package/dist/admin.d.ts +24 -4
- package/dist/admin.js +39 -9
- package/dist/cache.d.ts +33 -9
- package/dist/cache.js +81 -32
- package/dist/chain.d.ts +29 -7
- package/dist/chain.js +61 -12
- package/dist/cli.js +59 -7
- package/dist/errors.js +4 -2
- package/dist/import.d.ts +6 -0
- package/dist/import.js +196 -175
- package/dist/index.d.ts +27 -5
- package/dist/index.js +191 -29
- package/dist/internal.d.ts +16 -0
- package/dist/internal.js +14 -0
- package/dist/load.d.ts +63 -0
- package/dist/load.js +331 -44
- package/dist/mcp.d.ts +28 -0
- package/dist/mcp.js +810 -0
- package/dist/model.d.ts +34 -0
- package/dist/model.js +17 -0
- package/dist/project.d.ts +309 -0
- package/dist/project.js +1097 -0
- package/dist/registry.d.ts +17 -0
- package/dist/registry.js +13 -0
- package/dist/signals.d.ts +73 -0
- package/dist/signals.js +169 -0
- package/dist/sql.d.ts +1 -1
- package/dist/sql.js +26 -7
- package/dist/tx.d.ts +9 -7
- package/dist/tx.js +15 -5
- package/dist/typed.d.ts +21 -6
- package/dist/types.d.ts +77 -4
- package/dist/types.js +8 -3
- package/dist/up.d.ts +16 -2
- package/dist/up.js +34 -4
- package/dist/uuid.js +19 -8
- package/dist/watch.d.ts +23 -2
- package/dist/watch.js +44 -7
- package/dist/write.d.ts +11 -2
- package/dist/write.js +57 -10
- package/llms.txt +9 -2
- package/package.json +13 -2
- package/sql/10-core.sql +13 -2
- package/sql/15-errors.sql +4 -1
- package/sql/40-class.sql +57 -2
- package/sql/60-write.sql +611 -82
- package/sql/70-read.sql +33 -8
- package/sql/75-history.sql +358 -0
- package/sql/80-auth.sql +7 -7
- package/sql/88-derived.sql +1154 -0
- package/sql/90-time.sql +134 -21
- package/sql/95-seed.system.sql +24 -6
- package/sql/99-revision.sql +1 -1
- package/sql/README.md +5 -1
- package/sql/migrate/0006-changed-paths.sql +3 -0
package/dist/index.js
CHANGED
|
@@ -11,6 +11,8 @@ import postgres from 'postgres';
|
|
|
11
11
|
import { LetopisError, fromDbError, removed } from './errors.js';
|
|
12
12
|
import { Registry } from './registry.js';
|
|
13
13
|
import { AclPasses, ExplicitTx, Notifier, poolExecutor, runTx } from './tx.js';
|
|
14
|
+
import { checkReason } from './write.js';
|
|
15
|
+
import { registerDb } from './internal.js';
|
|
14
16
|
import { makeAuth } from './auth.js';
|
|
15
17
|
import { makeAcl } from './acl.js';
|
|
16
18
|
import { idOf as idOfClass } from './uuid.js';
|
|
@@ -19,7 +21,9 @@ import { makeBatch, startEntity, startStep } from './chain.js';
|
|
|
19
21
|
import { DDL_REVISION } from './types.js';
|
|
20
22
|
import { ACL_CLASSES, ResultCache } from './cache.js';
|
|
21
23
|
import { Subscription } from './watch.js';
|
|
22
|
-
import {
|
|
24
|
+
import { Projector } from './project.js';
|
|
25
|
+
import { SIGNAL_MODES, Signals, bucket, throttle } from './signals.js';
|
|
26
|
+
import { describe as describeModel, maintain as runMaintain, rebuildAggregates as runRebuildAggregates, rekeyClass as runRekeyClass, reset as runReset, scheduleMaintain, trimHistory as runTrim, verify as runVerify, } from './admin.js';
|
|
23
27
|
export { DDL_REVISION } from './types.js';
|
|
24
28
|
export { uuidv5, v5Name, v5Id, idOf } from './uuid.js';
|
|
25
29
|
export { LetopisError, ValidationError, fromDbError } from './errors.js';
|
|
@@ -64,6 +68,7 @@ function makeDb(sh, scope, exec, owner, tx) {
|
|
|
64
68
|
const txc = scope.txc;
|
|
65
69
|
const chainCtx = {
|
|
66
70
|
reg: scope.registry(), schema: sh.schema, exec, tenant: scope.tenant, owner, onQuery: sh.onQuery, slowMs: sh.slowMs,
|
|
71
|
+
reason: txc.reason ?? '', txReason: txc.reason ?? '',
|
|
67
72
|
...(sh.cache ? { cache: { store: sh.cache, who: () => ({ token: txc.token, account: txc.account, tenant: scope.tenant() }) } } : {}),
|
|
68
73
|
};
|
|
69
74
|
// реестр корня меняется после auth.switch — цепочки берут действующий
|
|
@@ -116,7 +121,10 @@ function makeDb(sh, scope, exec, owner, tx) {
|
|
|
116
121
|
const classes = o.classes ? [...new Set(o.classes.flatMap((c) => reg.family(c).map((x) => x.name)))] : null;
|
|
117
122
|
return new Subscription({
|
|
118
123
|
S: sh.S, exec: poolExecutor(txc), classes,
|
|
119
|
-
...(sh.listen ? {
|
|
124
|
+
...(sh.listen ? {
|
|
125
|
+
onSignal: (fn) => { sh.signals.add(fn); return () => void sh.signals.delete(fn); },
|
|
126
|
+
tenant: () => (sh.allTenants ? null : sh.sig.tenantToken(scope.tenant())),
|
|
127
|
+
} : {}),
|
|
120
128
|
}, o);
|
|
121
129
|
},
|
|
122
130
|
cache: {
|
|
@@ -141,29 +149,48 @@ function makeDb(sh, scope, exec, owner, tx) {
|
|
|
141
149
|
const registry = await sh.registryFor(tenant, scoped);
|
|
142
150
|
return makeDb(sh, { txc: scoped, tenant: () => tenant, registry: () => registry, root: false }, poolExecutor(scoped), nextOwner);
|
|
143
151
|
},
|
|
144
|
-
begin: async () => {
|
|
152
|
+
begin: async (o) => {
|
|
145
153
|
if (tx)
|
|
146
154
|
throw new LetopisError('invalid_query', 'вложенных транзакций нет: db.begin() внутри db.begin()');
|
|
147
|
-
|
|
148
|
-
|
|
155
|
+
// причина блока ставится в начале транзакции (setupSql) и действует на все её операции
|
|
156
|
+
const btx = o?.reason !== undefined ? { ...txc, reason: checkReason(o.reason, 'db.begin()') } : txc;
|
|
157
|
+
const t = await ExplicitTx.open(btx);
|
|
158
|
+
return makeDb(sh, btx === txc ? scope : { ...scope, txc: btx }, t, owner, t);
|
|
149
159
|
},
|
|
150
160
|
load: async (input, o) => {
|
|
151
161
|
if (tx)
|
|
152
162
|
throw new LetopisError('invalid_query', 'load() — своей транзакцией, не внутри db.begin()');
|
|
153
163
|
return load({ sql: sh.sql, schema: sh.schema, tenant: scope.tenant(), admin: sh.admin, ownerRole: sh.ownerRole }, input, o);
|
|
154
164
|
},
|
|
155
|
-
|
|
165
|
+
projector: (name, o) => {
|
|
166
|
+
if (tx)
|
|
167
|
+
throw new LetopisError('invalid_query', 'projector() — от корня подключения, а не внутри db.begin()');
|
|
168
|
+
if (!sh.admin)
|
|
169
|
+
throw new LetopisError('admin_required', 'проектору нужно администраторское подключение (connect({ allowBypassRls: true }) роли, которая может стать владельцем схемы)');
|
|
170
|
+
return new Projector({
|
|
171
|
+
sql: sh.sql, schema: sh.schema, S: sh.S, ownerRole: sh.ownerRole,
|
|
172
|
+
...(sh.listen ? { onSignal: (fn) => { sh.signals.add(fn); return () => void sh.signals.delete(fn); } } : {}),
|
|
173
|
+
}, name, o);
|
|
174
|
+
},
|
|
175
|
+
rebuildAggregates: async (cls) => {
|
|
176
|
+
if (tx)
|
|
177
|
+
throw new LetopisError('invalid_query', 'rebuildAggregates() — своей транзакцией, не внутри db.begin()');
|
|
178
|
+
if (!sh.admin)
|
|
179
|
+
throw new LetopisError('admin_required', 'ремонту агрегатов нужно администраторское подключение (connect({ allowBypassRls: true }) роли, которая может стать владельцем схемы)');
|
|
180
|
+
return runRebuildAggregates({ sql: sh.sql, S: sh.S, ownerRole: sh.ownerRole }, cls);
|
|
181
|
+
},
|
|
182
|
+
batch: (name, o) => {
|
|
156
183
|
let q = batches.get(name);
|
|
157
184
|
if (!q)
|
|
158
185
|
batches.set(name, (q = []));
|
|
159
|
-
return makeBatch(chainCtx, q);
|
|
186
|
+
return makeBatch(o?.reason !== undefined ? { ...chainCtx, reason: checkReason(o.reason, 'db.batch()') } : chainCtx, q);
|
|
160
187
|
},
|
|
161
188
|
lock: async (...keys) => (tx ? tx.lock(...keys) : noTx('lock()')),
|
|
162
189
|
commit: async (tr) => (tr ? tr.commit() : tx ? tx.commit() : noTx('commit()')),
|
|
163
190
|
rollback: async (tr) => (tr ? tr.rollback() : tx ? tx.rollback() : noTx('rollback()')),
|
|
164
191
|
close: () => (tx ? tx.rollback() : sh.close()),
|
|
165
192
|
};
|
|
166
|
-
|
|
193
|
+
const facade = new Proxy(core, {
|
|
167
194
|
get(t, prop, r) {
|
|
168
195
|
if (typeof prop === 'symbol' || prop in t)
|
|
169
196
|
return Reflect.get(t, prop, r);
|
|
@@ -183,6 +210,9 @@ function makeDb(sh, scope, exec, owner, tx) {
|
|
|
183
210
|
return startStep(chainCtx, prop);
|
|
184
211
|
},
|
|
185
212
|
});
|
|
213
|
+
// модули пакета (letopis/mcp) узнают обход RLS по самому подключению, а не со слов вызывающего (internal.ts)
|
|
214
|
+
registerDb(facade, { allTenants: sh.allTenants, explicit: Boolean(tx) });
|
|
215
|
+
return facade;
|
|
186
216
|
}
|
|
187
217
|
export async function connect(opts) {
|
|
188
218
|
if (!SCHEMA_NAME.test(opts.schema))
|
|
@@ -227,10 +257,20 @@ export async function connect(opts) {
|
|
|
227
257
|
throw new LetopisError('invalid_data', `connect: схемы ${opts.schema} нет — установите её: up({ schema })`);
|
|
228
258
|
if (rev < DDL_REVISION)
|
|
229
259
|
console.warn(`letopis: движок схемы ${opts.schema} ревизии ${rev}, библиотека ждёт ${DDL_REVISION} — обновите: up({ schema: '${opts.schema}', upgrade: true })`);
|
|
260
|
+
// режим сигналов установки (план 1.1, §2.2); схема, накатанная до него (нет signal_mode()), — class
|
|
261
|
+
const [sm] = await sql.unsafe(`select ${S}.signal_mode() as m`).catch((e) => {
|
|
262
|
+
if (e.code === '42883')
|
|
263
|
+
return [{ m: 'class' }];
|
|
264
|
+
throw fromDbError(e);
|
|
265
|
+
});
|
|
266
|
+
const mode = SIGNAL_MODES.includes(sm?.m) ? sm.m : 'class';
|
|
267
|
+
const sig = new Signals(mode);
|
|
230
268
|
const notifier = new Notifier(sql, opts.schema, opts.notifyIntervalMs ?? 100);
|
|
231
269
|
const passes = new AclPasses(S);
|
|
232
|
-
|
|
233
|
-
const
|
|
270
|
+
// режим none: сигналов нет — без LISTEN, кэш только по сроку (ttlOnly), подписка опрашивает журнал
|
|
271
|
+
const listen = opts.listen !== false && mode !== 'none';
|
|
272
|
+
// подключение в обход RLS видит строки всех арендаторов: его ответы сбрасывает сигнал из любого
|
|
273
|
+
const cache = opts.cache === false ? null : new ResultCache(opts.cache ?? {}, sig, Boolean(bypass));
|
|
234
274
|
const signals = new Set();
|
|
235
275
|
// продление сессии (§2.9): отдельным вызовом не чаще раза в минуту; ошибка (реплика,
|
|
236
276
|
// транзакции только для чтения) не мешает работе
|
|
@@ -241,60 +281,182 @@ export async function connect(opts) {
|
|
|
241
281
|
refreshedAt = Date.now();
|
|
242
282
|
void runTx({ sql, token, notifier: null }, (q) => q.unsafe(`select ${S}.auth_refresh()`), { retries: 0 }).catch(() => { });
|
|
243
283
|
};
|
|
244
|
-
|
|
284
|
+
// в режиме none нотификатор не отправляет: letopis.changed своей транзакции сбрасывает только свой кэш
|
|
285
|
+
const ctx = { sql, token, notifier: mode === 'none' ? null : notifier, passes, after: touch,
|
|
286
|
+
...(cache ? { onChanged: (names, raw) => cache.changed(names, raw) } : {}) };
|
|
245
287
|
const root = { tenant: null, registry: new Registry() };
|
|
246
|
-
|
|
288
|
+
// классы арендатора tenant под контекстом txc; null — контекст уже не в этом арендаторе (сессия кончилась,
|
|
289
|
+
// пользователь выбыл из арендатора): тогда прежний реестр остаётся, а не пустеет (доработки после точки Б)
|
|
290
|
+
const loadClasses = (txc, tenant) => runTx(txc, async (q) => {
|
|
291
|
+
const [t] = await q.unsafe(`select ${S}.ctx_tenant() as t`);
|
|
292
|
+
if (String(t?.t ?? '').toLowerCase() !== tenant.toLowerCase())
|
|
293
|
+
return null;
|
|
294
|
+
return q.unsafe(`select tenant, rev, data from ${S}.entity where class = 'Class' and tenant in (${S}.ctx_tenant(), '${SYSTEM_ID}')`);
|
|
295
|
+
}, { read: true });
|
|
296
|
+
// реестры db.as() — общие на арендатора; txc — контекст последнего успешного as() этого арендатора
|
|
247
297
|
const others = new Map();
|
|
248
|
-
const
|
|
298
|
+
const loading = new Map();
|
|
299
|
+
// токены классов (режим opaque) — по именам всех реестров подключения
|
|
300
|
+
const tokens = () => sig.setClasses([root.registry, ...[...others.values()].map((o) => o.registry)].flatMap((r) => r.all().map((c) => c.name)));
|
|
301
|
+
// only — токены арендаторов из сигналов Class (режимы tenant и opaque): реестры db.as() только этих арендаторов
|
|
302
|
+
const refresh = async (only) => {
|
|
249
303
|
const rows = await runTx(ctx, async (q) => {
|
|
250
304
|
const [t] = await q.unsafe(`select ${S}.ctx_tenant() as t`);
|
|
251
305
|
root.tenant = t?.t ?? null;
|
|
252
306
|
return q.unsafe(`select tenant, rev, data from ${S}.entity where class = 'Class' and tenant in (${S}.ctx_tenant(), '${SYSTEM_ID}')`);
|
|
253
307
|
});
|
|
254
308
|
root.registry.load(rows);
|
|
255
|
-
for (const o of others
|
|
256
|
-
|
|
309
|
+
for (const [tenant, o] of others) {
|
|
310
|
+
if (only && !only.has(sig.tenantToken(tenant) ?? ''))
|
|
311
|
+
continue;
|
|
312
|
+
const loaded = await loadClasses(o.txc, tenant).catch(() => null);
|
|
313
|
+
if (loaded)
|
|
314
|
+
o.registry.load(loaded);
|
|
315
|
+
}
|
|
316
|
+
tokens();
|
|
257
317
|
};
|
|
258
318
|
const registryFor = async (tenant, txc) => {
|
|
259
319
|
if (tenant === root.tenant)
|
|
260
320
|
return root.registry;
|
|
261
321
|
const known = others.get(tenant);
|
|
262
|
-
if (known)
|
|
322
|
+
if (known) {
|
|
323
|
+
known.txc = txc;
|
|
263
324
|
return known.registry;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
325
|
+
}
|
|
326
|
+
// одновременные as() нового арендатора получают один реестр
|
|
327
|
+
let pending = loading.get(tenant);
|
|
328
|
+
if (!pending) {
|
|
329
|
+
pending = (async () => {
|
|
330
|
+
const registry = new Registry();
|
|
331
|
+
registry.load(((await loadClasses(txc, tenant)) ?? []));
|
|
332
|
+
others.set(tenant, { registry, txc });
|
|
333
|
+
tokens();
|
|
334
|
+
return registry;
|
|
335
|
+
})().finally(() => loading.delete(tenant));
|
|
336
|
+
loading.set(tenant, pending);
|
|
337
|
+
}
|
|
338
|
+
return pending;
|
|
268
339
|
};
|
|
340
|
+
// ключ сигналов (режимы tenant и opaque) — сессии с правом letopis.signals; без права процесс
|
|
341
|
+
// работает без ключа: арендатора (а в opaque — и класса) в сигнале не различает
|
|
342
|
+
const fetchKey = async () => {
|
|
343
|
+
if (mode !== 'tenant' && mode !== 'opaque')
|
|
344
|
+
return;
|
|
345
|
+
const key = await runTx(ctx, (q) => q.unsafe(`select ${S}.signal_key() as k`), { read: true }).then((r) => r[0]?.k ?? null, (e) => {
|
|
346
|
+
if (e instanceof LetopisError && (e.code === 'acl_denied' || e.code === 'no_session'))
|
|
347
|
+
return null;
|
|
348
|
+
throw e;
|
|
349
|
+
});
|
|
350
|
+
// ключ сменился (reset({ all })): ответы с токенами старого ключа сигналы больше не узнают
|
|
351
|
+
if (sig.setKey(key))
|
|
352
|
+
cache?.clear();
|
|
353
|
+
};
|
|
354
|
+
await fetchKey();
|
|
269
355
|
await refresh();
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
356
|
+
// ключ, реестр и пропуска прав — заново по *, незнакомому токену, нагрузке не по режиму, а у процесса
|
|
357
|
+
// без ключа в режиме opaque — по любому сигналу; не чаще раза в 5 с: заливка канала (в том числе
|
|
358
|
+
// поддельными сигналами) не становится потоком перечитываний
|
|
359
|
+
const resync = throttle(() => {
|
|
360
|
+
passes.clear();
|
|
361
|
+
void fetchKey().then(() => refresh()).catch(() => { });
|
|
362
|
+
}, 5000);
|
|
363
|
+
// сигнал Class — реестр сразу, как в 1.0: правка описания доходит без задержки; пока идёт перечитывание, новые
|
|
364
|
+
// сигналы сливаются в одно следующее. Три перечитывания подряд — сразу, дальше не чаще раза в 5 с: заливка
|
|
365
|
+
// канала, в том числе поддельными Class, не становится потоком перечитываний (план 1.1, §7.7 №32; ревью №1
|
|
366
|
+
// точки А)
|
|
367
|
+
let reloading = false;
|
|
368
|
+
let again = false;
|
|
369
|
+
// какие реестры db.as() перечитать: токены арендаторов из сигналов или все (сигнал без арендатора)
|
|
370
|
+
let reloadAll = false;
|
|
371
|
+
const reloadTokens = new Set();
|
|
372
|
+
const startReload = bucket(() => {
|
|
373
|
+
reloading = true;
|
|
374
|
+
const only = reloadAll ? null : new Set(reloadTokens);
|
|
375
|
+
reloadAll = false;
|
|
376
|
+
reloadTokens.clear();
|
|
377
|
+
void refresh(only).catch(() => { }).finally(() => {
|
|
378
|
+
reloading = false;
|
|
379
|
+
if (again) {
|
|
380
|
+
again = false;
|
|
381
|
+
reload(undefined);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}, 3, 5000);
|
|
385
|
+
// tenant: null — все реестры, строка — токен арендатора сигнала, undefined — то, что уже накоплено
|
|
386
|
+
const reload = (tenant) => {
|
|
387
|
+
if (tenant === null)
|
|
388
|
+
reloadAll = true;
|
|
389
|
+
else if (tenant !== undefined)
|
|
390
|
+
reloadTokens.add(tenant);
|
|
391
|
+
if (reloading) {
|
|
392
|
+
again = true;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
startReload();
|
|
396
|
+
};
|
|
397
|
+
// строки прав — пропуска заново; кэш результатов и подписки — по классу и арендатору сигнала;
|
|
398
|
+
// (пере)подключение слушателя — кэш целиком, после обрыва — и ключ с реестром (сигналы потеряны)
|
|
399
|
+
const onSignal = (payload) => {
|
|
400
|
+
const s = sig.parse(payload);
|
|
401
|
+
if (s.kind === 'class' && ACL_CLASSES.has(s.cls))
|
|
274
402
|
passes.clear();
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
|
|
403
|
+
if (s.kind === 'class' && s.cls === 'Class')
|
|
404
|
+
reload(s.tenant);
|
|
405
|
+
else if (s.kind === 'all' || s.kind === 'opaque' || s.kind === 'unknown' || s.kind === 'foreign')
|
|
406
|
+
resync();
|
|
407
|
+
cache?.signal(s);
|
|
278
408
|
for (const fn of signals)
|
|
279
|
-
fn(
|
|
409
|
+
fn(s);
|
|
280
410
|
};
|
|
411
|
+
let listened = false;
|
|
281
412
|
const listener = listen
|
|
282
413
|
? await sql.listen(opts.schema, onSignal, () => {
|
|
414
|
+
if (listened)
|
|
415
|
+
resync();
|
|
416
|
+
listened = true;
|
|
283
417
|
cache?.listen();
|
|
284
418
|
for (const fn of signals)
|
|
285
|
-
fn('
|
|
419
|
+
fn({ kind: 'all' });
|
|
286
420
|
})
|
|
287
421
|
: null;
|
|
422
|
+
// режим none: о новых и изменённых классах сигнала нет — отпечаток классов раз в 5 с
|
|
423
|
+
let stamper = null;
|
|
424
|
+
if (mode === 'none') {
|
|
425
|
+
const stampOf = (txc) => runTx(txc, (q) => q.unsafe(`select ${S}.class_stamp() as s`), { read: true }).then((r) => String(r[0]?.s ?? ''));
|
|
426
|
+
const stamp = async () => [await stampOf(ctx), ...(await Promise.all([...others.values()].map((o) => stampOf(o.txc).catch(() => ''))))].join('|');
|
|
427
|
+
let last = await stamp();
|
|
428
|
+
let busy = false;
|
|
429
|
+
stamper = setInterval(() => {
|
|
430
|
+
if (busy)
|
|
431
|
+
return;
|
|
432
|
+
busy = true;
|
|
433
|
+
void stamp().then(async (cur) => {
|
|
434
|
+
if (cur === last)
|
|
435
|
+
return;
|
|
436
|
+
last = cur;
|
|
437
|
+
await refresh();
|
|
438
|
+
}).catch(() => { }).finally(() => {
|
|
439
|
+
busy = false;
|
|
440
|
+
});
|
|
441
|
+
}, 5000);
|
|
442
|
+
stamper.unref?.();
|
|
443
|
+
}
|
|
288
444
|
let stopMaintain = null;
|
|
289
445
|
const shared = {
|
|
290
446
|
sql, schema: opts.schema, S, notifier, passes, root, registryFor, refresh, onQuery: opts.onQuery, slowMs: opts.slowMs,
|
|
291
447
|
admin: Boolean(bypass && (role.rolsuper || role.can_set)),
|
|
292
448
|
ownerRole: owner,
|
|
293
449
|
cache,
|
|
450
|
+
sig,
|
|
451
|
+
allTenants: Boolean(bypass),
|
|
294
452
|
signals,
|
|
295
453
|
listen,
|
|
296
454
|
async close() {
|
|
297
455
|
stopMaintain?.();
|
|
456
|
+
resync.stop();
|
|
457
|
+
startReload.stop();
|
|
458
|
+
if (stamper)
|
|
459
|
+
clearInterval(stamper);
|
|
298
460
|
await notifier.close();
|
|
299
461
|
await listener?.unlisten();
|
|
300
462
|
if (own)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Внутренний канал между модулями пакета (план 1.1, этап 5): свойства подключения, которых нет в публичном API.
|
|
3
|
+
* Модуля нет в exports package.json — снаружи пакета его не импортировать; letopis/mcp узнаёт по нему, что
|
|
4
|
+
* фасад db обходит RLS, не полагаясь на то, что сообщает о себе сам вызывающий.
|
|
5
|
+
*/
|
|
6
|
+
/** Что фасад db (корень, db.as(), db.begin()) сообщает модулям пакета. */
|
|
7
|
+
export interface DbInternals {
|
|
8
|
+
/** Подключение в обход RLS (суперпользователь, BYPASSRLS, член роли-владельца): видит строки всех арендаторов. */
|
|
9
|
+
readonly allTenants: boolean;
|
|
10
|
+
/** Фасад явной транзакции db.begin(). */
|
|
11
|
+
readonly explicit: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Запомнить фасад (зовёт makeDb для каждого фасада, который отдаёт наружу). */
|
|
14
|
+
export declare function registerDb(db: object, info: DbInternals): void;
|
|
15
|
+
/** Свойства фасада; undefined — объект не фасад connect() этого пакета. */
|
|
16
|
+
export declare function internalsOf(db: unknown): DbInternals | undefined;
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Внутренний канал между модулями пакета (план 1.1, этап 5): свойства подключения, которых нет в публичном API.
|
|
3
|
+
* Модуля нет в exports package.json — снаружи пакета его не импортировать; letopis/mcp узнаёт по нему, что
|
|
4
|
+
* фасад db обходит RLS, не полагаясь на то, что сообщает о себе сам вызывающий.
|
|
5
|
+
*/
|
|
6
|
+
const facades = new WeakMap();
|
|
7
|
+
/** Запомнить фасад (зовёт makeDb для каждого фасада, который отдаёт наружу). */
|
|
8
|
+
export function registerDb(db, info) {
|
|
9
|
+
facades.set(db, info);
|
|
10
|
+
}
|
|
11
|
+
/** Свойства фасада; undefined — объект не фасад connect() этого пакета. */
|
|
12
|
+
export function internalsOf(db) {
|
|
13
|
+
return db !== null && (typeof db === 'object' || typeof db === 'function') ? facades.get(db) : undefined;
|
|
14
|
+
}
|
package/dist/load.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type postgres from 'postgres';
|
|
2
2
|
import { type Issue } from './errors.js';
|
|
3
|
+
import { Registry } from './registry.js';
|
|
3
4
|
type Sql = postgres.Sql<Record<string, unknown>>;
|
|
4
5
|
type Raw = Record<string, unknown>;
|
|
5
6
|
export interface LoadOptions {
|
|
@@ -27,6 +28,12 @@ export interface LoadOptions {
|
|
|
27
28
|
* existing: 'update' — прежние) значения строк; цели ссылок ему видны; владелец — по ownerDefault.
|
|
28
29
|
*/
|
|
29
30
|
as?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Первая заливка (план 1.1, §2.3): только в пустую схему (кроме строк системных классов — иначе
|
|
33
|
+
* not_empty), одной транзакцией; на время загрузки схема закрыта, индексы и триггеры сняты и
|
|
34
|
+
* перестраиваются в конце. Без commitEvery, existing (кроме 'error') и { as }.
|
|
35
|
+
*/
|
|
36
|
+
initial?: boolean;
|
|
30
37
|
}
|
|
31
38
|
export interface LoadReport {
|
|
32
39
|
/** Строк во входе. */
|
|
@@ -65,6 +72,50 @@ export interface LoadEnv {
|
|
|
65
72
|
/** Подключение может стать владельцем (set role) — иначе admin_required. */
|
|
66
73
|
admin: boolean;
|
|
67
74
|
ownerRole: string;
|
|
75
|
+
/** Сессия первой заливки (initialSession): загрузка идёт на её соединении и в её транзакции. */
|
|
76
|
+
session?: InitialState;
|
|
77
|
+
}
|
|
78
|
+
/** Внутренние настройки первой заливки — для разбивки замера (bench/initial-load.mjs); в публичные типы не входят. */
|
|
79
|
+
export interface InitialTuning {
|
|
80
|
+
/** COPY … FREEZE (по умолчанию да). */
|
|
81
|
+
freeze?: boolean;
|
|
82
|
+
/** Оставить триггеры (BEFORE ROW делает COPY построчным). */
|
|
83
|
+
keepTriggers?: boolean;
|
|
84
|
+
/** Оставить первичный ключ и уникальные ограничения. */
|
|
85
|
+
keepKeys?: boolean;
|
|
86
|
+
/** Оставить индексы GIN. */
|
|
87
|
+
keepGin?: boolean;
|
|
88
|
+
}
|
|
89
|
+
/** Проверки загрузки, отложенные до конца первой заливки: по перестроенным индексам. */
|
|
90
|
+
interface Pending {
|
|
91
|
+
reg: Registry;
|
|
92
|
+
tenant: string;
|
|
93
|
+
external: Map<string, {
|
|
94
|
+
line: number;
|
|
95
|
+
role: string;
|
|
96
|
+
allowed: string[];
|
|
97
|
+
}>;
|
|
98
|
+
owners: Map<string, number>;
|
|
99
|
+
/** verify: true — id записанных строк. */
|
|
100
|
+
verify: string[] | null;
|
|
101
|
+
}
|
|
102
|
+
/** Состояние сессии первой заливки. */
|
|
103
|
+
export interface InitialState {
|
|
104
|
+
conn: Sql;
|
|
105
|
+
/** id строк системных классов и объектов, уже записанных сессией: первичного ключа на время загрузки нет. */
|
|
106
|
+
ids: Set<string>;
|
|
107
|
+
pending: Pending[];
|
|
108
|
+
/** Арендатор → классы записанных строк: сигнал в конце сессии. */
|
|
109
|
+
touched: Map<string, Set<string>>;
|
|
110
|
+
freeze: boolean;
|
|
111
|
+
}
|
|
112
|
+
/** Первая заливка для нескольких загрузок (импорт): соединение и транзакция сессии. */
|
|
113
|
+
export interface InitialSession {
|
|
114
|
+
/** Соединение сессии (роль владельца): свои операторы импорта — в той же транзакции. */
|
|
115
|
+
readonly sql: Sql;
|
|
116
|
+
load(input: LoadInput, opts?: LoadOptions): Promise<LoadReport>;
|
|
117
|
+
/** id уже есть в схеме: строка системного класса или объект, записанный этой сессией. */
|
|
118
|
+
taken(id: string): boolean;
|
|
68
119
|
}
|
|
69
120
|
/**
|
|
70
121
|
* Загрузить строки (§2.6). Одна транзакция (или порции commitEvery): описания классов под
|
|
@@ -73,4 +124,16 @@ export interface LoadEnv {
|
|
|
73
124
|
* называет строки и нарушения.
|
|
74
125
|
*/
|
|
75
126
|
export declare function load(env: LoadEnv, input: LoadInput, opts?: LoadOptions): Promise<LoadReport>;
|
|
127
|
+
/**
|
|
128
|
+
* Первая заливка (план 1.1, §2.3): загрузки fn — одной транзакцией в пустую схему. Блокировка таблиц до
|
|
129
|
+
* проверки; в схеме только строки системных классов и записи reset (иначе not_empty); они (с seq и tx
|
|
130
|
+
* журнала) и определения ограничений, индексов и триггеров из каталога (с режимом включения tgenabled:
|
|
131
|
+
* pg_get_triggerdef его не передаёт) сохраняются в памяти; TRUNCATE; триггеры, ограничения и индексы
|
|
132
|
+
* снимаются — BEFORE ROW делает COPY построчным одним своим наличием, триггеры после оператора копят
|
|
133
|
+
* переходные таблицы; системные строки возвращаются (seq журнала — generated always: overriding system
|
|
134
|
+
* value); загрузки пишут COPY FREEZE. В конце — ограничения (дубликат найдёт сортировка), индексы,
|
|
135
|
+
* триггеры с прежним режимом, отложенные проверки загрузок, сигнал, счётчик прав и ANALYZE. Любая ошибка
|
|
136
|
+
* откатывает всё: DDL в PostgreSQL транзакционен.
|
|
137
|
+
*/
|
|
138
|
+
export declare function initialSession<T>(env: LoadEnv, fn: (s: InitialSession) => Promise<T>, tune?: InitialTuning): Promise<T>;
|
|
76
139
|
export {};
|