letopis 0.20.3 → 1.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 (89) hide show
  1. package/AGENT-CHEATSHEET.en.md +368 -0
  2. package/AGENT-CHEATSHEET.md +354 -0
  3. package/CHANGELOG.md +348 -0
  4. package/MIGRATION.md +190 -0
  5. package/README.en.md +1937 -0
  6. package/README.md +1493 -3466
  7. package/dist/acl.d.ts +26 -50
  8. package/dist/acl.js +22 -267
  9. package/dist/admin.d.ts +138 -0
  10. package/dist/admin.js +170 -0
  11. package/dist/auth.d.ts +120 -73
  12. package/dist/auth.js +121 -306
  13. package/dist/cache.d.ts +73 -0
  14. package/dist/cache.js +148 -0
  15. package/dist/chain.d.ts +124 -191
  16. package/dist/chain.js +369 -551
  17. package/dist/cli.d.ts +2 -0
  18. package/dist/cli.js +164 -0
  19. package/dist/demo/booking.d.ts +289 -0
  20. package/dist/demo/booking.js +159 -0
  21. package/dist/errors.d.ts +29 -0
  22. package/dist/errors.js +70 -0
  23. package/dist/import.d.ts +179 -0
  24. package/dist/import.js +792 -0
  25. package/dist/index.d.ts +172 -26
  26. package/dist/index.js +304 -178
  27. package/dist/jsonschema.d.ts +22 -0
  28. package/dist/jsonschema.js +167 -0
  29. package/dist/load.d.ts +76 -0
  30. package/dist/load.js +884 -0
  31. package/dist/model.d.ts +166 -0
  32. package/dist/model.js +224 -0
  33. package/dist/ops.d.ts +7 -6
  34. package/dist/ops.js +7 -51
  35. package/dist/pglite.d.ts +22 -0
  36. package/dist/pglite.js +45 -0
  37. package/dist/registry.d.ts +57 -0
  38. package/dist/registry.js +82 -0
  39. package/dist/sql.d.ts +59 -142
  40. package/dist/sql.js +568 -654
  41. package/dist/sync.d.ts +31 -0
  42. package/dist/sync.js +108 -0
  43. package/dist/tx.d.ts +129 -8
  44. package/dist/tx.js +300 -73
  45. package/dist/typed.d.ts +97 -0
  46. package/dist/typed.js +1 -0
  47. package/dist/types.d.ts +71 -250
  48. package/dist/types.js +27 -108
  49. package/dist/up.d.ts +140 -47
  50. package/dist/up.js +339 -267
  51. package/dist/uuid.d.ts +21 -6
  52. package/dist/uuid.js +48 -64
  53. package/dist/validate.d.ts +24 -0
  54. package/dist/validate.js +251 -0
  55. package/dist/watch.d.ts +62 -0
  56. package/dist/watch.js +168 -0
  57. package/dist/write.d.ts +117 -74
  58. package/dist/write.js +658 -720
  59. package/llms.txt +26 -0
  60. package/package.json +49 -19
  61. package/sql/10-core.sql +136 -0
  62. package/sql/15-errors.sql +60 -0
  63. package/sql/20-context.sql +153 -0
  64. package/sql/30-validate.sql +423 -0
  65. package/sql/40-class.sql +259 -0
  66. package/sql/50-acl.sql +539 -0
  67. package/sql/60-write.sql +1369 -0
  68. package/sql/70-read.sql +245 -0
  69. package/sql/80-auth.sql +827 -0
  70. package/sql/90-time.sql +957 -0
  71. package/sql/95-seed.system.sql +178 -0
  72. package/sql/99-revision.sql +3 -0
  73. package/sql/README.md +56 -0
  74. package/sql/seed.booking.sql +39 -112
  75. package/dist/schema.d.ts +0 -15
  76. package/dist/schema.js +0 -351
  77. package/dist/sessions.d.ts +0 -32
  78. package/dist/sessions.js +0 -114
  79. package/dist/tables.d.ts +0 -105
  80. package/dist/tables.js +0 -248
  81. package/docker/Dockerfile +0 -40
  82. package/docker/start.sh +0 -18
  83. package/scripts/check-docs.mjs +0 -375
  84. package/scripts/gen-api-contract.mjs +0 -226
  85. package/scripts/gen-types.mjs +0 -350
  86. package/scripts/release-notes.mjs +0 -76
  87. package/scripts/schema-sync.mjs +0 -185
  88. package/sql/ddl.sql +0 -600
  89. package/sql/seed.auth.sql +0 -73
