letopis 1.4.2 → 3.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.md +516 -398
- package/CHANGELOG.md +739 -0
- package/LICENSE +75 -21
- package/LICENSE-COMMERCIAL.md +49 -0
- package/README.md +1312 -1190
- package/api-contract.json +381 -0
- package/dist/acl.d.ts +81 -1
- package/dist/acl.js +281 -13
- package/dist/admin.d.ts +11 -141
- package/dist/admin.js +28 -186
- package/dist/auth.d.ts +20 -5
- package/dist/auth.js +80 -21
- package/dist/cache.d.ts +8 -1
- package/dist/cache.js +30 -8
- package/dist/chain.d.ts +46 -48
- package/dist/chain.js +556 -223
- package/dist/cli.js +73 -96
- package/dist/demo/booking.d.ts +352 -36
- package/dist/describe.d.ts +47 -0
- package/dist/describe.js +195 -0
- package/dist/errors.d.ts +77 -13
- package/dist/errors.js +231 -30
- package/dist/index.d.ts +62 -74
- package/dist/index.js +312 -155
- package/dist/internal.d.ts +7 -0
- package/dist/jsonschema.js +18 -17
- package/dist/load.d.ts +29 -17
- package/dist/load.js +235 -190
- package/dist/log.d.ts +121 -0
- package/dist/log.js +633 -0
- package/dist/mcp.d.ts +5 -7
- package/dist/mcp.js +194 -225
- package/dist/messages/access.d.ts +90 -0
- package/dist/messages/access.js +113 -0
- package/dist/messages/api.d.ts +237 -0
- package/dist/messages/api.js +304 -0
- package/dist/messages/boot.d.ts +201 -0
- package/dist/messages/boot.js +256 -0
- package/dist/messages/codes.d.ts +172 -0
- package/dist/messages/codes.js +173 -0
- package/dist/messages/index.d.ts +98 -0
- package/dist/messages/index.js +121 -0
- package/dist/messages/log.d.ts +229 -0
- package/dist/messages/log.js +102 -0
- package/dist/messages/mcp.d.ts +306 -0
- package/dist/messages/mcp.js +387 -0
- package/dist/messages/model.d.ts +256 -0
- package/dist/messages/model.js +314 -0
- package/dist/messages/query.d.ts +141 -0
- package/dist/messages/query.js +171 -0
- package/dist/messages/sql-access.d.ts +70 -0
- package/dist/messages/sql-access.js +87 -0
- package/dist/messages/sql-core.d.ts +164 -0
- package/dist/messages/sql-core.js +198 -0
- package/dist/messages/sql-time.d.ts +86 -0
- package/dist/messages/sql-time.js +111 -0
- package/dist/messages/sql-write.d.ts +246 -0
- package/dist/messages/sql-write.js +270 -0
- package/dist/messages/time.d.ts +23 -0
- package/dist/messages/time.js +29 -0
- package/dist/messages/types.d.ts +27 -0
- package/dist/messages/types.js +5 -0
- package/dist/messages/write.d.ts +452 -0
- package/dist/messages/write.js +572 -0
- package/dist/model.d.ts +77 -60
- package/dist/model.js +43 -34
- package/dist/ops.d.ts +35 -0
- package/dist/ops.js +101 -0
- package/dist/pglite.d.ts +0 -9
- package/dist/pglite.js +3 -1
- package/dist/registry.d.ts +3 -19
- package/dist/registry.js +3 -16
- package/dist/signals.d.ts +1 -5
- package/dist/signals.js +1 -5
- package/dist/sql.d.ts +9 -1
- package/dist/sql.js +175 -107
- package/dist/sync.d.ts +10 -4
- package/dist/sync.js +90 -17
- package/dist/tx.d.ts +21 -2
- package/dist/tx.js +76 -13
- package/dist/typed.d.ts +252 -53
- package/dist/types.d.ts +93 -90
- package/dist/types.js +17 -14
- package/dist/up.d.ts +56 -35
- package/dist/up.js +229 -83
- package/dist/uuid.d.ts +8 -0
- package/dist/uuid.js +14 -6
- package/dist/validate.d.ts +13 -1
- package/dist/validate.js +82 -33
- package/dist/watch.d.ts +10 -16
- package/dist/watch.js +36 -77
- package/dist/write.d.ts +87 -17
- package/dist/write.js +441 -206
- package/llms.txt +48 -31
- package/package.json +13 -14
- package/sql/10-core.sql +7 -50
- package/sql/15-errors.sql +64 -10
- package/sql/20-context.sql +18 -9
- package/sql/30-validate.sql +79 -30
- package/sql/40-class.sql +41 -115
- package/sql/50-acl.sql +8 -13
- package/sql/60-write.sql +220 -676
- package/sql/70-read.sql +11 -13
- package/sql/80-auth.sql +109 -42
- package/sql/90-time.sql +108 -947
- package/sql/95-seed.system.sql +7 -14
- package/sql/99-revision.sql +6 -1
- package/sql/README.md +32 -24
- package/sql/migrate/0009-restore-rev.tenants-name.sql +10 -0
- package/sql/migrate/0010-impersonation-no-account.sql +5 -0
- package/sql/migrate/0011-key-lock.otp-enabled.sql +7 -0
- package/sql/migrate/README.md +18 -0
- package/AGENT-CHEATSHEET.en.md +0 -488
- package/MIGRATION.md +0 -190
- package/README.en.md +0 -2910
- package/dist/import.d.ts +0 -185
- package/dist/import.js +0 -820
- package/dist/project.d.ts +0 -359
- package/dist/project.js +0 -1258
- package/sql/75-history.sql +0 -436
- package/sql/88-derived.sql +0 -1236
- package/sql/migrate/0006-changed-paths.sql +0 -3
- package/sql/migrate/0007-secret-account-fk.sql +0 -12
package/sql/30-validate.sql
CHANGED
|
@@ -41,7 +41,8 @@ return to_jsonb(s)::text;
|
|
|
41
41
|
-- Перевод шаблона JavaScript в регулярное выражение PostgreSQL (этап 1, п. 5): разбор по символам,
|
|
42
42
|
-- как translatePattern в lib/src/jsonschema.ts. \d \w \s и их отрицания — явными наборами (внутри
|
|
43
43
|
-- [ ] — без скобок), экранированная обратная косая черта остаётся собой; \b, \p{…}, именованные
|
|
44
|
-
-- группы и обратные ссылки отклоняются.
|
|
44
|
+
-- группы и обратные ссылки отклоняются. Отказы здесь и в jp_compile — lt_fail(…, p_detail => 'null'): DETAIL у них
|
|
45
|
+
-- не было и до каталога (raise без detail), у ошибки библиотеки detail нет (15-errors.sql).
|
|
45
46
|
create or replace function @SCHEMA@.jp_pattern(p text) returns text
|
|
46
47
|
language plpgsql immutable strict
|
|
47
48
|
as $$
|
|
@@ -52,7 +53,6 @@ declare
|
|
|
52
53
|
n text;
|
|
53
54
|
in_class boolean := false;
|
|
54
55
|
sp constant text := ' \t\n\r\f\v';
|
|
55
|
-
why text;
|
|
56
56
|
begin
|
|
57
57
|
while i <= length(p) loop
|
|
58
58
|
c := substr(p, i, 1);
|
|
@@ -60,7 +60,7 @@ begin
|
|
|
60
60
|
n := substr(p, i + 1, 1);
|
|
61
61
|
i := i + 2;
|
|
62
62
|
if n = '' then
|
|
63
|
-
|
|
63
|
+
perform @SCHEMA@.lt_fail('sql-core.pattern_trailing_backslash', jsonb_build_object('pattern', p), p_detail => 'null');
|
|
64
64
|
elsif n = 'd' then
|
|
65
65
|
res := res || case when in_class then '0-9' else '[0-9]' end;
|
|
66
66
|
elsif n = 'w' then
|
|
@@ -69,26 +69,23 @@ begin
|
|
|
69
69
|
res := res || case when in_class then sp else '[' || sp || ']' end;
|
|
70
70
|
elsif n in ('D', 'W', 'S') then
|
|
71
71
|
if in_class then
|
|
72
|
-
|
|
72
|
+
perform @SCHEMA@.lt_fail('sql-core.pattern_escape_in_set', jsonb_build_object('pattern', p, 'escape', '\' || n), p_detail => 'null');
|
|
73
73
|
else
|
|
74
74
|
res := res || case n when 'D' then '[^0-9]' when 'W' then '[^A-Za-z0-9_]' else '[^' || sp || ']' end;
|
|
75
75
|
end if;
|
|
76
76
|
elsif n in ('b', 'B') then
|
|
77
|
-
|
|
77
|
+
perform @SCHEMA@.lt_fail('sql-core.pattern_word_boundary', jsonb_build_object('pattern', p, 'escape', '\' || n), p_detail => 'null');
|
|
78
78
|
elsif n in ('p', 'P') then
|
|
79
|
-
|
|
79
|
+
perform @SCHEMA@.lt_fail('sql-core.pattern_unicode_property', jsonb_build_object('pattern', p), p_detail => 'null');
|
|
80
80
|
elsif n = 'k' or n ~ '[1-9]' then
|
|
81
|
-
|
|
81
|
+
perform @SCHEMA@.lt_fail('sql-core.pattern_backreference', jsonb_build_object('pattern', p), p_detail => 'null');
|
|
82
82
|
else
|
|
83
83
|
res := res || c || n;
|
|
84
84
|
end if;
|
|
85
|
-
if why is not null then
|
|
86
|
-
raise exception using errcode = 'LT010', message = 'letopis: invalid_class: шаблон ' || p || ' несовместим с регулярными выражениями PostgreSQL (' || why || ')';
|
|
87
|
-
end if;
|
|
88
85
|
continue;
|
|
89
86
|
end if;
|
|
90
87
|
if not in_class and substr(p, i, 3) = '(?<' and substr(p, i + 3, 1) not in ('=', '!') then
|
|
91
|
-
|
|
88
|
+
perform @SCHEMA@.lt_fail('sql-core.pattern_named_group', jsonb_build_object('pattern', p), p_detail => 'null');
|
|
92
89
|
end if;
|
|
93
90
|
if c = '[' and not in_class then
|
|
94
91
|
in_class := true;
|
|
@@ -153,11 +150,11 @@ begin
|
|
|
153
150
|
rules := r; return;
|
|
154
151
|
end if;
|
|
155
152
|
if jsonb_typeof(p_schema) <> 'object' then
|
|
156
|
-
|
|
153
|
+
perform @SCHEMA@.lt_fail('sql-core.schema_node_not_object', jsonb_build_object('node', p_disp::text), p_detail => 'null');
|
|
157
154
|
end if;
|
|
158
155
|
for k in select jsonb_object_keys(p_schema) loop
|
|
159
156
|
if not (k = any (allowed) or k like 'x-%') then
|
|
160
|
-
|
|
157
|
+
perform @SCHEMA@.lt_fail('sql-core.keyword_unsupported', jsonb_build_object('keyword', k, 'node', p_disp::text), p_detail => 'null');
|
|
161
158
|
end if;
|
|
162
159
|
end loop;
|
|
163
160
|
|
|
@@ -170,7 +167,7 @@ begin
|
|
|
170
167
|
tcond := @SCHEMA@.jp_type_cond(p_schema->>'type');
|
|
171
168
|
end if;
|
|
172
169
|
if tcond is null then
|
|
173
|
-
|
|
170
|
+
perform @SCHEMA@.lt_fail('sql-core.unknown_type', jsonb_build_object('node', p_disp::text), p_detail => 'null');
|
|
174
171
|
end if;
|
|
175
172
|
r := r || jsonb_build_object('path', p_disp, 'keyword', 'type', 'path_expr', p_path, 'cond', tcond, 'vars', '{}'::jsonb);
|
|
176
173
|
end if;
|
|
@@ -181,7 +178,7 @@ begin
|
|
|
181
178
|
for v in select x from jsonb_array_elements(coalesce(p_schema->'enum', jsonb_build_array(p_schema->'const'))) x loop
|
|
182
179
|
if jsonb_typeof(v) in ('object', 'array') then
|
|
183
180
|
if p_in_any then
|
|
184
|
-
|
|
181
|
+
perform @SCHEMA@.lt_fail('sql-core.enum_objects_in_any_of', jsonb_build_object('node', p_disp::text), p_detail => 'null');
|
|
185
182
|
end if;
|
|
186
183
|
r := r || jsonb_build_object('path', p_disp, 'keyword', case when p_schema ? 'enum' then 'enum' else 'const' end,
|
|
187
184
|
'path_expr', p_path, 'cond', 'true', 'vars', '{}'::jsonb, 'extra', jsonb_build_object('kind', 'enum', 'values', coalesce(p_schema->'enum', jsonb_build_array(p_schema->'const'))));
|
|
@@ -227,11 +224,11 @@ begin
|
|
|
227
224
|
when 'email' then '^[^@[:space:]]+@[^@[:space:]]+\.[^@[:space:]]+$'
|
|
228
225
|
else null end) || ')');
|
|
229
226
|
if t not in ('uuid', 'date', 'date-time', 'email') then
|
|
230
|
-
|
|
227
|
+
perform @SCHEMA@.lt_fail('sql-core.format_unsupported', jsonb_build_object('format', t, 'node', p_disp::text), p_detail => 'null');
|
|
231
228
|
end if;
|
|
232
229
|
if t in ('date', 'date-time') then
|
|
233
230
|
if p_in_any then
|
|
234
|
-
|
|
231
|
+
perform @SCHEMA@.lt_fail('sql-core.format_in_any_of', jsonb_build_object('format', t, 'node', p_disp::text), p_detail => 'null');
|
|
235
232
|
end if;
|
|
236
233
|
r := r || jsonb_build_object('path', p_disp, 'keyword', 'format', 'path_expr', p_path, 'cond', 'true', 'vars', '{}'::jsonb,
|
|
237
234
|
'extra', jsonb_build_object('kind', 'date', 'type', case t when 'date' then 'date' else 'timestamptz' end,
|
|
@@ -241,7 +238,7 @@ begin
|
|
|
241
238
|
end if;
|
|
242
239
|
if p_schema ? 'minLength' or p_schema ? 'maxLength' then
|
|
243
240
|
if p_in_any then
|
|
244
|
-
|
|
241
|
+
perform @SCHEMA@.lt_fail('sql-core.string_length_in_any_of', jsonb_build_object('node', p_disp::text), p_detail => 'null');
|
|
245
242
|
end if;
|
|
246
243
|
r := r || jsonb_build_object('path', p_disp, 'keyword', case when p_schema ? 'minLength' then 'minLength' else 'maxLength' end,
|
|
247
244
|
'path_expr', p_path, 'cond', 'true', 'vars', '{}'::jsonb,
|
|
@@ -258,7 +255,7 @@ begin
|
|
|
258
255
|
end loop;
|
|
259
256
|
if coalesce((p_schema->>'uniqueItems')::boolean, false) then
|
|
260
257
|
if p_in_any then
|
|
261
|
-
|
|
258
|
+
perform @SCHEMA@.lt_fail('sql-core.unique_items_in_any_of', jsonb_build_object('node', p_disp::text), p_detail => 'null');
|
|
262
259
|
end if;
|
|
263
260
|
r := r || jsonb_build_object('path', p_disp, 'keyword', 'uniqueItems', 'path_expr', p_path, 'cond', 'true', 'vars', '{}'::jsonb,
|
|
264
261
|
'extra', jsonb_build_object('kind', 'unique'));
|
|
@@ -277,7 +274,7 @@ begin
|
|
|
277
274
|
end if;
|
|
278
275
|
if p_schema ? 'minProperties' or p_schema ? 'maxProperties' then
|
|
279
276
|
if p_in_any then
|
|
280
|
-
|
|
277
|
+
perform @SCHEMA@.lt_fail('sql-core.min_properties_in_any_of', jsonb_build_object('node', p_disp::text), p_detail => 'null');
|
|
281
278
|
end if;
|
|
282
279
|
r := r || jsonb_build_object('path', p_disp, 'keyword', case when p_schema ? 'minProperties' then 'minProperties' else 'maxProperties' end,
|
|
283
280
|
'path_expr', p_path, 'cond', 'true', 'vars', '{}'::jsonb,
|
|
@@ -354,8 +351,18 @@ begin
|
|
|
354
351
|
validator := ('strict ' || expr)::jsonpath;
|
|
355
352
|
end $$;
|
|
356
353
|
|
|
354
|
+
-- Допустимые значения перечисления для сообщения нарушения (план 2.1, A8): JSON через запятую, первые 20 — как у
|
|
355
|
+
-- валидатора на TypeScript (validate.ts, enumValues).
|
|
356
|
+
create or replace function @SCHEMA@.issue_values(p_values jsonb) returns text
|
|
357
|
+
language sql immutable
|
|
358
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
359
|
+
return (select string_agg(t.y::text, ', ' order by t.o) || case when jsonb_array_length(p_values) > 20 then ', …' else '' end
|
|
360
|
+
from jsonb_array_elements(p_values) with ordinality t(y, o) where t.o <= 20);
|
|
361
|
+
|
|
357
362
|
-- Проверка данных: общий вызов jsonb_path_match; при отказе — список нарушений по правилам.
|
|
358
363
|
-- Результат сравнивается через is true: ошибка внутри выражения даёт NULL, а не «да» (этап 1, п. 5).
|
|
364
|
+
-- Нарушение — { path, keyword, message, key, params }: message — русский текст по ключу каталога, рядом его key и
|
|
365
|
+
-- params; библиотека убирает key и params (fromDbError, загрузчик).
|
|
359
366
|
create or replace function @SCHEMA@.validate_data(p_rules jsonb, p_validator jsonpath, p_vars jsonb, p_data jsonb) returns jsonb
|
|
360
367
|
language plpgsql stable
|
|
361
368
|
as $$
|
|
@@ -364,6 +371,8 @@ declare
|
|
|
364
371
|
bad jsonb := '[]'::jsonb;
|
|
365
372
|
ok boolean;
|
|
366
373
|
x jsonb;
|
|
374
|
+
p jsonb;
|
|
375
|
+
t text;
|
|
367
376
|
begin
|
|
368
377
|
ok := jsonb_path_match(p_data, p_validator, p_vars, true) is true;
|
|
369
378
|
if ok and not exists (select 1 from jsonb_array_elements(p_rules) r where r ? 'extra') then
|
|
@@ -373,38 +382,78 @@ begin
|
|
|
373
382
|
if e ? 'extra' then
|
|
374
383
|
for x in select * from jsonb_path_query(p_data, ('strict ' || (e->>'path_expr'))::jsonpath, '{}'::jsonb, true) loop
|
|
375
384
|
if (e->'extra'->>'kind') = 'length' and jsonb_typeof(x) = 'string' then
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
385
|
+
-- текст называет границу (letopis 2.1, второй раунд, A6) — как у валидатора на TypeScript (validate.ts)
|
|
386
|
+
t := null;
|
|
387
|
+
if char_length(x #>> '{}') < coalesce((e->'extra'->>'min')::int, 0) then
|
|
388
|
+
t := 'sql-core.issue_string_too_short';
|
|
389
|
+
p := jsonb_build_object('limit', (e->'extra'->>'min')::int);
|
|
390
|
+
elsif char_length(x #>> '{}') > coalesce((e->'extra'->>'max')::int, 2147483647) then
|
|
391
|
+
t := 'sql-core.issue_string_too_long';
|
|
392
|
+
p := jsonb_build_object('limit', (e->'extra'->>'max')::int);
|
|
393
|
+
end if;
|
|
394
|
+
if t is not null then
|
|
395
|
+
bad := bad || jsonb_build_object('path', e->'path', 'keyword', e->>'keyword', 'message', @SCHEMA@.lt_text(t, p),
|
|
396
|
+
'key', t, 'params', p);
|
|
379
397
|
end if;
|
|
380
398
|
elsif (e->'extra'->>'kind') = 'unique' and jsonb_typeof(x) = 'array' then
|
|
381
399
|
if (select count(*) from jsonb_array_elements(x)) <> (select count(distinct y) from jsonb_array_elements(x) y) then
|
|
382
|
-
bad := bad || jsonb_build_object('path', e->'path', 'keyword', 'uniqueItems', 'message', '
|
|
400
|
+
bad := bad || jsonb_build_object('path', e->'path', 'keyword', 'uniqueItems', 'message', @SCHEMA@.lt_text('sql-core.issue_unique_items'),
|
|
401
|
+
'key', 'sql-core.issue_unique_items', 'params', '{}'::jsonb);
|
|
383
402
|
end if;
|
|
384
403
|
elsif (e->'extra'->>'kind') = 'date' and jsonb_typeof(x) = 'string' then
|
|
385
404
|
-- смысл проверяется только у строки правильной формы: форму проверяет правило format
|
|
386
405
|
if (x #>> '{}') ~ (e->'extra'->>'re') and not pg_input_is_valid(x #>> '{}', e->'extra'->>'type') then
|
|
387
|
-
bad := bad || jsonb_build_object('path', e->'path', 'keyword', 'format', 'message', '
|
|
406
|
+
bad := bad || jsonb_build_object('path', e->'path', 'keyword', 'format', 'message', @SCHEMA@.lt_text('sql-core.issue_date'),
|
|
407
|
+
'key', 'sql-core.issue_date', 'params', '{}'::jsonb);
|
|
388
408
|
end if;
|
|
389
409
|
elsif (e->'extra'->>'kind') = 'props' and jsonb_typeof(x) = 'object' then
|
|
390
410
|
if (select count(*) from jsonb_object_keys(x)) < coalesce((e->'extra'->>'min')::int, 0)
|
|
391
411
|
or (select count(*) from jsonb_object_keys(x)) > coalesce((e->'extra'->>'max')::int, 2147483647) then
|
|
392
|
-
bad := bad || jsonb_build_object('path', e->'path', 'keyword', e->>'keyword', 'message', '
|
|
412
|
+
bad := bad || jsonb_build_object('path', e->'path', 'keyword', e->>'keyword', 'message', @SCHEMA@.lt_text('sql-core.issue_property_count'),
|
|
413
|
+
'key', 'sql-core.issue_property_count', 'params', '{}'::jsonb);
|
|
393
414
|
end if;
|
|
394
415
|
elsif (e->'extra'->>'kind') = 'enum' then
|
|
395
416
|
if not exists (select 1 from jsonb_array_elements(e->'extra'->'values') y where y = x) then
|
|
396
|
-
|
|
417
|
+
p := jsonb_build_object('values', @SCHEMA@.issue_values(e->'extra'->'values'));
|
|
418
|
+
bad := bad || jsonb_build_object('path', e->'path', 'keyword', e->>'keyword', 'message', @SCHEMA@.lt_text('sql-core.issue_enum', p),
|
|
419
|
+
'key', 'sql-core.issue_enum', 'params', p);
|
|
397
420
|
end if;
|
|
398
421
|
end if;
|
|
399
422
|
end loop;
|
|
400
423
|
elsif not ok then
|
|
401
424
|
if jsonb_path_match(p_data, ('strict !exists(' || (e->>'path_expr') || ' ? (!(' || (e->>'cond') || ')))')::jsonpath, e->'vars', true) is not true then
|
|
402
|
-
|
|
425
|
+
t := null;
|
|
426
|
+
if e->>'keyword' in ('enum', 'const') then
|
|
427
|
+
-- перечисление скаляров: значения — переменные правила v<N> (план 2.1, A8)
|
|
428
|
+
p := jsonb_build_object('values', @SCHEMA@.issue_values(
|
|
429
|
+
coalesce((select jsonb_agg(v.value order by (substr(v.key, 2))::int) from jsonb_each(e->'vars') v), '[]'::jsonb)));
|
|
430
|
+
bad := bad || jsonb_build_object('path', e->'path', 'keyword', e->>'keyword', 'message', @SCHEMA@.lt_text('sql-core.issue_enum', p),
|
|
431
|
+
'key', 'sql-core.issue_enum', 'params', p);
|
|
432
|
+
continue;
|
|
433
|
+
elsif e->>'keyword' = 'additionalProperties' then
|
|
434
|
+
-- лишние ключи: ключ, которого условие правила не принимает и в одиночку (план 2.1, A8)
|
|
435
|
+
select string_agg(z.k, ', ' order by z.k) into t from (
|
|
436
|
+
select distinct kv.key as k
|
|
437
|
+
from jsonb_path_query(p_data, ('strict ' || (e->>'path_expr'))::jsonpath, '{}'::jsonb, true) n(v)
|
|
438
|
+
cross join lateral jsonb_each(case when jsonb_typeof(n.v) = 'object' then n.v else '{}'::jsonb end) kv
|
|
439
|
+
where jsonb_path_exists(jsonb_build_object(kv.key, null), ('strict $ ? (' || (e->>'cond') || ')')::jsonpath, e->'vars', true) is not true
|
|
440
|
+
order by 1 limit 20) z;
|
|
441
|
+
end if;
|
|
442
|
+
if t is not null then
|
|
443
|
+
p := jsonb_build_object('keys', t);
|
|
444
|
+
bad := bad || jsonb_build_object('path', e->'path', 'keyword', e->>'keyword', 'message', @SCHEMA@.lt_text('sql-core.issue_additional', p),
|
|
445
|
+
'key', 'sql-core.issue_additional', 'params', p);
|
|
446
|
+
else
|
|
447
|
+
p := jsonb_build_object('keyword', e->>'keyword');
|
|
448
|
+
bad := bad || jsonb_build_object('path', e->'path', 'keyword', e->>'keyword', 'message', @SCHEMA@.lt_text('sql-core.issue_rule', p),
|
|
449
|
+
'key', 'sql-core.issue_rule', 'params', p);
|
|
450
|
+
end if;
|
|
403
451
|
end if;
|
|
404
452
|
end if;
|
|
405
453
|
end loop;
|
|
406
454
|
if not ok and bad = '[]'::jsonb then
|
|
407
|
-
bad := jsonb_build_array(jsonb_build_object('path', '[]'::jsonb, 'keyword', 'schema', 'message', '
|
|
455
|
+
bad := jsonb_build_array(jsonb_build_object('path', '[]'::jsonb, 'keyword', 'schema', 'message', @SCHEMA@.lt_text('sql-core.issue_schema'),
|
|
456
|
+
'key', 'sql-core.issue_schema', 'params', '{}'::jsonb));
|
|
408
457
|
end if;
|
|
409
458
|
return bad;
|
|
410
459
|
end $$;
|
|
@@ -415,7 +464,7 @@ begin
|
|
|
415
464
|
foreach f in array array[
|
|
416
465
|
'uuid_v5(uuid, text)', 'v5_name(text, text[])', 'v5_id(uuid, text, text[])', 'jp_str(text)', 'jp_pattern(text)',
|
|
417
466
|
'jp_type_cond(text)', 'jp_prop(text, text)', 'jp_items(text)', 'jp_compile(jsonb, text, jsonb, boolean, int)', 'compile_validator(jsonb)',
|
|
418
|
-
'validate_data(jsonb, jsonpath, jsonb, jsonb)'] loop
|
|
467
|
+
'issue_values(jsonb)', 'validate_data(jsonb, jsonpath, jsonb, jsonb)'] loop
|
|
419
468
|
execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
|
|
420
469
|
execute format('revoke all on function @SCHEMA@.%s from public', f);
|
|
421
470
|
end loop;
|
package/sql/40-class.sql
CHANGED
|
@@ -10,14 +10,9 @@
|
|
|
10
10
|
-- нескольким полям (anyOf, oneOf) потомок заменяет целиком, как поле
|
|
11
11
|
-- ends { <роль>: { class: <класс> | [<класс>…], many, required, min, max, onDelete, meta } }
|
|
12
12
|
-- key [<поле или роль>…] — из них вычисляется id (§2.3); [] — класс без ключа
|
|
13
|
-
-- history true | false
|
|
13
|
+
-- history true | false — вести ли журнал версий
|
|
14
14
|
-- ownerDefault 'tenant' | 'actor'; cache { ttl }; meta {…} до 64 КБ
|
|
15
|
-
--
|
|
16
|
-
-- aggregate { <поле>: { sum: <поле источника> | count: true, of: <класс-источник>, via: <роль>, where?: JSON Schema } }
|
|
17
|
-
-- агрегаты в базе (план 1.1, §2.4): поле держит триггер по живым строкам источника; наследуется,
|
|
18
|
-
-- потомок заменяет целиком
|
|
19
|
-
-- resolved развёрнутое описание с учётом предков — пишет триггер, присланное значение перезаписывается; у
|
|
20
|
-
-- источника агрегатов в нём ещё метка aggregatedBy — какие цели считают его строки (88-derived.sql)
|
|
15
|
+
-- resolved развёрнутое описание с учётом предков — пишет триггер, присланное значение перезаписывается
|
|
21
16
|
-- Скомпилированная проверка данных класса лежит в колонке validator строки Class.
|
|
22
17
|
|
|
23
18
|
create or replace function @SCHEMA@.class_metaschema() returns jsonb
|
|
@@ -62,35 +57,10 @@ return $json${
|
|
|
62
57
|
}
|
|
63
58
|
},
|
|
64
59
|
"key": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
|
65
|
-
"history": {"
|
|
66
|
-
"type": "object",
|
|
67
|
-
"additionalProperties": false,
|
|
68
|
-
"required": ["all"],
|
|
69
|
-
"properties": {
|
|
70
|
-
"all": {"type": "string"}, "daily": {"type": "string"}, "weekly": {"type": "string"},
|
|
71
|
-
"monthly": {"type": "string"}, "yearly": {"type": "string"}, "tz": {"type": "string"}
|
|
72
|
-
}
|
|
73
|
-
}]},
|
|
60
|
+
"history": {"type": "boolean"},
|
|
74
61
|
"ownerDefault": {"type": "string", "enum": ["tenant", "actor"]},
|
|
75
62
|
"cache": {"type": "object"},
|
|
76
63
|
"meta": {"type": "object"},
|
|
77
|
-
"derivedBy": {"type": "string", "pattern": "^[a-z0-9][a-z0-9_.:-]{0,63}$"},
|
|
78
|
-
"aggregate": {
|
|
79
|
-
"type": "object",
|
|
80
|
-
"propertyNames": {"type": "string", "pattern": "^[^$]"},
|
|
81
|
-
"additionalProperties": {
|
|
82
|
-
"type": "object",
|
|
83
|
-
"required": ["of", "via"],
|
|
84
|
-
"additionalProperties": false,
|
|
85
|
-
"properties": {
|
|
86
|
-
"sum": {"type": "string", "pattern": "^[^$]"},
|
|
87
|
-
"count": {"type": "boolean", "enum": [true]},
|
|
88
|
-
"of": {"type": "string", "pattern": "^[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{0,62}$"},
|
|
89
|
-
"via": {"type": "string", "pattern": "^[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{0,62}$"},
|
|
90
|
-
"where": {"type": "object"}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
64
|
"resolved": {"type": "object"}
|
|
95
65
|
}
|
|
96
66
|
}$json$::jsonb;
|
|
@@ -103,11 +73,12 @@ begin
|
|
|
103
73
|
select c.rules, c.validator, c.vars into rules, validator, vars from @SCHEMA@.compile_validator(@SCHEMA@.class_metaschema()) c;
|
|
104
74
|
end $$;
|
|
105
75
|
|
|
106
|
-
-- Строка класса по имени: собственная строка
|
|
76
|
+
-- Строка класса по имени: собственная строка арендатора, иначе системная. Поиск — по индексу
|
|
107
77
|
-- entity_class_name, без вычисления v5 (два SHA-1 стоили 30 мкс на поиск, а запись ищет описание
|
|
108
|
-
-- несколько раз). Пара «арендатор, имя» однозначна: id строки Class — v5 от имени.
|
|
109
|
-
--
|
|
110
|
-
--
|
|
78
|
+
-- несколько раз). Пара «арендатор, имя» однозначна: id строки Class — v5 от имени. Порядок важен:
|
|
79
|
+
-- арендатор не заведёт класс с именем уже существующего класса System (class_admit,
|
|
80
|
+
-- system_name_taken), но класс System может появиться позже одноимённого класса арендатора — тогда
|
|
81
|
+
-- строк две, и действует собственная (атака №54 в engine-attacks). Нет строки — null.
|
|
111
82
|
create or replace function @SCHEMA@.class_row(p_name text, p_tenant uuid) returns @SCHEMA@.entity
|
|
112
83
|
language sql stable
|
|
113
84
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
@@ -136,8 +107,6 @@ declare
|
|
|
136
107
|
hist jsonb;
|
|
137
108
|
od text;
|
|
138
109
|
cache jsonb;
|
|
139
|
-
der text;
|
|
140
|
-
agg jsonb;
|
|
141
110
|
comp jsonb := '{}';
|
|
142
111
|
i int;
|
|
143
112
|
v jsonb;
|
|
@@ -146,10 +115,10 @@ begin
|
|
|
146
115
|
while cur->>'parent' is not null loop
|
|
147
116
|
cur := (@SCHEMA@.class_row(cur->>'parent', p_tenant)).data;
|
|
148
117
|
if cur is null then
|
|
149
|
-
perform @SCHEMA@.
|
|
118
|
+
perform @SCHEMA@.lt_fail('sql-core.parent_not_found', jsonb_build_object('parent', chain[cardinality(chain)]->>'parent'));
|
|
150
119
|
end if;
|
|
151
120
|
if cur->>'name' = any (names) then
|
|
152
|
-
perform @SCHEMA@.
|
|
121
|
+
perform @SCHEMA@.lt_fail('sql-core.inheritance_cycle', jsonb_build_object('cls', cur->>'name'));
|
|
153
122
|
end if;
|
|
154
123
|
names := names || (cur->>'name');
|
|
155
124
|
chain := chain || cur;
|
|
@@ -167,16 +136,6 @@ begin
|
|
|
167
136
|
if v ? 'history' then hist := v->'history'; end if;
|
|
168
137
|
if v ? 'ownerDefault' then od := v->>'ownerDefault'; end if;
|
|
169
138
|
if v ? 'cache' then cache := v->'cache'; end if;
|
|
170
|
-
-- производный класс (план 1.1, §2.4): проектор задаёт предок, потомок его не меняет
|
|
171
|
-
if v ? 'derivedBy' then
|
|
172
|
-
if der is not null and der <> v->>'derivedBy' then
|
|
173
|
-
perform @SCHEMA@.lt_raise('LT010', 'invalid_class',
|
|
174
|
-
format('derivedBy класса %s (%s) не совпадает с предком (%s)', v->>'name', v->>'derivedBy', der));
|
|
175
|
-
end if;
|
|
176
|
-
der := v->>'derivedBy';
|
|
177
|
-
end if;
|
|
178
|
-
-- агрегаты (план 1.1, §2.4): потомок заменяет описание предка целиком, {} — снимает унаследованное
|
|
179
|
-
if v ? 'aggregate' then agg := v->'aggregate'; end if;
|
|
180
139
|
for m in select * from jsonb_each(coalesce(v->'meta', '{}')) loop
|
|
181
140
|
if m.value = 'null'::jsonb then meta := meta - m.key; else meta := meta || jsonb_build_object(m.key, m.value); end if;
|
|
182
141
|
end loop;
|
|
@@ -192,12 +151,10 @@ begin
|
|
|
192
151
|
'history', coalesce(hist, 'true'),
|
|
193
152
|
'ownerDefault', coalesce(od, 'tenant'),
|
|
194
153
|
'cache', cache,
|
|
195
|
-
|
|
196
|
-
'meta', meta) || case when der is not null then jsonb_build_object('derivedBy', der) else '{}'::jsonb end
|
|
197
|
-
|| case when agg is not null and agg <> '{}'::jsonb then jsonb_build_object('aggregate', agg) else '{}'::jsonb end;
|
|
154
|
+
'meta', meta);
|
|
198
155
|
end $$;
|
|
199
156
|
|
|
200
|
-
-- Проверка описания класса: метасхема, правила ключа (§2.3), правило unset (§2.10), размер meta
|
|
157
|
+
-- Проверка описания класса: метасхема, правила ключа (§2.3), правило unset (§2.10), размер meta, журнал системных классов.
|
|
201
158
|
-- Возвращает строку с заполненными resolved и validator.
|
|
202
159
|
create or replace function @SCHEMA@.class_prepare(p_data jsonb, p_tenant uuid, out data jsonb, out validator jsonpath)
|
|
203
160
|
language plpgsql stable
|
|
@@ -211,18 +168,15 @@ declare
|
|
|
211
168
|
fld jsonb;
|
|
212
169
|
e record;
|
|
213
170
|
c record;
|
|
214
|
-
prev interval;
|
|
215
|
-
tiers text[];
|
|
216
|
-
i int;
|
|
217
171
|
begin
|
|
218
172
|
data := p_data - 'resolved';
|
|
219
173
|
select * into ms from @SCHEMA@.class_meta_validator();
|
|
220
174
|
bad := @SCHEMA@.validate_data(ms.rules, ms.validator, ms.vars, data);
|
|
221
175
|
if bad <> '[]'::jsonb then
|
|
222
|
-
perform @SCHEMA@.
|
|
176
|
+
perform @SCHEMA@.lt_fail('sql-core.class_metaschema', '{}'::jsonb, jsonb_build_object('issues', bad));
|
|
223
177
|
end if;
|
|
224
178
|
if octet_length(coalesce(data->'meta', '{}')::text) > 65536 then
|
|
225
|
-
perform @SCHEMA@.
|
|
179
|
+
perform @SCHEMA@.lt_fail('sql-core.meta_too_large');
|
|
226
180
|
end if;
|
|
227
181
|
res := @SCHEMA@.class_resolve(data, p_tenant);
|
|
228
182
|
-- ключ: только обязательные строковые поля и обязательные одиночные концы
|
|
@@ -230,85 +184,57 @@ begin
|
|
|
230
184
|
fld := res->'schema'->'properties'->k;
|
|
231
185
|
if fld is not null then
|
|
232
186
|
if not (res->'schema'->'required' ? k) or coalesce(fld->>'type', '') <> 'string' then
|
|
233
|
-
perform @SCHEMA@.
|
|
187
|
+
perform @SCHEMA@.lt_fail('sql-core.key_field_type', jsonb_build_object('field', k));
|
|
234
188
|
end if;
|
|
235
189
|
elsif res->'ends' ? k then
|
|
236
190
|
if coalesce((res->'ends'->k->>'many')::boolean, false) or not coalesce((res->'ends'->k->>'required')::boolean, false) then
|
|
237
|
-
perform @SCHEMA@.
|
|
191
|
+
perform @SCHEMA@.lt_fail('sql-core.key_end_type', jsonb_build_object('end', k));
|
|
238
192
|
end if;
|
|
239
193
|
else
|
|
240
|
-
perform @SCHEMA@.
|
|
194
|
+
perform @SCHEMA@.lt_fail('sql-core.key_unknown_name', jsonb_build_object('name', k));
|
|
241
195
|
end if;
|
|
242
196
|
end loop;
|
|
243
197
|
-- unset — только для необязательного одиночного конца и множественного без min
|
|
244
198
|
for e in select key, value from jsonb_each(res->'ends') loop
|
|
245
199
|
if e.value->>'onDelete' = 'unset' and (coalesce((e.value->>'required')::boolean, false) or coalesce((e.value->>'min')::int, 0) > 0) then
|
|
246
|
-
perform @SCHEMA@.
|
|
200
|
+
perform @SCHEMA@.lt_fail('sql-core.unset_required_end', jsonb_build_object('end', e.key));
|
|
247
201
|
end if;
|
|
248
202
|
end loop;
|
|
249
|
-
--
|
|
250
|
-
if coalesce((data->>'system')::boolean, false) and jsonb_typeof(res->'history') <> 'boolean' then
|
|
251
|
-
perform @SCHEMA@.lt_raise('LT010', 'invalid_class', 'системным классам политика хранения не задаётся');
|
|
252
|
-
end if;
|
|
203
|
+
-- журнал системных классов — аудит прав и схемы: его не выключают
|
|
253
204
|
if coalesce((data->>'system')::boolean, false) and res->'history' = 'false'::jsonb then
|
|
254
|
-
perform @SCHEMA@.
|
|
205
|
+
perform @SCHEMA@.lt_fail('sql-core.system_history_off');
|
|
255
206
|
end if;
|
|
256
|
-
-- политика хранения (§2.11): сроки ярусов по возрастанию, all не меньше суток, 'forever' — только последний
|
|
257
|
-
if jsonb_typeof(res->'history') = 'object' then
|
|
258
|
-
prev := interval '0';
|
|
259
|
-
tiers := array(select u.tn from unnest(array['all', 'daily', 'weekly', 'monthly', 'yearly']) with ordinality u(tn, ord)
|
|
260
|
-
where res->'history' ? u.tn order by u.ord);
|
|
261
|
-
for i in 1 .. cardinality(tiers) loop
|
|
262
|
-
k := res->'history'->>tiers[i];
|
|
263
|
-
if k = 'forever' then
|
|
264
|
-
if i <> cardinality(tiers) then
|
|
265
|
-
perform @SCHEMA@.lt_raise('LT010', 'invalid_class', format('history: forever допустим только у последнего яруса, а стоит у %s', tiers[i]));
|
|
266
|
-
end if;
|
|
267
|
-
elsif not pg_input_is_valid(k, 'interval') then
|
|
268
|
-
perform @SCHEMA@.lt_raise('LT010', 'invalid_class', format('history: срок яруса %s не разбирается: %s', tiers[i], k));
|
|
269
|
-
elsif k::interval <= prev then
|
|
270
|
-
perform @SCHEMA@.lt_raise('LT010', 'invalid_class', format('history: сроки ярусов должны возрастать (%s)', tiers[i]));
|
|
271
|
-
else
|
|
272
|
-
prev := k::interval;
|
|
273
|
-
end if;
|
|
274
|
-
end loop;
|
|
275
|
-
if (res->'history'->>'all') <> 'forever' and (res->'history'->>'all')::interval < interval '1 day' then
|
|
276
|
-
perform @SCHEMA@.lt_raise('LT010', 'invalid_class', 'history: ярус all не меньше суток');
|
|
277
|
-
end if;
|
|
278
|
-
if res->'history' ? 'tz' and not exists (select 1 from pg_timezone_names where name = res->'history'->>'tz') then
|
|
279
|
-
perform @SCHEMA@.lt_raise('LT010', 'invalid_class', format('history: неизвестный часовой пояс %s', res->'history'->>'tz'));
|
|
280
|
-
end if;
|
|
281
|
-
end if;
|
|
282
|
-
-- производный класс (план 1.1, §2.4): системным не бывает; его строки проектор удаляет и строит заново,
|
|
283
|
-
-- поэтому они не мешают удалять строки, на которые ссылаются (onDelete только cascade или unset)
|
|
284
|
-
if res ? 'derivedBy' then
|
|
285
|
-
if coalesce((data->>'system')::boolean, false) then
|
|
286
|
-
perform @SCHEMA@.lt_raise('LT010', 'invalid_class', 'системный класс не бывает производным');
|
|
287
|
-
end if;
|
|
288
|
-
for e in select key, value from jsonb_each(res->'ends') loop
|
|
289
|
-
if coalesce(e.value->>'onDelete', 'restrict') not in ('cascade', 'unset') then
|
|
290
|
-
perform @SCHEMA@.lt_raise('LT010', 'invalid_class',
|
|
291
|
-
format('конец %s производного класса — onDelete cascade или unset: производная строка не мешает удалять цель', e.key));
|
|
292
|
-
end if;
|
|
293
|
-
end loop;
|
|
294
|
-
end if;
|
|
295
|
-
-- агрегаты (план 1.1, §2.4; 88-derived.sql): цель — поля, источники, цикл, условия where; источник — метка
|
|
296
|
-
-- aggregatedBy и та же сверка против агрегатов, которые считают его строки (источник часто описывают после цели)
|
|
297
|
-
if res ? 'aggregate' then
|
|
298
|
-
res := @SCHEMA@.agg_prepare(res, data, p_tenant);
|
|
299
|
-
end if;
|
|
300
|
-
res := @SCHEMA@.agg_source(res, p_tenant);
|
|
301
207
|
select * into c from @SCHEMA@.compile_validator(res->'schema');
|
|
302
208
|
data := data || jsonb_build_object('resolved', res || jsonb_build_object('rules', c.rules, 'vars', c.vars));
|
|
303
209
|
validator := c.validator;
|
|
304
210
|
end $$;
|
|
305
211
|
|
|
212
|
+
-- Нарушения данных строки класса по его описанию в базе, без записи (план 2.1, A8): валидатор на TypeScript не исполняет
|
|
213
|
+
-- шаблоны pattern (ревью, ReDoS), и у класса с ними библиотека берёт полный список нарушений здесь. Класс — свой
|
|
214
|
+
-- арендатора сессии или System, как у записи; умолчания подставляются, как перед проверкой при записи. Описание класса
|
|
215
|
+
-- сессия и так видит (реестр), поэтому ответ ничего нового не открывает. Нет класса — null.
|
|
216
|
+
create or replace function @SCHEMA@.validate_row(p_class text, p_data jsonb) returns jsonb
|
|
217
|
+
language plpgsql stable security definer
|
|
218
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
219
|
+
as $$
|
|
220
|
+
declare
|
|
221
|
+
cls @SCHEMA@.entity;
|
|
222
|
+
begin
|
|
223
|
+
cls := @SCHEMA@.class_row(p_class, @SCHEMA@.ctx_tenant());
|
|
224
|
+
if cls.id is null or cls.validator is null then
|
|
225
|
+
return null;
|
|
226
|
+
end if;
|
|
227
|
+
return @SCHEMA@.validate_data(cls.data->'resolved'->'rules', cls.validator, cls.data->'resolved'->'vars',
|
|
228
|
+
@SCHEMA@.apply_defaults(cls.data->'resolved'->'schema', p_data));
|
|
229
|
+
end $$;
|
|
230
|
+
|
|
306
231
|
do $$
|
|
307
232
|
declare f text;
|
|
308
233
|
begin
|
|
309
234
|
foreach f in array array['class_metaschema()', 'class_meta_validator()', 'class_row(text, uuid)',
|
|
310
|
-
'class_resolve(jsonb, uuid)', 'class_prepare(jsonb, uuid)'] loop
|
|
235
|
+
'class_resolve(jsonb, uuid)', 'class_prepare(jsonb, uuid)', 'validate_row(text, jsonb)'] loop
|
|
311
236
|
execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
|
|
312
237
|
execute format('revoke all on function @SCHEMA@.%s from public', f);
|
|
313
238
|
end loop;
|
|
314
239
|
end $$;
|
|
240
|
+
grant execute on function @SCHEMA@.validate_row(text, jsonb) to @APP@;
|
package/sql/50-acl.sql
CHANGED
|
@@ -306,7 +306,7 @@ declare
|
|
|
306
306
|
c record;
|
|
307
307
|
begin
|
|
308
308
|
if p_op not in ('READ', 'WRITE', 'DELETE') then
|
|
309
|
-
perform @SCHEMA@.lt_fail('
|
|
309
|
+
perform @SCHEMA@.lt_fail('sql-access.acl_bad_op', jsonb_build_object('op', p_op));
|
|
310
310
|
end if;
|
|
311
311
|
select * into c from @SCHEMA@.ctx();
|
|
312
312
|
if c.account is null then
|
|
@@ -382,14 +382,14 @@ begin
|
|
|
382
382
|
select e.data into a from @SCHEMA@.entity e
|
|
383
383
|
where e.id = p_account and e.class = 'Account' and e.tenant = '@SYSTEM@'::uuid and coalesce((e.data->>'enabled')::boolean, true);
|
|
384
384
|
if a is null then
|
|
385
|
-
perform @SCHEMA@.lt_fail('
|
|
385
|
+
perform @SCHEMA@.lt_fail('sql-access.account_unavailable');
|
|
386
386
|
end if;
|
|
387
387
|
if p_tenant <> p_account then
|
|
388
388
|
select x.data into m from @SCHEMA@.entity x
|
|
389
389
|
where x.id = @SCHEMA@.v5_id(p_tenant, 'member', array[p_account::text, p_tenant::text]) and x.class = 'member' and x.tenant = p_tenant;
|
|
390
390
|
if m is null or not exists (select 1 from @SCHEMA@.entity e where e.id = p_tenant and e.class = 'Account'
|
|
391
391
|
and e.tenant = '@SYSTEM@'::uuid and coalesce((e.data->>'enabled')::boolean, true)) then
|
|
392
|
-
perform @SCHEMA@.lt_fail('
|
|
392
|
+
perform @SCHEMA@.lt_fail('sql-access.no_membership');
|
|
393
393
|
end if;
|
|
394
394
|
end if;
|
|
395
395
|
cats := array(select jsonb_array_elements_text(coalesce(a->'categories', '[]'::jsonb)));
|
|
@@ -417,7 +417,7 @@ begin
|
|
|
417
417
|
where @SCHEMA@.acl_endpoint_ok(r.pattern, p_endpoint)
|
|
418
418
|
order by r.weight desc, r.deny desc, r.alias, r.id
|
|
419
419
|
limit 1) w),
|
|
420
|
-
jsonb_build_object('allow', false, 'message',
|
|
420
|
+
jsonb_build_object('allow', false, 'message', @SCHEMA@.lt_text('sql-access.default_deny')));
|
|
421
421
|
end $$;
|
|
422
422
|
|
|
423
423
|
-- db.acl.check(endpoint): решение для актора сессии по правилам System и его арендатора.
|
|
@@ -430,7 +430,7 @@ declare
|
|
|
430
430
|
begin
|
|
431
431
|
select * into c from @SCHEMA@.ctx();
|
|
432
432
|
if c.account is null then
|
|
433
|
-
perform @SCHEMA@.lt_fail('no_session'
|
|
433
|
+
perform @SCHEMA@.lt_fail('sql-access.no_session');
|
|
434
434
|
end if;
|
|
435
435
|
return @SCHEMA@.acl_api_for(c.tenant, c.categories, c.roles, p_endpoint, false);
|
|
436
436
|
end $$;
|
|
@@ -445,10 +445,10 @@ declare
|
|
|
445
445
|
begin
|
|
446
446
|
select * into c from @SCHEMA@.ctx();
|
|
447
447
|
if c.account is null then
|
|
448
|
-
perform @SCHEMA@.lt_fail('no_session'
|
|
448
|
+
perform @SCHEMA@.lt_fail('sql-access.no_session');
|
|
449
449
|
end if;
|
|
450
450
|
if not coalesce((@SCHEMA@.acl_api_for(c.tenant, c.categories, c.roles, p_endpoint, true)->>'allow')::boolean, false) then
|
|
451
|
-
perform @SCHEMA@.lt_fail('
|
|
451
|
+
perform @SCHEMA@.lt_fail('sql-access.no_right', jsonb_build_object('endpoint', p_endpoint));
|
|
452
452
|
end if;
|
|
453
453
|
end $$;
|
|
454
454
|
|
|
@@ -479,9 +479,7 @@ return (p_tenant = c_tenant and (p_class = 'Class' or @SCHEMA@.acl_row_ok(c_plan
|
|
|
479
479
|
or (p_class = 'Account' and p_id = c_account)
|
|
480
480
|
or (p_class = 'member' and p_links->>'User' = c_account::text);
|
|
481
481
|
|
|
482
|
-
--
|
|
483
|
-
drop function if exists @SCHEMA@.row_visible(uuid, text, uuid, uuid, uuid);
|
|
484
|
-
|
|
482
|
+
-- Политики: create policy не знает or replace — повторный накат пересоздаёт их (drop policy if exists и create policy).
|
|
485
483
|
drop policy if exists entity_select on @SCHEMA@.entity;
|
|
486
484
|
create policy entity_select on @SCHEMA@.entity for select to @APP@
|
|
487
485
|
using (
|
|
@@ -520,9 +518,6 @@ create policy log_select on @SCHEMA@.log for select to @APP@
|
|
|
520
518
|
or (class = 'member' and links->>'User' = (select @SCHEMA@.ctx_account())::text)
|
|
521
519
|
);
|
|
522
520
|
|
|
523
|
-
-- ранняя редакция этапа 5: проверка строки по одной карте плана в политиках
|
|
524
|
-
drop function if exists @SCHEMA@.acl_ok(jsonb, text, uuid, uuid, uuid, text[], jsonb, jsonb);
|
|
525
|
-
|
|
526
521
|
do $$
|
|
527
522
|
declare f text;
|
|
528
523
|
begin
|