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/chain.js CHANGED
@@ -1,10 +1,17 @@
1
- import { buildRead, runQuery, leafType } from './sql.js';
1
+ import { buildRead, runQuery, leafType, resolveHop, nextNodeKey, } from './sql.js';
2
2
  import { runPlan, executeBatch, toRow } from './write.js';
3
3
  import { beginTx, lock } from './tx.js';
4
4
  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' &&
@@ -16,7 +23,74 @@ function normalizeFilter(f) {
16
23
  return f;
17
24
  }
18
25
  const clsOf = (steps) => steps.map((s) => s.cls.id);
26
+ /** Внутренний канал: план чужой ленивой цепочки (слот-значения, entity()). */
27
+ export const PLAN = Symbol('letopis.plan');
28
+ const peekPlan = (x) => x !== null && typeof x === 'object' ? x[PLAN] : undefined;
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
37
+ function findEnd(owner, slot) {
38
+ return owner.links.find((e) => e.classes.includes(slot.id) || (!owner.strictEnds && e.classes.includes('Entity') && slot.category === 'HUB'));
39
+ }
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
48
+ function slotValue(slotCls, v) {
49
+ if (typeof v === 'string')
50
+ return v;
51
+ const plan = peekPlan(v);
52
+ if (plan) {
53
+ const [head] = plan.steps;
54
+ if (plan.steps.length === 1 && !head.op && typeof head.filter === 'string') {
55
+ if (head.cls.id !== slotCls.id) {
56
+ throw new Error(`letopis: slot "${slotCls.id}" got a chain of class "${head.cls.id}"`);
57
+ }
58
+ return head.filter; // просто адрес db.Класс(id) — без вложенного исполнения
59
+ }
60
+ return { plan: plan.steps, mods: plan.mods };
61
+ }
62
+ if (v !== null && typeof v === 'object' && typeof v.id === 'string') {
63
+ return v.id; // Row / Account
64
+ }
65
+ throw new Error(`letopis: slot "${slotCls.id}" accepts an id, a Row or a chain`);
66
+ }
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
75
+ function pivotNodeOf(steps, cls) {
76
+ for (let i = steps.length - 1; i >= 0; i--) {
77
+ const s = steps[i];
78
+ if (s.op)
79
+ return undefined; // операция закрывает паттерн — узлы до неё недоступны
80
+ if (s.pivotKey === undefined && s.cls.id === cls.id)
81
+ return s;
82
+ }
83
+ return undefined;
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
19
92
  async function runPaths(ctx, steps, mods) {
93
+ // START_BLOCK_RUN_PATHS
20
94
  const q = buildRead(ctx, steps, mods, 'paths');
21
95
  const res = await runQuery(ctx, q.text, q.params, 'paths', clsOf(steps));
22
96
  return res.map((r) => {
@@ -25,8 +99,17 @@ async function runPaths(ctx, steps, mods) {
25
99
  out[k] = toRow(v);
26
100
  return out;
27
101
  });
102
+ // END_BLOCK_RUN_PATHS
28
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
29
111
  async function runRows(ctx, steps, mods) {
112
+ // START_BLOCK_RUN_ROWS
30
113
  const q = buildRead(ctx, steps, mods, 'rows');
31
114
  const res = await runQuery(ctx, q.text, q.params, 'rows', clsOf(steps));
32
115
  return res.map((r) => {
@@ -34,9 +117,89 @@ async function runRows(ctx, steps, mods) {
34
117
  const d = r.row.depth;
35
118
  return d != null ? { ...row, $depth: d } : row;
36
119
  });
120
+ // END_BLOCK_RUN_ROWS
121
+ }
122
+ const NO_NODES = new Map();
123
+ /**
124
+ * Свойство-класс (вызов = шаг/pivot; без скобок = слот). ownerSteps undefined — корень
125
+ * (db/batch): слот без владельца — ошибка.
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
134
+ function classProp(ctx, name, cls, ownerSteps, mods, batch, nodes, applySlot) {
135
+ // START_BLOCK_CLASS_STEP
136
+ const step = (filter) => {
137
+ const prev = ownerSteps?.[ownerSteps.length - 1];
138
+ // повтор LINK-класса в текущем паттерне — возврат к узлу (pivot)
139
+ const node = prev && cls.category === 'LINK' ? pivotNodeOf(ownerSteps, cls) : undefined;
140
+ if (node) {
141
+ const pivot = { name, cls, filter: normalizeFilter(filter), pivotKey: node.nodeKey };
142
+ return makeChain(ctx, [...ownerSteps, pivot], mods, batch, nodes);
143
+ }
144
+ if (prev && !prev.op)
145
+ resolveHop(prev.cls, cls); // ранняя проверка пути (после op — продолжение от результата)
146
+ const next = { name, cls, filter: normalizeFilter(filter), nodeKey: nextNodeKey() };
147
+ return makeChain(ctx, [...(ownerSteps ?? []), next], ownerSteps ? mods : {}, batch, nodes);
148
+ };
149
+ // END_BLOCK_CLASS_STEP
150
+ // START_BLOCK_LINK_SLOT
151
+ const slotGuard = () => {
152
+ if (!ownerSteps?.length || !applySlot) {
153
+ throw new Error(`letopis: slot "${name}" needs an owner step — start with a class call: db.Класс(…).${name}.set(…)`);
154
+ }
155
+ const owner = ownerSteps[ownerSteps.length - 1];
156
+ if (!findEnd(owner.cls, cls)) {
157
+ throw new Error(`letopis: "${cls.id}" is not a link end of "${owner.cls.id}" (Schema.links: ${owner.cls.links.map((e) => e.classes.join('|')).join(', ') || '—'})`);
158
+ }
159
+ return owner.cls;
160
+ };
161
+ return new Proxy(step, {
162
+ apply: (t, _self, args) => t(...args),
163
+ get(t, p, r) {
164
+ if (p === 'set') {
165
+ return (v) => {
166
+ slotGuard();
167
+ return applySlot(cls, slotValue(cls, v));
168
+ };
169
+ }
170
+ if (p === 'unset') {
171
+ return () => {
172
+ const ownerCls = slotGuard();
173
+ const end = findEnd(ownerCls, cls);
174
+ if (ownerCls.strictEnds && !end.optional) {
175
+ throw new Error(`letopis: link end "${cls.id}" of "${ownerCls.id}" is required — cannot unset`);
176
+ }
177
+ return applySlot(cls, null);
178
+ };
179
+ }
180
+ if (p === 'delete') {
181
+ return () => {
182
+ throw new Error(`letopis: slot .delete() renamed to .unset() (0.16.0) — .${name}.unset() removes the link end`);
183
+ };
184
+ }
185
+ if (typeof p === 'symbol' || p === 'then' || p in Function.prototype || p === 'name' || p === 'length' || p === 'prototype') {
186
+ return Reflect.get(t, p, r);
187
+ }
188
+ throw new Error(`letopis: "${name}" without call is a link slot (set/unset); for navigation call ${name}(…)`);
189
+ },
190
+ });
191
+ // END_BLOCK_LINK_SLOT
37
192
  }
38
- function makeChain(ctx, steps, mods, batch) {
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
200
+ function makeChain(ctx, steps, mods, batch, nodes = NO_NODES) {
39
201
  const hasOps = steps.some((s) => s.op);
202
+ // START_BLOCK_STEP_MUTATORS
40
203
  const withLast = (patch) => {
41
204
  const last = steps[steps.length - 1];
42
205
  if (last.op) {
@@ -44,25 +207,89 @@ function makeChain(ctx, steps, mods, batch) {
44
207
  }
45
208
  const next = steps.slice();
46
209
  next[next.length - 1] = { ...last, ...patch };
47
- return makeChain(ctx, next, mods, batch);
210
+ return makeChain(ctx, next, mods, batch, nodes);
211
+ };
212
+ const withMods = (patch) => makeChain(ctx, steps, { ...mods, ...patch }, batch, nodes);
213
+ // END_BLOCK_STEP_MUTATORS
214
+ // START_BLOCK_WRITE_DISPATCH
215
+ /** Обновить план в батче (регистрируется на первой операции, дальше мутируется). */
216
+ const syncBatch = (next) => {
217
+ if (!batch)
218
+ return;
219
+ if (batch.entry)
220
+ batch.entry.steps = next;
221
+ else {
222
+ batch.entry = { steps: next };
223
+ batch.queue.push(batch.entry);
224
+ }
48
225
  };
49
- const withMods = (patch) => makeChain(ctx, steps, { ...mods, ...patch }, batch);
50
226
  /** Операция-звено: op на последний шаг (op уже есть → self-шаг «те же сущности»); mods сбрасываются. */
51
227
  const withOp = (op) => {
52
228
  const last = steps[steps.length - 1];
53
229
  const next = last.op
54
- ? [...steps, { name: last.name, cls: last.cls, self: true, op }]
230
+ ? [...steps, { name: last.name, cls: last.cls, self: true, op, nodeKey: nextNodeKey() }]
55
231
  : [...steps.slice(0, -1), { ...last, op }];
56
- if (batch) {
57
- if (batch.entry)
58
- batch.entry.steps = next;
59
- else {
60
- batch.entry = { steps: next };
61
- batch.queue.push(batch.entry);
232
+ syncBatch(next);
233
+ return makeChain(ctx, next, {}, batch, nodes);
234
+ };
235
+ /**
236
+ * Слот связи: значение конца ЗАПИСЫВАЕМОЙ версии последнего шага — только после
237
+ * операции записи (…create(…).Класс.set(x) / …update(…).Класс.unset()): та же версия.
238
+ */
239
+ const applySlot = (slotCls, value) => {
240
+ const last = steps[steps.length - 1];
241
+ if (!last.op) {
242
+ throw new Error(`letopis: link slot "${slotCls.alias}" needs a write — add .create(…)/.update(…) before .${slotCls.alias}.${value === null ? 'unset()' : 'set(…)'}`);
243
+ }
244
+ if (last.op.kind !== 'create' && last.op.kind !== 'update') {
245
+ throw new Error(`letopis: link slot after ${last.op.kind}() — slots apply to create()/update()`);
246
+ }
247
+ if (last.extraLinks && slotCls.id in last.extraLinks) {
248
+ throw new Error(`letopis: duplicate link slot "${slotCls.id}" in one write`);
249
+ }
250
+ const patched = {
251
+ ...last,
252
+ extraLinks: { ...(last.extraLinks ?? {}), [slotCls.id]: value },
253
+ };
254
+ const next = [...steps.slice(0, -1), patched];
255
+ syncBatch(next);
256
+ return makeChain(ctx, next, mods, batch, nodes);
257
+ };
258
+ // END_BLOCK_WRITE_DISPATCH
259
+ // START_BLOCK_ENTITY_EMBED
260
+ /** entity(x): вклейка узла/паттерна; та же переменная повторно — возврат к её узлу. */
261
+ const entityStep = (x) => {
262
+ const plan = peekPlan(x);
263
+ if (plan) {
264
+ const known = nodes.get(x);
265
+ if (known !== undefined) {
266
+ const lastX = plan.steps[plan.steps.length - 1];
267
+ const pivot = { name: lastX.name, cls: lastX.cls, pivotKey: known };
268
+ return makeChain(ctx, [...steps, pivot], mods, batch, nodes);
62
269
  }
270
+ if (plan.steps.some((s) => s.op)) {
271
+ throw new Error('letopis: entity() takes a read pattern — a chain with operations cannot be embedded');
272
+ }
273
+ const prev = steps[steps.length - 1];
274
+ if (prev && !prev.op)
275
+ resolveHop(prev.cls, plan.steps[0].cls); // стык по правилам пути
276
+ const nodeKey = plan.steps[plan.steps.length - 1].nodeKey;
277
+ const nextNodes = new Map(nodes);
278
+ nextNodes.set(x, nodeKey);
279
+ return makeChain(ctx, [...steps, ...plan.steps], mods, batch, nextNodes);
280
+ }
281
+ if (x !== null && typeof x === 'object' && typeof x.id === 'string' && typeof x.class === 'string') {
282
+ const row = x;
283
+ const cls = ctx.registry.resolve(row.class);
284
+ const prev = steps[steps.length - 1];
285
+ if (prev && !prev.op)
286
+ resolveHop(prev.cls, cls);
287
+ const next = { name: cls.alias, cls, filter: row.id, nodeKey: nextNodeKey() };
288
+ return makeChain(ctx, [...steps, next], mods, batch, nodes);
63
289
  }
64
- return makeChain(ctx, next, {}, batch);
290
+ throw new Error('letopis: entity() accepts a lazy chain or a Row');
65
291
  };
292
+ // END_BLOCK_ENTITY_EMBED
66
293
  /** Терминал: план (есть операции) — runPlan одной транзакцией; иначе прямое чтение. */
67
294
  const guardBatch = () => {
68
295
  if (batch && hasOps) {
@@ -71,9 +298,14 @@ function makeChain(ctx, steps, mods, batch) {
71
298
  };
72
299
  const handler = {
73
300
  get(_t, prop) {
301
+ if (prop === PLAN)
302
+ return { steps, mods };
74
303
  if (typeof prop === 'symbol' || prop === 'then')
75
304
  return undefined;
76
305
  switch (prop) {
306
+ // START_BLOCK_READ_TERMINALS
307
+ case 'entity':
308
+ return entityStep;
77
309
  case 'run':
78
310
  return () => {
79
311
  guardBatch();
@@ -112,6 +344,8 @@ function makeChain(ctx, steps, mods, batch) {
112
344
  const res = await runQuery(ctx, q.text, q.params, 'count', clsOf(steps));
113
345
  return res[0].n;
114
346
  };
347
+ // END_BLOCK_READ_TERMINALS
348
+ // START_BLOCK_READ_MODIFIERS
115
349
  case 'limit':
116
350
  return (n) => withMods({ limit: n });
117
351
  case 'offset':
@@ -122,6 +356,8 @@ function makeChain(ctx, steps, mods, batch) {
122
356
  return (t) => withMods({ asOf: t instanceof Date ? t.toISOString() : t });
123
357
  case 'deep':
124
358
  return (max = 32) => withLast({ deepMax: max });
359
+ // END_BLOCK_READ_MODIFIERS
360
+ // START_BLOCK_AGGREGATIONS
125
361
  case 'sum':
126
362
  case 'avg':
127
363
  case 'min':
@@ -158,6 +394,8 @@ function makeChain(ctx, steps, mods, batch) {
158
394
  })();
159
395
  return Object.fromEntries(res.map((r) => [r.k ?? 'null', r.n]));
160
396
  };
397
+ // END_BLOCK_AGGREGATIONS
398
+ // START_BLOCK_VERSIONS_CURSOR
161
399
  case 'after':
162
400
  return (cursor) => withMods({ after: cursor });
163
401
  case 'versions':
@@ -173,12 +411,31 @@ function makeChain(ctx, steps, mods, batch) {
173
411
  return raw.deleted ? { ...row, $deleted: true } : row;
174
412
  });
175
413
  };
414
+ // END_BLOCK_VERSIONS_CURSOR
415
+ // START_BLOCK_WRITE_TERMINALS
416
+ case 'create':
417
+ return (data = {}) => {
418
+ const last = steps[steps.length - 1];
419
+ if (last && !last.op) { // после op create пишет результат — фильтр там подставит план
420
+ if (last.pivotKey !== undefined) {
421
+ throw new Error('letopis: create() on a pivot step — pivot returns to an existing node, use update()');
422
+ }
423
+ if (last.filter !== undefined && typeof last.filter !== 'string') {
424
+ throw new Error(`letopis: create() takes no filter — ${last.name}(id).create(…) fixes the id, searching is update()`);
425
+ }
426
+ }
427
+ return withOp({ kind: 'create', data, mods });
428
+ };
429
+ case 'update':
430
+ return (data = {}) => withOp({ kind: 'update', data, mods });
176
431
  case 'set':
177
- return (data = {}) => withOp({ kind: 'set', data, mods });
432
+ throw new Error('letopis: set() split into create()/update() (0.16.0) — create() inserts, update() versions what the path finds');
178
433
  case 'delete':
179
434
  return (opts) => withOp({ kind: 'delete', confirm: opts?.confirm === true, mods });
180
435
  case 'anonymize':
181
436
  return (fields) => withOp({ kind: 'anonymize', fields, mods });
437
+ // END_BLOCK_WRITE_TERMINALS
438
+ // START_BLOCK_COLUMN_MODS
182
439
  case 'alias':
183
440
  return (name) => withLast({ aliasKey: name });
184
441
  case 'tags':
@@ -188,30 +445,34 @@ function makeChain(ctx, steps, mods, batch) {
188
445
  case 'owner':
189
446
  return (v) => withLast({ ownerFilter: typeof v === 'object' ? v.id : v });
190
447
  case 'link':
191
- return (clsName, target) => {
192
- const linkCls = ctx.registry.resolve(clsName);
193
- const last = steps[steps.length - 1];
194
- return withLast({
195
- extraLinks: { ...(last.extraLinks ?? {}), [linkCls.id]: typeof target === 'object' ? target.id : target },
196
- });
197
- };
448
+ throw new Error('letopis: .link() removed (0.15.0) — use the link slot: .Класс.set(target)');
449
+ // END_BLOCK_COLUMN_MODS
198
450
  }
451
+ // START_BLOCK_CLASS_RESOLVE
199
452
  if (ctx.registry.has(prop)) {
200
- const cls = ctx.registry.resolve(prop);
201
- return (filter) => makeChain(ctx, [...steps, { name: prop, cls, filter: normalizeFilter(filter) }], mods, batch);
453
+ return classProp(ctx, prop, ctx.registry.resolve(prop), steps, mods, batch, nodes, applySlot);
202
454
  }
203
455
  ctx.registry.resolve(prop); // неизвестный класс — понятная ошибка со списком
204
456
  return undefined;
457
+ // END_BLOCK_CLASS_RESOLVE
205
458
  },
206
459
  };
207
460
  return new Proxy({}, handler);
208
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
209
469
  function makeBatch(ctx, queue) {
210
470
  const handler = {
211
471
  get(_t, prop) {
212
472
  if (typeof prop === 'symbol' || prop === 'then')
213
473
  return undefined;
214
474
  switch (prop) {
475
+ // START_BLOCK_BATCH_QUEUE
215
476
  case 'run':
216
477
  return async () => {
217
478
  const plans = queue.splice(0, queue.length);
@@ -223,10 +484,10 @@ function makeBatch(ctx, queue) {
223
484
  return () => void queue.splice(0, queue.length);
224
485
  case 'size':
225
486
  return () => queue.length;
487
+ // END_BLOCK_BATCH_QUEUE
226
488
  }
227
489
  if (ctx.registry.has(prop)) {
228
- const cls = ctx.registry.resolve(prop);
229
- return (filter) => makeChain(ctx, [{ name: String(prop), cls, filter: normalizeFilter(filter) }], {}, { queue });
490
+ return classProp(ctx, String(prop), ctx.registry.resolve(prop), undefined, {}, { queue }, NO_NODES);
230
491
  }
231
492
  ctx.registry.resolve(prop);
232
493
  return undefined;
@@ -234,6 +495,13 @@ function makeBatch(ctx, queue) {
234
495
  };
235
496
  return new Proxy({}, handler);
236
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
237
505
  export function makeDb(ctx, root) {
238
506
  const state = root ?? { batches: new Map() };
239
507
  let tables; // лениво, на ctx этого фасада (работает и в tr)
@@ -243,6 +511,7 @@ export function makeDb(ctx, root) {
243
511
  get(_t, prop) {
244
512
  if (typeof prop === 'symbol' || prop === 'then')
245
513
  return undefined;
514
+ // START_BLOCK_TABLES_AUTH_ACL
246
515
  if (prop === 'accounts' || prop === 'credentials' || prop === 'resources' || prop === 'rules') {
247
516
  tables ??= makeTables(ctx);
248
517
  return tables[prop];
@@ -257,7 +526,9 @@ export function makeDb(ctx, root) {
257
526
  acl ??= makeAcl(tables, ctx.registry);
258
527
  return acl;
259
528
  }
529
+ // END_BLOCK_TABLES_AUTH_ACL
260
530
  switch (prop) {
531
+ // START_BLOCK_TX_LOCK
261
532
  case 'begin':
262
533
  return async () => makeDb(await beginTx(ctx), state);
263
534
  case 'commit':
@@ -280,6 +551,8 @@ export function makeDb(ctx, root) {
280
551
  };
281
552
  case 'lock':
282
553
  return (...keys) => lock(ctx, ...keys);
554
+ // END_BLOCK_TX_LOCK
555
+ // START_BLOCK_BATCH_OPEN
283
556
  case 'batch':
284
557
  return (name) => {
285
558
  let q = state.batches.get(name);
@@ -289,6 +562,8 @@ export function makeDb(ctx, root) {
289
562
  }
290
563
  return makeBatch(ctx, q);
291
564
  };
565
+ // END_BLOCK_BATCH_OPEN
566
+ // START_BLOCK_WATCH_LISTEN
292
567
  case 'watch':
293
568
  return async (a, b, c) => {
294
569
  const cls = typeof a === 'string' ? ctx.registry.resolve(a).id : undefined;
@@ -328,19 +603,27 @@ export function makeDb(ctx, root) {
328
603
  const { unlisten } = await ctx.sql.listen(ctx.pgSchema, listener, onlisten);
329
604
  return unlisten;
330
605
  };
606
+ // END_BLOCK_WATCH_LISTEN
607
+ // START_BLOCK_CLOSE_META
331
608
  case 'close':
332
609
  return () => ctx.sql.end();
333
610
  case 'registry':
334
611
  return ctx.registry;
335
612
  case 'sql':
336
613
  return ctx.sql;
614
+ // END_BLOCK_CLOSE_META
615
+ }
616
+ // START_BLOCK_ENTITY_START
617
+ if (prop === 'entity') {
618
+ // старт пути с готового паттерна/строки: db.entity(pos).…
619
+ return (x) => makeChain(ctx, [], {}, undefined, NO_NODES).entity(x);
337
620
  }
338
621
  if (ctx.registry.has(prop)) {
339
- const cls = ctx.registry.resolve(prop);
340
- return (filter) => makeChain(ctx, [{ name: String(prop), cls, filter: normalizeFilter(filter) }], {});
622
+ return classProp(ctx, String(prop), ctx.registry.resolve(prop), undefined, {}, undefined, NO_NODES);
341
623
  }
342
624
  ctx.registry.resolve(prop);
343
625
  return undefined;
626
+ // END_BLOCK_ENTITY_START
344
627
  },
345
628
  };
346
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>;
@@ -5,6 +11,7 @@ export declare function connect(opts: ConnectOpts): Promise<EntityDb>;
5
11
  export declare function cursorOf(row: import('./types.js').Row, field?: string): import('./types.js').Cursor;
6
12
  export { up } from './up.js';
7
13
  export type { UpOpts } from './up.js';
14
+ export { uuidv5, uuidv7, LETOPIS_NS } from './uuid.js';
8
15
  export { ne, gt, gte, lt, lte, between, inList, like, ilike, starts, ends, has, hasAny, hasAll, exists, isNull, not, or, } from './ops.js';
9
16
  export type { Row, Path, Filter, ChainMods, Cursor, ConnectOpts, QueryEvent, Account, Credential, Resource, Rule, AclOp, AclDecision, } from './types.js';
10
17
  export type { EntityDb, EntityTx, Chain, Batch, WatchEvent, WatchOpts } from './chain.js';
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
@@ -57,6 +93,8 @@ export function cursorOf(row, field = 'updated') {
57
93
  }
58
94
  // одна точка входа: контейнер + готовность + схема + connect
59
95
  export { up } from './up.js';
96
+ // генерация id (attributes.id): формула v5 открыта — id считается ДО создания
97
+ export { uuidv5, uuidv7, LETOPIS_NS } from './uuid.js';
60
98
  // операторы фильтров
61
99
  export { ne, gt, gte, lt, lte, between, inList, like, ilike, starts, ends, has, hasAny, hasAll, exists, isNull, not, or, } from './ops.js';
62
100
  export { totpCode } from './auth.js';