letopis 0.13.0 → 0.18.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/ops.js CHANGED
@@ -2,6 +2,44 @@
2
2
  * Операторы фильтров. Symbol-tagged объекты — не пересекаются с данными.
3
3
  * Касты по типам полей делает sql.ts на основе Schema.attributes.
4
4
  */
5
+ //
6
+ // FILE: lib/src/ops.ts
7
+ // VERSION: 1.0.0
8
+ // START_MODULE_CONTRACT
9
+ // PURPOSE: Билдеры операторов фильтра, создающие узлы Op для DSL фильтров.
10
+ // SCOPE: type guard isOp + конструкторы операторов сравнения / списков / строк / массивов / наличия / логики (ne…or) поверх локальной фабрики op().
11
+ // DEPENDS: M-TYPES
12
+ // LINKS: M-OPS, V-M-OPS
13
+ // ROLE: RUNTIME
14
+ // MAP_MODE: EXPORTS
15
+ // END_MODULE_CONTRACT
16
+ //
17
+ // START_MODULE_MAP
18
+ // op - (локальный) фабрика узла Op: { [OP]: name, args }.
19
+ // isOp - type guard: значение является узлом Op (содержит символ OP).
20
+ // ne - ≠ (IS DISTINCT FROM).
21
+ // gt - >.
22
+ // gte - ≥.
23
+ // lt - <.
24
+ // lte - ≤.
25
+ // between - a ≤ x ≤ b.
26
+ // inList - значение из списка (IN).
27
+ // like - LIKE (шаблон с % и _).
28
+ // ilike - ILIKE (без регистра).
29
+ // starts - начинается с (LIKE s%).
30
+ // ends - заканчивается на (LIKE %s).
31
+ // has - массив-поле содержит значение.
32
+ // hasAny - массив-поле содержит хотя бы одно из.
33
+ // hasAll - массив-поле содержит все.
34
+ // exists - поле присутствует (true) / отсутствует (false) в data.
35
+ // isNull - поле NULL или отсутствует.
36
+ // not - НЕ-условие по полю (скаляр → «не равно»).
37
+ // or - ИЛИ на уровне фильтра шага.
38
+ // END_MODULE_MAP
39
+ //
40
+ // START_CHANGE_SUMMARY
41
+ // LAST_CHANGE: [v1.0.0 - Documented existing module: reverse-engineered contract + markup]
42
+ // END_CHANGE_SUMMARY
5
43
  import { OP } from './types.js';
6
44
  const op = (name, ...args) => ({ [OP]: name, args });
7
45
  export const isOp = (v) => typeof v === 'object' && v !== null && OP in v;
@@ -39,5 +77,12 @@ export const exists = (yes = true) => op('exists', yes);
39
77
  export const isNull = () => op('isNull');
40
78
  /** НЕ-условие по полю: not(ilike('%тест%')); скаляр → «не равно» */
41
79
  export const not = (v) => op('not', v);
80
+ // START_CONTRACT: or
81
+ // PURPOSE: Логическое ИЛИ на уровне фильтра шага — объединяет несколько фильтров-объектов в один узел Op.
82
+ // INPUTS: { filters: Record<string, unknown>[] - варьируемое число фильтров-объектов (каждый — свой набор условий) }
83
+ // OUTPUTS: { Op - узел { [OP]: 'or', args: [filters] } }
84
+ // SIDE_EFFECTS: none (чистая фабрика через op())
85
+ // LINKS: M-OPS, V-M-OPS
86
+ // END_CONTRACT: or
42
87
  /** ИЛИ на уровне фильтра шага: db.Запись(or({status:'created'}, {status:'confirmed'})) */
43
88
  export const or = (...filters) => op('or', filters);
package/dist/schema.js CHANGED
@@ -2,9 +2,32 @@
2
2
  * Загрузка таблицы Schema → реестр классов:
3
3
  * - резолв имени по id и alias;
4
4
  * - цепочка ancestors (walk по ancestor, если в БД не заполнена);
5
+ * - attributes НАСЛЕДУЮТСЯ по цепочке (потомок поверх предка; поле = замена правила
6
+ * целиком) — включая правило id (генерация v4/v5/v7); links НЕ наследуются;
5
7
  * - скомпилированный fastest-validator per class;
