letopis 1.4.1 → 2.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 (113) hide show
  1. package/AGENT-CHEATSHEET.md +329 -278
  2. package/CHANGELOG.md +558 -1
  3. package/README.md +606 -1037
  4. package/dist/acl.d.ts +78 -1
  5. package/dist/acl.js +264 -13
  6. package/dist/admin.d.ts +10 -141
  7. package/dist/admin.js +24 -175
  8. package/dist/auth.d.ts +2 -2
  9. package/dist/auth.js +5 -15
  10. package/dist/cache.d.ts +8 -1
  11. package/dist/cache.js +33 -10
  12. package/dist/chain.d.ts +19 -41
  13. package/dist/chain.js +169 -201
  14. package/dist/cli.js +51 -88
  15. package/dist/demo/booking.d.ts +352 -36
  16. package/dist/describe.d.ts +47 -0
  17. package/dist/describe.js +195 -0
  18. package/dist/errors.d.ts +59 -13
  19. package/dist/errors.js +190 -29
  20. package/dist/index.d.ts +52 -71
  21. package/dist/index.js +245 -130
  22. package/dist/internal.d.ts +12 -0
  23. package/dist/internal.js +5 -0
  24. package/dist/jsonschema.js +18 -17
  25. package/dist/load.d.ts +20 -12
  26. package/dist/load.js +225 -186
  27. package/dist/mcp.d.ts +5 -7
  28. package/dist/mcp.js +171 -215
  29. package/dist/messages/access.d.ts +90 -0
  30. package/dist/messages/access.js +113 -0
  31. package/dist/messages/api.d.ts +171 -0
  32. package/dist/messages/api.js +215 -0
  33. package/dist/messages/boot.d.ts +201 -0
  34. package/dist/messages/boot.js +256 -0
  35. package/dist/messages/codes.d.ts +172 -0
  36. package/dist/messages/codes.js +173 -0
  37. package/dist/messages/index.d.ts +95 -0
  38. package/dist/messages/index.js +118 -0
  39. package/dist/messages/mcp.d.ts +306 -0
  40. package/dist/messages/mcp.js +387 -0
  41. package/dist/messages/model.d.ts +233 -0
  42. package/dist/messages/model.js +283 -0
  43. package/dist/messages/query.d.ts +131 -0
  44. package/dist/messages/query.js +158 -0
  45. package/dist/messages/sql-access.d.ts +70 -0
  46. package/dist/messages/sql-access.js +87 -0
  47. package/dist/messages/sql-core.d.ts +150 -0
  48. package/dist/messages/sql-core.js +181 -0
  49. package/dist/messages/sql-time.d.ts +71 -0
  50. package/dist/messages/sql-time.js +88 -0
  51. package/dist/messages/sql-write.d.ts +246 -0
  52. package/dist/messages/sql-write.js +270 -0
  53. package/dist/messages/time.d.ts +23 -0
  54. package/dist/messages/time.js +29 -0
  55. package/dist/messages/types.d.ts +27 -0
  56. package/dist/messages/types.js +5 -0
  57. package/dist/messages/write.d.ts +405 -0
  58. package/dist/messages/write.js +509 -0
  59. package/dist/model.d.ts +73 -60
  60. package/dist/model.js +26 -29
  61. package/dist/ops.d.ts +35 -0
  62. package/dist/ops.js +101 -0
  63. package/dist/pglite.d.ts +0 -9
  64. package/dist/pglite.js +3 -1
  65. package/dist/registry.d.ts +3 -19
  66. package/dist/registry.js +3 -16
  67. package/dist/signals.d.ts +5 -9
  68. package/dist/signals.js +5 -9
  69. package/dist/sql.d.ts +13 -6
  70. package/dist/sql.js +176 -83
  71. package/dist/sync.d.ts +9 -3
  72. package/dist/sync.js +51 -21
  73. package/dist/tx.d.ts +26 -1
  74. package/dist/tx.js +44 -14
  75. package/dist/typed.d.ts +240 -45
  76. package/dist/types.d.ts +23 -91
  77. package/dist/types.js +12 -14
  78. package/dist/up.d.ts +53 -32
  79. package/dist/up.js +184 -73
  80. package/dist/uuid.js +5 -5
  81. package/dist/validate.d.ts +13 -1
  82. package/dist/validate.js +61 -29
  83. package/dist/watch.d.ts +11 -10
  84. package/dist/watch.js +33 -70
  85. package/dist/write.d.ts +9 -16
  86. package/dist/write.js +118 -187
  87. package/llms.txt +22 -25
  88. package/package.json +8 -11
  89. package/sql/10-core.sql +7 -50
  90. package/sql/15-errors.sql +64 -10
  91. package/sql/20-context.sql +7 -6
  92. package/sql/30-validate.sql +35 -27
  93. package/sql/40-class.sql +20 -114
  94. package/sql/50-acl.sql +8 -13
  95. package/sql/60-write.sql +220 -667
  96. package/sql/70-read.sql +20 -16
  97. package/sql/80-auth.sql +24 -27
  98. package/sql/90-time.sql +70 -859
  99. package/sql/95-seed.system.sql +7 -14
  100. package/sql/99-revision.sql +2 -1
  101. package/sql/README.md +32 -24
  102. package/sql/migrate/README.md +14 -0
  103. package/AGENT-CHEATSHEET.en.md +0 -485
  104. package/MIGRATION.md +0 -190
  105. package/README.en.md +0 -2859
  106. package/dist/import.d.ts +0 -185
  107. package/dist/import.js +0 -820
  108. package/dist/project.d.ts +0 -350
  109. package/dist/project.js +0 -1224
  110. package/sql/75-history.sql +0 -358
  111. package/sql/88-derived.sql +0 -1235
  112. package/sql/migrate/0006-changed-paths.sql +0 -3
  113. package/sql/migrate/0007-secret-account-fk.sql +0 -12
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,74 +184,26 @@ 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 для системных классов запрещён');
255
- 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);
205
+ perform @SCHEMA@.lt_fail('sql-core.system_history_off');
299
206
  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;
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