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.
- package/AGENT-CHEATSHEET.en.md +368 -0
- package/AGENT-CHEATSHEET.md +354 -0
- package/CHANGELOG.md +348 -0
- package/MIGRATION.md +190 -0
- package/README.en.md +1937 -0
- package/README.md +1493 -3466
- package/dist/acl.d.ts +26 -50
- package/dist/acl.js +22 -267
- package/dist/admin.d.ts +138 -0
- package/dist/admin.js +170 -0
- package/dist/auth.d.ts +120 -73
- package/dist/auth.js +121 -306
- package/dist/cache.d.ts +73 -0
- package/dist/cache.js +148 -0
- package/dist/chain.d.ts +124 -191
- package/dist/chain.js +369 -551
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +164 -0
- package/dist/demo/booking.d.ts +289 -0
- package/dist/demo/booking.js +159 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +70 -0
- package/dist/import.d.ts +179 -0
- package/dist/import.js +792 -0
- package/dist/index.d.ts +172 -26
- package/dist/index.js +304 -178
- package/dist/jsonschema.d.ts +22 -0
- package/dist/jsonschema.js +167 -0
- package/dist/load.d.ts +76 -0
- package/dist/load.js +884 -0
- package/dist/model.d.ts +166 -0
- package/dist/model.js +224 -0
- package/dist/ops.d.ts +7 -6
- package/dist/ops.js +7 -51
- package/dist/pglite.d.ts +22 -0
- package/dist/pglite.js +45 -0
- package/dist/registry.d.ts +57 -0
- package/dist/registry.js +82 -0
- package/dist/sql.d.ts +59 -142
- package/dist/sql.js +568 -654
- package/dist/sync.d.ts +31 -0
- package/dist/sync.js +108 -0
- package/dist/tx.d.ts +129 -8
- package/dist/tx.js +300 -73
- package/dist/typed.d.ts +97 -0
- package/dist/typed.js +1 -0
- package/dist/types.d.ts +71 -250
- package/dist/types.js +27 -108
- package/dist/up.d.ts +140 -47
- package/dist/up.js +339 -267
- package/dist/uuid.d.ts +21 -6
- package/dist/uuid.js +48 -64
- package/dist/validate.d.ts +24 -0
- package/dist/validate.js +251 -0
- package/dist/watch.d.ts +62 -0
- package/dist/watch.js +168 -0
- package/dist/write.d.ts +117 -74
- package/dist/write.js +658 -720
- package/llms.txt +26 -0
- package/package.json +49 -19
- package/sql/10-core.sql +136 -0
- package/sql/15-errors.sql +60 -0
- package/sql/20-context.sql +153 -0
- package/sql/30-validate.sql +423 -0
- package/sql/40-class.sql +259 -0
- package/sql/50-acl.sql +539 -0
- package/sql/60-write.sql +1369 -0
- package/sql/70-read.sql +245 -0
- package/sql/80-auth.sql +827 -0
- package/sql/90-time.sql +957 -0
- package/sql/95-seed.system.sql +178 -0
- package/sql/99-revision.sql +3 -0
- package/sql/README.md +56 -0
- package/sql/seed.booking.sql +39 -112
- package/dist/schema.d.ts +0 -15
- package/dist/schema.js +0 -351
- package/dist/sessions.d.ts +0 -32
- package/dist/sessions.js +0 -114
- package/dist/tables.d.ts +0 -105
- package/dist/tables.js +0 -248
- package/docker/Dockerfile +0 -40
- package/docker/start.sh +0 -18
- package/scripts/check-docs.mjs +0 -375
- package/scripts/gen-api-contract.mjs +0 -226
- package/scripts/gen-types.mjs +0 -350
- package/scripts/release-notes.mjs +0 -76
- package/scripts/schema-sync.mjs +0 -185
- package/sql/ddl.sql +0 -600
- package/sql/seed.auth.sql +0 -73
package/sql/40-class.sql
ADDED
|
@@ -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 $$;
|