letopis 1.4.2 → 2.0.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.
Files changed (112) hide show
  1. package/AGENT-CHEATSHEET.md +329 -281
  2. package/CHANGELOG.md +418 -0
  3. package/README.md +572 -1053
  4. package/dist/acl.d.ts +78 -1
  5. package/dist/acl.js +264 -13
  6. package/dist/admin.d.ts +10 -141
  7. package/dist/admin.js +24 -184
  8. package/dist/auth.d.ts +2 -2
  9. package/dist/auth.js +5 -15
  10. package/dist/cache.d.ts +8 -1
  11. package/dist/cache.js +30 -8
  12. package/dist/chain.d.ts +18 -41
  13. package/dist/chain.js +165 -203
  14. package/dist/cli.js +45 -90
  15. package/dist/demo/booking.d.ts +352 -36
  16. package/dist/describe.d.ts +47 -0
  17. package/dist/describe.js +195 -0
  18. package/dist/errors.d.ts +59 -13
  19. package/dist/errors.js +190 -29
  20. package/dist/index.d.ts +52 -70
  21. package/dist/index.js +189 -122
  22. package/dist/internal.d.ts +5 -0
  23. package/dist/jsonschema.js +18 -17
  24. package/dist/load.d.ts +20 -12
  25. package/dist/load.js +207 -181
  26. package/dist/mcp.d.ts +5 -7
  27. package/dist/mcp.js +168 -214
  28. package/dist/messages/access.d.ts +90 -0
  29. package/dist/messages/access.js +113 -0
  30. package/dist/messages/api.d.ts +171 -0
  31. package/dist/messages/api.js +215 -0
  32. package/dist/messages/boot.d.ts +201 -0
  33. package/dist/messages/boot.js +256 -0
  34. package/dist/messages/codes.d.ts +172 -0
  35. package/dist/messages/codes.js +173 -0
  36. package/dist/messages/index.d.ts +95 -0
  37. package/dist/messages/index.js +118 -0
  38. package/dist/messages/mcp.d.ts +306 -0
  39. package/dist/messages/mcp.js +387 -0
  40. package/dist/messages/model.d.ts +233 -0
  41. package/dist/messages/model.js +283 -0
  42. package/dist/messages/query.d.ts +131 -0
  43. package/dist/messages/query.js +158 -0
  44. package/dist/messages/sql-access.d.ts +70 -0
  45. package/dist/messages/sql-access.js +87 -0
  46. package/dist/messages/sql-core.d.ts +150 -0
  47. package/dist/messages/sql-core.js +181 -0
  48. package/dist/messages/sql-time.d.ts +71 -0
  49. package/dist/messages/sql-time.js +88 -0
  50. package/dist/messages/sql-write.d.ts +246 -0
  51. package/dist/messages/sql-write.js +270 -0
  52. package/dist/messages/time.d.ts +23 -0
  53. package/dist/messages/time.js +29 -0
  54. package/dist/messages/types.d.ts +27 -0
  55. package/dist/messages/types.js +5 -0
  56. package/dist/messages/write.d.ts +405 -0
  57. package/dist/messages/write.js +509 -0
  58. package/dist/model.d.ts +73 -60
  59. package/dist/model.js +20 -29
  60. package/dist/ops.d.ts +35 -0
  61. package/dist/ops.js +101 -0
  62. package/dist/pglite.d.ts +0 -9
  63. package/dist/pglite.js +3 -1
  64. package/dist/registry.d.ts +3 -19
  65. package/dist/registry.js +3 -16
  66. package/dist/signals.d.ts +1 -5
  67. package/dist/signals.js +1 -5
  68. package/dist/sql.d.ts +7 -1
  69. package/dist/sql.js +101 -85
  70. package/dist/sync.d.ts +9 -3
  71. package/dist/sync.js +40 -17
  72. package/dist/tx.d.ts +11 -1
  73. package/dist/tx.js +32 -11
  74. package/dist/typed.d.ts +240 -45
  75. package/dist/types.d.ts +15 -90
  76. package/dist/types.js +12 -14
  77. package/dist/up.d.ts +53 -32
  78. package/dist/up.js +180 -74
  79. package/dist/uuid.js +5 -5
  80. package/dist/validate.d.ts +13 -1
  81. package/dist/validate.js +61 -29
  82. package/dist/watch.d.ts +5 -16
  83. package/dist/watch.js +23 -77
  84. package/dist/write.d.ts +9 -16
  85. package/dist/write.js +102 -185
  86. package/llms.txt +22 -25
  87. package/package.json +2 -9
  88. package/sql/10-core.sql +7 -50
  89. package/sql/15-errors.sql +64 -10
  90. package/sql/20-context.sql +7 -6
  91. package/sql/30-validate.sql +35 -27
  92. package/sql/40-class.sql +20 -114
  93. package/sql/50-acl.sql +8 -13
  94. package/sql/60-write.sql +220 -676
  95. package/sql/70-read.sql +11 -13
  96. package/sql/80-auth.sql +23 -27
  97. package/sql/90-time.sql +77 -944
  98. package/sql/95-seed.system.sql +7 -14
  99. package/sql/99-revision.sql +2 -1
  100. package/sql/README.md +32 -24
  101. package/sql/migrate/README.md +14 -0
  102. package/AGENT-CHEATSHEET.en.md +0 -488
  103. package/MIGRATION.md +0 -190
  104. package/README.en.md +0 -2910
  105. package/dist/import.d.ts +0 -185
  106. package/dist/import.js +0 -820
  107. package/dist/project.d.ts +0 -359
  108. package/dist/project.js +0 -1258
  109. package/sql/75-history.sql +0 -436
  110. package/sql/88-derived.sql +0 -1236
  111. package/sql/migrate/0006-changed-paths.sql +0 -3
  112. package/sql/migrate/0007-secret-account-fk.sql +0 -12
