letopis 0.19.0 → 0.20.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/dist/acl.d.ts CHANGED
@@ -33,7 +33,11 @@ export interface AclApi {
33
33
  checkData(account: string | {
34
34
  id: string;
35
35
  }, className: string, op: AclOp): Promise<AclDecision>;
36
- /** Сбросить кэш Resource/Rule фасада (enforceAcl-цепочки перечитывают только новым connect). */
36
+ /**
37
+ * Сбросить кэш Resource/Rule ЭТОГО фасада (db.acl.check/checkData).
38
+ * NB: энфорсер цепочек под `enforceAcl` — отдельная подсистема; его пересобирает
39
+ * `db.reloadSchema()` (реестр + ACL-резолвер), а не этот reload().
40
+ */
37
41
  reload(): void;
38
42
  }
39
43
  interface AclSource {
@@ -41,7 +45,8 @@ interface AclSource {
41
45
  rules: Rule[];
42
46
  }
43
47
  export declare function makeAcl(tables: Tables, registry: Registry): AclApi;
44
- /** Синхронный резолвер для цепочек: категории субъекта и правила зафиксированы на connect. */
48
+ /** Синхронный резолвер для цепочек: категории субъекта и правила — снимок на момент
49
+ * компиляции (connect либо db.reloadSchema(), который пересобирает энфорсер). */
45
50
  export declare function compileEnforcer(src: AclSource, account: string, cats: string[], registry: Registry): (cls: ClassDef, op: AclOp) => AclDecision;
46
51
  /** Читаемая ошибка отказа для цепочек. */
47
52
  export declare function aclDenied(op: AclOp, clsId: string, d: AclDecision): Error;
package/dist/acl.js CHANGED
@@ -232,13 +232,14 @@ export function makeAcl(tables, registry) {
232
232
  }
233
233
  // --- enforce-компилятор (connect({enforceAcl: true})) ----------------------------
234
234
  // START_CONTRACT: compileEnforcer
235
- // PURPOSE: Скомпилировать синхронный энфорсер (ClassDef, op) → AclDecision с мемоизацией — субъект и правила зафиксированы на connect.
235
+ // PURPOSE: Скомпилировать синхронный энфорсер (ClassDef, op) → AclDecision с мемоизацией — субъект и правила снимаются В МОМЕНТ вызова (на connect либо на db.reloadSchema()).
236
236
  // INPUTS: { src: AclSource; account: string; cats: string[]; registry: Registry }
237
237
  // OUTPUTS: { (cls: ClassDef, op: AclOp) => AclDecision }
238
238
  // SIDE_EFFECTS: none (мемо-кэш решений)
239
239
  // LINKS: M-ACL, V-M-ACL, M-SQL, M-WRITE
240
240
  // END_CONTRACT: compileEnforcer
241
- /** Синхронный резолвер для цепочек: категории субъекта и правила зафиксированы на connect. */
241
+ /** Синхронный резолвер для цепочек: категории субъекта и правила — снимок на момент
242
+ * компиляции (connect либо db.reloadSchema(), который пересобирает энфорсер). */
242
243
  export function compileEnforcer(src, account, cats, registry) {
243
244
  // START_BLOCK_ENFORCER_MEMO
244
245
  const subjects = subjectAliases(src.resources, cats);
package/dist/chain.d.ts CHANGED
@@ -42,6 +42,14 @@ export interface ChainCore {
42
42
  versions(): Promise<Row[]>;
43
43
  /** Рекурсивный self-обход: дети любой глубины (тот же класс), $depth в Row. */
44
44
  deep(max?: number): Chain;
45
+ /**
46
+ * Только ЭТОТ класс, без классов-потомков. По умолчанию шаг полиморфен: родитель отдаёт
47
+ * объединение с потомками (`db.Контрагент()` → Мастера + Клиенты). `.exact()` нужен, когда
48
+ * наследование в домене — переиспользование attributes, а не «is-a» для выборки:
49
+ * в демо `запись` наследует `окно`, но смена ≠ бронь, поэтому «смены мастера» —
50
+ * `db.Мастер(id).окно().exact()`.
51
+ */
52
+ exact(): Chain;
45
53
  /** Агрегации по полю последнего шага ('data.<путь>'), считает БД. */
46
54
  sum(field: string): Promise<number | null>;
47
55
  avg(field: string): Promise<number | null>;
@@ -125,6 +133,23 @@ export type Chain = ChainCore & {
125
133
  [className: string]: StepProp;
126
134
  };
127
135
  export interface DbCore {
136
+ /**
137
+ * Идентичность ВЫЗОВА: хендл того же пула, работающий от имени account.
138
+ * Арендатор — свойство вызова, а не подключения (в connect() опции account нет):
139
+ *
140
+ * const db = await connect({ dsn, schema }) // пул, безличный
141
+ * const t = await db.as(req.accountId) // кто именно делает этот вызов
142
+ * await t.Клиент().rows() // изолировано по account
143
+ *
144
+ * При enforceAccount (default) чтение/запись возможны только с такого хендла.
145
+ * При enforceAcl энфорсер компилится под ЭТОГО субъекта (у каждого scope свой).
146
+ * owner по умолчанию = account; переопределяется вторым аргументом.
147
+ */
148
+ as(account: string | {
149
+ id: string;
150
+ }, opts?: {
151
+ owner?: string;
152
+ }): Promise<EntityDb>;
128
153
  /** Открыть транзакцию: tr — тот же API + commit/rollback/lock. */
129
154
  begin(): Promise<EntityTx>;
130
155
  commit(tr: EntityTx): Promise<void>;
package/dist/chain.js CHANGED
@@ -358,6 +358,9 @@ function makeChain(ctx, steps, mods, batch, nodes = NO_NODES) {
358
358
  return () => withMods({ withDeleted: true });
359
359
  case 'deep':
360
360
  return (max = 32) => withLast({ deepMax: max });
361
+ case 'exact':
362
+ // снять полиморфизм ТЕКУЩЕГО шага: только свой класс, без потомков
363
+ return () => withLast({ exactClass: true });
361
364
  // END_BLOCK_READ_MODIFIERS
362
365
  // START_BLOCK_AGGREGATIONS
363
366
  case 'sum':
@@ -448,8 +451,10 @@ function makeChain(ctx, steps, mods, batch, nodes = NO_NODES) {
448
451
  return (v) => withLast({ accountFilter: typeof v === 'object' ? v.id : v });
449
452
  case 'owner':
450
453
  return (v) => withLast({ ownerFilter: typeof v === 'object' ? v.id : v });
451
- case 'link':
452
- throw new Error('letopis: .link() removed (0.15.0) — use the link slot: .Класс.set(target)');
454
+ // NB: охранника `case 'link'` здесь больше нет (снят в 0.20.0). Он затенял РЕАЛЬНЫЙ
455
+ // класс схемы: в демо-домене `link` — абстрактный корень всех связок, и шаг
456
+ // db.Клиент(c).link() падал migration-ошибкой вместо обхода. Обратная совместимость
457
+ // со снесённым в 0.15.0 методом .link() принесена в жертву достижимости класса.
453
458
  // END_BLOCK_COLUMN_MODS
454
459
  }
455
460
  // START_BLOCK_CLASS_RESOLVE
@@ -532,6 +537,27 @@ export function makeDb(ctx, root) {
532
537
  }
533
538
  // END_BLOCK_TABLES_AUTH_ACL
534
539
  switch (prop) {
540
+ // START_BLOCK_SCOPE_AS
541
+ // db.as(account): тот же пул, но вызов от имени этого арендатора. Дешёвый клон ctx
542
+ // (как beginTx), НЕ новое соединение. Батчи не наследуются: очередь планов, общая
543
+ // для разных арендаторов, — это утечка записи между ними.
544
+ case 'as':
545
+ return async (a, o) => {
546
+ const account = typeof a === 'object' ? a?.id : a;
547
+ if (!account)
548
+ throw new Error('letopis: db.as(account) requires an account id (uuid or { id })');
549
+ const scoped = { ...ctx, account, owner: o?.owner, aclDecide: undefined };
550
+ // reloadSchema() со scope: реестр перечитывает КОРЕНЬ (единый источник), затем
551
+ // scope подхватывает его и перекомпилирует свой энфорсер под своего субъекта.
552
+ scoped.reload = async () => {
553
+ await ctx.reload?.();
554
+ scoped.registry = ctx.registry;
555
+ await ctx.compileAcl?.(scoped);
556
+ };
557
+ await ctx.compileAcl?.(scoped);
558
+ return makeDb(scoped);
559
+ };
560
+ // END_BLOCK_SCOPE_AS
535
561
  // START_BLOCK_TX_LOCK
536
562
  case 'begin':
537
563
  return async () => makeDb(await beginTx(ctx), state);
package/dist/index.d.ts CHANGED
@@ -1,8 +1,13 @@
1
1
  /**
2
2
  * letopis: dot-цепочки над append-only Entity-хранилищем (TimescaleDB).
3
3
  *
4
- * const db = await connect({ dsn, schema: 'booking' })
5
- * await db.Сотрудник({ name: 'Вася' }).навык().Услуга().run()
4
+ * schema — ПОЛНОЕ имя PG-схемы С версией движка ("v1.booking"); префикс либа НЕ достраивает
5
+ * (создаёт up({ schema: 'booking', version: 1 }) либо db/apply.mjs). Шаг цепочки — id ИЛИ
6
+ * alias класса из таблицы Schema (db.Staff ≡ db.Мастер).
7
+ *
8
+ * const db = await connect({ dsn, schema: 'v1.booking' }) // безличный пул
9
+ * const t = await db.as(accountId) // арендатор ВЫЗОВА
10
+ * await t.Мастер({ name: 'Вася' }).навык().Услуга().run()
6
11
  */
7
12
  import { type EntityDb } from './chain.js';
8
13
  import type { ConnectOpts } from './types.js';
package/dist/index.js CHANGED
@@ -1,11 +1,16 @@
1
1
  /**
2
2
  * letopis: dot-цепочки над append-only Entity-хранилищем (TimescaleDB).
3
3
  *
4
- * const db = await connect({ dsn, schema: 'booking' })
5
- * await db.Сотрудник({ name: 'Вася' }).навык().Услуга().run()
4
+ * schema — ПОЛНОЕ имя PG-схемы С версией движка ("v1.booking"); префикс либа НЕ достраивает
5
+ * (создаёт up({ schema: 'booking', version: 1 }) либо db/apply.mjs). Шаг цепочки — id ИЛИ
6
+ * alias класса из таблицы Schema (db.Staff ≡ db.Мастер).
7
+ *
8
+ * const db = await connect({ dsn, schema: 'v1.booking' }) // безличный пул
9
+ * const t = await db.as(accountId) // арендатор ВЫЗОВА
10
+ * await t.Мастер({ name: 'Вася' }).навык().Услуга().run()
6
11
  */
7
12
  // FILE: lib/src/index.ts
8
- // VERSION: 1.0.0
13
+ // VERSION: 1.2.0
9
14
  // START_MODULE_CONTRACT
10
15
  // PURPOSE: Публичная точка входа пакета — открыть соединение (реестр, опциональный ACL, сборка db) и ре-экспорт публичной поверхности; хелпер курсора.
11
16
  // SCOPE: connect, cursorOf + barrel-реэкспорты
@@ -22,7 +27,10 @@
22
27
  // END_MODULE_MAP
23
28
  //
24
29
  // START_CHANGE_SUMMARY
25
- // LAST_CHANGE: [v1.0.0 - Documented existing module: reverse-engineered contract + markup]
30
+ // LAST_CHANGE: [v1.2.0 - BREAKING: opts.account/opts.owner сняты — подключение безличное,
31
+ // арендатора и субъекта ACL называет db.as(account, { owner }). Компиляция энфорсера
32
+ // вынесена в ctx.compileAcl(ctx-scope), ctx.reload обновляет только реестр.
33
+ // Ранее: enforceAccount по умолчанию true; предупреждение при выключенном enforceAcl]
26
34
  // END_CHANGE_SUMMARY
27
35
  import postgres from 'postgres';
28
36
  import { loadRegistry } from './schema.js';
@@ -30,12 +38,14 @@ import { makeDb } from './chain.js';
30
38
  import { makeTables } from './tables.js';
31
39
  import { compileEnforcer } from './acl.js';
32
40
  // START_CONTRACT: connect
33
- // PURPOSE: Открыть соединение: postgres-пул, реестр схемы, System-аккаунт, опциональный ACL — и собрать EntityDb.
34
- // INPUTS: { opts: ConnectOpts { dsn, schema, partition?='entity', max?=10, account?, owner?, enforceAcl?, enforceAccount?, onQuery?, slowMs? } }
35
- // OUTPUTS: { Promise<EntityDb> - dot-цепочный db }
36
- // SIDE_EFFECTS: открывает пул postgres (timestamptz строкой), читает реестр и System-аккаунт; при enforceAcl без account бросает 'letopis: enforceAcl requires connect({ account })', при ненайденном — 'letopis: enforceAcl — account "…" not found', иначе читает resources/rules и компилит aclDecide; NB: ESM-цикл M-CONNECT↔M-UP (up ре-экспортится здесь)
41
+ // PURPOSE: Открыть БЕЗЛИЧНОЕ соединение: postgres-пул, реестр схемы, System-аккаунт, хук компиляции ACL — и собрать EntityDb (арендатора вызова называет db.as()).
42
+ // INPUTS: { opts: ConnectOpts { dsn, schema, partition?='entity', max?=10, enforceAcl?, enforceAccount?=true, onQuery?, slowMs? } }
43
+ // OUTPUTS: { Promise<EntityDb> - dot-цепочный db без идентичности; db.as(account) даёт scoped-хендл }
44
+ // SIDE_EFFECTS: открывает пул postgres (timestamptz строкой), читает реестр и System-аккаунт; ставит ctx.compileAcl (зовётся из db.as: читает resources/rules и компилит aclDecide под субъекта scope; при ненайденном аккаунте — 'letopis: enforceAcl — account "…" not found'); без enforceAcl печатает предупреждение один раз на процесс; NB: ESM-цикл M-CONNECT↔M-UP (up ре-экспортится здесь)
37
45
  // LINKS: M-CONNECT, V-M-CONNECT, M-SCHEMA, M-CHAIN, M-TABLES, M-ACL, M-SQL
38
46
  // END_CONTRACT: connect
47
+ /** Предупреждение об отключённом enforceAcl печатается один раз на процесс. */
48
+ let warnedNoAcl = false;
39
49
  export async function connect(opts) {
40
50
  // timestamptz — строкой (JS Date режет микросекунды → ломал бы asOf/cursorOf по updated)
41
51
  const sql = postgres(opts.dsn, {
@@ -52,35 +62,48 @@ export async function connect(opts) {
52
62
  registry,
53
63
  pgSchema: opts.schema,
54
64
  partition,
55
- account: opts.account,
56
- owner: opts.owner,
65
+ // account/owner тут НЕ задаются: подключение безличное, арендатора называет db.as().
57
66
  systemAccount: sys[0]?.id,
58
- enforceAccount: opts.enforceAccount,
67
+ // Изоляция арендатора ВКЛЮЧЕНА по умолчанию (0.20.0): забыть её было слишком легко,
68
+ // а цена забывчивости — чужие строки в выдаче. Без идентичности вызова (корневой
69
+ // хендл) чтение/запись при ней запрещены — см. guardScoped в sql.ts.
70
+ enforceAccount: opts.enforceAccount ?? true,
59
71
  onQuery: opts.onQuery,
60
72
  slowMs: opts.slowMs,
61
73
  };
62
74
  // START_BLOCK_ENFORCE_ACL
63
- // enforceAcl: правила и категории субъекта компилятся в резолвер (перечитываются reloadSchema/reload)
64
- const compileAcl = async () => {
75
+ // enforceAcl: правила и категории субъекта компилятся в резолвер ПОД КОНКРЕТНЫЙ scope.
76
+ // Функция живёт на ctx, потому что зовут её двое: db.as() (новый субъект) и reload
77
+ // (тот же субъект на свежем реестре). Субъект берётся из c.account, т.е. из scope.
78
+ ctx.compileAcl = async (c) => {
65
79
  if (!opts.enforceAcl)
66
80
  return;
67
- if (!opts.account)
68
- throw new Error('letopis: enforceAcl requires connect({ account })');
69
- const tables = makeTables(ctx);
81
+ if (!c.account)
82
+ throw new Error('letopis: enforceAcl is on — call db.as(account) to name the subject');
83
+ const tables = makeTables(c);
70
84
  const [account, resources, rules] = await Promise.all([
71
- tables.accounts.get(opts.account),
85
+ tables.accounts.get(c.account),
72
86
  tables.resources.find(),
73
87
  tables.rules.find({ enabled: true }),
74
88
  ]);
75
89
  if (!account)
76
- throw new Error(`letopis: enforceAcl — account "${opts.account}" not found`);
77
- ctx.aclDecide = compileEnforcer({ resources, rules }, account.id, account.categories, ctx.registry);
90
+ throw new Error(`letopis: enforceAcl — account "${c.account}" not found`);
91
+ c.aclDecide = compileEnforcer({ resources, rules }, account.id, account.categories, c.registry);
78
92
  };
79
- await compileAcl();
80
- // db.reloadSchema(): перечитать определения классов из таблицы Schema (registry + enforcer) без реконнекта
93
+ // enforceAcl остаётся OPT-IN: включить его по умолчанию нельзя (он работает
94
+ // deny-by-default, т.е. без настроенных Resource/Rule выдача стала бы пустой).
95
+ // Поэтому — предупреждение: молчаливое отсутствие авторизации хуже шумного.
96
+ // Один раз на процесс, иначе утонет в логах приложения с пулом подключений.
97
+ if (!opts.enforceAcl && !warnedNoAcl) {
98
+ warnedNoAcl = true;
99
+ console.warn('letopis: enforceAcl is off — Resource/Rule are NOT checked, reads and writes pass ' +
100
+ 'authorization unconditionally. For production use connect({ enforceAcl: true }) + db.as(account).');
101
+ }
102
+ // db.reloadSchema(): перечитать определения классов из таблицы Schema без реконнекта.
103
+ // На корневом хендле обновляется только реестр (субъекта тут нет); scoped-хендлы
104
+ // подхватывают его и перекомпилируют свой энфорсер сами (chain.ts, case 'as').
81
105
  ctx.reload = async () => {
82
106
  ctx.registry = await loadRegistry(sql, opts.schema, partition);
83
- await compileAcl();
84
107
  };
85
108
  // END_BLOCK_ENFORCE_ACL
86
109
  return makeDb(ctx);
package/dist/schema.js CHANGED
@@ -9,7 +9,7 @@
9
9
  */
10
10
  //
11
11
  // FILE: lib/src/schema.ts
12
- // VERSION: 1.0.0
12
+ // VERSION: 1.1.0
13
13
  // START_MODULE_CONTRACT
14
14
  // PURPOSE: Строит реестр классов из таблицы Schema — резолв наследования/link-ends, типы полей, компиляция валидаторов.
15
15
  // SCOPE: Registry (add/find/resolve/has), loadRegistry, fieldTypeOf; локальные parseEnd/parseIdGen/buildDef.
@@ -26,9 +26,11 @@
26
26
  // END_MODULE_MAP
27
27
  //
28
28
  // START_CHANGE_SUMMARY
29
- // LAST_CHANGE: [v1.0.0 - Documented existing module: reverse-engineered contract + markup]
29
+ // LAST_CHANGE: [v1.1.0 - loadRegistry: 42P01/3F000 перехватываются и заменяются адресной ошибкой
30
+ // (передано базовое имя вместо "vN.имя") со списком letopis-схем — паритет с валидацией up()]
30
31
  // END_CHANGE_SUMMARY
31
32
  import { createRequire } from 'node:module';
33
+ import { reservedNamesOf } from './types.js';
32
34
  const Validator = createRequire(import.meta.url)('fastest-validator');
33
35
  const v = new Validator({ useNewCustomCheckerFunction: true });
34
36
  export class Registry {
@@ -288,15 +290,34 @@ function buildDef(row, byId) {
288
290
  // PURPOSE: Прочитать классы партиции из таблицы Schema и построить Registry со скомпилированными check.
289
291
  // INPUTS: { sql: postgres.Sql; pgSchema: string; partition: string }
290
292
  // OUTPUTS: { Promise<Registry> - реестр всех классов партиции }
291
- // SIDE_EFFECTS: SELECT из "<pgSchema>"."Schema"
292
- // ERRORS: schema has no classes for partition
293
+ // SIDE_EFFECTS: SELECT из "<pgSchema>"."Schema"; на 42P01/3F000 — доп. SELECT списка схем для подсказки
294
+ // ERRORS: schema has no classes for partition; schema has no "Schema" table (передано базовое имя вместо "vN.имя")
293
295
  // LINKS: M-SCHEMA, V-M-SCHEMA, M-DDL
294
296
  // END_CONTRACT: loadRegistry
297
+ /** Уже предупреждённые «схема:класс» — чтобы не повторять на каждый connect/reloadSchema. */
298
+ const warnedReserved = new Set();
295
299
  export async function loadRegistry(sql, pgSchema, partition) {
296
300
  // START_BLOCK_LOAD_QUERY
297
301
  const ident = `"${pgSchema.replace(/"/g, '""')}"`;
298
- const rows = (await sql.unsafe(`SELECT id, alias, category, ancestor, attributes, links, meta, "order", ancestors, descendants
299
- FROM ${ident}."Schema" WHERE partition = $1 ORDER BY category, "order"`, [partition]));
302
+ let rows;
303
+ try {
304
+ rows = (await sql.unsafe(`SELECT id, alias, category, ancestor, attributes, links, meta, "order", ancestors, descendants
305
+ FROM ${ident}."Schema" WHERE partition = $1 ORDER BY category, "order"`, [partition]));
306
+ }
307
+ catch (e) {
308
+ // 42P01 undefined_table / 3F000 invalid_schema_name — типовая ошибка: передали БАЗОВОЕ имя
309
+ // ('booking') вместо полного с версией ('v1.booking'). Подсказываем вместо сырой ошибки PG.
310
+ const code = e.code;
311
+ if (code !== '42P01' && code !== '3F000')
312
+ throw e;
313
+ // перечисляем ТОЛЬКО схемы letopis (те, где есть таблица Schema), а не все namespace БД
314
+ const found = (await sql.unsafe(`SELECT table_schema AS s FROM information_schema.tables
315
+ WHERE table_name = 'Schema' ORDER BY 1`));
316
+ throw new Error(`letopis: schema "${pgSchema}" has no "Schema" table — connect({ schema }) takes the FULL ` +
317
+ `PG-schema name WITH the engine version ("v1.booking"); the library adds no prefix. ` +
318
+ `letopis schemas here: ${found.map((r) => r.s).join(', ') || '(none)'}. ` +
319
+ `Create one with up({ schema, version }) or db/apply.mjs --schema=… --version=…`);
320
+ }
300
321
  if (!rows.length) {
301
322
  throw new Error(`letopis: schema "${pgSchema}" has no classes for partition "${partition}"`);
302
323
  }
@@ -305,5 +326,26 @@ export async function loadRegistry(sql, pgSchema, partition) {
305
326
  const registry = new Registry();
306
327
  for (const row of rows)
307
328
  registry.add(buildDef(row, byId));
329
+ // START_BLOCK_RESERVED_WARN
330
+ // Имя, перехватываемое Proxy до резолва класса, делает шаг недостижимым ПОД ЭТИМ именем
331
+ // (второе имя класса, если оно свободно, работает). Предупреждаем, а НЕ бросаем: демо-сид
332
+ // содержит класс id "link", и throw уронил бы up() на штатной схеме.
333
+ // Один раз на (схема, класс) за процесс: loadRegistry зовётся на каждый connect и
334
+ // reloadSchema — иначе штатный прогон утонул бы в повторах.
335
+ for (const def of registry.all) {
336
+ const clash = reservedNamesOf(def);
337
+ if (!clash.length)
338
+ continue;
339
+ const seen = `${pgSchema}:${def.id}`;
340
+ if (warnedReserved.has(seen))
341
+ continue;
342
+ warnedReserved.add(seen);
343
+ const free = [def.id, def.alias].filter((n) => !clash.includes(n));
344
+ console.warn(`letopis: class name ${clash.map((n) => `"${n}"`).join(' / ')} is reserved by the chain API — ` +
345
+ (free.length
346
+ ? `use ${free.map((n) => `"${n}"`).join(' / ')} for chain steps`
347
+ : 'this class is unreachable as a chain step; rename it'));
348
+ }
349
+ // END_BLOCK_RESERVED_WARN
308
350
  return registry;
309
351
  }
package/dist/sql.d.ts CHANGED
@@ -25,14 +25,22 @@ export interface Ctx {
25
25
  registry: Registry;
26
26
  pgSchema: string;
27
27
  partition: string;
28
+ /** Арендатор ВЫЗОВА: заполнен у scoped-хендла db.as(account), пуст у корневого. */
28
29
  account?: string;
30
+ /** Дефолтный owner записей этого scope (db.as(account, { owner })). Default = account. */
29
31
  owner?: string;
30
32
  /** System-аккаунт — fallback для NOT NULL Entity.account (загружается при connect). */
31
33
  systemAccount?: string;
32
34
  /** Жёсткая изоляция арендатора: все чтения фильтруются, записи пришпилены к account. */
33
35
  enforceAccount?: boolean;
34
- /** enforceAcl: скомпилированный на connect резолвер Rule/Resource (READ/WRITE/DELETE). */
36
+ /** enforceAcl: скомпилированный резолвер Rule/Resource (READ/WRITE/DELETE) для account ЭТОГО ctx. */
35
37
  aclDecide?: (cls: ClassDef, op: AclOp) => AclDecision;
38
+ /**
39
+ * enforceAcl: скомпилировать энфорсер под субъекта переданного ctx (пишет в c.aclDecide).
40
+ * Ставит connect(); зовут db.as() и reload. Один энфорсер = один субъект: memo в acl.ts
41
+ * ключуется классом+операцией БЕЗ аккаунта, переиспользование между scope — утечка решения.
42
+ */
43
+ compileAcl?: (c: Ctx) => Promise<void>;
36
44
  /** true внутри db.begin()-транзакции (write не оборачивает в begin повторно). */
37
45
  inTx?: boolean;
38
46
  /** true только у db.begin()-транзакций (не у внутренних): управляет подсказкой при 40P01/40001. */
@@ -78,6 +86,13 @@ export interface Step {
78
86
  pivotKey?: number;
79
87
  /** .alias(name) — ключ шага в путях. */
80
88
  aliasKey?: string;
89
+ /**
90
+ * .exact() — только ЭТОТ класс, без классов-потомков. По умолчанию шаг полиморфен
91
+ * (родитель отдаёт объединение с потомками); нужно, когда наследование в домене
92
+ * использовано для переиспользования attributes, а не как «is-a» для выборки
93
+ * (демо: `запись` наследует `окно`, но смена ≠ бронь).
94
+ */
95
+ exactClass?: boolean;
81
96
  /** .tags(…) — фильтр по колонке tags: строка | string[] (все) | has/hasAny/hasAll. */
82
97
  tagsFilter?: unknown;
83
98
  /** .account(uuid|Row) — фильтр по колонке account. */
@@ -116,6 +131,8 @@ export interface BuiltQuery {
116
131
  /** Ключи шагов в путях (после alias/dedupe). */
117
132
  keys: string[];
118
133
  }
134
+ /** Корневой хендл при enforceAccount читать/писать не может: арендатора называет db.as(). */
135
+ export declare function guardScoped(ctx: Ctx): void;
119
136
  /** Построить читающий запрос по цепочке. */
120
137
  export declare function buildRead(ctx: Ctx, steps: Step[], mods: ChainMods, mode: ReadMode): BuiltQuery;
121
138
  /** INSERT новой версии/tombstone. $9 = updated прошлой версии (или null), $10 = deleted. */