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.
Files changed (123) hide show
  1. package/AGENT-CHEATSHEET.md +516 -398
  2. package/CHANGELOG.md +739 -0
  3. package/LICENSE +75 -21
  4. package/LICENSE-COMMERCIAL.md +49 -0
  5. package/README.md +1312 -1190
  6. package/api-contract.json +381 -0
  7. package/dist/acl.d.ts +81 -1
  8. package/dist/acl.js +281 -13
  9. package/dist/admin.d.ts +11 -141
  10. package/dist/admin.js +28 -186
  11. package/dist/auth.d.ts +20 -5
  12. package/dist/auth.js +80 -21
  13. package/dist/cache.d.ts +8 -1
  14. package/dist/cache.js +30 -8
  15. package/dist/chain.d.ts +46 -48
  16. package/dist/chain.js +556 -223
  17. package/dist/cli.js +73 -96
  18. package/dist/demo/booking.d.ts +352 -36
  19. package/dist/describe.d.ts +47 -0
  20. package/dist/describe.js +195 -0
  21. package/dist/errors.d.ts +77 -13
  22. package/dist/errors.js +231 -30
  23. package/dist/index.d.ts +62 -74
  24. package/dist/index.js +312 -155
  25. package/dist/internal.d.ts +7 -0
  26. package/dist/jsonschema.js +18 -17
  27. package/dist/load.d.ts +29 -17
  28. package/dist/load.js +235 -190
  29. package/dist/log.d.ts +121 -0
  30. package/dist/log.js +633 -0
  31. package/dist/mcp.d.ts +5 -7
  32. package/dist/mcp.js +194 -225
  33. package/dist/messages/access.d.ts +90 -0
  34. package/dist/messages/access.js +113 -0
  35. package/dist/messages/api.d.ts +237 -0
  36. package/dist/messages/api.js +304 -0
  37. package/dist/messages/boot.d.ts +201 -0
  38. package/dist/messages/boot.js +256 -0
  39. package/dist/messages/codes.d.ts +172 -0
  40. package/dist/messages/codes.js +173 -0
  41. package/dist/messages/index.d.ts +98 -0
  42. package/dist/messages/index.js +121 -0
  43. package/dist/messages/log.d.ts +229 -0
  44. package/dist/messages/log.js +102 -0
  45. package/dist/messages/mcp.d.ts +306 -0
  46. package/dist/messages/mcp.js +387 -0
  47. package/dist/messages/model.d.ts +256 -0
  48. package/dist/messages/model.js +314 -0
  49. package/dist/messages/query.d.ts +141 -0
  50. package/dist/messages/query.js +171 -0
  51. package/dist/messages/sql-access.d.ts +70 -0
  52. package/dist/messages/sql-access.js +87 -0
  53. package/dist/messages/sql-core.d.ts +164 -0
  54. package/dist/messages/sql-core.js +198 -0
  55. package/dist/messages/sql-time.d.ts +86 -0
  56. package/dist/messages/sql-time.js +111 -0
  57. package/dist/messages/sql-write.d.ts +246 -0
  58. package/dist/messages/sql-write.js +270 -0
  59. package/dist/messages/time.d.ts +23 -0
  60. package/dist/messages/time.js +29 -0
  61. package/dist/messages/types.d.ts +27 -0
  62. package/dist/messages/types.js +5 -0
  63. package/dist/messages/write.d.ts +452 -0
  64. package/dist/messages/write.js +572 -0
  65. package/dist/model.d.ts +77 -60
  66. package/dist/model.js +43 -34
  67. package/dist/ops.d.ts +35 -0
  68. package/dist/ops.js +101 -0
  69. package/dist/pglite.d.ts +0 -9
  70. package/dist/pglite.js +3 -1
  71. package/dist/registry.d.ts +3 -19
  72. package/dist/registry.js +3 -16
  73. package/dist/signals.d.ts +1 -5
  74. package/dist/signals.js +1 -5
  75. package/dist/sql.d.ts +9 -1
  76. package/dist/sql.js +175 -107
  77. package/dist/sync.d.ts +10 -4
  78. package/dist/sync.js +90 -17
  79. package/dist/tx.d.ts +21 -2
  80. package/dist/tx.js +76 -13
  81. package/dist/typed.d.ts +252 -53
  82. package/dist/types.d.ts +93 -90
  83. package/dist/types.js +17 -14
  84. package/dist/up.d.ts +56 -35
  85. package/dist/up.js +229 -83
  86. package/dist/uuid.d.ts +8 -0
  87. package/dist/uuid.js +14 -6
  88. package/dist/validate.d.ts +13 -1
  89. package/dist/validate.js +82 -33
  90. package/dist/watch.d.ts +10 -16
  91. package/dist/watch.js +36 -77
  92. package/dist/write.d.ts +87 -17
  93. package/dist/write.js +441 -206
  94. package/llms.txt +48 -31
  95. package/package.json +13 -14
  96. package/sql/10-core.sql +7 -50
  97. package/sql/15-errors.sql +64 -10
  98. package/sql/20-context.sql +18 -9
  99. package/sql/30-validate.sql +79 -30
  100. package/sql/40-class.sql +41 -115
  101. package/sql/50-acl.sql +8 -13
  102. package/sql/60-write.sql +220 -676
  103. package/sql/70-read.sql +11 -13
  104. package/sql/80-auth.sql +109 -42
  105. package/sql/90-time.sql +108 -947
  106. package/sql/95-seed.system.sql +7 -14
  107. package/sql/99-revision.sql +6 -1
  108. package/sql/README.md +32 -24
  109. package/sql/migrate/0009-restore-rev.tenants-name.sql +10 -0
  110. package/sql/migrate/0010-impersonation-no-account.sql +5 -0
  111. package/sql/migrate/0011-key-lock.otp-enabled.sql +7 -0
  112. package/sql/migrate/README.md +18 -0
  113. package/AGENT-CHEATSHEET.en.md +0 -488
  114. package/MIGRATION.md +0 -190
  115. package/README.en.md +0 -2910
  116. package/dist/import.d.ts +0 -185
  117. package/dist/import.js +0 -820
  118. package/dist/project.d.ts +0 -359
  119. package/dist/project.js +0 -1258
  120. package/sql/75-history.sql +0 -436
  121. package/sql/88-derived.sql +0 -1236
  122. package/sql/migrate/0006-changed-paths.sql +0 -3
  123. package/sql/migrate/0007-secret-account-fk.sql +0 -12
