letopis 0.16.0 → 0.18.1

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/chain.js CHANGED
@@ -5,6 +5,13 @@ import { makeTables } from './tables.js';
5
5
  import { makeAuth } from './auth.js';
6
6
  import { makeAcl } from './acl.js';
7
7
  /** Фильтр-аргумент принимает и Row/Account-объект — берётся его id. */
8
+ // START_CONTRACT: normalizeFilter
9
+ // PURPOSE: Нормализует фильтр-аргумент — Row/Account-объект сворачивается в свой id.
10
+ // INPUTS: { f: Filter | undefined - id-строка, объект-предикат, массив либо Row/Account }
11
+ // OUTPUTS: { Filter | undefined - id-строка для Row/Account, иначе исходный фильтр }
12
+ // SIDE_EFFECTS: none
13
+ // LINKS: M-CHAIN, V-M-CHAIN
14
+ // END_CONTRACT: normalizeFilter
8
15
  function normalizeFilter(f) {
9
16
  if (f !== undefined &&
10
17
  typeof f === 'object' &&
@@ -20,10 +27,24 @@ const clsOf = (steps) => steps.map((s) => s.cls.id);
20
27
  export const PLAN = Symbol('letopis.plan');
21
28
  const peekPlan = (x) => x !== null && typeof x === 'object' ? x[PLAN] : undefined;
22
29
  /** Конец схемы владельца, принимающий класс слота (legacy: 'Entity'-конец берёт любой HUB). */
30
+ // START_CONTRACT: findEnd
31
+ // PURPOSE: Находит конец Schema.links владельца, принимающий класс слота (legacy: конец 'Entity' берёт любой HUB при нестрогих концах).
32
+ // INPUTS: { owner: ClassDef - класс-владелец связей; slot: ClassDef - класс устанавливаемого конца }
33
+ // OUTPUTS: { LinkEnd | undefined - подходящий конец связи или undefined }
34
+ // SIDE_EFFECTS: none
35
+ // LINKS: M-CHAIN, V-M-CHAIN
36
+ // END_CONTRACT: findEnd
23
37
  function findEnd(owner, slot) {
24
38
  return owner.links.find((e) => e.classes.includes(slot.id) || (!owner.strictEnds && e.classes.includes('Entity') && slot.category === 'HUB'));
25
39
  }
26
40
  /** Значение слота: id-строка | Row | цепочка (одношаговый адрес сворачивается в id). */
41
+ // START_CONTRACT: slotValue
42
+ // PURPOSE: Приводит значение слота к SlotValue — id-строка, Row (берётся id) или ленивая цепочка (одношаговый адрес db.Класс(id) сворачивается в id, иначе вложенный план).
43
+ // INPUTS: { slotCls: ClassDef - класс конца связи; v: unknown - id | Row | Chain }
44
+ // OUTPUTS: { SlotValue - id-строка либо { plan, mods } вложенной цепочки }
45
+ // SIDE_EFFECTS: бросает при цепочке чужого класса или неподдержанном значении
46
+ // LINKS: M-CHAIN, V-M-CHAIN
47
+ // END_CONTRACT: slotValue
27
48
  function slotValue(slotCls, v) {
28
49
  if (typeof v === 'string')
29
50
  return v;
@@ -44,6 +65,13 @@ function slotValue(slotCls, v) {
44
65
  throw new Error(`letopis: slot "${slotCls.id}" accepts an id, a Row or a chain`);
45
66
  }
46
67
  /** Узел для pivot: последний реальный шаг класса cls в ТЕКУЩЕМ читающем сегменте (до op). */
68
+ // START_CONTRACT: pivotNodeOf
69
+ // PURPOSE: Ищет последний реальный шаг класса cls в текущем читающем сегменте (до первой операции) — узел для pivot-возврата.
70
+ // INPUTS: { steps: Step[] - шаги цепочки; cls: ClassDef - искомый класс }
71
+ // OUTPUTS: { Step | undefined - шаг-узел, либо undefined если операция закрыла паттерн }
72
+ // SIDE_EFFECTS: none
73
+ // LINKS: M-CHAIN, V-M-CHAIN
74
+ // END_CONTRACT: pivotNodeOf
47
75
  function pivotNodeOf(steps, cls) {
48
76
  for (let i = steps.length - 1; i >= 0; i--) {
49
77
  const s = steps[i];
@@ -54,7 +82,15 @@ function pivotNodeOf(steps, cls) {
54
82
  }
55
83
  return undefined;
56
84
  }
85
+ // START_CONTRACT: runPaths
86
+ // PURPOSE: Исполняет читающий план в режиме путей — строит SQL через buildRead и мапит строки в Path (узлы каждого варианта).
87
+ // INPUTS: { ctx: Ctx - контекст соединения/схемы; steps: Step[] - шаги; mods: ChainMods - модификаторы }
88
+ // OUTPUTS: { Promise<Path[]> - массив путей вида { ключ_шага: Row } }
89
+ // SIDE_EFFECTS: читает БД через M-SQL.buildRead/runQuery
90
+ // LINKS: M-CHAIN, V-M-CHAIN
91
+ // END_CONTRACT: runPaths
57
92
  async function runPaths(ctx, steps, mods) {
93
+ // START_BLOCK_RUN_PATHS
58
94
  const q = buildRead(ctx, steps, mods, 'paths');
59
95
  const res = await runQuery(ctx, q.text, q.params, 'paths', clsOf(steps));
60
96
  return res.map((r) => {
@@ -63,8 +99,17 @@ async function runPaths(ctx, steps, mods) {
63
99
  out[k] = toRow(v);
64
100
  return out;
65
101
  });
102
+ // END_BLOCK_RUN_PATHS
66
103
  }
104
+ // START_CONTRACT: runRows
105
+ // PURPOSE: Исполняет читающий план в режиме строк — уникальные сущности последнего шага, с $depth при рекурсивном обходе.
106
+ // INPUTS: { ctx: Ctx; steps: Step[]; mods: ChainMods }
107
+ // OUTPUTS: { Promise<Row[]> - строки последнего шага (с $depth при deep) }
108
+ // SIDE_EFFECTS: читает БД через M-SQL.buildRead/runQuery
109
+ // LINKS: M-CHAIN, V-M-CHAIN
110
+ // END_CONTRACT: runRows
67
111
  async function runRows(ctx, steps, mods) {
112
+ // START_BLOCK_RUN_ROWS
68
113
  const q = buildRead(ctx, steps, mods, 'rows');
69
114
  const res = await runQuery(ctx, q.text, q.params, 'rows', clsOf(steps));
70
115
  return res.map((r) => {
@@ -72,13 +117,22 @@ async function runRows(ctx, steps, mods) {
72
117
  const d = r.row.depth;
73
118
  return d != null ? { ...row, $depth: d } : row;
74
119
  });
120
+ // END_BLOCK_RUN_ROWS
75
121
  }
76
122
  const NO_NODES = new Map();
77
123
  /**
78
124
  * Свойство-класс (вызов = шаг/pivot; без скобок = слот). ownerSteps undefined — корень
79
125
  * (db/batch): слот без владельца — ошибка.
80
126
  */
127
+ // START_CONTRACT: classProp
128
+ // PURPOSE: Строит StepProp для класса — вызов даёт шаг-навигацию или pivot-возврат, обращение без скобок — слот связи (set/unset) записываемой версии.
129
+ // INPUTS: { ctx: Ctx; name: string - имя/alias класса; cls: ClassDef; ownerSteps: Step[] | undefined - шаги владельца (undefined = корень db/batch); mods: ChainMods; batch: BatchRef | undefined; nodes: NodeMap - карта entity-идентичности; applySlot?: (slotCls, value) => Chain }
130
+ // OUTPUTS: { StepProp - вызываемый (шаг) Proxy с методами set/unset }
131
+ // SIDE_EFFECTS: возвращает новые Chain; бросает подсказки при слоте без владельца / не-конце / обращении без вызова
132
+ // LINKS: M-CHAIN, V-M-CHAIN
133
+ // END_CONTRACT: classProp
81
134
  function classProp(ctx, name, cls, ownerSteps, mods, batch, nodes, applySlot) {
135
+ // START_BLOCK_CLASS_STEP
82
136
  const step = (filter) => {
83
137
  const prev = ownerSteps?.[ownerSteps.length - 1];
84
138
  // повтор LINK-класса в текущем паттерне — возврат к узлу (pivot)
@@ -92,6 +146,8 @@ function classProp(ctx, name, cls, ownerSteps, mods, batch, nodes, applySlot) {
92
146
  const next = { name, cls, filter: normalizeFilter(filter), nodeKey: nextNodeKey() };
93
147
  return makeChain(ctx, [...(ownerSteps ?? []), next], ownerSteps ? mods : {}, batch, nodes);
94
148
  };
149
+ // END_BLOCK_CLASS_STEP
150
+ // START_BLOCK_LINK_SLOT
95
151
  const slotGuard = () => {
96
152
  if (!ownerSteps?.length || !applySlot) {
97
153
  throw new Error(`letopis: slot "${name}" needs an owner step — start with a class call: db.Класс(…).${name}.set(…)`);
@@ -132,9 +188,18 @@ function classProp(ctx, name, cls, ownerSteps, mods, batch, nodes, applySlot) {
132
188
  throw new Error(`letopis: "${name}" without call is a link slot (set/unset); for navigation call ${name}(…)`);
133
189
  },
134
190
  });
191
+ // END_BLOCK_LINK_SLOT
135
192
  }
193
+ // START_CONTRACT: makeChain
194
+ // PURPOSE: Собирает Proxy-цепочку над списком шагов — модификаторы и слоты порождают новые цепочки, терминалы делегируют чтение (M-SQL) или исполнение плана (M-WRITE.runPlan).
195
+ // INPUTS: { ctx: Ctx; steps: Step[] - накопленные шаги; mods: ChainMods; batch?: BatchRef - контекст батча; nodes?: NodeMap - entity-идентичность }
196
+ // OUTPUTS: { Chain - Proxy с терминалами/модификаторами и свойствами-классами }
197
+ // SIDE_EFFECTS: терминалы читают/пишут БД (runPaths/runRows/runQuery, runPlan); в батче мутирует очередь через syncBatch
198
+ // LINKS: M-CHAIN, V-M-CHAIN
199
+ // END_CONTRACT: makeChain
136
200
  function makeChain(ctx, steps, mods, batch, nodes = NO_NODES) {
137
201
  const hasOps = steps.some((s) => s.op);
202
+ // START_BLOCK_STEP_MUTATORS
138
203
  const withLast = (patch) => {
139
204
  const last = steps[steps.length - 1];
140
205
  if (last.op) {
@@ -145,6 +210,8 @@ function makeChain(ctx, steps, mods, batch, nodes = NO_NODES) {
145
210
  return makeChain(ctx, next, mods, batch, nodes);
146
211
  };
147
212
  const withMods = (patch) => makeChain(ctx, steps, { ...mods, ...patch }, batch, nodes);
213
+ // END_BLOCK_STEP_MUTATORS
214
+ // START_BLOCK_WRITE_DISPATCH
148
215
  /** Обновить план в батче (регистрируется на первой операции, дальше мутируется). */
149
216
  const syncBatch = (next) => {
150
217
  if (!batch)
@@ -188,6 +255,8 @@ function makeChain(ctx, steps, mods, batch, nodes = NO_NODES) {
188
255
  syncBatch(next);
189
256
  return makeChain(ctx, next, mods, batch, nodes);
190
257
  };
258
+ // END_BLOCK_WRITE_DISPATCH
259
+ // START_BLOCK_ENTITY_EMBED
191
260
  /** entity(x): вклейка узла/паттерна; та же переменная повторно — возврат к её узлу. */
192
261
  const entityStep = (x) => {
193
262
  const plan = peekPlan(x);
@@ -220,6 +289,7 @@ function makeChain(ctx, steps, mods, batch, nodes = NO_NODES) {
220
289
  }
221
290
  throw new Error('letopis: entity() accepts a lazy chain or a Row');
222
291
  };
292
+ // END_BLOCK_ENTITY_EMBED
223
293
  /** Терминал: план (есть операции) — runPlan одной транзакцией; иначе прямое чтение. */
224
294
  const guardBatch = () => {
225
295
  if (batch && hasOps) {
@@ -233,6 +303,7 @@ function makeChain(ctx, steps, mods, batch, nodes = NO_NODES) {
233
303
  if (typeof prop === 'symbol' || prop === 'then')
234
304
  return undefined;
235
305
  switch (prop) {
306
+ // START_BLOCK_READ_TERMINALS
236
307
  case 'entity':
237
308
  return entityStep;
238
309
  case 'run':
@@ -273,6 +344,8 @@ function makeChain(ctx, steps, mods, batch, nodes = NO_NODES) {
273
344
  const res = await runQuery(ctx, q.text, q.params, 'count', clsOf(steps));
274
345
  return res[0].n;
275
346
  };
347
+ // END_BLOCK_READ_TERMINALS
348
+ // START_BLOCK_READ_MODIFIERS
276
349
  case 'limit':
277
350
  return (n) => withMods({ limit: n });
278
351
  case 'offset':
@@ -283,6 +356,8 @@ function makeChain(ctx, steps, mods, batch, nodes = NO_NODES) {
283
356
  return (t) => withMods({ asOf: t instanceof Date ? t.toISOString() : t });
284
357
  case 'deep':
285
358
  return (max = 32) => withLast({ deepMax: max });
359
+ // END_BLOCK_READ_MODIFIERS
360
+ // START_BLOCK_AGGREGATIONS
286
361
  case 'sum':
287
362
  case 'avg':
288
363
  case 'min':
@@ -319,6 +394,8 @@ function makeChain(ctx, steps, mods, batch, nodes = NO_NODES) {
319
394
  })();
320
395
  return Object.fromEntries(res.map((r) => [r.k ?? 'null', r.n]));
321
396
  };
397
+ // END_BLOCK_AGGREGATIONS
398
+ // START_BLOCK_VERSIONS_CURSOR
322
399
  case 'after':
323
400
  return (cursor) => withMods({ after: cursor });
324
401
  case 'versions':
@@ -334,6 +411,8 @@ function makeChain(ctx, steps, mods, batch, nodes = NO_NODES) {
334
411
  return raw.deleted ? { ...row, $deleted: true } : row;
335
412
  });
336
413
  };
414
+ // END_BLOCK_VERSIONS_CURSOR
415
+ // START_BLOCK_WRITE_TERMINALS
337
416
  case 'create':
338
417
  return (data = {}) => {
339
418
  const last = steps[steps.length - 1];
@@ -355,6 +434,8 @@ function makeChain(ctx, steps, mods, batch, nodes = NO_NODES) {
355
434
  return (opts) => withOp({ kind: 'delete', confirm: opts?.confirm === true, mods });
356
435
  case 'anonymize':
357
436
  return (fields) => withOp({ kind: 'anonymize', fields, mods });
437
+ // END_BLOCK_WRITE_TERMINALS
438
+ // START_BLOCK_COLUMN_MODS
358
439
  case 'alias':
359
440
  return (name) => withLast({ aliasKey: name });
360
441
  case 'tags':
@@ -365,22 +446,33 @@ function makeChain(ctx, steps, mods, batch, nodes = NO_NODES) {
365
446
  return (v) => withLast({ ownerFilter: typeof v === 'object' ? v.id : v });
366
447
  case 'link':
367
448
  throw new Error('letopis: .link() removed (0.15.0) — use the link slot: .Класс.set(target)');
449
+ // END_BLOCK_COLUMN_MODS
368
450
  }
451
+ // START_BLOCK_CLASS_RESOLVE
369
452
  if (ctx.registry.has(prop)) {
370
453
  return classProp(ctx, prop, ctx.registry.resolve(prop), steps, mods, batch, nodes, applySlot);
371
454
  }
372
455
  ctx.registry.resolve(prop); // неизвестный класс — понятная ошибка со списком
373
456
  return undefined;
457
+ // END_BLOCK_CLASS_RESOLVE
374
458
  },
375
459
  };
376
460
  return new Proxy({}, handler);
377
461
  }
462
+ // START_CONTRACT: makeBatch
463
+ // PURPOSE: Собирает Proxy-батч над общей очередью планов — свойства-классы копят цепочки, run() исполняет всю очередь одной транзакцией.
464
+ // INPUTS: { ctx: Ctx; queue: BatchPlan[] - общая очередь планов батча }
465
+ // OUTPUTS: { Batch - Proxy с run/discard/size и свойствами-классами }
466
+ // SIDE_EFFECTS: run() исполняет и очищает очередь через M-WRITE.executeBatch; discard() очищает очередь
467
+ // LINKS: M-CHAIN, V-M-CHAIN
468
+ // END_CONTRACT: makeBatch
378
469
  function makeBatch(ctx, queue) {
379
470
  const handler = {
380
471
  get(_t, prop) {
381
472
  if (typeof prop === 'symbol' || prop === 'then')
382
473
  return undefined;
383
474
  switch (prop) {
475
+ // START_BLOCK_BATCH_QUEUE
384
476
  case 'run':
385
477
  return async () => {
386
478
  const plans = queue.splice(0, queue.length);
@@ -392,6 +484,7 @@ function makeBatch(ctx, queue) {
392
484
  return () => void queue.splice(0, queue.length);
393
485
  case 'size':
394
486
  return () => queue.length;
487
+ // END_BLOCK_BATCH_QUEUE
395
488
  }
396
489
  if (ctx.registry.has(prop)) {
397
490
  return classProp(ctx, String(prop), ctx.registry.resolve(prop), undefined, {}, { queue }, NO_NODES);
@@ -402,6 +495,13 @@ function makeBatch(ctx, queue) {
402
495
  };
403
496
  return new Proxy({}, handler);
404
497
  }
498
+ // START_CONTRACT: makeDb
499
+ // PURPOSE: Фабрика корневого фасада EntityDb над Ctx — классы как стартовые шаги, транзакции (begin/commit/rollback/lock), батчи, watch, служебные таблицы/auth/acl, entity()-старт.
500
+ // INPUTS: { ctx: Ctx - контекст соединения/схемы (или TxCtx внутри begin()); root?: RootState - общее состояние батчей }
501
+ // OUTPUTS: { EntityDb - Proxy-фасад }
502
+ // SIDE_EFFECTS: begin() открывает транзакцию (M-TX.beginTx); watch() ставит LISTEN на канал = имя схемы (ctx.pgSchema); close() закрывает соединение; лениво поднимает M-TABLES/M-AUTH/M-ACL
503
+ // LINKS: M-CHAIN, V-M-CHAIN
504
+ // END_CONTRACT: makeDb
405
505
  export function makeDb(ctx, root) {
406
506
  const state = root ?? { batches: new Map() };
407
507
  let tables; // лениво, на ctx этого фасада (работает и в tr)
@@ -411,6 +511,7 @@ export function makeDb(ctx, root) {
411
511
  get(_t, prop) {
412
512
  if (typeof prop === 'symbol' || prop === 'then')
413
513
  return undefined;
514
+ // START_BLOCK_TABLES_AUTH_ACL
414
515
  if (prop === 'accounts' || prop === 'credentials' || prop === 'resources' || prop === 'rules') {
415
516
  tables ??= makeTables(ctx);
416
517
  return tables[prop];
@@ -425,7 +526,9 @@ export function makeDb(ctx, root) {
425
526
  acl ??= makeAcl(tables, ctx.registry);
426
527
  return acl;
427
528
  }
529
+ // END_BLOCK_TABLES_AUTH_ACL
428
530
  switch (prop) {
531
+ // START_BLOCK_TX_LOCK
429
532
  case 'begin':
430
533
  return async () => makeDb(await beginTx(ctx), state);
431
534
  case 'commit':
@@ -448,6 +551,8 @@ export function makeDb(ctx, root) {
448
551
  };
449
552
  case 'lock':
450
553
  return (...keys) => lock(ctx, ...keys);
554
+ // END_BLOCK_TX_LOCK
555
+ // START_BLOCK_BATCH_OPEN
451
556
  case 'batch':
452
557
  return (name) => {
453
558
  let q = state.batches.get(name);
@@ -457,6 +562,8 @@ export function makeDb(ctx, root) {
457
562
  }
458
563
  return makeBatch(ctx, q);
459
564
  };
565
+ // END_BLOCK_BATCH_OPEN
566
+ // START_BLOCK_WATCH_LISTEN
460
567
  case 'watch':
461
568
  return async (a, b, c) => {
462
569
  const cls = typeof a === 'string' ? ctx.registry.resolve(a).id : undefined;
@@ -496,13 +603,17 @@ export function makeDb(ctx, root) {
496
603
  const { unlisten } = await ctx.sql.listen(ctx.pgSchema, listener, onlisten);
497
604
  return unlisten;
498
605
  };
606
+ // END_BLOCK_WATCH_LISTEN
607
+ // START_BLOCK_CLOSE_META
499
608
  case 'close':
500
609
  return () => ctx.sql.end();
501
610
  case 'registry':
502
611
  return ctx.registry;
503
612
  case 'sql':
504
613
  return ctx.sql;
614
+ // END_BLOCK_CLOSE_META
505
615
  }
616
+ // START_BLOCK_ENTITY_START
506
617
  if (prop === 'entity') {
507
618
  // старт пути с готового паттерна/строки: db.entity(pos).…
508
619
  return (x) => makeChain(ctx, [], {}, undefined, NO_NODES).entity(x);
@@ -512,6 +623,7 @@ export function makeDb(ctx, root) {
512
623
  }
513
624
  ctx.registry.resolve(prop);
514
625
  return undefined;
626
+ // END_BLOCK_ENTITY_START
515
627
  },
516
628
  };
517
629
  return new Proxy({}, handler);
package/dist/index.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * letopis: dot-цепочки над append-only Entity-хранилищем (TimescaleDB).
3
+ *
4
+ * const db = await connect({ dsn, schema: 'booking' })
5
+ * await db.Сотрудник({ name: 'Вася' }).навык().Услуга().run()
6
+ */
1
7
  import { type EntityDb } from './chain.js';
2
8
  import type { ConnectOpts } from './types.js';
3
9
  export declare function connect(opts: ConnectOpts): Promise<EntityDb>;
package/dist/index.js CHANGED
@@ -4,11 +4,38 @@
4
4
  * const db = await connect({ dsn, schema: 'booking' })
5
5
  * await db.Сотрудник({ name: 'Вася' }).навык().Услуга().run()
6
6
  */
7
+ // FILE: lib/src/index.ts
8
+ // VERSION: 1.0.0
9
+ // START_MODULE_CONTRACT
10
+ // PURPOSE: Публичная точка входа пакета — открыть соединение (реестр, опциональный ACL, сборка db) и ре-экспорт публичной поверхности; хелпер курсора.
11
+ // SCOPE: connect, cursorOf + barrel-реэкспорты
12
+ // DEPENDS: M-SCHEMA, M-CHAIN, M-TABLES, M-ACL, M-SQL, M-TYPES, M-UP
13
+ // LINKS: M-CONNECT, V-M-CONNECT
14
+ // ROLE: RUNTIME
15
+ // MAP_MODE: EXPORTS
16
+ // END_MODULE_CONTRACT
17
+ //
18
+ // START_MODULE_MAP
19
+ // connect - открывает pg-соединение, грузит реестр, опц. включает ACL, собирает EntityDb
20
+ // cursorOf - курсор keyset-пагинации из последней строки страницы
21
+ // NOTE (re-exports) - up/UpOpts←M-UP; uuidv5/uuidv7/LETOPIS_NS←M-UUID; ops←M-OPS; totpCode/AuthApi/AuthResult←M-AUTH; AclApi←M-ACL; ValidationError←M-WRITE; Registry←M-SCHEMA; Session-типы←M-SESSIONS; базовые типы←M-TYPES; типы chain/tables
22
+ // END_MODULE_MAP
23
+ //
24
+ // START_CHANGE_SUMMARY
25
+ // LAST_CHANGE: [v1.0.0 - Documented existing module: reverse-engineered contract + markup]
26
+ // END_CHANGE_SUMMARY
7
27
  import postgres from 'postgres';
8
28
  import { loadRegistry } from './schema.js';
9
29
  import { makeDb } from './chain.js';
10
30
  import { makeTables } from './tables.js';
11
31
  import { compileEnforcer } from './acl.js';
32
+ // 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 ре-экспортится здесь)
37
+ // LINKS: M-CONNECT, V-M-CONNECT, M-SCHEMA, M-CHAIN, M-TABLES, M-ACL, M-SQL
38
+ // END_CONTRACT: connect
12
39
  export async function connect(opts) {
13
40
  // timestamptz — строкой (JS Date режет микросекунды → ломал бы asOf/cursorOf по updated)
14
41
  const sql = postgres(opts.dsn, {
@@ -32,6 +59,7 @@ export async function connect(opts) {
32
59
  onQuery: opts.onQuery,
33
60
  slowMs: opts.slowMs,
34
61
  };
62
+ // START_BLOCK_ENFORCE_ACL
35
63
  // enforceAcl: правила и категории субъекта фиксируются на connect (перечитка — новый connect)
36
64
  if (opts.enforceAcl) {
37
65
  if (!opts.account)
@@ -46,9 +74,17 @@ export async function connect(opts) {
46
74
  throw new Error(`letopis: enforceAcl — account "${opts.account}" not found`);
47
75
  ctx.aclDecide = compileEnforcer({ resources, rules }, account.id, account.categories, registry);
48
76
  }
77
+ // END_BLOCK_ENFORCE_ACL
49
78
  return makeDb(ctx);
50
79
  }
51
80
  /** Курсор keyset-пагинации из последней строки страницы (field как в .sort(), вложенные пути поддержаны). */
81
+ // START_CONTRACT: cursorOf
82
+ // PURPOSE: Собрать курсор keyset-пагинации из последней строки страницы.
83
+ // INPUTS: { row: Row - строка результата; field?: string='updated' - поле сортировки как в .sort() (вложенные пути 'data.*' поддержаны) }
84
+ // OUTPUTS: { Cursor - { v, id } для следующей страницы }
85
+ // SIDE_EFFECTS: none
86
+ // LINKS: M-CONNECT, V-M-CONNECT, M-TYPES
87
+ // END_CONTRACT: cursorOf
52
88
  export function cursorOf(row, field = 'updated') {
53
89
  const v = field === 'updated'
54
90
  ? row.updated
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
@@ -7,6 +7,27 @@
7
7
  * - скомпилированный fastest-validator per class;
8
8
  * - карта типов полей (для SQL-кастов фильтров).
9
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
10
31
  import { createRequire } from 'node:module';
11
32
  const Validator = createRequire(import.meta.url)('fastest-validator');
12
33
  const v = new Validator({ useNewCustomCheckerFunction: true });
@@ -22,6 +43,13 @@ export class Registry {
22
43
  find(name) {
23
44
  return this.byName.get(name);
24
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
25
53
  /** Класс по id или alias; иначе понятная ошибка со списком. */
26
54
  resolve(name) {
27
55
  const def = this.byName.get(name);
@@ -35,6 +63,13 @@ export class Registry {
35
63
  return this.byName.has(name);
36
64
  }
37
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
38
73
  /** Тип поля из fastest-validator DSL — для каста в SQL. */
39
74
  export function fieldTypeOf(attr) {
40
75
  if (typeof attr === 'string') {
@@ -94,6 +129,14 @@ export function fieldTypeOf(attr) {
94
129
  * Элемент Schema.links: объект-конец v2 (jsonb) либо legacy-строка 'Org'
95
130
  * (включая JSON-текст в text[] у старых схем). Возвращает [конец, isV2].
96
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
97
140
  function parseEnd(cls, raw) {
98
141
  let o;
99
142
  if (typeof raw === 'string') {
@@ -119,6 +162,14 @@ function parseEnd(cls, raw) {
119
162
  * attributes.id: правило валидации + (объектом) спецификация генерации.
120
163
  * generate/from — ключи letopis, вырезаются из правила перед компиляцией валидатора.
121
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
122
173
  function parseIdGen(cls, attributes) {
123
174
  const raw = attributes.id;
124
175
  if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
@@ -142,7 +193,16 @@ function parseIdGen(cls, attributes) {
142
193
  }
143
194
  return { idGen: { version: generate }, attrs };
144
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
145
204
  function buildDef(row, byId) {
205
+ // START_BLOCK_INHERIT_ANCESTORS
146
206
  // цепочка наследования: из БД или walk по ancestor
147
207
  let ancestors = row.ancestors ?? [];
148
208
  if (!ancestors.length) {
@@ -161,6 +221,7 @@ function buildDef(row, byId) {
161
221
  for (let i = ancestors.length - 1; i >= 0; i--) {
162
222
  Object.assign(merged, byId.get(ancestors[i])?.attributes ?? {});
163
223
  }
224
+ // END_BLOCK_INHERIT_ANCESTORS
164
225
  // attributes.id: спецификация генерации (generate/from) отделяется от правила валидации
165
226
  const { idGen, attrs } = parseIdGen(row.id, merged);
166
227
  // fastest-validator: валидируем {id, ...data}; СТРОГО — лишние поля запрещены
@@ -187,6 +248,7 @@ function buildDef(row, byId) {
187
248
  strictEnds = true;
188
249
  return end;
189
250
  });
251
+ // START_BLOCK_ID_V5_VALIDATE
190
252
  // id v5: каждый источник from — обязательный конец links (класс или полное имя союза
191
253
  // 'Service|Complex') ЛИБО поле data
192
254
  if (idGen.version === 5) {
@@ -203,6 +265,7 @@ function buildDef(row, byId) {
203
265
  throw new Error(`letopis: class "${row.id}" — id "from" source "${f}" is neither a link end nor a data field`);
204
266
  }
205
267
  }
268
+ // END_BLOCK_ID_V5_VALIDATE
206
269
  return {
207
270
  id: row.id,
208
271
  alias: row.alias,
@@ -221,13 +284,23 @@ function buildDef(row, byId) {
221
284
  fieldTypes,
222
285
  };
223
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
224
295
  export async function loadRegistry(sql, pgSchema, partition) {
296
+ // START_BLOCK_LOAD_QUERY
225
297
  const ident = `"${pgSchema.replace(/"/g, '""')}"`;
226
298
  const rows = (await sql.unsafe(`SELECT id, alias, category, ancestor, attributes, links, meta, "order", ancestors, descendants
227
299
  FROM ${ident}."Schema" WHERE partition = $1 ORDER BY category, "order"`, [partition]));
228
300
  if (!rows.length) {
229
301
  throw new Error(`letopis: schema "${pgSchema}" has no classes for partition "${partition}"`);
230
302
  }
303
+ // END_BLOCK_LOAD_QUERY
231
304
  const byId = new Map(rows.map((r) => [r.id, r]));
232
305
  const registry = new Registry();
233
306
  for (const row of rows)