package/dist/tx.js CHANGED
@@ -1,4 +1,4 @@
1
- import { LetopisError, fromDbError } from './errors.js';
1
+ import { LetopisError, fromDbError, lerr, lmsg } from './errors.js';
2
2
  /** Транзиентные ошибки PG, которые снимает повтор: взаимная блокировка, сериализация. */
3
3
  export function isTransient(e) {
4
4
  const code = e.code;
@@ -37,12 +37,14 @@ export function release(conn, err) {
37
37
  return;
38
38
  conn.release();
39
39
  }
40
- const commitUnknown = (cause) => Object.assign(new LetopisError('commit_unknown', 'связь оборвалась во время commit: транзакция могла зафиксироваться — '
41
- + 'проверьте исход повтором той же операции с тем же ключом'), { cause });
42
- /** Взаимная блокировка в явной транзакции: повторить нужно весь блок (подсказка, как hintTxRetry 0.21). */
40
+ const commitUnknown = (cause) => lerr('write.commit_unknown', undefined, { cause });
41
+ /**
42
+ * Взаимная блокировка в явной транзакции: повторить нужно весь блок (подсказка, как hintTxRetry 0.21). Хвост —
43
+ * запись write.tx_retry_hint; он называет db.begin(), поэтому второй раз не дописывается.
44
+ */
43
45
  function hintTxRetry(e) {
44
46
  if (isTransient(e) && e instanceof Error && !e.message.includes('db.begin()')) {
45
- e.message += ' — откатите транзакцию и повторите весь блок db.begin()';
47
+ e.message += lmsg('write.tx_retry_hint');
46
48
  }
47
49
  return e;
48
50
  }
@@ -156,6 +158,9 @@ const passSql = (q, ctx) => q.unsafe(`select set_config('letopis.acl', coalesce(
156
158
  */
157
159
  export async function runTx(ctx, fn, opts = {}) {
158
160
  const retries = opts.retries ?? RETRIES;
161
+ let renewed = false;
162
+ if (ctx.before)
163
+ await ctx.before(ctx);
159
164
  for (let attempt = 1;; attempt++) {
160
165
  let bodyDone = false;
161
166
  let dead = false;
@@ -202,13 +207,23 @@ export async function runTx(ctx, fn, opts = {}) {
202
207
  await retryDelay(attempt);
203
208
  continue;
204
209
  }
205
- throw fromDbError(e);
210
+ const le = fromDbError(e);
211
+ // сессия кончилась по простою: подключение по ключу выдаёт новую — повтор один раз, транзакция уже откачена
212
+ if (!renewed && !bodyDone && ctx.renew && isNoSession(le)) {
213
+ renewed = true;
214
+ if (await ctx.renew(ctx)) {
215
+ attempt--;
216
+ continue;
217
+ }
218
+ }
219
+ throw le;
206
220
  }
207
221
  finally {
208
222
  release(conn, failure);
209
223
  }
210
224
  }
211
225
  }
226
+ const isNoSession = (e) => e instanceof LetopisError && e.code === 'no_session';
212
227
  /** Исполнитель пула: каждая операция — своя транзакция с повторами. */
213
228
  export function poolExecutor(ctx) {
214
229
  return { explicit: false, run: (fn, opts) => runTx(ctx, fn, opts) };
@@ -232,7 +247,9 @@ export class ExplicitTx {
232
247
  this.ctx = ctx;
233
248
  this.conn = conn;
234
249
  }
235
- static async open(ctx) {
250
+ static async open(ctx, renewed = false) {
251
+ if (!renewed && ctx.before)
252
+ await ctx.before(ctx);
236
253
  const conn = await ctx.sql.reserve();
237
254
  try {
238
255
  await conn.unsafe('begin isolation level read committed');
@@ -244,7 +261,11 @@ export class ExplicitTx {
244
261
  catch (e) {
245
262
  await rollbackAfter(conn, e);
246
263
  release(conn, e);
247
- throw fromDbError(e);
264
+ const le = fromDbError(e);
265
+ // сессия кончилась по простою (как у runTx): подключение по ключу выдаёт новую — открыть ещё раз
266
+ if (!renewed && ctx.renew && isNoSession(le) && (await ctx.renew(ctx)))
267
+ return ExplicitTx.open(ctx, true);
268
+ throw le;
248
269
  }
249
270
  return new ExplicitTx(ctx, conn);
250
271
  }
@@ -254,7 +275,7 @@ export class ExplicitTx {
254
275
  }
255
276
  async run(fn, opts) {
256
277
  if (this.done)
257
- throw new LetopisError('tx_required', 'транзакция db.begin() уже завершена');
278
+ throw lerr('write.tx_done');
258
279
  if (this.broken)
259
280
  throw this.broken;
260
281
  if (opts?.raw)
@@ -271,7 +292,7 @@ export class ExplicitTx {
271
292
  /** Advisory-блокировка до конца транзакции (отдельное от блокировок семейств пространство ключей). */
272
293
  async lock(...keys) {
273
294
  if (this.done)
274
- throw new LetopisError('tx_required', 'lock(): транзакция db.begin() уже завершена');
295
+ throw lerr('write.lock_tx_done');
275
296
  await this.run((q) => q.unsafe('select pg_advisory_xact_lock(hashtextextended($1, 0))', [keys.join('|')]));
276
297
  }
277
298
  async commit() {
@@ -303,7 +324,7 @@ export class ExplicitTx {
303
324
  }));
304
325
  changed = rows[0]?.changed ?? null;
305
326
  if (res.command === 'ROLLBACK') {
306
- this.failure = new LetopisError('tx_rolled_back', 'commit: PostgreSQL откатил транзакцию (в ней была ошибка)');
327
+ this.failure = lerr('write.tx_rolled_back');
307
328
  throw this.failure;
308
329
  }
309
330
  }
package/dist/typed.d.ts CHANGED
@@ -1,35 +1,171 @@
1
1
  /**
2
- * Типы цепочек по моделям (план, этап 3, п. 9): `connect({ models })` даёт типы шагов по именам и
3
- * алиасам классов, типы строк ответа и фильтров. Допустимость переходов проверяется во время
4
- * выполнения, как в 0.21. Модель описана структурно (ModelLike), чтобы основная точка входа не
5
- * зависела от Zod: модели letopis/model ей соответствуют.
2
+ * Строгие типы цепочек по моделям (план 2.0, этап 4, п. 1; типы шагов, строк и фильтров — план 1.0, этап 3, п. 9).
3
+ * `connect({ models })` — модели letopis/model — или `connect<Models>()` — объявления `letopis types` (generateTypes,
4
+ * sync.ts) — дают TypedDb без индексных сигнатур:
5
+ * - шаг от корня — только известный класс или алиас: моделей подключения и системных (встроенный набор SystemModels);
6
+ * - переходы цепочки выводятся из концов, как в resolveHop (lib/src/sql.ts): от класса C — роли его семейства (C и
7
+ * потомков) и классы, связанные с семейством C концом в любую сторону (роль, имя которой совпало с именем класса,
8
+ * во время выполнения — шаг класса, так же и здесь);
9
+ * - слоты — только роли класса C: `set(id | строка | цепочка)`, `add` и `remove` — у множественного конца, `unset` —
10
+ * у необязательного;
11
+ * - поля фильтров, `or()` и агрегатов `sum`, `avg`, `min`, `max`, `countBy` — поля класса.
12
+ * Чего типы не знают: возврат к узлу связи из пути (pivot) от класса, с которым связь не связана концом, и роль чужого
13
+ * класса шагом. Это и имена, известные только во время выполнения, — через loose(db) (index.ts): проверки во время
14
+ * выполнения те же. db.as(), db.begin(), db.batch() и entity() отдают цепочки без строгих типов (Db, Batch, Chain).
15
+ * Модель описана структурно (ModelLike), поэтому основная точка входа не зависит от Zod.
6
16
  */
7
- import type { ChainCore, StepFn } from './chain.js';
8
- import type { Cursor, Op, Path, Row, Scalar } from './types.js';
17
+ import type { ChainCore, ChainRef } from './chain.js';
18
+ import type { Cursor, Op, OP, Path, Row, Scalar } from './types.js';
9
19
  import type { IncMarker } from './write.js';
10
- /** То, что цепочкам нужно от модели: имя, алиас и выведенные типы данных и концов. */
11
- export interface ModelLike<N extends string = string, D = unknown, L = unknown, A extends string = string> {
20
+ /**
21
+ * То, что цепочкам нужно от модели: имя, алиас и типы для вывода — данные строки, концы строки, концы класса (роль →
22
+ * `{ targets: имена классов целей; many; required }`) и имена класса и его предков. Модели letopis/model и объявления
23
+ * `letopis types` ему соответствуют.
24
+ */
25
+ export interface ModelLike<N extends string = string, D = unknown, L = unknown, A extends string = string, E = unknown, P extends string = never> {
12
26
  readonly name: N;
13
27
  readonly alias?: A;
14
28
  readonly __types?: {
15
29
  data: D;
16
30
  links: L;
31
+ ends?: E;
32
+ ancestors?: P;
17
33
  };
18
34
  }
19
- export type AnyModelLike = ModelLike<string, any, any, any>;
20
- type DataOf<C> = C extends ModelLike<string, infer D, unknown, string> ? (D extends Record<string, unknown> ? D : Record<string, unknown>) : Record<string, unknown>;
21
- type LinksOf<C> = C extends ModelLike<string, unknown, infer L, string> ? (L extends Record<string, unknown> ? L : Record<string, string>) : Record<string, string>;
35
+ export type AnyModelLike = ModelLike<string, any, any, any, any, any>;
36
+ /** Обязательный одиночный конец на класс T (системные классы). */
37
+ type One<T extends string> = {
38
+ targets: T;
39
+ many: false;
40
+ required: true;
41
+ };
42
+ type NoKeys = Record<never, never>;
43
+ /**
44
+ * Встроенный набор моделей системных классов (lib/sql/95-seed.system.sql): шаги Class, Account, member, Resource, rule
45
+ * есть у любого TypedDb. Модель с тем же именем в `models` (например, из `letopis types --system`) — вместо встроенной.
46
+ */
47
+ export type SystemModels = readonly [
48
+ ModelLike<'Class', {
49
+ name: string;
50
+ }, NoKeys, never, NoKeys>,
51
+ ModelLike<'Account', {
52
+ name: string;
53
+ enabled?: boolean;
54
+ categories?: string[];
55
+ }, NoKeys, never, NoKeys>,
56
+ ModelLike<'member', {
57
+ roles?: string[];
58
+ }, {
59
+ User: string;
60
+ Tenant: string;
61
+ }, never, {
62
+ User: One<'Account'>;
63
+ Tenant: One<'Account'>;
64
+ }>,
65
+ ModelLike<'Resource', {
66
+ alias: string;
67
+ category: 'ACCOUNT' | 'API' | 'READ' | 'WRITE' | 'DELETE';
68
+ pattern?: {
69
+ categories?: string;
70
+ roles?: string;
71
+ endpoint?: string;
72
+ class?: string;
73
+ };
74
+ description?: string;
75
+ }, NoKeys, never, NoKeys>,
76
+ ModelLike<'rule', {
77
+ permission: 'allow' | 'deny';
78
+ weight?: number;
79
+ enabled?: boolean;
80
+ code?: number;
81
+ message?: string;
82
+ description?: string;
83
+ }, {
84
+ Group: string;
85
+ Object: string;
86
+ }, never, {
87
+ Group: One<'Resource'>;
88
+ Object: One<'Resource'>;
89
+ }>
90
+ ];
91
+ type NameOf<K> = K extends {
92
+ readonly name: infer N extends string;
93
+ } ? N : never;
94
+ /** Алиас модели; нет алиаса (или он не литерал) — never. */
95
+ type AliasOf<K> = K extends {
96
+ readonly alias?: infer A;
97
+ } ? (A extends string ? (string extends A ? never : A) : never) : never;
98
+ /** Имена, которыми модель вызывается шагом: имя и алиас. */
99
+ type StepNames<K> = NameOf<K> | AliasOf<K>;
100
+ type TypesOf<K> = K extends {
101
+ readonly __types?: infer T;
102
+ } ? NonNullable<T> : NoKeys;
103
+ /** Данные строки класса C (интерфейсы `letopis types` — тоже: индексная сигнатура не требуется). */
104
+ type DataOf<C> = C extends unknown ? (TypesOf<C> extends {
105
+ data: infer D;
106
+ } ? (D extends object ? D : Record<string, unknown>) : Record<string, unknown>) : never;
107
+ type LinksOf<C> = C extends unknown ? (TypesOf<C> extends {
108
+ links: infer L;
109
+ } ? (L extends object ? L : Record<string, string>) : Record<string, string>) : never;
110
+ /** Концы класса C: роль → { targets, many, required }; модель без них — без концов. */
111
+ type EndsOf<C> = C extends unknown ? (TypesOf<C> extends {
112
+ ends?: infer E;
113
+ } ? EndsOrNone<NonNullable<E>> : NoKeys) : never;
114
+ type EndsOrNone<E> = [E] extends [object] ? E : NoKeys;
115
+ /** Имя класса и его предков (ancestors реестра). */
116
+ type AncestorsOf<K> = K extends unknown ? NameOf<K> | (TypesOf<K> extends {
117
+ ancestors?: infer P;
118
+ } ? Extract<P, string> : never) : never;
119
+ type EndTargets<S> = S extends {
120
+ targets: infer T;
121
+ } ? Extract<T, string> : never;
122
+ /** Классы целей всех концов модели. */
123
+ type TargetsOf<K> = K extends unknown ? {
124
+ [R in keyof EndsOf<K>]: EndTargets<EndsOf<K>[R]>;
125
+ }[keyof EndsOf<K>] : never;
126
+ /** Роли модели. */
127
+ type RolesOf<K> = K extends unknown ? Extract<keyof EndsOf<K>, string> : never;
128
+ /** Модели подключения и встроенные системные (своя модель системного класса — вместо встроенной). */
129
+ type AllOf<M extends readonly AnyModelLike[]> = M[number] | Exclude<SystemModels[number], {
130
+ readonly name: NameOf<M[number]>;
131
+ }>;
132
+ /** Семейство класса C: сам класс и потомки (reg.family). */
133
+ type FamilyOf<C, All> = C extends unknown ? C | DescendantsOf<NameOf<C>, All> : never;
134
+ type DescendantsOf<N, All> = All extends unknown ? (N extends AncestorsOf<All> ? All : never) : never;
135
+ type FamTargets<C, All> = TargetsOf<FamilyOf<C, All>>;
136
+ type FamAncestors<C, All> = AncestorsOf<FamilyOf<C, All>>;
137
+ /**
138
+ * Связаны ли классы C и X концом: роль семейства C принимает класс семейства X (вперёд) или роль семейства X — класс
139
+ * семейства C (назад); конец принимает класс и его потомков (accepts).
140
+ */
141
+ type Linked<C, X, All> = [FamTargets<C, All> & FamAncestors<X, All>] extends [never] ? ([FamTargets<X, All> & FamAncestors<C, All>] extends [never] ? false : true) : true;
142
+ /** Классы, в которые ведёт шаг от C. */
143
+ type HopClasses<C, All> = HopClassesOf<C, All, All>;
144
+ type HopClassesOf<C, X, All> = X extends unknown ? (Linked<C, X, All> extends true ? X : never) : never;
145
+ /** Модель по имени или алиасу шага. */
146
+ type ByStepName<P, U> = U extends unknown ? (P extends StepNames<U> ? U : never) : never;
147
+ /** Классы целей роли R: имя роли однозначно во всей схеме (проверяет база). */
148
+ type RoleTargets<R, All> = All extends unknown ? (R extends RolesOf<All> ? EndTargets<EndsOf<All>[R & keyof EndsOf<All>]> : never) : never;
149
+ /** Модели классов T; класса нет среди моделей — только имя (строки без типов данных). */
150
+ type ModelsNamed<T, All> = (All extends unknown ? (NameOf<All> extends T ? All : never) : never) | BareOf<Exclude<T, NameOf<All>>>;
151
+ type BareOf<T> = T extends string ? {
152
+ readonly name: T;
153
+ } : never;
154
+ /**
155
+ * Анонимный тип объекта вместо интерфейса: у интерфейса (`<Класс>Data` из `letopis types`) нет неявной индексной
156
+ * сигнатуры, и без этого его строка не подошла бы туда, где ждут `Row` (`entity()`, `cursorOf()`).
157
+ */
158
+ type Plain<T> = {
159
+ [K in keyof T]: T[K];
160
+ };
22
161
  /** Строка ответа класса C. */
23
- export type RowOf<C> = Row<DataOf<C>, LinksOf<C>>;
162
+ export type RowOf<C> = C extends unknown ? Row<Plain<DataOf<C>>, Plain<LinksOf<C>>> : never;
24
163
  /** Фильтр по значению поля: равенство, оператор, вложенный объект любой глубины. */
25
164
  export type FieldFilterOf<T> = Op | (T extends readonly unknown[] ? T : T extends Record<string, unknown> ? {
26
165
  [P in keyof T]?: FieldFilterOf<T[P]>;
27
166
  } : T extends Scalar ? T | null : never);
28
- /** Фильтр шага класса C: id, список id, строка ответа, or(…) или объект полей и ролей. */
29
- export type FilterOf<C> = string | string[] | Op | {
30
- id: string;
31
- class: string;
32
- } | ({
167
+ /** Фильтр-объект класса C: поля данных, роли концов и id. */
168
+ type ObjectFilterOf<C> = C extends unknown ? {
33
169
  [P in keyof DataOf<C>]?: FieldFilterOf<DataOf<C>[P]>;
34
170
  } & {
35
171
  [R in keyof LinksOf<C>]?: string | {
@@ -37,26 +173,98 @@ export type FilterOf<C> = string | string[] | Op | {
37
173
  } | null;
38
174
  } & {
39
175
  id?: string | string[];
40
- });
41
- /** Шаги по именам и алиасам моделей M. */
42
- export type StepsOf<M extends readonly AnyModelLike[]> = {
43
- [K in M[number] as K['name']]: (filter?: FilterOf<K>) => TypedChain<K, M>;
176
+ } : never;
177
+ /**
178
+ * Результат `or(…)` (index.ts): оператор `or` и фильтры, которыми он типизирован; `F` — только для типов. В шаге класса C
179
+ * фильтры внутри `or()` — фильтры-объекты класса C.
180
+ */
181
+ export type OrOp<F = Record<string, unknown>> = Op & {
182
+ readonly [K in typeof OP]: 'or';
44
183
  } & {
45
- [K in M[number] as K extends ModelLike<string, unknown, unknown, infer A> ? (string extends A ? never : A) : never]: (filter?: FilterOf<K>) => TypedChain<K, M>;
184
+ readonly __filters?: F;
46
185
  };
186
+ /** Фильтр шага класса C: id, список id, строка ответа, or(…) из фильтров класса или объект полей и ролей. */
187
+ export type FilterOf<C> = string | string[] | {
188
+ id: string;
189
+ class: string;
190
+ } | OrOp<ObjectFilterOf<C>> | ObjectFilterOf<C>;
47
191
  /** Патч update: поля любой глубины необязательны; inc() — только у числовых полей (§2.5). */
48
192
  export type PatchOf<T> = {
49
193
  [P in keyof T]?: T[P] extends number | undefined ? T[P] | IncMarker : T[P] extends readonly unknown[] | undefined ? T[P] : T[P] extends Record<string, unknown> | undefined ? PatchOf<NonNullable<T[P]>> : T[P];
50
194
  };
51
- type Retyped = 'paths' | 'rows' | 'first' | 'versions' | 'limit' | 'offset' | 'sort' | 'asOf' | 'after' | 'withDeleted' | 'deep' | 'exact' | 'alias' | 'tags' | 'owner' | 'create' | 'update' | 'upsert' | 'delete' | 'anonymize' | 'reclass' | 'forUpdate' | 'restore' | 'purge' | 'rekey' | 'cache';
52
- /** Цепочка с текущим классом C: терминалы отдают RowOf<C>, шаги — по моделям M; роли и прочие классы — без типов. */
195
+ /** Счётчик глубины путей полей. */
196
+ type Depth = [never, 0, 1, 2, 3, 4, 5];
197
+ /** Пути полей данных через точку — без элементов массивов. */
198
+ type FieldPaths<T, D extends number = 6> = [D] extends [never] ? never : T extends readonly unknown[] ? never : T extends object ? {
199
+ [K in keyof T & string]-?: K | `${K}.${FieldPaths<NonNullable<T[K]>, Depth[D]>}`;
200
+ }[keyof T & string] : never;
201
+ /** Пути числовых полей (и полей без типа). */
202
+ type NumberPaths<T, D extends number = 6> = [D] extends [never] ? never : T extends readonly unknown[] ? never : T extends object ? {
203
+ [K in keyof T & string]-?: (unknown extends T[K] ? K : NonNullable<T[K]> extends number ? K : never) | `${K}.${NumberPaths<NonNullable<T[K]>, Depth[D]>}`;
204
+ }[keyof T & string] : never;
205
+ /** Поле агрегата класса C: `'data.<путь>'`. */
206
+ type DataField<C> = `data.${FieldPaths<DataOf<C>>}`;
207
+ /** Числовое поле агрегата класса C (sum, avg). */
208
+ type NumberField<C> = `data.${NumberPaths<DataOf<C>>}`;
209
+ /** Шаг класса C: фильтр класса и цепочка с ним. */
210
+ type StepFnOf<C, M extends readonly AnyModelLike[]> = (filter?: FilterOf<C>) => TypedChain<C, M>;
211
+ /** Шаги корня: имена и алиасы моделей подключения и системных классов. */
212
+ export type StepsOf<M extends readonly AnyModelLike[]> = {
213
+ [K in AllOf<M> as NameOf<K>]: StepFnOf<K, M>;
214
+ } & {
215
+ [K in AllOf<M> as AliasOf<K>]: StepFnOf<K, M>;
216
+ };
217
+ /** Значение слота одиночного конца: id, строка ответа или цепочка. */
218
+ type SlotOne = string | {
219
+ readonly id: string;
220
+ } | ChainRef;
221
+ /** Значение слота множественного конца: ещё и список id или строк. */
222
+ type SlotMany = SlotOne | readonly (string | {
223
+ readonly id: string;
224
+ })[];
225
+ /** Слоты роли с концом S записываемой строки класса C. */
226
+ type SlotFns<S, C, M extends readonly AnyModelLike[]> = {
227
+ set(value: S extends {
228
+ many: true;
229
+ } ? SlotMany : SlotOne): TypedChain<C, M>;
230
+ } & (S extends {
231
+ many: false;
232
+ } ? unknown : {
233
+ add(value: SlotMany): TypedChain<C, M>;
234
+ remove(value: SlotMany): TypedChain<C, M>;
235
+ }) & (S extends {
236
+ required: true;
237
+ } ? unknown : {
238
+ unset(): TypedChain<C, M>;
239
+ });
240
+ /** Шаг от C по имени P: класс или алиас, связанный с C, или роль семейства C (имя роли, совпавшее с классом, — шаг класса). */
241
+ type HopStep<C, P, M extends readonly AnyModelLike[], All> = P extends StepNames<All> ? ([ByStepName<P, HopClasses<C, All>>] extends [never] ? unknown : StepFnOf<ByStepName<P, HopClasses<C, All>>, M>) : P extends RolesOf<FamilyOf<C, All>> ? StepFnOf<ModelsNamed<RoleTargets<P, All>, All>, M> : unknown;
242
+ /** Слоты имени P: только роль самого класса C. */
243
+ type HopSlots<C, P, M extends readonly AnyModelLike[]> = P extends keyof EndsOf<C> ? SlotFns<EndsOf<C>[P], C, M> : unknown;
244
+ /** Свойства-шаги цепочки класса C. */
245
+ type HopsOf<C, M extends readonly AnyModelLike[], All = AllOf<M>> = {
246
+ [P in StepNames<HopClasses<C, All>> | Exclude<RolesOf<FamilyOf<C, All>>, StepNames<All>> | Extract<keyof EndsOf<C>, string>]: HopStep<C, P, M, All> & HopSlots<C, P, M>;
247
+ };
248
+ type Retyped = 'paths' | 'rows' | 'first' | 'versions' | 'limit' | 'offset' | 'sort' | 'asOf' | 'after' | 'withDeleted' | 'deep' | 'exact' | 'alias' | 'tags' | 'owner' | 'create' | 'update' | 'upsert' | 'delete' | 'forUpdate' | 'restore' | 'cache' | 'sum' | 'avg' | 'min' | 'max' | 'countBy';
249
+ /**
250
+ * Цепочка с текущим классом C (у шага-роли с несколькими целями — их союз): терминалы отдают RowOf<C>, шаги — переходы
251
+ * из концов, слоты — роли C, агрегаты — поля C.
252
+ */
53
253
  export type TypedChain<C, M extends readonly AnyModelLike[]> = Omit<ChainCore, Retyped> & {
54
254
  paths(): Promise<Path[]>;
55
255
  rows(): Promise<RowOf<C>[]>;
56
256
  first(): Promise<RowOf<C> | null>;
57
- versions(opts?: {
58
- follow?: boolean;
59
- }): Promise<RowOf<C>[]>;
257
+ versions(): Promise<RowOf<C>[]>;
258
+ /** Сумма числового поля `'data.<путь>'` класса; нет ни одного значения — `null`. */
259
+ sum(field: NumberField<C>): Promise<number | null>;
260
+ /** Среднее числового поля `'data.<путь>'` класса; нет ни одного значения — `null`. */
261
+ avg(field: NumberField<C>): Promise<number | null>;
262
+ /** Наименьшее значение поля `'data.<путь>'` класса. */
263
+ min(field: DataField<C>): Promise<unknown>;
264
+ /** Наибольшее значение поля `'data.<путь>'` класса. */
265
+ max(field: DataField<C>): Promise<unknown>;
266
+ /** Сколько объектов с каждым значением поля `'data.<путь>'` класса. */
267
+ countBy(field: DataField<C>): Promise<Record<string, number>>;
60
268
  limit(n: number): TypedChain<C, M>;
61
269
  offset(n: number): TypedChain<C, M>;
62
270
  sort(field: 'at' | 'rev' | 'id' | `data.${string}`, dir?: 'asc' | 'desc' | boolean): TypedChain<C, M>;
@@ -73,40 +281,27 @@ export type TypedChain<C, M extends readonly AnyModelLike[]> = Omit<ChainCore, R
73
281
  create(data: Partial<DataOf<C>>, opts?: {
74
282
  reason?: string;
75
283
  }): TypedChain<C, M>;
284
+ /** Без фильтра, id, `tags()`, `owner()` и `limit()` у шагов — `invalid_query`; правка всех строк — `{ all: true }`. */
76
285
  update(patch?: PatchOf<DataOf<C>>, opts?: {
77
286
  rev?: number;
287
+ all?: boolean;
78
288
  reason?: string;
79
289
  }): TypedChain<C, M>;
80
290
  upsert(data: Partial<DataOf<C>>, opts?: {
81
291
  reason?: string;
82
292
  }): TypedChain<C, M>;
293
+ /** С `{ confirm: true }` без фильтра, id, `tags()`, `owner()` и `limit()` у шагов — `invalid_query`; всех — `{ all: true }`. */
83
294
  delete(opts?: {
84
295
  confirm?: boolean;
296
+ all?: boolean;
85
297
  reason?: string;
86
298
  }): TypedChain<C, M>;
87
- anonymize(fields: (keyof DataOf<C> & string)[], opts?: {
88
- reason?: string;
89
- }): TypedChain<C, M>;
90
- reclass(cls: string, data?: Record<string, unknown>, opts?: {
91
- reason?: string;
92
- }): TypedChain<unknown, M>;
93
299
  forUpdate(): TypedChain<C, M>;
94
300
  restore(opts?: {
95
301
  reason?: string;
96
302
  }): TypedChain<C, M>;
97
- purge(opts?: {
98
- confirm?: boolean;
99
- reason?: string;
100
- }): TypedChain<C, M>;
101
- rekey(patch: Partial<DataOf<C>> & Partial<Record<keyof LinksOf<C>, string | {
102
- id: string;
103
- }>>, opts?: {
104
- reason?: string;
105
- }): TypedChain<C, M>;
106
303
  cache(opts?: {
107
304
  ttl?: number;
108
305
  }): TypedChain<C, M>;
109
- } & StepsOf<M> & {
110
- [step: string]: StepFn;
111
- };
306
+ } & HopsOf<C, M>;
112
307
  export {};
package/dist/types.d.ts CHANGED
@@ -3,35 +3,36 @@
3
3
  */
4
4
  /**
5
5
  * Ревизия схемы, которую знает эта версия пакета, — номер версии SQL-части letopis (таблиц и функций в схеме установки).
6
- * После `up()` у схемы та же ревизия (`r.revision`); схему отставшей ревизии `up()` обновляет только с `upgrade: true`,
7
- * а `connect()` о ней предупреждает.
6
+ * После `up()` у схемы та же ревизия (`r.revision`); схему отставшей ревизии 2.x `up()` обновляет только с
7
+ * `upgrade: true`, а `connect()` о ней предупреждает.
8
8
  */
9
- export declare const DDL_REVISION = 7;
9
+ export declare const DDL_REVISION = 8;
10
10
  /**
11
11
  * Классы, строки которых меняют права и схему: их запись повышает счётчик прав (загрузчик), а сигнал
12
12
  * сбрасывает весь кэш результатов и пропуска прав. Тот же список — в триггерах базы (60-write.sql).
13
13
  */
14
14
  export declare const ACL_CLASS_NAMES: readonly string[];
15
15
  /**
16
- * Имена методов цепочки: модификаторы, терминалы, глаголы записи и слоты концов, а также `then` и снятые методы прежних
17
- * версий. После шага цепочки эти имена библиотека не ищет среди классов; все они входят в `RESERVED_CLASS_NAMES`.
16
+ * Имена методов цепочки: модификаторы, терминалы, глаголы записи и слоты концов, а также `then`. После шага цепочки эти
17
+ * имена библиотека не ищет среди классов; все они входят в `RESERVED_CLASS_NAMES`.
18
18
  */
19
- export declare const CHAIN_METHODS: readonly ["then", "entity", "paths", "rows", "first", "ids", "count", "limit", "offset", "sort", "asOf", "withDeleted", "deep", "exact", "sum", "avg", "min", "max", "countBy", "after", "versions", "create", "update", "upsert", "set", "unset", "add", "remove", "delete", "anonymize", "reclass", "restore", "purge", "alias", "tags", "owner", "forUpdate", "cache", "rekey", "run", "execute", "account", "diff", "blame"];
19
+ export declare const CHAIN_METHODS: readonly ["then", "entity", "paths", "rows", "first", "ids", "count", "limit", "offset", "sort", "asOf", "withDeleted", "deep", "exact", "sum", "avg", "min", "max", "countBy", "after", "versions", "create", "update", "upsert", "set", "unset", "add", "remove", "delete", "restore", "alias", "tags", "owner", "forUpdate", "cache"];
20
20
  /** Свойства корня db (и транзакции db.begin()). */
21
- export declare const DB_METHODS: readonly ["as", "begin", "commit", "rollback", "lock", "batch", "watch", "close", "registry", "sql", "load", "verify", "maintain", "reset", "describe", "auth", "acl", "idOf", "refresh", "transaction", "flushSignals", "schema", "token", "tenant", "reloadSchema", "accounts", "credentials", "resources", "rules", "compact", "trimHistory", "rekeyClass", "cache", "projector", "rebuildAggregates"];
21
+ export declare const DB_METHODS: readonly ["as", "begin", "commit", "rollback", "lock", "batch", "watch", "close", "registry", "sql", "load", "maintain", "auth", "acl", "idOf", "refresh", "transaction", "flushSignals", "schema", "token", "tenant", "cache"];
22
22
  /** Свойства батча db.batch(name). */
23
- export declare const BATCH_METHODS: readonly ["discard", "size"];
23
+ export declare const BATCH_METHODS: readonly ["run", "discard", "size"];
24
24
  /**
25
25
  * Имена методов цепочки, корня `db` и батча. Там, где имя занято методом, библиотека не ищет его среди классов, и шаг с
26
- * таким именем до класса не дойдёт; класс-связь (`link`) с таким именем база не создаст (`invalid_class`). Не давайте
27
- * классам эти имена.
26
+ * таким именем до класса не дойдёт. Хаб с таким именем отклоняют `hub()`, `syncModels` и база, связь (`link`) — база
27
+ * (`invalid_class`; план 2.0, этап 4, п. 2); хаб, заведённый мимо проверки базы, `up()` оставляет и называет в
28
+ * предупреждении.
28
29
  */
29
30
  export declare const RESERVED_CLASS_NAMES: readonly string[];
30
31
  /** Строка ответа: текущая версия (или версия журнала для asOf, versions, withDeleted). */
31
32
  export interface Row<D = Record<string, unknown>, L = Record<string, string | string[]>> {
32
33
  id: string;
33
34
  class: string;
34
- /** Номер версии — для update(patch, { rev }) (этап 4). */
35
+ /** Номер версии — для update(patch, { rev }). */
35
36
  rev: number;
36
37
  tenant: string;
37
38
  owner: string;
@@ -42,23 +43,17 @@ export interface Row<D = Record<string, unknown>, L = Record<string, string | st
42
43
  at: string;
43
44
  author: string;
44
45
  agent?: string;
45
- /** Вид операции версии: create, update, delete, reclass, … */
46
+ /** Вид операции версии: create, update, delete, restore, … */
46
47
  op: string;
47
48
  reason?: string;
48
- /** Связь со старым или новым id при reclass и rekey. */
49
- moved?: string;
50
49
  /** Надгробие: удалённая сущность (withDeleted) или версия удаления (versions). */
51
50
  $deleted?: true;
52
51
  /** Глубина узла при deep() (1 — прямой потомок); в превью удаления — уровень каскада (0 — цель). */
53
52
  $depth?: number;
54
- /** Что сделал upsert (этап 4). */
53
+ /** Что сделал upsert. */
55
54
  $upsert?: 'created' | 'updated' | 'unchanged';
56
55
  /** Превью удаления (delete() без confirm): удалится, снимется конец или помешает (restrict). */
57
56
  $action?: 'delete' | 'unset' | 'restrict';
58
- /** purge(): число версий в журнале (превью) или стёртых. */
59
- $versions?: number;
60
- /** purge({ confirm: true }): история стёрта. */
61
- $purged?: true;
62
57
  }
63
58
  /** Путь paths(): узел на каждый шаг цепочки. */
64
59
  export type Path = Record<string, Row>;
@@ -111,24 +106,14 @@ export interface ChainMods {
111
106
  withDeleted?: boolean;
112
107
  /** Чтение с блокировкой строк последнего шага (только в db.begin()). */
113
108
  forUpdate?: boolean;
114
- /** versions({ follow: true }): история и через смену id (reclass, rekey) по колонке moved. */
115
- follow?: boolean;
116
109
  /** Кэш результатов (§2.12): .cache() — срок по умолчанию, .cache({ ttl }) — свой, секунды. */
117
110
  cache?: {
118
111
  ttl?: number;
119
112
  };
120
- /** Параметры терминалов diff() и blame() (план 1.1, §2.1): номера версий или моменты (ISO). */
121
- hist?: {
122
- from?: number | string;
123
- to?: number | string;
124
- at?: string;
125
- follow?: boolean;
126
- limit?: number;
127
- };
128
113
  }
129
114
  /** Событие хука onQuery: один запрос цепочки. */
130
115
  export interface QueryEvent {
131
- mode: 'paths' | 'rows' | 'ids' | 'count' | 'versions' | 'agg' | 'diff' | 'blame';
116
+ mode: 'paths' | 'rows' | 'ids' | 'count' | 'versions' | 'agg';
132
117
  /** Классы шагов цепочки. */
133
118
  classes: string[];
134
119
  ms: number;
@@ -136,63 +121,3 @@ export interface QueryEvent {
136
121
  /** ms > slowMs. */
137
122
  slow: boolean;
138
123
  }
139
- /** Изменение пути: added — не было, removed — не стало, changed — другое значение. */
140
- export interface DiffChange {
141
- /** Путь: первый сегмент — раздел строки (data, links, tags, owner, class). */
142
- path: string[];
143
- kind: 'added' | 'removed' | 'changed';
144
- before?: unknown;
145
- after?: unknown;
146
- }
147
- /** Различия объекта между двумя версиями (diff()). */
148
- export interface DiffResult {
149
- id: string;
150
- class: string | null;
151
- from: {
152
- rev: number;
153
- at: string;
154
- id?: string;
155
- } | null;
156
- to: {
157
- rev: number;
158
- at: string;
159
- id?: string;
160
- deleted?: true;
161
- } | null;
162
- changes: DiffChange[];
163
- }
164
- /** Кто и почему последним изменил лист версии (blame()). */
165
- export interface BlameField {
166
- path: string[];
167
- rev: number;
168
- at: string;
169
- author: string;
170
- agent?: string;
171
- op: string;
172
- reason?: string;
173
- /** Другой id (история прошла через смену id при reclass или rekey). */
174
- id?: string;
175
- /** false — до найденной версии история неполная: изменение могло быть раньше. */
176
- exact: boolean;
177
- }
178
- /** Поля версии объекта с версиями, которые их изменили (blame()). */
179
- export interface BlameResult {
180
- id: string;
181
- class: string;
182
- rev: number;
183
- deleted?: true;
184
- /** История просмотрена целиком и без пропусков. */
185
- complete: boolean;
186
- fields: BlameField[];
187
- }
188
- /** diff(): «до» и «после» — номер версии или момент (ISO-строка, Date); follow — через смену id. */
189
- export interface DiffOptions {
190
- from?: number | string | Date;
191
- to?: number | string | Date;
192
- follow?: boolean;
193
- }
194
- /** blame(): версия на момент at (по умолчанию текущая); limit — сколько версий просматривать (1000). */
195
- export interface BlameOptions {
196
- at?: string | Date;
197
- limit?: number;
198
- }