package/dist/uuid.d.ts CHANGED
@@ -1,6 +1,21 @@
1
- /** Namespace letopis для uuidv5 (фиксированная константа — стабильность id между релизами). */
2
- export declare const LETOPIS_NS = "c7a2f9d4-3b61-4e8a-9f05-8d2c1e6b7a90";
3
- /** RFC 4122 v5: sha1(namespace + name); детерминирован — одно имя → один uuid. */
4
- export declare function uuidv5(name: string, ns?: string): string;
5
- /** RFC 9562 v7: 48 бит unix-ms + random — время в старших битах, вставки ложатся в хвост индекса. */
6
- export declare function uuidv7(): string;
1
+ /** RFC 4122 v5: sha1(пространство + имя); одно имя в одном пространстве — один id. */
2
+ export declare function uuidv5(name: string, ns: string): string;
3
+ /** Имя v5: JSON-массив из имени класса и значений ключа — как `jsonb_build_array(…)::text` в базе. */
4
+ export declare function v5Name(cls: string, values: readonly string[]): string;
5
+ /** id строки класса по значениям ключа в объявленном порядке. */
6
+ export declare function v5Id(tenant: string, cls: string, values: readonly string[]): string;
7
+ /** Что нужно idOf() о классе: имя, ключ и какие его части — концы (их значения — id целей). */
8
+ export interface KeyedClass {
9
+ name: string;
10
+ key: readonly string[];
11
+ /** Имена концов класса: значение конца в ключе — id цели, он приводится к строчным буквам. */
12
+ ends?: readonly string[];
13
+ }
14
+ /**
15
+ * id объекта класса с ключом: `idOf(класс, { поле: значение, Роль: id }, { tenant })`. Значения
16
+ * ключевых полей — строки как есть (база их не нормализует), значения концов — id целей.
17
+ * Для класса без ключа id выдаёт база — ошибка `no_key`.
18
+ */
19
+ export declare function idOf(cls: KeyedClass, key: Readonly<Record<string, unknown>>, opts: {
20
+ tenant: string;
21
+ }): string;
package/dist/uuid.js CHANGED
@@ -1,68 +1,52 @@
1
1
  /**
2
- * Генерация id по Schema (attributes.id):
3
- * "uuid" → v4 (random) — дефолт;
4
- * { type: 'uuid', generate: 7 } → v7 (unix-время в старших битах: btree-локальность);
5
- * { type: 'uuid', generate: 5, from: […] } → v5: sha1 от «схема:партиция:класс:значения from» —
6
- * детерминированный id из концов/полей: та же комбинация → тот же id (идемпотентный create,
7
- * дубль невозможен даже в гонке — оба запроса вычислят один id, второй станет версией).
2
+ * Идентификаторы v5 (план, §2.3, этап 2, п. 4).
3
+ *
4
+ * id = uuid_v5(арендатор, '["<класс>", "<значение 1>", …]'). Имя собирается так же, как в базе
5
+ * (`v5_name` в lib/sql/30-validate.sql — текстовый вид jsonb-массива): элементы через JSON.stringify,
6
+ * разделитель — запятая с пробелом. Совпадение закрепляет общий файл векторов lib/test/vectors/v5.json.
8
7
  */