6
8
  * - карта типов полей (для SQL-кастов фильтров).
7
9
  */
10
+ //
11
+ // FILE: lib/src/schema.ts
12
+ // VERSION: 1.0.0
13
+ // START_MODULE_CONTRACT
14
+ // PURPOSE: Строит реестр классов из таблицы Schema — резолв наследования/link-ends, типы полей, компиляция валидаторов.
15
+ // SCOPE: Registry (add/find/resolve/has), loadRegistry, fieldTypeOf; локальные parseEnd/parseIdGen/buildDef.
16
+ // DEPENDS: M-TYPES
17
+ // LINKS: M-SCHEMA, V-M-SCHEMA
18
+ // ROLE: RUNTIME
19
+ // MAP_MODE: EXPORTS
20
+ // END_MODULE_CONTRACT
21
+ //
22
+ // START_MODULE_MAP
23
+ // Registry - реестр классов (add/find/resolve/has, all)
24
+ // fieldTypeOf - attribute-спека fastest-validator → FieldType (для SQL-кастов)
25
+ // loadRegistry - загрузка Schema-строк партиции → Registry со скомпилированными check
26
+ // END_MODULE_MAP
27
+ //
28
+ // START_CHANGE_SUMMARY
29
+ // LAST_CHANGE: [v1.0.0 - Documented existing module: reverse-engineered contract + markup]
30
+ // END_CHANGE_SUMMARY
8
31
  import { createRequire } from 'node:module';
9
32
  const Validator = createRequire(import.meta.url)('fastest-validator');
10
33
  const v = new Validator({ useNewCustomCheckerFunction: true });