@@ -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
- why := 'обратная косая черта в конце шаблона';
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
- why := '\' || n || ' внутри набора [ ] не переводится';
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
- why := '\' || n || ' в PostgreSQL значит другое';
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
- why := '\p{…} не поддерживается';
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
- why := 'обратные ссылки не поддерживаются';
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
- raise exception using errcode = 'LT010', message = 'letopis: invalid_class: шаблон ' || p || ' несовместим с регулярными выражениями PostgreSQL (именованные группы не поддерживаются)';
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
- raise exception using errcode = 'LT010', message = format('letopis: invalid_class: описание узла %s — не объект', p_disp);
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
- raise exception using errcode = 'LT010', message = format('letopis: invalid_class: ключевое слово %s не поддерживается (узел %s)', k, p_disp);
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
- raise exception using errcode = 'LT010', message = format('letopis: invalid_class: неизвестный тип в узле %s', p_disp);
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
- raise exception using errcode = 'LT010', message = format('letopis: invalid_class: перечисление объектов внутри anyOf не поддерживается (узел %s)', p_disp);
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
- raise exception using errcode = 'LT010', message = format('letopis: invalid_class: format %s не поддерживается (узел %s)', t, p_disp);
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
- raise exception using errcode = 'LT010', message = format('letopis: invalid_class: format %s внутри anyOf не поддерживается (узел %s)', t, p_disp);
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
- raise exception using errcode = 'LT010', message = format('letopis: invalid_class: длина строки внутри anyOf не поддерживается (узел %s)', p_disp);
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
- raise exception using errcode = 'LT010', message = format('letopis: invalid_class: uniqueItems внутри anyOf не поддерживается (узел %s)', p_disp);
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
- raise exception using errcode = 'LT010', message = format('letopis: invalid_class: minProperties внутри anyOf не поддерживается (узел %s)', p_disp);
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
- if char_length(x #>> '{}') < coalesce((e->'extra'->>'min')::int, 0)
377
- or char_length(x #>> '{}') > coalesce((e->'extra'->>'max')::int, 2147483647) then
378
- bad := bad || jsonb_build_object('path', e->'path', 'keyword', e->>'keyword', 'message', 'длина строки вне границ');
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
- bad := bad || jsonb_build_object('path', e->'path', 'keyword', e->>'keyword', 'message', 'значение не из перечисления');
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
- bad := bad || jsonb_build_object('path', e->'path', 'keyword', e->>'keyword', 'message', 'нарушено правило ' || (e->>'keyword'));
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 | политика хранения (§2.11)
13
+ -- history true | false — вести ли журнал версий
14
14
  -- ownerDefault 'tenant' | 'actor'; cache { ttl }; meta {…} до 64 КБ
15
- -- derivedBy имя проектора (план 1.1, §2.4): строки класса пишет только он; наследуется, потомок не меняет
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": {"anyOf": [{"type": "boolean"}, {
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
- -- порядок нужен только для ясности. Нет строки — null.
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@.lt_raise('LT010', 'invalid_class', format('предок %s не найден', chain[cardinality(chain)]->>'parent'));
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@.lt_raise('LT010', 'invalid_class', 'цикл в наследовании через ' || (cur->>'name'));
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
- -- ключи только у производных и у целей агрегатов: у описаний до 1.2 их нет, и сравнение «было — стало» их не задевает
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@.lt_raise('LT003', 'invalid_data', 'описание класса не проходит метасхему', jsonb_build_object('issues', bad));
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@.lt_raise('LT010', 'invalid_class', 'meta класса больше 64 КБ');
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@.lt_raise('LT010', 'invalid_class', format('поле ключа %s должно быть обязательной строкой', k));
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@.lt_raise('LT010', 'invalid_class', format('конец ключа %s должен быть обязательным и одиночным', k));
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@.lt_raise('LT010', 'invalid_class', format('ключ ссылается на неизвестное поле или конец %s', k));
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@.lt_raise('LT010', 'invalid_class', format('onDelete unset недопустим для обязательного конца %s', e.key));
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
- -- политика хранения: для системных классов запрещена (§2.11)
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@.lt_raise('LT010', 'invalid_class', 'history: false для системных классов запрещён');
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('invalid_data', format('операция %s: нужна READ, WRITE или DELETE', p_op));
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('target_not_found', 'аккаунт не найден или отключён');
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('tenant_denied', 'нет членства в арендаторе');
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('acl_denied', format('нет права %s', p_endpoint));
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
- -- до этапа 5 видимость не зависела от прав
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