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
@@ -0,0 +1,259 @@
1
+ -- letopis 1.0 — классы как сущности (план, §2.2, §2.3, §2.13, этап 1, п. 6).
2
+ --
3
+ -- Описание класса — данные строки класса Class (метасхема ниже — договор базы и библиотеки):
4
+ -- name имя класса (ключ строки Class); после появления строк класса не меняется
5
+ -- kind 'hub' | 'link'
6
+ -- parent имя предка или null
7
+ -- abstract строк класса нет, только у потомков
8
+ -- alias имя для людей
9
+ -- fields JSON Schema собственных полей: { properties, required[, anyOf | oneOf] } — условия по
10
+ -- нескольким полям (anyOf, oneOf) потомок заменяет целиком, как поле
11
+ -- ends { <роль>: { class: <класс> | [<класс>…], many, required, min, max, onDelete, meta } }
12
+ -- key [<поле или роль>…] — из них вычисляется id (§2.3); [] — класс без ключа
13
+ -- history true | false | политика хранения (§2.11)
14
+ -- ownerDefault 'tenant' | 'actor'; cache { ttl }; meta {…} до 64 КБ
15
+ -- resolved развёрнутое описание с учётом предков — пишет триггер, присланное значение перезаписывается
16
+ -- Скомпилированная проверка данных класса лежит в колонке validator строки Class.
17
+
18
+ create or replace function @SCHEMA@.class_metaschema() returns jsonb
19
+ language sql immutable parallel safe
20
+ return $json${
21
+ "type": "object",
22
+ "required": ["name", "kind"],
23
+ "additionalProperties": false,
24
+ "properties": {
25
+ "name": {"type": "string", "pattern": "^[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{0,62}$"},
26
+ "kind": {"type": "string", "enum": ["hub", "link"]},
27
+ "parent": {"type": ["string", "null"]},
28
+ "abstract": {"type": "boolean"},
29
+ "alias": {"type": ["string", "null"]},
30
+ "system": {"type": "boolean"},
31
+ "fields": {
32
+ "type": "object",
33
+ "additionalProperties": false,
34
+ "properties": {
35
+ "properties": {"type": "object", "propertyNames": {"type": "string", "pattern": "^[^$]"}},
36
+ "required": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
37
+ "anyOf": {"type": "array", "minItems": 1, "items": {"type": "object"}},
38
+ "oneOf": {"type": "array", "minItems": 1, "items": {"type": "object"}}
39
+ }
40
+ },
41
+ "ends": {
42
+ "type": "object",
43
+ "propertyNames": {"type": "string", "pattern": "^[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{0,62}$"},
44
+ "additionalProperties": {
45
+ "type": "object",
46
+ "required": ["class"],
47
+ "additionalProperties": false,
48
+ "properties": {
49
+ "class": {"anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}, "minItems": 1}]},
50
+ "many": {"type": "boolean"},
51
+ "required": {"type": "boolean"},
52
+ "min": {"type": "integer", "minimum": 0},
53
+ "max": {"type": "integer", "minimum": 1},
54
+ "onDelete": {"type": "string", "enum": ["restrict", "cascade", "unset"]},
55
+ "meta": {"type": "object"}
56
+ }
57
+ }
58
+ },
59
+ "key": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
60
+ "history": {"anyOf": [{"type": "boolean"}, {
61
+ "type": "object",
62
+ "additionalProperties": false,
63
+ "required": ["all"],
64
+ "properties": {
65
+ "all": {"type": "string"}, "daily": {"type": "string"}, "weekly": {"type": "string"},
66
+ "monthly": {"type": "string"}, "yearly": {"type": "string"}, "tz": {"type": "string"}
67
+ }
68
+ }]},
69
+ "ownerDefault": {"type": "string", "enum": ["tenant", "actor"]},
70
+ "cache": {"type": "object"},
71
+ "meta": {"type": "object"},
72
+ "resolved": {"type": "object"}
73
+ }
74
+ }$json$::jsonb;
75
+
76
+ -- Скомпилированная метасхема — один раз на сеанс не нужна: функция immutable, планировщик кэширует.
77
+ create or replace function @SCHEMA@.class_meta_validator(out rules jsonb, out validator jsonpath, out vars jsonb)
78
+ language plpgsql immutable
79
+ as $$
80
+ begin
81
+ select c.rules, c.validator, c.vars into rules, validator, vars from @SCHEMA@.compile_validator(@SCHEMA@.class_metaschema()) c;
82
+ end $$;
83
+
84
+ -- Строка класса по имени: собственная строка арендатора или системная. Поиск — по индексу
85
+ -- entity_class_name, без вычисления v5 (два SHA-1 стоили 30 мкс на поиск, а запись ищет описание
86
+ -- несколько раз). Пара «арендатор, имя» однозначна: id строки Class — v5 от имени. Имя
87
+ -- пользовательского класса не совпадает с именем системного (проверка при определении), поэтому
88
+ -- порядок нужен только для ясности. Нет строки — null.
89
+ create or replace function @SCHEMA@.class_row(p_name text, p_tenant uuid) returns @SCHEMA@.entity
90
+ language sql stable
91
+ set search_path = pg_catalog, @SCHEMA@, pg_temp
92
+ as $$
93
+ select e.* from @SCHEMA@.entity e
94
+ where e.class = 'Class' and e.data->>'name' = p_name and e.tenant in (p_tenant, '@SYSTEM@'::uuid)
95
+ order by e.tenant = '@SYSTEM@'::uuid
96
+ limit 1
97
+ $$;
98
+
99
+ -- Развёрнутое описание класса с учётом предков: поля, концы и ключ наследуются по правилу 0.21
100
+ -- «потомок заменяет поле целиком» (buildDef), метаданные — по ключам, null убирает ключ (§2.13).
101
+ create or replace function @SCHEMA@.class_resolve(p_data jsonb, p_tenant uuid) returns jsonb
102
+ language plpgsql stable
103
+ set search_path = pg_catalog, @SCHEMA@, pg_temp
104
+ as $$
105
+ declare
106
+ chain jsonb[] := array[p_data];
107
+ cur jsonb := p_data;
108
+ names text[] := array[p_data->>'name'];
109
+ props jsonb := '{}';
110
+ req jsonb := '[]';
111
+ ends jsonb := '{}';
112
+ meta jsonb := '{}';
113
+ key jsonb;
114
+ hist jsonb;
115
+ od text;
116
+ cache jsonb;
117
+ comp jsonb := '{}';
118
+ i int;
119
+ v jsonb;
120
+ m record;
121
+ begin
122
+ while cur->>'parent' is not null loop
123
+ cur := (@SCHEMA@.class_row(cur->>'parent', p_tenant)).data;
124
+ if cur is null then
125
+ perform @SCHEMA@.lt_raise('LT010', 'invalid_class', format('предок %s не найден', chain[cardinality(chain)]->>'parent'));
126
+ end if;
127
+ if cur->>'name' = any (names) then
128
+ perform @SCHEMA@.lt_raise('LT010', 'invalid_class', 'цикл в наследовании через ' || (cur->>'name'));
129
+ end if;
130
+ names := names || (cur->>'name');
131
+ chain := chain || cur;
132
+ end loop;
133
+ -- от корня к самому классу: потомок поверх предка
134
+ for i in reverse cardinality(chain) .. 1 loop
135
+ v := chain[i];
136
+ props := props || coalesce(v->'fields'->'properties', '{}');
137
+ req := (select coalesce(jsonb_agg(distinct x), '[]') from jsonb_array_elements_text(req || coalesce(v->'fields'->'required', '[]')) x);
138
+ ends := ends || coalesce(v->'ends', '{}');
139
+ if v->'fields' ? 'anyOf' or v->'fields' ? 'oneOf' then
140
+ comp := jsonb_strip_nulls(jsonb_build_object('anyOf', v->'fields'->'anyOf', 'oneOf', v->'fields'->'oneOf'));
141
+ end if;
142
+ if v ? 'key' then key := v->'key'; end if;
143
+ if v ? 'history' then hist := v->'history'; end if;
144
+ if v ? 'ownerDefault' then od := v->>'ownerDefault'; end if;
145
+ if v ? 'cache' then cache := v->'cache'; end if;
146
+ for m in select * from jsonb_each(coalesce(v->'meta', '{}')) loop
147
+ if m.value = 'null'::jsonb then meta := meta - m.key; else meta := meta || jsonb_build_object(m.key, m.value); end if;
148
+ end loop;
149
+ end loop;
150
+ return jsonb_build_object(
151
+ 'name', p_data->>'name',
152
+ 'kind', p_data->>'kind',
153
+ 'abstract', coalesce((p_data->>'abstract')::boolean, false),
154
+ 'ancestors', to_jsonb(names),
155
+ 'schema', jsonb_build_object('type', 'object', 'properties', props, 'required', req, 'additionalProperties', false) || comp,
156
+ 'ends', ends,
157
+ 'key', coalesce(key, '[]'),
158
+ 'history', coalesce(hist, 'true'),
159
+ 'ownerDefault', coalesce(od, 'tenant'),
160
+ 'cache', cache,
161
+ 'meta', meta);
162
+ end $$;
163
+
164
+ -- Проверка описания класса: метасхема, правила ключа (§2.3), правило unset (§2.10), размер meta.
165
+ -- Возвращает строку с заполненными resolved и validator.
166
+ create or replace function @SCHEMA@.class_prepare(p_data jsonb, p_tenant uuid, out data jsonb, out validator jsonpath)
167
+ language plpgsql stable
168
+ set search_path = pg_catalog, @SCHEMA@, pg_temp
169
+ as $$
170
+ declare
171
+ ms record;
172
+ bad jsonb;
173
+ res jsonb;
174
+ k text;
175
+ fld jsonb;
176
+ e record;
177
+ c record;
178
+ prev interval;
179
+ tiers text[];
180
+ i int;
181
+ begin
182
+ data := p_data - 'resolved';
183
+ select * into ms from @SCHEMA@.class_meta_validator();
184
+ bad := @SCHEMA@.validate_data(ms.rules, ms.validator, ms.vars, data);
185
+ if bad <> '[]'::jsonb then
186
+ perform @SCHEMA@.lt_raise('LT003', 'invalid_data', 'описание класса не проходит метасхему', jsonb_build_object('issues', bad));
187
+ end if;
188
+ if octet_length(coalesce(data->'meta', '{}')::text) > 65536 then
189
+ perform @SCHEMA@.lt_raise('LT010', 'invalid_class', 'meta класса больше 64 КБ');
190
+ end if;
191
+ res := @SCHEMA@.class_resolve(data, p_tenant);
192
+ -- ключ: только обязательные строковые поля и обязательные одиночные концы
193
+ for k in select jsonb_array_elements_text(res->'key') loop
194
+ fld := res->'schema'->'properties'->k;
195
+ if fld is not null then
196
+ if not (res->'schema'->'required' ? k) or coalesce(fld->>'type', '') <> 'string' then
197
+ perform @SCHEMA@.lt_raise('LT010', 'invalid_class', format('поле ключа %s должно быть обязательной строкой', k));
198
+ end if;
199
+ elsif res->'ends' ? k then
200
+ if coalesce((res->'ends'->k->>'many')::boolean, false) or not coalesce((res->'ends'->k->>'required')::boolean, false) then
201
+ perform @SCHEMA@.lt_raise('LT010', 'invalid_class', format('конец ключа %s должен быть обязательным и одиночным', k));
202
+ end if;
203
+ else
204
+ perform @SCHEMA@.lt_raise('LT010', 'invalid_class', format('ключ ссылается на неизвестное поле или конец %s', k));
205
+ end if;
206
+ end loop;
207
+ -- unset — только для необязательного одиночного конца и множественного без min
208
+ for e in select key, value from jsonb_each(res->'ends') loop
209
+ if e.value->>'onDelete' = 'unset' and (coalesce((e.value->>'required')::boolean, false) or coalesce((e.value->>'min')::int, 0) > 0) then
210
+ perform @SCHEMA@.lt_raise('LT010', 'invalid_class', format('onDelete unset недопустим для обязательного конца %s', e.key));
211
+ end if;
212
+ end loop;
213
+ -- политика хранения: для системных классов запрещена (§2.11)
214
+ if coalesce((data->>'system')::boolean, false) and jsonb_typeof(res->'history') <> 'boolean' then
215
+ perform @SCHEMA@.lt_raise('LT010', 'invalid_class', 'системным классам политика хранения не задаётся');
216
+ end if;
217
+ if coalesce((data->>'system')::boolean, false) and res->'history' = 'false'::jsonb then
218
+ perform @SCHEMA@.lt_raise('LT010', 'invalid_class', 'history: false для системных классов запрещён');
219
+ end if;
220
+ -- политика хранения (§2.11): сроки ярусов по возрастанию, all не меньше суток, 'forever' — только последний
221
+ if jsonb_typeof(res->'history') = 'object' then
222
+ prev := interval '0';
223
+ tiers := array(select u.tn from unnest(array['all', 'daily', 'weekly', 'monthly', 'yearly']) with ordinality u(tn, ord)
224
+ where res->'history' ? u.tn order by u.ord);
225
+ for i in 1 .. cardinality(tiers) loop
226
+ k := res->'history'->>tiers[i];
227
+ if k = 'forever' then
228
+ if i <> cardinality(tiers) then
229
+ perform @SCHEMA@.lt_raise('LT010', 'invalid_class', format('history: forever допустим только у последнего яруса, а стоит у %s', tiers[i]));
230
+ end if;
231
+ elsif not pg_input_is_valid(k, 'interval') then
232
+ perform @SCHEMA@.lt_raise('LT010', 'invalid_class', format('history: срок яруса %s не разбирается: %s', tiers[i], k));
233
+ elsif k::interval <= prev then
234
+ perform @SCHEMA@.lt_raise('LT010', 'invalid_class', format('history: сроки ярусов должны возрастать (%s)', tiers[i]));
235
+ else
236
+ prev := k::interval;
237
+ end if;
238
+ end loop;
239
+ if (res->'history'->>'all') <> 'forever' and (res->'history'->>'all')::interval < interval '1 day' then
240
+ perform @SCHEMA@.lt_raise('LT010', 'invalid_class', 'history: ярус all не меньше суток');
241
+ end if;
242
+ if res->'history' ? 'tz' and not exists (select 1 from pg_timezone_names where name = res->'history'->>'tz') then
243
+ perform @SCHEMA@.lt_raise('LT010', 'invalid_class', format('history: неизвестный часовой пояс %s', res->'history'->>'tz'));
244
+ end if;
245
+ end if;
246
+ select * into c from @SCHEMA@.compile_validator(res->'schema');
247
+ data := data || jsonb_build_object('resolved', res || jsonb_build_object('rules', c.rules, 'vars', c.vars));
248
+ validator := c.validator;
249
+ end $$;
250
+
251
+ do $$
252
+ declare f text;
253
+ begin
254
+ foreach f in array array['class_metaschema()', 'class_meta_validator()', 'class_row(text, uuid)',
255
+ 'class_resolve(jsonb, uuid)', 'class_prepare(jsonb, uuid)'] loop
256
+ execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
257
+ execute format('revoke all on function @SCHEMA@.%s from public', f);
258
+ end loop;
259
+ end $$;