@@ -20,6 +43,13 @@ export class Registry {
20
43
  find(name) {
21
44
  return this.byName.get(name);
22
45
  }
46
+ // START_CONTRACT: Registry.resolve
47
+ // PURPOSE: Вернуть класс по id или alias, иначе бросить ошибку со списком известных.
48
+ // INPUTS: { name: string - id или alias класса }
49
+ // OUTPUTS: { ClassDef - найденный класс }
50
+ // SIDE_EFFECTS: none
51
+ // LINKS: M-SCHEMA, V-M-SCHEMA
52
+ // END_CONTRACT: Registry.resolve
23
53
  /** Класс по id или alias; иначе понятная ошибка со списком. */
24
54
  resolve(name) {
25
55
  const def = this.byName.get(name);
@@ -33,6 +63,13 @@ export class Registry {
33
63
  return this.byName.has(name);
34
64
  }
35
65
  }
66
+ // START_CONTRACT: fieldTypeOf
67
+ // PURPOSE: Свести attribute-спеку fastest-validator (строка/массив/объект) к FieldType.
68
+ // INPUTS: { attr: unknown - правило поля из attributes }
69
+ // OUTPUTS: { FieldType - вид поля (number/date/boolean/string/array/record/object/any) }
70
+ // SIDE_EFFECTS: none (рекурсивно по вложенным props/value)
71
+ // LINKS: M-SCHEMA, V-M-SCHEMA, type-FieldType
72
+ // END_CONTRACT: fieldTypeOf
36
73
  /** Тип поля из fastest-validator DSL — для каста в SQL. */
37
74
  export function fieldTypeOf(attr) {
38
75
  if (typeof attr === 'string') {
@@ -88,7 +125,84 @@ export function fieldTypeOf(attr) {
88
125
  }
89
126
  return { kind: 'any' };
90
127
  }
128
+ /**
129
+ * Элемент Schema.links: объект-конец v2 (jsonb) либо legacy-строка 'Org'
130
+ * (включая JSON-текст в text[] у старых схем). Возвращает [конец, isV2].
131
+ */
132
+ // START_CONTRACT: parseEnd
133
+ // PURPOSE: Разобрать один элемент Schema.links в LinkEnd (объект-конец v2 или legacy-строка).
134
+ // INPUTS: { cls: string - id класса; raw: string|object - сырой конец из jsonb/text[] }
135
+ // OUTPUTS: { [LinkEnd, boolean] - конец и признак v2-формы (для strictEnds) }
136
+ // SIDE_EFFECTS: none
137
+ // ERRORS: malformed link end (bad JSON); link end without classes
138
+ // LINKS: M-SCHEMA, V-M-SCHEMA
139
+ // END_CONTRACT: parseEnd
140
+ function parseEnd(cls, raw) {
141
+ let o;
142
+ if (typeof raw === 'string') {
143
+ if (!raw.trimStart().startsWith('{'))
144
+ return [{ classes: [raw] }, false];
145
+ try {
146
+ o = JSON.parse(raw);
147
+ }
148
+ catch {
149
+ throw new Error(`letopis: class "${cls}" has malformed link end (bad JSON): ${raw}`);
150
+ }
151
+ }
152
+ else {
153
+ o = raw;
154
+ }
155
+ const classes = o.classes ?? (o.class ? [o.class] : []);
156
+ if (!classes.length || classes.some((c) => typeof c !== 'string' || !c)) {
157
+ throw new Error(`letopis: class "${cls}" has link end without classes: ${JSON.stringify(raw)}`);
158
+ }
159
+ return [{ classes, optional: o.optional === true, cardinality: o.cardinality }, true];
160
+ }
161
+ /**
162
+ * attributes.id: правило валидации + (объектом) спецификация генерации.
163
+ * generate/from — ключи letopis, вырезаются из правила перед компиляцией валидатора.
164
+ */
165
+ // START_CONTRACT: parseIdGen
166
+ // PURPOSE: Отделить спецификацию генерации id (generate/from) от правила валидации attributes.id.
167
+ // INPUTS: { cls: string - id класса; attributes: object - слитые attributes класса }
168
+ // OUTPUTS: { { idGen: IdGen; attrs: object } - версия генерации id и очищенное правило }
169
+ // SIDE_EFFECTS: none
170
+ // ERRORS: generate must be 4|5|7; generate:5 needs "from"; from is for generate:5 only
171
+ // LINKS: M-SCHEMA, V-M-SCHEMA
172
+ // END_CONTRACT: parseIdGen
173
+ function parseIdGen(cls, attributes) {
174
+ const raw = attributes.id;
175
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
176
+ return { idGen: { version: 4 }, attrs: attributes };
177
+ }
178
+ const { generate, from, ...rule } = raw;
179
+ const attrs = { ...attributes, id: rule };
180
+ if (generate === undefined)
181
+ return { idGen: { version: 4 }, attrs };
182
+ if (generate !== 4 && generate !== 5 && generate !== 7) {
183
+ throw new Error(`letopis: class "${cls}" — attributes.id.generate must be 4 | 5 | 7, got ${JSON.stringify(generate)}`);
184
+ }
185
+ if (generate === 5) {
186
+ if (!Array.isArray(from) || !from.length || from.some((f) => typeof f !== 'string' || !f)) {
187
+ throw new Error(`letopis: class "${cls}" — id generate:5 needs "from": non-empty string[] (link ends / data fields)`);
188
+ }
189
+ return { idGen: { version: 5, from: from }, attrs };
190
+ }
191
+ if (from !== undefined) {
192
+ throw new Error(`letopis: class "${cls}" — attributes.id.from is for generate:5 only`);
193
+ }
194
+ return { idGen: { version: generate }, attrs };
195
+ }
196
+ // START_CONTRACT: buildDef
197
+ // PURPOSE: Собрать ClassDef из строки Schema — цепочка ancestors, наследование attributes, id-gen, компиляция check, типы полей, link-ends.
198
+ // INPUTS: { row: SchemaRow - строка класса; byId: Map<string,SchemaRow> - все классы партиции }
199
+ // OUTPUTS: { ClassDef - полностью разрешённое определение класса }
200
+ // SIDE_EFFECTS: none (компилирует fastest-validator)
201
+ // ERRORS: id v5 can't depend on optional end; id "from" source is neither end nor field
202
+ // LINKS: M-SCHEMA, V-M-SCHEMA, type-ClassDef
203
+ // END_CONTRACT: buildDef
91
204
  function buildDef(row, byId) {
205
+ // START_BLOCK_INHERIT_ANCESTORS
92
206
  // цепочка наследования: из БД или walk по ancestor
93
207
  let ancestors = row.ancestors ?? [];
94
208
  if (!ancestors.length) {
@@ -101,18 +215,57 @@ function buildDef(row, byId) {
101
215
  cur = byId.get(cur)?.ancestor ?? null;
102
216
  }
103
217
  }
218
+ // attributes НАСЛЕДУЮТСЯ по цепочке ancestor: потомок ПОВЕРХ предка, переопределение
219
+ // поля — замена правила ЦЕЛИКОМ (не слияние объекта-правила). links не наследуются.
220
+ const merged = {};
221
+ for (let i = ancestors.length - 1; i >= 0; i--) {
222
+ Object.assign(merged, byId.get(ancestors[i])?.attributes ?? {});
223
+ }
224
+ // END_BLOCK_INHERIT_ANCESTORS
225
+ // attributes.id: спецификация генерации (generate/from) отделяется от правила валидации
226
+ const { idGen, attrs } = parseIdGen(row.id, merged);
104
227
  // fastest-validator: валидируем {id, ...data}; СТРОГО — лишние поля запрещены
105
- const compiled = v.compile({ ...row.attributes, $$strict: true });
228
+ const compiled = v.compile({ ...attrs, $$strict: true });
106
229
  const check = (data) => {
107
230
  const res = compiled(data);
108
231
  return res === true ? true : res;
109
232
  };
110
233
  const fieldTypes = new Map();
111
- for (const [field, attr] of Object.entries(row.attributes)) {
234
+ for (const [field, attr] of Object.entries(attrs)) {
112
235
  if (field.startsWith('$$'))
113
236
  continue;
114
237
  fieldTypes.set(field, fieldTypeOf(attr));
115
238
  }
239
+ let strictEnds = false;
240
+ // jsonb-массив; text[] у legacy-схем даёт string[]; jsonb-строка "[…]" (двойная
241
+ // сериализация внешних писателей) — распарсить
242
+ let rawLinks = row.links ?? [];
243
+ if (typeof rawLinks === 'string')
244
+ rawLinks = JSON.parse(rawLinks);
245
+ const ends = rawLinks.map((raw) => {
246
+ const [end, v2] = parseEnd(row.id, raw);
247
+ if (v2)
248
+ strictEnds = true;
249
+ return end;
250
+ });
251
+ // START_BLOCK_ID_V5_VALIDATE
252
+ // id v5: каждый источник from — обязательный конец links (класс или полное имя союза
253
+ // 'Service|Complex') ЛИБО поле data
254
+ if (idGen.version === 5) {
255
+ for (const f of idGen.from) {
256
+ const end = ends.find((e) => e.classes.includes(f) || e.classes.join('|') === f);
257
+ if (end) {
258
+ if (end.optional) {
259
+ throw new Error(`letopis: class "${row.id}" — id (uuid v5) can't depend on optional end "${f}"`);
260
+ }
261
+ continue;
262
+ }
263
+ if (f !== 'id' && f in attrs)
264
+ continue;
265
+ throw new Error(`letopis: class "${row.id}" — id "from" source "${f}" is neither a link end nor a data field`);
266
+ }
267
+ }
268
+ // END_BLOCK_ID_V5_VALIDATE
116
269
  return {
117
270
  id: row.id,
118
271
  alias: row.alias,
@@ -120,8 +273,10 @@ function buildDef(row, byId) {
120
273
  ancestor: row.ancestor,
121
274
  ancestors,
122
275
  descendants: row.descendants ?? [], // считает триггер schema_lineage
123
- attributes: row.attributes,
124
- links: row.links ?? [],
276
+ attributes: merged,
277
+ links: ends,
278
+ strictEnds,
279
+ idGen,
125
280
  meta: row.meta ?? {},
126
281
  abstract: row.meta?.abstract === true || row.meta?.abstract === 'true',
127
282
  order: row.order,
@@ -129,13 +284,23 @@ function buildDef(row, byId) {
129
284
  fieldTypes,
130
285
  };
131
286
  }
287
+ // START_CONTRACT: loadRegistry
288
+ // PURPOSE: Прочитать классы партиции из таблицы Schema и построить Registry со скомпилированными check.
289
+ // INPUTS: { sql: postgres.Sql; pgSchema: string; partition: string }
290
+ // OUTPUTS: { Promise<Registry> - реестр всех классов партиции }
291
+ // SIDE_EFFECTS: SELECT из "<pgSchema>"."Schema"
292
+ // ERRORS: schema has no classes for partition
293
+ // LINKS: M-SCHEMA, V-M-SCHEMA, M-DDL
294
+ // END_CONTRACT: loadRegistry
132
295
  export async function loadRegistry(sql, pgSchema, partition) {
296
+ // START_BLOCK_LOAD_QUERY
133
297
  const ident = `"${pgSchema.replace(/"/g, '""')}"`;
134
298
  const rows = (await sql.unsafe(`SELECT id, alias, category, ancestor, attributes, links, meta, "order", ancestors, descendants
135
299
  FROM ${ident}."Schema" WHERE partition = $1 ORDER BY category, "order"`, [partition]));
136
300
  if (!rows.length) {
137
301
  throw new Error(`letopis: schema "${pgSchema}" has no classes for partition "${partition}"`);
138
302
  }
303
+ // END_BLOCK_LOAD_QUERY
139
304
  const byId = new Map(rows.map((r) => [r.id, r]));
140
305
  const registry = new Registry();
141
306
  for (const row of rows)
package/dist/sessions.js CHANGED
@@ -3,16 +3,56 @@
3
3
  * Клиент НЕ входит в зависимости — инжектируется пользователем (интерфейс ioredis-совместим).
4
4
  * В store лежит только sha256-хэш токена: дамп Redis не раскрывает действующие токены.
5
5
  */
6
+ //
7
+ // FILE: lib/src/sessions.ts
8
+ // VERSION: 1.0.0
9
+ // START_MODULE_CONTRACT
10
+ // PURPOSE: Жизненный цикл сессий поверх инжектируемого Redis-совместимого хранилища (start/check/revoke/revokeAll).
11
+ // SCOPE: интерфейсы SessionStore/Session/Sessions + фабрика makeSessions с операциями старта, проверки и отзыва сессий; токены хранятся как sha256 под ключами sess:* / sess:acc:* с TTL.
12
+ // DEPENDS: none
13
+ // LINKS: M-SESSIONS, V-M-SESSIONS
14
+ // ROLE: RUNTIME
15
+ // MAP_MODE: EXPORTS
16
+ // END_MODULE_CONTRACT
17
+ //
18
+ // START_MODULE_MAP
19
+ // SessionStore - минимальный срез Redis-клиента (set/get/del/sadd/srem/smembers/expire).
20
+ // Session - полезная нагрузка сессии: account, meta, created.
21
+ // Sessions - контракт сервиса сессий: start/check/revoke/revokeAll.
22
+ // sha256 - (локальный) hex-хэш строки по sha256.
23
+ // accId - (локальный) нормализует account: string | { id } → id.
24
+ // DEFAULT_TTL - (локальный) TTL по умолчанию: 7 суток в секундах.
25
+ // makeSessions - фабрика: связывает store и возвращает объект Sessions.
26
+ // END_MODULE_MAP
27
+ //
28
+ // START_CHANGE_SUMMARY
29
+ // LAST_CHANGE: [v1.0.0 - Documented existing module: reverse-engineered contract + markup]
30
+ // END_CHANGE_SUMMARY
6
31
  import { createHash, randomBytes } from 'node:crypto';
7
32
  const sha256 = (s) => createHash('sha256').update(s).digest('hex');
8
33
  const accId = (a) => (typeof a === 'object' ? a.id : a);
9
34
  const DEFAULT_TTL = 7 * 24 * 3600;
35
+ // START_CONTRACT: makeSessions
36
+ // PURPOSE: Фабрика сервиса сессий: связывает инжектируемый store и возвращает объект Sessions.
37
+ // INPUTS: { store: SessionStore - Redis-совместимое хранилище (инжектируется пользователем) }
38
+ // OUTPUTS: { Sessions - объект с методами start/check/revoke/revokeAll }
39
+ // SIDE_EFFECTS: none при создании; возвращённые методы читают/пишут store (ключи sess:* / sess:acc:*)
40
+ // LINKS: M-SESSIONS, V-M-SESSIONS
41
+ // END_CONTRACT: makeSessions
10
42
  export function makeSessions(store) {
11
43
  const kTok = (h) => `sess:${h}`;
12
44
  // индекс аккаунта без TTL: протухшие хэши безвредны, полная чистка — revokeAll
13
45
  const kAcc = (id) => `sess:acc:${id}`;
14
46
  return {
47
+ // START_CONTRACT: start
48
+ // PURPOSE: Создать новую сессию и вернуть одноразовый токен (в store — только его sha256).
49
+ // INPUTS: { account: string | { id: string } - аккаунт, opts?: { ttlSec?: number; meta?: Record<string, unknown> } - TTL (default 7 суток) и метаданные }
50
+ // OUTPUTS: { Promise<string> - секретный токен (32 байта hex, отдаётся один раз) }
51
+ // SIDE_EFFECTS: store.set(sess:<h>, payload, EX ttl) + store.sadd(sess:acc:<id>, h)
52
+ // LINKS: M-SESSIONS, V-M-SESSIONS
53
+ // END_CONTRACT: start
15
54
  async start(account, opts = {}) {
55
+ // START_BLOCK_START_MINT_AND_PERSIST
16
56
  const id = accId(account);
17
57
  const token = randomBytes(32).toString('hex');
18
58
  const h = sha256(token);
@@ -20,13 +60,29 @@ export function makeSessions(store) {
20
60
  const payload = { account: id, meta: opts.meta ?? {}, created: new Date().toISOString() };
21
61
  await store.set(kTok(h), JSON.stringify(payload), 'EX', ttl);
22
62
  await store.sadd(kAcc(id), h);
63
+ // END_BLOCK_START_MINT_AND_PERSIST
23
64
  return token;
24
65
  },
66
+ // START_CONTRACT: check
67
+ // PURPOSE: Проверить токен и вернуть данные сессии либо null.
68
+ // INPUTS: { token: string - секретный токен }
69
+ // OUTPUTS: { Promise<Session | null> - null = нет / просрочена / отозвана }
70
+ // SIDE_EFFECTS: store.get(sess:<sha256(token)>) (только чтение)
71
+ // LINKS: M-SESSIONS, V-M-SESSIONS
72
+ // END_CONTRACT: check
25
73
  async check(token) {
26
74
  const raw = await store.get(kTok(sha256(token)));
27
75
  return raw ? JSON.parse(raw) : null;
28
76
  },
77
+ // START_CONTRACT: revoke
78
+ // PURPOSE: Отозвать одну сессию по токену.
79
+ // INPUTS: { token: string - секретный токен }
80
+ // OUTPUTS: { Promise<boolean> - true если сессия была и удалена, false если её нет }
81
+ // SIDE_EFFECTS: store.del(sess:<h>) + store.srem(sess:acc:<account>, h)
82
+ // LINKS: M-SESSIONS, V-M-SESSIONS
83
+ // END_CONTRACT: revoke
29
84
  async revoke(token) {
85
+ // START_BLOCK_REVOKE_LOOKUP_AND_DELETE
30
86
  const h = sha256(token);
31
87
  const raw = await store.get(kTok(h));
32
88
  if (!raw)
@@ -35,14 +91,24 @@ export function makeSessions(store) {
35
91
  await store.del(kTok(h));
36
92
  await store.srem(kAcc(account), h);
37
93
  return true;
94
+ // END_BLOCK_REVOKE_LOOKUP_AND_DELETE
38
95
  },
96
+ // START_CONTRACT: revokeAll
97
+ // PURPOSE: Погасить все сессии аккаунта и очистить его индекс.
98
+ // INPUTS: { account: string | { id: string } - аккаунт }
99
+ // OUTPUTS: { Promise<number> - сколько хэшей было в индексе аккаунта }
100
+ // SIDE_EFFECTS: store.del(все sess:<h>) + store.del(sess:acc:<id>)
101
+ // LINKS: M-SESSIONS, V-M-SESSIONS
102
+ // END_CONTRACT: revokeAll
39
103
  async revokeAll(account) {
104
+ // START_BLOCK_REVOKE_ALL_PURGE
40
105
  const id = accId(account);
41
106
  const hs = await store.smembers(kAcc(id));
42
107
  if (hs.length)
43
108
  await store.del(...hs.map(kTok));
44
109
  await store.del(kAcc(id));
45
110
  return hs.length;
111
+ // END_BLOCK_REVOKE_ALL_PURGE
46
112
  },
47
113
  };
48
114
  }
package/dist/sql.d.ts CHANGED
@@ -55,11 +55,25 @@ export declare const retryDelay: (attempt: number) => Promise<void>;
55
55
  * Служебные запросы (loadRegistry, tables, listen) через неё не идут.
56
56
  */
57
57
  export declare function runQuery<T>(ctx: Ctx, text: string, params: unknown[], mode: QueryEvent['mode'], classes: string[]): Promise<T[]>;
58
+ /** Значение слота связи: id | вложенный план | null (снять конец). */
59
+ export type SlotValue = string | {
60
+ plan: Step[];
61
+ mods?: ChainMods;
62
+ } | null;
63
+ /** Уникальная метка узла пути (для pivot-возвратов; переживает нарезку плана). */
64
+ export declare const nextNodeKey: () => number;
58
65
  export interface Step {
59
66
  /** Имя, как вызвано в цепочке (алиас или id). */
60
67
  name: string;
61
68
  cls: ClassDef;
62
69
  filter?: Filter;
70
+ /** Метка узла (есть у каждого реального шага). */
71
+ nodeKey?: number;
72
+ /**
73
+ * Pivot: шаг-ВОЗВРАТ к уже введённому узлу (повтор LINK-класса / entity(та же переменная)).
74
+ * Не создаёт узел; его фильтры дофильтровывают узел (AND); следующий шаг ветвится от узла.
75
+ */
76
+ pivotKey?: number;
63
77
  /** .alias(name) — ключ шага в путях. */
64
78
  aliasKey?: string;
65
79
  /** .tags(…) — фильтр по колонке tags: строка | string[] (все) | has/hasAny/hasAll. */
@@ -70,13 +84,17 @@ export interface Step {
70
84
  ownerFilter?: string;
71
85
  /** Внутреннее (write-цепочки): containment по links — контекст-связи. */
72
86
  linksFilter?: Record<string, string>;
73
- /** .link(Класс, target) — значение связи при записи БЕЗ участия в фильтре целей. */
74
- extraLinks?: Record<string, string>;
87
+ /**
88
+ * Слоты связей (`.Класс.set(target)` / `.Класс.unset()`): значения концов записываемой
89
+ * версии БЕЗ участия в фильтре целей. string — id; {plan} — вложенная цепочка (та же
90
+ * транзакция, ровно одна сущность класса конца); null — снять optional-конец.
91
+ */
92
+ extraLinks?: Record<string, SlotValue>;
75
93
  /** Внутреннее (enforceAcl): предикат WRITE/DELETE-правила — цели ищутся только среди своих. */
76
94
  aclFilter?: Record<string, unknown>;
77
95
  /** .deep(max): рекурсивный self-обход (дети любой глубины), только reverse того же класса. */
78
96
  deepMax?: number;
79
- /** Операция записи на шаге (.set/.delete/.anonymize) — исполняется терминалом плана. */
97
+ /** Операция записи на шаге (.create/.update/.delete/.anonymize) — исполняется терминалом плана. */
80
98
  op?: import('./types.js').PlanOp;
81
99
  /** Внутреннее: шаг «те же сущности» (операция сразу после операции) — цель = строки старта. */
82
100
  self?: boolean;
@@ -85,6 +103,8 @@ export declare const entityTable: (pgSchema: string) => string;
85
103
  /** Тип листа data-пути по Schema: спуск через record (значение) и object (props). */
86
104
  export declare function leafType(cls: ClassDef, path: string[]): FieldType | undefined;
87
105
  type HopMode = 'forward' | 'reverse';
106
+ /** Класс target входит в какой-нибудь конец def (союзы учитываются). */
107
+ export declare function linksTo(def: ClassDef, target: string): boolean;
88
108
  /** Правило обхода между шагами (по реестру Schema). */
89
109
  export declare function resolveHop(prev: ClassDef, next: ClassDef): HopMode;
90
110
  export type ReadMode = 'paths' | 'rows' | 'ids' | 'count' | 'versions' | 'agg';