9
- //
10
- // FILE: lib/src/uuid.ts
11
- // VERSION: 1.0.0
12
- // START_MODULE_CONTRACT
13
- // PURPOSE: Детерминированная (v5) и упорядоченная по времени (v7) генерация UUID для id сущностей.
14
- // SCOPE: namespace-константа LETOPIS_NS, форматирование 16 байт в каноничный UUID, v5 (sha1 от namespace+name), v7 (unix-ms в старших битах + random).
15
- // DEPENDS: none
16
- // LINKS: M-UUID, V-M-UUID
17
- // ROLE: RUNTIME
18
- // MAP_MODE: EXPORTS
19
- // END_MODULE_CONTRACT
20
- //
21
- // START_MODULE_MAP
22
- // LETOPIS_NS - фиксированный namespace letopis для uuidv5 (стабильность id между релизами).
23
- // fmt - (локальная) форматирует 16-байтный Buffer в каноничную строку UUID 8-4-4-4-12.
24
- // uuidv5 - RFC 4122 v5: детерминированный uuid = sha1(namespace + name).
25
- // uuidv7 - RFC 9562 v7: 48 бит unix-ms в старших битах + random.
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
31
- import { createHash, randomBytes } from 'node:crypto';
32
- /** Namespace letopis для uuidv5 (фиксированная константа — стабильность id между релизами). */
33
- export const LETOPIS_NS = 'c7a2f9d4-3b61-4e8a-9f05-8d2c1e6b7a90';
34
- const fmt = (b) => {
35
- const h = b.toString('hex');
36
- return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
37
- };
38
- // START_CONTRACT: uuidv5
39
- // PURPOSE: Детерминированный UUID v5 из имени и namespace (одно имя → один uuid).
40
- // INPUTS: { name: string - имя (склеенные значения from класса), ns: string - namespace (default LETOPIS_NS) }
41
- // OUTPUTS: { string - каноничный UUID v5 (version=5, variant=10xx) }
42
- // SIDE_EFFECTS: none (чистая функция — только хэширование входа)
43
- // LINKS: M-UUID, V-M-UUID
44
- // END_CONTRACT: uuidv5
45
- /** RFC 4122 v5: sha1(namespace + name); детерминирован — одно имя → один uuid. */
46
- export function uuidv5(name, ns = LETOPIS_NS) {
47
- const nsBytes = Buffer.from(ns.replace(/-/g, ''), 'hex');
48
- const h = createHash('sha1').update(nsBytes).update(name, 'utf8').digest().subarray(0, 16);
49
- h[6] = (h[6] & 0x0f) | 0x50; // version 5
50
- h[8] = (h[8] & 0x3f) | 0x80; // variant 10xx
51
- return fmt(h);
8
+ import { createHash } from 'node:crypto';
9
+ import { LetopisError } from './errors.js';
10
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11
+ /** RFC 4122 v5: sha1(пространство + имя); одно имя в одном пространстве — один id. */
12
+ export function uuidv5(name, ns) {
13
+ if (!UUID.test(ns))
14
+ throw new LetopisError('invalid_data', `пространство имён v5 — не uuid: ${ns}`);
15
+ const h = createHash('sha1').update(Buffer.from(ns.replace(/-/g, ''), 'hex')).update(name, 'utf8').digest().subarray(0, 16);
16
+ h[6] = (h[6] & 0x0f) | 0x50; // версия 5
17
+ h[8] = (h[8] & 0x3f) | 0x80; // вариант 10xx
18
+ const x = h.toString('hex');
19
+ return `${x.slice(0, 8)}-${x.slice(8, 12)}-${x.slice(12, 16)}-${x.slice(16, 20)}-${x.slice(20, 32)}`;
52
20
  }
53
- // START_CONTRACT: uuidv7
54
- // PURPOSE: Упорядоченный по времени UUID v7 — 48 бит unix-ms в старших битах + random.
55
- // INPUTS: { none }
56
- // OUTPUTS: { string - каноничный UUID v7 (version=7, variant=10xx) }
57
- // SIDE_EFFECTS: недетерминирован — читает Date.now() и криптослучайные байты (randomBytes)
58
- // LINKS: M-UUID, V-M-UUID
59
- // END_CONTRACT: uuidv7
60
- /** RFC 9562 v7: 48 бит unix-ms + random — время в старших битах, вставки ложатся в хвост индекса. */
61
- export function uuidv7() {
62
- const b = Buffer.alloc(16);
63
- b.writeUIntBE(Date.now(), 0, 6);
64
- randomBytes(10).copy(b, 6);
65
- b[6] = (b[6] & 0x0f) | 0x70; // version 7
66
- b[8] = (b[8] & 0x3f) | 0x80; // variant 10xx
67
- return fmt(b);
21
+ /** Имя v5: JSON-массив из имени класса и значений ключа — как `jsonb_build_array(…)::text` в базе. */
22
+ export function v5Name(cls, values) {
23
+ return `[${[cls, ...values].map((v) => JSON.stringify(v)).join(', ')}]`;
24
+ }
25
+ /** id строки класса по значениям ключа в объявленном порядке. */
26
+ export function v5Id(tenant, cls, values) {
27
+ return uuidv5(v5Name(cls, values), tenant);
28
+ }
29
+ /**
30
+ * id объекта класса с ключом: `idOf(класс, { поле: значение, Роль: id }, { tenant })`. Значения
31
+ * ключевых полей — строки как есть (база их не нормализует), значения концов — id целей.
32
+ * Для класса без ключа id выдаёт база — ошибка `no_key`.
33
+ */
34
+ export function idOf(cls, key, opts) {
35
+ if (!cls.key.length) {
36
+ throw new LetopisError('no_key', `у класса ${cls.name} нет ключа: id выдаёт база при создании`);
37
+ }
38
+ const ends = new Set(cls.ends ?? []);
39
+ const values = cls.key.map((k) => {
40
+ const v = key[k];
41
+ if (typeof v !== 'string') {
42
+ throw new LetopisError('invalid_data', `idOf(${cls.name}): часть ключа ${k} должна быть строкой`, { key: cls.key });
43
+ }
44
+ if (ends.has(k)) {
45
+ if (!UUID.test(v))
46
+ throw new LetopisError('invalid_data', `idOf(${cls.name}): конец ${k} — не uuid`);
47
+ return v.toLowerCase();
48
+ }
49
+ return v;
50
+ });
51
+ return v5Id(opts.tenant, cls.name, values);
68
52
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Валидатор на TypeScript (план, §2.6, этап 2, п. 11): та же проверка, что `compile_validator` и
3
+ * `validate_data` в lib/sql/30-validate.sql, — то же подмножество JSON Schema, те же пути и ключевые
4
+ * слова нарушений. Им пользуются загрузчик (этап 4) и ранние ошибки библиотеки; совпадение с базой
5
+ * закрепляет дифференциальный тест validator-parity.
6
+ *
7
+ * Как и в базе, правило — «нет узла по пути, нарушающего условие»: обычное правило даёт одно
8
+ * нарушение, сколько бы узлов его ни нарушили; дополнительные проверки (длина строки, uniqueItems,
9
+ * смысл даты, перечисления объектов, число ключей) — по нарушению на каждый узел.
10
+ */
11
+ import { type Issue } from './errors.js';
12
+ import { type JsonSchema } from './jsonschema.js';
13
+ type J = unknown;
14
+ /** Равенство значений JSON, как у jsonb: порядок ключей не важен, числа сравниваются по значению. */
15
+ export declare function jsonEqual(a: J, b: J): boolean;
16
+ export interface Validator {
17
+ /** Нарушения ([] — данные приняты). */
18
+ (data: unknown): Issue[];
19
+ }
20
+ /** Компилирует JSON Schema (разворот resolved.schema класса) в функцию проверки. */
21
+ export declare function compileValidator(schema: JsonSchema): Validator;
22
+ /** Умолчания, как apply_defaults в базе: на каждом уровне, во вложенные объекты — только если они есть. */
23
+ export declare function applyDefaults(schema: JsonSchema, data: unknown): unknown;
24
+ export {};
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Валидатор на TypeScript (план, §2.6, этап 2, п. 11): та же проверка, что `compile_validator` и
3
+ * `validate_data` в lib/sql/30-validate.sql, — то же подмножество JSON Schema, те же пути и ключевые
4
+ * слова нарушений. Им пользуются загрузчик (этап 4) и ранние ошибки библиотеки; совпадение с базой
5
+ * закрепляет дифференциальный тест validator-parity.
6
+ *
7
+ * Как и в базе, правило — «нет узла по пути, нарушающего условие»: обычное правило даёт одно
8
+ * нарушение, сколько бы узлов его ни нарушили; дополнительные проверки (длина строки, uniqueItems,
9
+ * смысл даты, перечисления объектов, число ключей) — по нарушению на каждый узел.
10
+ */
11
+ import { LetopisError } from './errors.js';
12
+ import { SUPPORTED, translatePattern } from './jsonschema.js';
13
+ const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
14
+ const typeOf = (v) => (v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v === 'object' ? 'object' : typeof v === 'number' ? 'number' : typeof v === 'string' ? 'string' : typeof v === 'boolean' ? 'boolean' : 'unknown');
15
+ /** Равенство значений JSON, как у jsonb: порядок ключей не важен, числа сравниваются по значению. */
16
+ export function jsonEqual(a, b) {
17
+ if (typeof a === 'number' && typeof b === 'number')
18
+ return a === b;
19
+ if (typeOf(a) !== typeOf(b))
20
+ return false;
21
+ if (Array.isArray(a))
22
+ return a.length === b.length && a.every((x, i) => jsonEqual(x, b[i]));
23
+ if (isObj(a)) {
24
+ const ka = Object.keys(a);
25
+ const kb = Object.keys(b);
26
+ return ka.length === kb.length && ka.every((k) => Object.prototype.hasOwnProperty.call(b, k) && jsonEqual(a[k], b[k]));
27
+ }
28
+ return a === b;
29
+ }
30
+ /** Точная проверка кратности (в базе — numeric): через десятичную запись, без ошибок округления. */
31
+ function isMultiple(x, m) {
32
+ const dec = (v) => {
33
+ const s = v.toString();
34
+ if (/e/i.test(s)) {
35
+ const [mant, exp] = s.toLowerCase().split('e');
36
+ const [i, f = ''] = mant.replace('-', '').split('.');
37
+ const e = Number(exp) - f.length;
38
+ const digits = BigInt(i + f) * (v < 0 ? -1n : 1n);
39
+ return e >= 0 ? [digits * 10n ** BigInt(e), 0] : [digits, -e];
40
+ }
41
+ const [i, f = ''] = s.replace('-', '').split('.');
42
+ return [BigInt(i + f) * (v < 0 ? -1n : 1n), f.length];
43
+ };
44
+ const [xi, xs] = dec(x);
45
+ const [mi, ms] = dec(m);
46
+ if (mi === 0n)
47
+ return false;
48
+ const scale = Math.max(xs, ms);
49
+ const X = xi * 10n ** BigInt(scale - xs);
50
+ const M = mi * 10n ** BigInt(scale - ms);
51
+ return X % M === 0n;
52
+ }
53
+ const FORMAT_RE = {
54
+ uuid: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/,
55
+ date: /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/,
56
+ 'date-time': /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,6})?(Z|[+-][0-9]{2}:[0-9]{2})$/,
57
+ email: /^[^@\s]+@[^@\s]+\.[^@\s]+$/,
58
+ };
59
+ /** Смысл даты и времени, как pg_input_is_valid(…, 'date' | 'timestamptz') для уже проверенной формы. */
60
+ function validDate(s, withTime) {
61
+ const y = Number(s.slice(0, 4));
62
+ const mo = Number(s.slice(5, 7));
63
+ const d = Number(s.slice(8, 10));
64
+ if (y < 1 || mo < 1 || mo > 12 || d < 1)
65
+ return false;
66
+ const days = [31, (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0 ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][mo - 1];
67
+ if (d > days)
68
+ return false;
69
+ if (!withTime)
70
+ return true;
71
+ const h = Number(s.slice(11, 13));
72
+ const mi = Number(s.slice(14, 16));
73
+ const se = Number(s.slice(17, 19));
74
+ if (h > 24 || mi > 59 || se > 60)
75
+ return false;
76
+ if (h === 24 && (mi > 0 || se > 0 || /\.[0-9]*[1-9]/.test(s.slice(19))))
77
+ return false;
78
+ const tz = /([+-])([0-9]{2}):([0-9]{2})$/.exec(s);
79
+ if (tz && (Number(tz[2]) > 15 || Number(tz[3]) > 59))
80
+ return false;
81
+ return true;
82
+ }
83
+ const TYPE_TEST = {
84
+ integer: (v) => typeof v === 'number' && Number.isInteger(v),
85
+ number: (v) => typeof v === 'number',
86
+ string: (v) => typeof v === 'string',
87
+ boolean: (v) => typeof v === 'boolean',
88
+ object: isObj,
89
+ array: Array.isArray,
90
+ null: (v) => v === null,
91
+ };
92
+ const prop = (sel, k) => (root) => sel(root).flatMap((n) => (isObj(n) && Object.prototype.hasOwnProperty.call(n, k) ? [n[k]] : []));
93
+ const items = (sel) => (root) => sel(root).flatMap((n) => (Array.isArray(n) ? n : []));
94
+ const others = (sel, known) => (root) => sel(root).flatMap((n) => (isObj(n) ? Object.keys(n).filter((k) => !known.includes(k)).map((k) => n[k]) : []));
95
+ const keys = (sel) => (root) => sel(root).flatMap((n) => (isObj(n) ? Object.keys(n) : []));
96
+ function fail(path, msg) {
97
+ throw new LetopisError('invalid_class', `${msg} (узел ${JSON.stringify(path)})`);
98
+ }
99
+ /** Компиляция узла схемы в правила — порядок и состав как в jp_compile. */
100
+ function compileNode(s, sel, path, inAny, out) {
101
+ if (s === undefined || s === true || (isObj(s) && Object.keys(s).length === 0))
102
+ return;
103
+ if (!isObj(s))
104
+ fail(path, `описание узла ${JSON.stringify(path)} — не объект`);
105
+ const sch = s;
106
+ for (const k of Object.keys(sch)) {
107
+ if (!SUPPORTED.has(k) && !k.startsWith('x-'))
108
+ fail(path, `ключевое слово ${k} не поддерживается`);
109
+ }
110
+ if (sch.type !== undefined) {
111
+ const types = Array.isArray(sch.type) ? sch.type : [sch.type];
112
+ const tests = types.map((t) => TYPE_TEST[t] ?? fail(path, `неизвестный тип в узле ${JSON.stringify(path)}`));
113
+ out.push({ path, keyword: 'type', select: sel, test: (v) => tests.some((f) => f(v)) });
114
+ }
115
+ if (sch.enum !== undefined || sch.const !== undefined) {
116
+ const keyword = sch.enum !== undefined ? 'enum' : 'const';
117
+ const values = (sch.enum ?? [sch.const]);
118
+ if (values.some((v) => typeof v === 'object' && v !== null)) {
119
+ if (inAny)
120
+ fail(path, 'перечисление объектов внутри anyOf не поддерживается');
121
+ out.push({ path, keyword, select: sel, test: () => true, extra: (v) => (values.some((x) => jsonEqual(x, v)) ? null : 'значение не из перечисления') });
122
+ }
123
+ else {
124
+ out.push({ path, keyword, select: sel, test: (v) => values.some((x) => typeOf(x) === typeOf(v) && jsonEqual(x, v)) });
125
+ }
126
+ }
127
+ const num = (f, b) => (v) => typeof v !== 'number' || f(v, b);
128
+ if (sch.minimum !== undefined)
129
+ out.push({ path, keyword: 'minimum', select: sel, test: num((v, b) => v >= b, sch.minimum) });
130
+ if (sch.maximum !== undefined)
131
+ out.push({ path, keyword: 'maximum', select: sel, test: num((v, b) => v <= b, sch.maximum) });
132
+ if (sch.exclusiveMinimum !== undefined)
133
+ out.push({ path, keyword: 'exclusiveMinimum', select: sel, test: num((v, b) => v > b, sch.exclusiveMinimum) });
134
+ if (sch.exclusiveMaximum !== undefined)
135
+ out.push({ path, keyword: 'exclusiveMaximum', select: sel, test: num((v, b) => v < b, sch.exclusiveMaximum) });
136
+ if (sch.multipleOf !== undefined)
137
+ out.push({ path, keyword: 'multipleOf', select: sel, test: num((v, b) => isMultiple(v, b), sch.multipleOf) });
138
+ if (sch.pattern !== undefined) {
139
+ const re = new RegExp(translatePattern(sch.pattern));
140
+ out.push({ path, keyword: 'pattern', select: sel, test: (v) => typeof v !== 'string' || re.test(v) });
141
+ }
142
+ if (sch.format !== undefined) {
143
+ const f = sch.format;
144
+ const re = FORMAT_RE[f] ?? fail(path, `format ${f} не поддерживается`);
145
+ out.push({ path, keyword: 'format', select: sel, test: (v) => typeof v !== 'string' || re.test(v) });
146
+ if (f === 'date' || f === 'date-time') {
147
+ if (inAny)
148
+ fail(path, `format ${f} внутри anyOf не поддерживается`);
149
+ out.push({ path, keyword: 'format', select: sel, test: () => true,
150
+ extra: (v) => (typeof v !== 'string' || !re.test(v) || validDate(v, f === 'date-time') ? null : 'такой даты не бывает') });
151
+ }
152
+ }
153
+ if (sch.minLength !== undefined || sch.maxLength !== undefined) {
154
+ if (inAny)
155
+ fail(path, 'длина строки внутри anyOf не поддерживается');
156
+ const min = sch.minLength ?? 0;
157
+ const max = sch.maxLength ?? Infinity;
158
+ out.push({ path, keyword: sch.minLength !== undefined ? 'minLength' : 'maxLength', select: sel, test: () => true,
159
+ extra: (v) => { if (typeof v !== 'string')
160
+ return null; const n = [...v].length; return n < min || n > max ? 'длина строки вне границ' : null; } });
161
+ }
162
+ if (sch.minItems !== undefined)
163
+ out.push({ path, keyword: 'minItems', select: sel, test: (v) => !Array.isArray(v) || v.length >= sch.minItems });
164
+ if (sch.maxItems !== undefined)
165
+ out.push({ path, keyword: 'maxItems', select: sel, test: (v) => !Array.isArray(v) || v.length <= sch.maxItems });
166
+ if (sch.uniqueItems === true) {
167
+ if (inAny)
168
+ fail(path, 'uniqueItems внутри anyOf не поддерживается');
169
+ out.push({ path, keyword: 'uniqueItems', select: sel, test: () => true,
170
+ extra: (v) => (Array.isArray(v) && v.some((x, i) => v.findIndex((y) => jsonEqual(x, y)) !== i) ? 'элементы повторяются' : null) });
171
+ }
172
+ if (sch.items !== undefined)
173
+ compileNode(sch.items, items(sel), [...path, '*'], inAny, out);
174
+ if (Array.isArray(sch.required)) {
175
+ for (const k of sch.required)
176
+ out.push({ path: [...path, k], keyword: 'required', select: sel, test: (v) => !isObj(v) || Object.prototype.hasOwnProperty.call(v, k) });
177
+ }
178
+ if (sch.minProperties !== undefined || sch.maxProperties !== undefined) {
179
+ if (inAny)
180
+ fail(path, 'minProperties внутри anyOf не поддерживается');
181
+ const min = sch.minProperties ?? 0;
182
+ const max = sch.maxProperties ?? Infinity;
183
+ out.push({ path, keyword: sch.minProperties !== undefined ? 'minProperties' : 'maxProperties', select: sel, test: () => true,
184
+ extra: (v) => { if (!isObj(v))
185
+ return null; const n = Object.keys(v).length; return n < min || n > max ? 'число ключей вне границ' : null; } });
186
+ }
187
+ const known = isObj(sch.properties) ? Object.keys(sch.properties) : [];
188
+ if (isObj(sch.properties)) {
189
+ for (const [k, v] of Object.entries(sch.properties))
190
+ compileNode(v, prop(sel, k), [...path, k], inAny, out);
191
+ }
192
+ if (sch.additionalProperties === false) {
193
+ out.push({ path, keyword: 'additionalProperties', select: sel, test: (v) => !isObj(v) || Object.keys(v).every((k) => known.includes(k)) });
194
+ }
195
+ else if (isObj(sch.additionalProperties)) {
196
+ compileNode(sch.additionalProperties, others(sel, known), [...path, '*'], inAny, out);
197
+ }
198
+ if (sch.propertyNames !== undefined) {
199
+ compileNode({ ...sch.propertyNames, type: 'string' }, keys(sel), [...path, '<ключ>'], inAny, out);
200
+ }
201
+ for (const kw of ['anyOf', 'oneOf']) {
202
+ if (!Array.isArray(sch[kw]))
203
+ continue;
204
+ const branches = sch[kw].map((b) => {
205
+ const rules = [];
206
+ compileNode(b, (root) => [root], path, true, rules);
207
+ return (node) => rules.every((r) => r.select(node).every((n) => r.test(n)));
208
+ });
209
+ out.push({ path, keyword: kw, select: sel, test: kw === 'anyOf'
210
+ ? (v) => branches.some((b) => b(v))
211
+ : (v) => branches.filter((b) => b(v)).length === 1 });
212
+ }
213
+ }
214
+ /** Компилирует JSON Schema (разворот resolved.schema класса) в функцию проверки. */
215
+ export function compileValidator(schema) {
216
+ const rules = [];
217
+ compileNode(schema, (root) => [root], [], false, rules);
218
+ return (data) => {
219
+ const issues = [];
220
+ for (const r of rules) {
221
+ const nodes = r.select(data);
222
+ if (r.extra) {
223
+ for (const n of nodes) {
224
+ const msg = r.extra(n);
225
+ if (msg)
226
+ issues.push({ path: r.path, keyword: r.keyword, message: msg });
227
+ }
228
+ }
229
+ else if (!nodes.every((n) => r.test(n))) {
230
+ issues.push({ path: r.path, keyword: r.keyword, message: `нарушено правило ${r.keyword}` });
231
+ }
232
+ }
233
+ return issues;
234
+ };
235
+ }
236
+ /** Умолчания, как apply_defaults в базе: на каждом уровне, во вложенные объекты — только если они есть. */
237
+ export function applyDefaults(schema, data) {
238
+ if (!isObj(data) || !isObj(schema.properties))
239
+ return data;
240
+ const out = { ...data };
241
+ for (const [k, s] of Object.entries(schema.properties)) {
242
+ if (!Object.prototype.hasOwnProperty.call(out, k)) {
243
+ if (isObj(s) && 'default' in s)
244
+ out[k] = structuredClone(s.default);
245
+ }
246
+ else if (isObj(out[k]) && isObj(s)) {
247
+ out[k] = applyDefaults(s, out[k]);
248
+ }
249
+ }
250
+ return out;
251
+ }
@@ -0,0 +1,62 @@
1
+ import type { Executor } from './tx.js';
2
+ import type { Row } from './types.js';
3
+ export interface WatchOptions {
4
+ /** Откуда: курсор события, 'now' (по умолчанию — изменения после подписки) или 'start' (весь журнал). */
5
+ from?: string;
6
+ /** Только эти классы (с потомками). */
7
+ classes?: string[];
8
+ /** Событий за запрос (по умолчанию 500). */
9
+ batch?: number;
10
+ /** Запасной опрос, мс (по умолчанию 1000; без LISTEN — единственный способ узнать о событиях). */
11
+ pollMs?: number;
12
+ }
13
+ export interface WatchEvent {
14
+ /** Курсор этого события: watch({ from: cursor }) продолжит после него. */
15
+ cursor: string;
16
+ /** Версия из журнала (надгробие — $deleted). */
17
+ row: Row;
18
+ id: string;
19
+ class: string;
20
+ op: string;
21
+ rev: number;
22
+ at: string;
23
+ }
24
+ export interface WatchCtx {
25
+ S: string;
26
+ exec: Executor;
27
+ /** Классы подписки с потомками (null — все). */
28
+ classes: string[] | null;
29
+ /** Подписка на сигналы базы; возвращает отписку. */
30
+ onSignal?: (fn: (name: string) => void) => () => void;
31
+ }
32
+ export declare class Subscription implements AsyncIterable<WatchEvent> {
33
+ private ctx;
34
+ private opts;
35
+ private pos;
36
+ private buf;
37
+ private closed;
38
+ private wake;
39
+ private dirty;
40
+ private off;
41
+ private timer;
42
+ private failure;
43
+ private readonly batch;
44
+ private readonly pollMs;
45
+ /** 'now' — голова журнала в момент подписки (запрос уходит сразу). */
46
+ private start;
47
+ constructor(ctx: WatchCtx, opts: WatchOptions);
48
+ /** Курсор последнего выданного события (или начальной точки). */
49
+ get cursor(): string | null;
50
+ private head;
51
+ /** Следующая пачка событий ниже горизонта; пусто — горизонт ещё не дошёл или событий нет. */
52
+ private fetch;
53
+ next(): Promise<IteratorResult<WatchEvent>>;
54
+ /**
55
+ * Отставание, мс: сколько ждёт самое старое событие, которое уже зафиксировано, но ещё не ушло за
56
+ * горизонт (его держит более старая открытая транзакция); 0 — ждать нечего.
57
+ */
58
+ lag(): Promise<number>;
59
+ close(): void;
60
+ return(): Promise<IteratorResult<WatchEvent>>;
61
+ [Symbol.asyncIterator](): AsyncIterator<WatchEvent>;
62
+ }