letopis 1.4.2 → 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.
- package/AGENT-CHEATSHEET.md +329 -281
- package/CHANGELOG.md +418 -0
- package/README.md +572 -1053
- package/dist/acl.d.ts +78 -1
- package/dist/acl.js +264 -13
- package/dist/admin.d.ts +10 -141
- package/dist/admin.js +24 -184
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +5 -15
- package/dist/cache.d.ts +8 -1
- package/dist/cache.js +30 -8
- package/dist/chain.d.ts +18 -41
- package/dist/chain.js +165 -203
- package/dist/cli.js +45 -90
- 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 +59 -13
- package/dist/errors.js +190 -29
- package/dist/index.d.ts +52 -70
- package/dist/index.js +189 -122
- package/dist/internal.d.ts +5 -0
- package/dist/jsonschema.js +18 -17
- package/dist/load.d.ts +20 -12
- package/dist/load.js +207 -181
- package/dist/mcp.d.ts +5 -7
- package/dist/mcp.js +168 -214
- package/dist/messages/access.d.ts +90 -0
- package/dist/messages/access.js +113 -0
- package/dist/messages/api.d.ts +171 -0
- package/dist/messages/api.js +215 -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 +95 -0
- package/dist/messages/index.js +118 -0
- package/dist/messages/mcp.d.ts +306 -0
- package/dist/messages/mcp.js +387 -0
- package/dist/messages/model.d.ts +233 -0
- package/dist/messages/model.js +283 -0
- package/dist/messages/query.d.ts +131 -0
- package/dist/messages/query.js +158 -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 +150 -0
- package/dist/messages/sql-core.js +181 -0
- package/dist/messages/sql-time.d.ts +71 -0
- package/dist/messages/sql-time.js +88 -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 +405 -0
- package/dist/messages/write.js +509 -0
- package/dist/model.d.ts +73 -60
- package/dist/model.js +20 -29
- 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 +7 -1
- package/dist/sql.js +101 -85
- package/dist/sync.d.ts +9 -3
- package/dist/sync.js +40 -17
- package/dist/tx.d.ts +11 -1
- package/dist/tx.js +32 -11
- package/dist/typed.d.ts +240 -45
- package/dist/types.d.ts +15 -90
- package/dist/types.js +12 -14
- package/dist/up.d.ts +53 -32
- package/dist/up.js +180 -74
- package/dist/uuid.js +5 -5
- package/dist/validate.d.ts +13 -1
- package/dist/validate.js +61 -29
- package/dist/watch.d.ts +5 -16
- package/dist/watch.js +23 -77
- package/dist/write.d.ts +9 -16
- package/dist/write.js +102 -185
- package/llms.txt +22 -25
- package/package.json +2 -9
- package/sql/10-core.sql +7 -50
- package/sql/15-errors.sql +64 -10
- package/sql/20-context.sql +7 -6
- package/sql/30-validate.sql +35 -27
- package/sql/40-class.sql +20 -114
- 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 +23 -27
- package/sql/90-time.sql +77 -944
- package/sql/95-seed.system.sql +7 -14
- package/sql/99-revision.sql +2 -1
- package/sql/README.md +32 -24
- package/sql/migrate/README.md +14 -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/60-write.sql
CHANGED
|
@@ -10,9 +10,9 @@ create or replace function @SCHEMA@.now_ts() returns timestamptz
|
|
|
10
10
|
language sql volatile
|
|
11
11
|
return clock_timestamp();
|
|
12
12
|
|
|
13
|
-
-- Внутренние блокировки (семейства,
|
|
13
|
+
-- Внутренние блокировки (семейства, обслуживание) — строки-замки: строки вида LOCK таблицы secret, которой у
|
|
14
14
|
-- приложения нет, а не advisory-блокировки. Advisory-блокировку берёт любая роль базы, а её ключ виден всем в pg_locks:
|
|
15
|
-
-- посторонняя роль занимала бы её и останавливала запись
|
|
15
|
+
-- посторонняя роль занимала бы её и останавливала запись семейства или обслуживание для всех (ревью
|
|
16
16
|
-- безопасности №3, №2). id строки-замка — lock_id имени; строки семейств заводятся заранее (class_admit, seed_system),
|
|
17
17
|
-- иначе строку вставляет первое обращение — и параллельное ждёт его транзакцию.
|
|
18
18
|
-- lock_id — первые 16 байт sha256 имени: его считает каждая строка записи, а v5_id собирает jsonb и зовёт pgcrypto.
|
|
@@ -34,18 +34,14 @@ begin
|
|
|
34
34
|
insert into @SCHEMA@.secret (id, account, kind, meta) values (v, '@SYSTEM@'::uuid, 'LOCK', jsonb_build_object('name', p_name))
|
|
35
35
|
on conflict do nothing;
|
|
36
36
|
if not exists (select 1 from @SCHEMA@.secret s where s.id = v) then
|
|
37
|
-
|
|
37
|
+
-- внутренняя ошибка (SQLSTATE XX000) по ключу каталога: библиотека отдаёт её кодом internal (fromDbError)
|
|
38
|
+
perform @SCHEMA@.lt_raise('XX000', 'sql-write.lock_row_not_created', jsonb_build_object('name', p_name), 'null'::jsonb);
|
|
38
39
|
end if;
|
|
39
40
|
end if;
|
|
40
41
|
return v;
|
|
41
42
|
end $$;
|
|
42
43
|
|
|
43
|
-
--
|
|
44
|
-
-- помечаются удалёнными; повторный накат их уже не находит.
|
|
45
|
-
update @SCHEMA@.secret s set deleted_at = clock_timestamp()
|
|
46
|
-
where s.kind = 'LOCK' and s.account <> '@SYSTEM@'::uuid and s.deleted_at is null;
|
|
47
|
-
|
|
48
|
-
-- Строка-замок без ожидания (обслуживание и проектор — один исполнитель): true — взята до конца транзакции, false — её
|
|
44
|
+
-- Строка-замок без ожидания (обслуживание — один исполнитель): true — взята до конца транзакции, false — её
|
|
49
45
|
-- держит другая транзакция. Блокировка — отдельным оператором: строку, которую вставила lock_row, видит уже он.
|
|
50
46
|
create or replace function @SCHEMA@.lock_try(p_name text) returns boolean
|
|
51
47
|
language plpgsql volatile
|
|
@@ -59,8 +55,8 @@ begin
|
|
|
59
55
|
end $$;
|
|
60
56
|
|
|
61
57
|
-- Блокировки семейства (этап 1, п. 10): писатели и загрузчик берут разделяемую блокировку корня семейства, ужесточение
|
|
62
|
-
-- описания и
|
|
63
|
-
-- ужесточение
|
|
58
|
+
-- описания и удаление класса — исключительную. Имя — арендатор корневого класса и имя корня (итоговое ревью
|
|
59
|
+
-- безопасности): ужесточение одноимённого класса в другом арендаторе запись сюда не останавливает; корень из System
|
|
64
60
|
-- (общий для всех арендаторов) — арендатор System. p_data — описание класса с resolved.
|
|
65
61
|
create or replace function @SCHEMA@.family_name(p_tenant uuid, p_data jsonb) returns text
|
|
66
62
|
language sql stable
|
|
@@ -104,7 +100,7 @@ begin
|
|
|
104
100
|
perform @SCHEMA@.lock_row(n);
|
|
105
101
|
perform 1 from @SCHEMA@.secret s where s.id = k for update;
|
|
106
102
|
if not found then
|
|
107
|
-
|
|
103
|
+
perform @SCHEMA@.lt_raise('XX000', 'sql-write.lock_row_not_found', jsonb_build_object('name', n), 'null'::jsonb);
|
|
108
104
|
end if;
|
|
109
105
|
else
|
|
110
106
|
if strpos(held, '|' || n || '|') = 0 then
|
|
@@ -121,7 +117,7 @@ begin
|
|
|
121
117
|
perform @SCHEMA@.lock_row(n);
|
|
122
118
|
perform 1 from @SCHEMA@.secret s where s.id = k for key share;
|
|
123
119
|
if not found then
|
|
124
|
-
|
|
120
|
+
perform @SCHEMA@.lt_raise('XX000', 'sql-write.lock_row_not_found', jsonb_build_object('name', n), 'null'::jsonb);
|
|
125
121
|
end if;
|
|
126
122
|
end if;
|
|
127
123
|
end if;
|
|
@@ -129,8 +125,6 @@ begin
|
|
|
129
125
|
perform set_config('letopis.family_locks', held || '|' || n || '|', true);
|
|
130
126
|
end if;
|
|
131
127
|
end $$;
|
|
132
|
-
-- до итогового ревью безопасности ключ был без арендатора
|
|
133
|
-
drop function if exists @SCHEMA@.family_lock(text, boolean);
|
|
134
128
|
|
|
135
129
|
-- Список классов конца: строка или массив строк → отсортированный jsonb-массив.
|
|
136
130
|
create or replace function @SCHEMA@.class_set(p jsonb) returns jsonb
|
|
@@ -195,8 +189,9 @@ end $$;
|
|
|
195
189
|
-- Разбор и нормализация концов (этап 1, п. 7): объявлены, обязательные на месте, у множественных
|
|
196
190
|
-- нет повторов, соблюдены min и max; uuid — в каноническом виде строчными. К таблицам не обращается:
|
|
197
191
|
-- цели проверяет targets_bad() — триггером после оператора, одним запросом на все строки.
|
|
198
|
-
-- Ошибку не бросает: возвращает
|
|
199
|
-
|
|
192
|
+
-- Ошибку не бросает: возвращает ключ каталога сообщений и параметры отказа (план 2.0, этап 3, п. 3; вызывающий бросает
|
|
193
|
+
-- ошибку по ним).
|
|
194
|
+
create or replace function @SCHEMA@.links_shape(p_res jsonb, p_links jsonb, out links jsonb, out key text, out params jsonb)
|
|
200
195
|
language plpgsql immutable
|
|
201
196
|
as $$
|
|
202
197
|
#variable_conflict use_variable
|
|
@@ -212,47 +207,48 @@ begin
|
|
|
212
207
|
p_links := '{}'::jsonb;
|
|
213
208
|
end if;
|
|
214
209
|
if jsonb_typeof(p_links) <> 'object' then
|
|
215
|
-
|
|
210
|
+
key := 'sql-write.links_not_object'; params := '{}'::jsonb; return;
|
|
216
211
|
end if;
|
|
217
|
-
|
|
212
|
+
-- столбцы jsonb_each — с псевдонимом: key — ещё и выходной параметр (variable_conflict use_variable)
|
|
213
|
+
for l in select j.key, j.value from jsonb_each(p_links) j loop
|
|
218
214
|
if not coalesce(p_res->'ends' ? l.key, false) then
|
|
219
|
-
|
|
215
|
+
key := 'sql-write.end_undeclared'; params := jsonb_build_object('end', l.key, 'cls', p_res->>'name'); return;
|
|
220
216
|
end if;
|
|
221
217
|
continue when l.value = 'null'::jsonb;
|
|
222
218
|
if coalesce((p_res->'ends'->l.key->>'many')::boolean, false) then
|
|
223
219
|
if jsonb_typeof(l.value) <> 'array' then
|
|
224
|
-
|
|
220
|
+
key := 'sql-write.end_not_list'; params := jsonb_build_object('end', l.key); return;
|
|
225
221
|
end if;
|
|
226
222
|
arr := '[]'::jsonb;
|
|
227
223
|
for v in select value from jsonb_array_elements(l.value) loop
|
|
228
224
|
if jsonb_typeof(v) <> 'string' or not pg_input_is_valid(v #>> '{}', 'uuid') then
|
|
229
|
-
|
|
225
|
+
key := 'sql-write.end_not_uuid'; params := jsonb_build_object('end', l.key); return;
|
|
230
226
|
end if;
|
|
231
227
|
v := to_jsonb((v #>> '{}')::uuid::text);
|
|
232
228
|
if arr @> jsonb_build_array(v) then
|
|
233
|
-
|
|
229
|
+
key := 'sql-write.end_target_repeated'; params := jsonb_build_object('end', l.key); return;
|
|
234
230
|
end if;
|
|
235
231
|
arr := arr || jsonb_build_array(v);
|
|
236
232
|
end loop;
|
|
237
233
|
links := links || jsonb_build_object(l.key, arr);
|
|
238
234
|
else
|
|
239
235
|
if jsonb_typeof(l.value) <> 'string' or not pg_input_is_valid(l.value #>> '{}', 'uuid') then
|
|
240
|
-
|
|
236
|
+
key := 'sql-write.end_not_uuid'; params := jsonb_build_object('end', l.key); return;
|
|
241
237
|
end if;
|
|
242
238
|
links := links || jsonb_build_object(l.key, (l.value #>> '{}')::uuid::text);
|
|
243
239
|
end if;
|
|
244
240
|
end loop;
|
|
245
|
-
for e in select key, value from jsonb_each(coalesce(p_res->'ends', '{}'::jsonb)) loop
|
|
241
|
+
for e in select j.key, j.value from jsonb_each(coalesce(p_res->'ends', '{}'::jsonb)) j loop
|
|
246
242
|
if coalesce((e.value->>'many')::boolean, false) then
|
|
247
243
|
n := coalesce(jsonb_array_length(links->e.key), 0);
|
|
248
244
|
if n < greatest(coalesce((e.value->>'min')::int, 0), case when coalesce((e.value->>'required')::boolean, false) then 1 else 0 end) then
|
|
249
|
-
|
|
245
|
+
key := 'sql-write.end_too_few'; params := jsonb_build_object('end', e.key); return;
|
|
250
246
|
end if;
|
|
251
247
|
if e.value ? 'max' and n > (e.value->>'max')::int then
|
|
252
|
-
|
|
248
|
+
key := 'sql-write.end_too_many'; params := jsonb_build_object('end', e.key); return;
|
|
253
249
|
end if;
|
|
254
250
|
elsif coalesce((e.value->>'required')::boolean, false) and not links ? e.key then
|
|
255
|
-
|
|
251
|
+
key := 'sql-write.end_required'; params := jsonb_build_object('end', e.key); return;
|
|
256
252
|
end if;
|
|
257
253
|
end loop;
|
|
258
254
|
end $$;
|
|
@@ -262,13 +258,16 @@ end $$;
|
|
|
262
258
|
-- (ends — описание концов, если его ещё нет в строке Class). При p_lock цели сначала блокируются
|
|
263
259
|
-- for key share в порядке id — каждая один раз на оператор, только свои и системные: строку чужого
|
|
264
260
|
-- арендатора не заблокировать и по ожиданию не обнаружить. Проверка идёт после блокировки, по
|
|
265
|
-
-- свежим строкам: цель, которую успели
|
|
261
|
+
-- свежим строкам: цель, которую успели удалить, не пройдёт. Класс цели
|
|
266
262
|
-- прямо в списке конца — без поиска описания; иначе — предки из строки Class цели.
|
|
267
263
|
-- При p_acl ({tenant, account, plan} — план READ актора) цель ещё и должна быть видна актору (этап 5,
|
|
268
264
|
-- п. 6): строку, которую он не видит, нельзя использовать как цель — ответ тот же, что для
|
|
269
265
|
-- несуществующей. Цели-аккаунты (Account) не проверяются: на них ссылаются членства и владельцы.
|
|
270
|
-
--
|
|
271
|
-
--
|
|
266
|
+
-- Роль сеанса, к которой RLS не применяется (суперпользователь, BYPASSRLS, член владельца),
|
|
267
|
+
-- видит любую строку, а пишет и строки, которых план актора не показывает: прятать от неё цель незачем, и p_acl
|
|
268
|
+
-- она не проверяет. До этого администраторское подключение с сессией без прав заводило ресурсы, а правило на них —
|
|
269
|
+
-- нет: target_not_found (внедрение, агентский прогон плана 2.0).
|
|
270
|
+
-- Возвращает роль первой непрошедшей цели или null.
|
|
272
271
|
create or replace function @SCHEMA@.targets_bad(p_rows jsonb, p_lock boolean, p_acl jsonb default null) returns text
|
|
273
272
|
language plpgsql volatile
|
|
274
273
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
@@ -279,6 +278,11 @@ begin
|
|
|
279
278
|
if p_rows is null or jsonb_array_length(p_rows) = 0 then
|
|
280
279
|
return null;
|
|
281
280
|
end if;
|
|
281
|
+
if p_acl is not null
|
|
282
|
+
and (exists (select 1 from pg_roles r where r.rolname = @SCHEMA@.session_role() and (r.rolsuper or r.rolbypassrls))
|
|
283
|
+
or pg_has_role(@SCHEMA@.session_role(), '@OWNER_NAME@', 'USAGE')) then
|
|
284
|
+
p_acl := null;
|
|
285
|
+
end if;
|
|
282
286
|
if p_lock then
|
|
283
287
|
perform 1 from @SCHEMA@.entity t
|
|
284
288
|
where t.id in (select (v #>> '{}')::uuid from jsonb_array_elements(p_rows) r
|
|
@@ -299,7 +303,7 @@ begin
|
|
|
299
303
|
left join lateral jsonb_array_elements(case jsonb_typeof(k.value) when 'array' then k.value else '[]'::jsonb end) a on true
|
|
300
304
|
where jsonb_typeof(k.value) = 'string' or a.value is not null
|
|
301
305
|
)
|
|
302
|
-
select
|
|
306
|
+
select x.role into bad
|
|
303
307
|
from (
|
|
304
308
|
select p.role,
|
|
305
309
|
t.id is null
|
|
@@ -308,14 +312,11 @@ begin
|
|
|
308
312
|
or @SCHEMA@.class_set(c.ends->p.role->'class') ?| array(select jsonb_array_elements_text(kr.res->'ancestors')), false)
|
|
309
313
|
or (p_acl is not null and t.class <> 'Account'
|
|
310
314
|
and not @SCHEMA@.row_visible(t.tenant, t.class, t.id, t.owner, t.tags, t.data, t.links,
|
|
311
|
-
(p_acl->>'tenant')::uuid, (p_acl->>'account')::uuid, p_acl->'plan')) as miss
|
|
312
|
-
coalesce(kr.res ? 'derivedBy', false) as derived
|
|
315
|
+
(p_acl->>'tenant')::uuid, (p_acl->>'account')::uuid, p_acl->'plan')) as miss
|
|
313
316
|
from p
|
|
314
317
|
join c on c.cls = p.cls and c.tenant = p.tenant and c.ends is not distinct from coalesce(p.ends, c.ends)
|
|
315
318
|
left join @SCHEMA@.entity t on t.id = p.tid
|
|
316
|
-
-- описание класса цели — только когда класс не назван в конце прямо: тогда нужны его
|
|
317
|
-
-- производного класса (конец, названный предком; план 1.1, §2.4) не проходит — прямо назвать производный
|
|
318
|
-
-- класс в конце не даёт описание (class_admit)
|
|
319
|
+
-- описание класса цели — только когда класс не назван в конце прямо: тогда нужны его предки
|
|
319
320
|
left join lateral (
|
|
320
321
|
select case when t.id is null or @SCHEMA@.class_set(c.ends->p.role->'class') ? t.class then null
|
|
321
322
|
else (select k.data->'resolved' from @SCHEMA@.entity k
|
|
@@ -323,18 +324,17 @@ begin
|
|
|
323
324
|
order by k.tenant = '@SYSTEM@'::uuid limit 1) end as res
|
|
324
325
|
) kr on true
|
|
325
326
|
) x
|
|
326
|
-
where x.miss
|
|
327
|
+
where x.miss
|
|
327
328
|
limit 1;
|
|
328
329
|
return bad;
|
|
329
330
|
end $$;
|
|
330
|
-
-- до этапа 5 цели не сверялись с правами актора
|
|
331
|
-
drop function if exists @SCHEMA@.targets_bad(jsonb, boolean);
|
|
332
331
|
|
|
333
|
-
-- Разбор концов и проверка целей одной строки — для функций владельца (
|
|
332
|
+
-- Разбор концов и проверка целей одной строки — для функций владельца (restore, проверка строк
|
|
334
333
|
-- семейства), которые пишут мимо триггеров. При p_lock цели блокируются for key share; p_acl — как
|
|
335
|
-
-- у targets_bad (цели видны актору).
|
|
334
|
+
-- у targets_bad (цели видны актору). Отказ — ключ каталога и параметры, как у links_shape (вызывающий бросает
|
|
335
|
+
-- ошибку по ним).
|
|
336
336
|
create or replace function @SCHEMA@.check_links(p_res jsonb, p_links jsonb, p_tenant uuid, p_lock boolean, p_acl jsonb,
|
|
337
|
-
out links jsonb, out
|
|
337
|
+
out links jsonb, out key text, out params jsonb)
|
|
338
338
|
language plpgsql volatile
|
|
339
339
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
340
340
|
as $$
|
|
@@ -342,19 +342,16 @@ as $$
|
|
|
342
342
|
declare
|
|
343
343
|
bad text;
|
|
344
344
|
begin
|
|
345
|
-
select s.links, s.
|
|
346
|
-
if
|
|
345
|
+
select s.links, s.key, s.params into links, key, params from @SCHEMA@.links_shape(p_res, p_links) s;
|
|
346
|
+
if key is not null or links = '{}'::jsonb then
|
|
347
347
|
return;
|
|
348
348
|
end if;
|
|
349
349
|
bad := @SCHEMA@.targets_bad(jsonb_build_array(jsonb_build_object(
|
|
350
350
|
'class', p_res->>'name', 'tenant', p_tenant, 'links', links, 'ends', coalesce(p_res->'ends', '{}'::jsonb))), p_lock, p_acl);
|
|
351
|
-
if bad
|
|
352
|
-
|
|
353
|
-
elsif bad is not null then
|
|
354
|
-
code := 'target_not_found'; msg := format('цель конца %s не найдена', bad);
|
|
351
|
+
if bad is not null then
|
|
352
|
+
key := 'sql-write.end_target_missing'; params := jsonb_build_object('end', bad);
|
|
355
353
|
end if;
|
|
356
354
|
end $$;
|
|
357
|
-
drop function if exists @SCHEMA@.check_links(jsonb, jsonb, uuid, boolean);
|
|
358
355
|
|
|
359
356
|
-- id класса с ключом — v5 от арендатора, класса и значений ключа (§2.3); у класса без ключа — null.
|
|
360
357
|
create or replace function @SCHEMA@.key_id(p_res jsonb, p_class text, p_tenant uuid, p_data jsonb, p_links jsonb) returns uuid
|
|
@@ -377,8 +374,6 @@ return coalesce((@SCHEMA@.class_row(p_class, p_tenant)).data->'resolved'->>'hist
|
|
|
377
374
|
-- 96 бит HMAC-SHA256 от «назначение:значение» на ключе сигналов (HMAC ключа строки KEY по
|
|
378
375
|
-- 'letopis:signals', его отдаёт signal_key()): без ключа псевдоним не сопоставить с именем, а
|
|
379
376
|
-- нагрузка детерминирована — PostgreSQL склеивает одинаковые сигналы транзакции.
|
|
380
|
-
-- до плана 1.1 сигнал принимал только имена классов
|
|
381
|
-
drop function if exists @SCHEMA@.signal(text[]);
|
|
382
377
|
|
|
383
378
|
create or replace function @SCHEMA@.signal_token(p_purpose text, p_value text, p_key bytea) returns text
|
|
384
379
|
language sql immutable parallel safe
|
|
@@ -440,31 +435,23 @@ begin
|
|
|
440
435
|
end $$;
|
|
441
436
|
|
|
442
437
|
-- Версия одной строки в журнал и сигнал — для функций владельца, которые пишут строку сами
|
|
443
|
-
-- (пересчёт потомков, снятие конца,
|
|
438
|
+
-- (пересчёт потомков, снятие конца, восстановление, надгробие). Классы с history: false журнал не ведут,
|
|
444
439
|
-- но сигнал посылают. Обычную запись журналируют триггеры после оператора — одной вставкой.
|
|
445
440
|
create or replace function @SCHEMA@.log_version(e @SCHEMA@.entity) returns void
|
|
446
441
|
language plpgsql volatile
|
|
447
442
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
448
443
|
as $$
|
|
449
|
-
declare
|
|
450
|
-
p @SCHEMA@.log;
|
|
451
444
|
begin
|
|
452
445
|
if @SCHEMA@.history_on(e.class, e.tenant) then
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
where l.id = case when e.rev > 1 then e.id else e.moved end and l.op not in ('purge', 'trim', 'reset')
|
|
456
|
-
order by l.rev desc limit 1;
|
|
457
|
-
insert into @SCHEMA@.log (id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason, moved, prev_hash, changed)
|
|
458
|
-
values (e.id, e.rev, e.class, e.cv, e.tenant, e.owner, e.links, e.data, e.tags, e.at, e.author, e.agent, e.op, e.reason, e.moved, e.prev_hash,
|
|
459
|
-
case when p.id is not null then @SCHEMA@.change_paths(p.data, p.links, p.tags, p.owner, p.class, e.data, e.links, e.tags, e.owner, e.class) end);
|
|
446
|
+
insert into @SCHEMA@.log (id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason)
|
|
447
|
+
values (e.id, e.rev, e.class, e.cv, e.tenant, e.owner, e.links, e.data, e.tags, e.at, e.author, e.agent, e.op, e.reason);
|
|
460
448
|
end if;
|
|
461
449
|
perform @SCHEMA@.signal(array[e.class], array[e.tenant]);
|
|
462
450
|
end $$;
|
|
463
451
|
|
|
464
452
|
-- Новая версия строки от имени владельца (пересчёт описаний потомков, снятие конца при удалении):
|
|
465
453
|
-- служебные колонки, журнал, сигнал. Триггеры при этом не срабатывают.
|
|
466
|
-
create or replace function @SCHEMA@.sys_update(p_id uuid, p_data jsonb, p_links jsonb, p_validator jsonpath, p_op text
|
|
467
|
-
p_class text default null)
|
|
454
|
+
create or replace function @SCHEMA@.sys_update(p_id uuid, p_data jsonb, p_links jsonb, p_validator jsonpath, p_op text)
|
|
468
455
|
returns @SCHEMA@.entity
|
|
469
456
|
language plpgsql volatile
|
|
470
457
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
@@ -472,38 +459,23 @@ as $$
|
|
|
472
459
|
declare
|
|
473
460
|
e @SCHEMA@.entity;
|
|
474
461
|
c record;
|
|
475
|
-
was text;
|
|
476
462
|
begin
|
|
477
463
|
select * into c from @SCHEMA@.ctx();
|
|
478
|
-
if p_class is not null then
|
|
479
|
-
select x.class into was from @SCHEMA@.entity x where x.id = p_id;
|
|
480
|
-
end if;
|
|
481
464
|
update @SCHEMA@.entity x
|
|
482
|
-
set
|
|
483
|
-
rev = x.rev + 1,
|
|
465
|
+
set data = p_data, links = p_links, validator = p_validator,
|
|
466
|
+
rev = x.rev + 1,
|
|
484
467
|
at = greatest(@SCHEMA@.now_ts(), x.at + interval '1 microsecond'),
|
|
485
468
|
author = coalesce(c.author, '@SYSTEM@'::uuid), agent = c.agent, op = p_op,
|
|
486
|
-
reason = nullif(current_setting('letopis.reason', true), ''),
|
|
487
|
-
cv = coalesce((@SCHEMA@.class_row(
|
|
469
|
+
reason = nullif(current_setting('letopis.reason', true), ''),
|
|
470
|
+
cv = coalesce((@SCHEMA@.class_row(x.class, x.tenant)).rev, x.cv)
|
|
488
471
|
where x.id = p_id
|
|
489
472
|
returning x.* into e;
|
|
490
|
-
-- смена класса: агрегатные поля новой цели триггер после правки посчитал в эту же версию (план 1.1, §2.4) —
|
|
491
|
-
-- в журнал идёт строка таблицы, а не returning
|
|
492
|
-
if p_class is not null then
|
|
493
|
-
select x.* into e from @SCHEMA@.entity x where x.id = p_id;
|
|
494
|
-
end if;
|
|
495
473
|
perform @SCHEMA@.log_version(e);
|
|
496
|
-
-- смена класса: сигнал и прежнего класса — его ответы кэша и подписки тоже устарели (log_version сигналит только новый;
|
|
497
|
-
-- путь через триггер шлёт оба; план 2.0, 7.4 №4)
|
|
498
|
-
if was is not null and was <> e.class then
|
|
499
|
-
perform @SCHEMA@.signal(array[was], array[e.tenant]);
|
|
500
|
-
end if;
|
|
501
474
|
return e;
|
|
502
475
|
end $$;
|
|
503
476
|
|
|
504
|
-
-- Надгробие: последний снимок удалённой строки следующей версией с видом delete
|
|
505
|
-
|
|
506
|
-
create or replace function @SCHEMA@.tombstone(e @SCHEMA@.entity, p_op text default 'delete', p_moved uuid default null) returns void
|
|
477
|
+
-- Надгробие: последний снимок удалённой строки следующей версией с видом delete.
|
|
478
|
+
create or replace function @SCHEMA@.tombstone(e @SCHEMA@.entity) returns void
|
|
507
479
|
language plpgsql volatile
|
|
508
480
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
509
481
|
as $$
|
|
@@ -512,17 +484,15 @@ declare
|
|
|
512
484
|
begin
|
|
513
485
|
select * into c from @SCHEMA@.ctx();
|
|
514
486
|
e.rev := e.rev + 1;
|
|
515
|
-
e.prev_hash := e.hash;
|
|
516
487
|
e.at := greatest(@SCHEMA@.now_ts(), e.at + interval '1 microsecond');
|
|
517
488
|
e.author := coalesce(c.author, '@SYSTEM@'::uuid);
|
|
518
489
|
e.agent := c.agent;
|
|
519
|
-
e.op :=
|
|
490
|
+
e.op := 'delete';
|
|
520
491
|
e.reason := nullif(current_setting('letopis.reason', true), '');
|
|
521
|
-
e.moved := p_moved;
|
|
522
492
|
perform @SCHEMA@.log_version(e);
|
|
523
493
|
end $$;
|
|
524
494
|
|
|
525
|
-
-- Входящие ссылки на строку p_id, роли которых не принимают класс p_class (смена
|
|
495
|
+
-- Входящие ссылки на строку p_id, роли которых не принимают класс p_class (смена родителя класса).
|
|
526
496
|
create or replace function @SCHEMA@.incoming_conflicts(p_id uuid, p_class text, p_tenant uuid) returns jsonb
|
|
527
497
|
language sql stable
|
|
528
498
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
@@ -535,7 +505,7 @@ as $$
|
|
|
535
505
|
and not @SCHEMA@.role_accepts(r.class, r.tenant, k.key, p_class, p_tenant)
|
|
536
506
|
$$;
|
|
537
507
|
|
|
538
|
-
-- Перечень видимых актору строк и число остальных — для ошибок restrict
|
|
508
|
+
-- Перечень видимых актору строк и число остальных — для ошибок restrict и ужесточения.
|
|
539
509
|
-- Видимость — та же, что у чтения (row_visible с планом READ актора), а не только арендатор.
|
|
540
510
|
create or replace function @SCHEMA@.visible_detail(p_rows jsonb, p_tenant uuid) returns jsonb
|
|
541
511
|
language plpgsql stable
|
|
@@ -567,16 +537,13 @@ return (select min(r->>'class') from jsonb_array_elements(p_rows) r, (select @SC
|
|
|
567
537
|
(x.a->>'tenant')::uuid, (x.a->>'account')::uuid, x.a->'plan'));
|
|
568
538
|
|
|
569
539
|
-- Безопасно ли изменение описания класса (этап 1, п. 10): новые необязательные поля и концы,
|
|
570
|
-
-- метаданные, алиас,
|
|
571
|
-
-- любая правка условий anyOf и oneOf (1.0.1: прежде они не сравнивались, и строки не перепроверялись)
|
|
572
|
-
-- признака производного класса derivedBy и описания агрегатов aggregate (план 1.1, §2.6).
|
|
540
|
+
-- метаданные, алиас, history, кэш. Всё остальное — ужесточение с проверкой строк, в том числе
|
|
541
|
+
-- любая правка условий anyOf и oneOf (1.0.1: прежде они не сравнивались, и строки не перепроверялись).
|
|
573
542
|
create or replace function @SCHEMA@.class_change_safe(o jsonb, n jsonb) returns boolean
|
|
574
543
|
language sql immutable
|
|
575
544
|
return
|
|
576
545
|
o->'key' = n->'key'
|
|
577
546
|
and o->'ancestors' = n->'ancestors'
|
|
578
|
-
and o->'derivedBy' is not distinct from n->'derivedBy'
|
|
579
|
-
and o->'aggregate' is not distinct from n->'aggregate'
|
|
580
547
|
and (o->'abstract' = n->'abstract' or n->>'abstract' = 'false')
|
|
581
548
|
and o->'schema'->'anyOf' is not distinct from n->'schema'->'anyOf'
|
|
582
549
|
and o->'schema'->'oneOf' is not distinct from n->'schema'->'oneOf'
|
|
@@ -590,7 +557,8 @@ return
|
|
|
590
557
|
and (coalesce((e.value->>'required')::boolean, false) or coalesce((e.value->>'min')::int, 0) > 0));
|
|
591
558
|
|
|
592
559
|
-- Строки семейства, которые не проходят текущие описания своих классов (после ужесточения).
|
|
593
|
-
-- Не больше 100 строк; p_incoming — проверять ещё и входящие ссылки (смена родителя).
|
|
560
|
+
-- Не больше 100 строк; p_incoming — проверять ещё и входящие ссылки (смена родителя). У строки — problem (русский текст
|
|
561
|
+
-- по ключу каталога) и сами key и params: библиотека их убирает (fromDbError).
|
|
594
562
|
create or replace function @SCHEMA@.family_conflicts(p_classes text[], p_tenant uuid, p_incoming boolean) returns jsonb
|
|
595
563
|
language plpgsql volatile
|
|
596
564
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
@@ -602,6 +570,7 @@ declare
|
|
|
602
570
|
bad jsonb;
|
|
603
571
|
lk record;
|
|
604
572
|
problem text;
|
|
573
|
+
msg jsonb; -- ключ и параметры problem
|
|
605
574
|
inc jsonb;
|
|
606
575
|
outv jsonb := '[]'::jsonb;
|
|
607
576
|
a jsonb := @SCHEMA@.acl_actor('READ');
|
|
@@ -614,18 +583,26 @@ begin
|
|
|
614
583
|
cls := @SCHEMA@.class_row(r.class, r.tenant);
|
|
615
584
|
res := cls.data->'resolved';
|
|
616
585
|
problem := null;
|
|
586
|
+
msg := null;
|
|
617
587
|
if coalesce((res->>'abstract')::boolean, false) then
|
|
618
|
-
problem := '
|
|
588
|
+
problem := @SCHEMA@.lt_text('sql-write.problem_abstract');
|
|
589
|
+
msg := jsonb_build_object('key', 'sql-write.problem_abstract', 'params', '{}'::jsonb);
|
|
619
590
|
else
|
|
620
591
|
bad := @SCHEMA@.validate_data(res->'rules', cls.validator, res->'vars', r.data);
|
|
621
592
|
if bad <> '[]'::jsonb then
|
|
622
|
-
|
|
593
|
+
-- в русском тексте — нарушения без key и params (текст прежний), а в params строки — сами нарушения с ключами:
|
|
594
|
+
-- по ним библиотека переведёт и их сообщения
|
|
595
|
+
problem := @SCHEMA@.lt_text('sql-write.problem_data', jsonb_build_object('issues',
|
|
596
|
+
(select jsonb_agg(x.i - 'key' - 'params' order by x.n) from jsonb_array_elements(bad) with ordinality x(i, n))::text));
|
|
597
|
+
msg := jsonb_build_object('key', 'sql-write.problem_data', 'params', jsonb_build_object('issues', bad));
|
|
623
598
|
else
|
|
624
599
|
select * into lk from @SCHEMA@.check_links(res, r.links, r.tenant, false, null);
|
|
625
|
-
if lk.
|
|
626
|
-
problem := lk.
|
|
600
|
+
if lk.key is not null then
|
|
601
|
+
problem := @SCHEMA@.lt_text(lk.key, lk.params);
|
|
602
|
+
msg := jsonb_build_object('key', lk.key, 'params', coalesce(lk.params, '{}'::jsonb));
|
|
627
603
|
elsif coalesce(@SCHEMA@.key_id(res, r.class, r.tenant, r.data, r.links), r.id) <> r.id then
|
|
628
|
-
problem := '
|
|
604
|
+
problem := @SCHEMA@.lt_text('sql-write.problem_key');
|
|
605
|
+
msg := jsonb_build_object('key', 'sql-write.problem_key', 'params', '{}'::jsonb);
|
|
629
606
|
end if;
|
|
630
607
|
end if;
|
|
631
608
|
end if;
|
|
@@ -640,13 +617,14 @@ begin
|
|
|
640
617
|
from jsonb_array_elements(inc) i(v)
|
|
641
618
|
left join @SCHEMA@.entity x on x.id = (i.v->>'id')::uuid
|
|
642
619
|
order by seen desc, i.v->>'id' limit 1;
|
|
643
|
-
|
|
644
|
-
then
|
|
645
|
-
else '
|
|
620
|
+
msg := case when vis
|
|
621
|
+
then jsonb_build_object('key', 'sql-write.problem_ref', 'params', jsonb_build_object('role', ref->>'role', 'id', ref->>'id'))
|
|
622
|
+
else jsonb_build_object('key', 'sql-write.problem_ref_hidden', 'params', '{}'::jsonb) end;
|
|
623
|
+
problem := @SCHEMA@.lt_text(msg->>'key', msg->'params');
|
|
646
624
|
end if;
|
|
647
625
|
end if;
|
|
648
626
|
if problem is not null then
|
|
649
|
-
outv := outv || jsonb_build_object('id', r.id, 'class', r.class, 'tenant', r.tenant, 'problem', problem);
|
|
627
|
+
outv := outv || (jsonb_build_object('id', r.id, 'class', r.class, 'tenant', r.tenant, 'problem', problem) || coalesce(msg, '{}'::jsonb));
|
|
650
628
|
exit when jsonb_array_length(outv) >= 100;
|
|
651
629
|
end if;
|
|
652
630
|
end loop;
|
|
@@ -660,26 +638,26 @@ language sql immutable parallel safe
|
|
|
660
638
|
return array[@RESERVED_NAMES@]::text[];
|
|
661
639
|
|
|
662
640
|
-- Правка класса p_class арендатора p_tenant меняет имена, по которым его строки находят правила прав: предков
|
|
663
|
-
-- (parent)
|
|
641
|
+
-- (parent) или алиас (ревью безопасности №2). План прав берёт для класса имена предков и их алиасы
|
|
664
642
|
-- (acl_plan_for), поэтому строки класса и его потомков попали бы под правила READ, WRITE и DELETE нового предка или
|
|
665
643
|
-- алиаса — право WRITE на Class открывало бы чужие строки. Актору нужно право READ, WRITE и DELETE без условий на
|
|
666
644
|
-- все строки самого класса и каждого потомка своего арендатора, иначе acl_denied. Потомков у арендаторов класса
|
|
667
|
-
-- System план прав сессии System не видит — они
|
|
645
|
+
-- System план прав сессии System не видит — они пропускаются. Проверка — до
|
|
668
646
|
-- правки (class_admit): план видит прежние имена, по которым строки уже защищены.
|
|
669
647
|
create or replace function @SCHEMA@.class_acl_admit(p_class text, p_tenant uuid) returns void
|
|
670
648
|
language plpgsql stable
|
|
671
649
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
672
650
|
as $$
|
|
673
651
|
begin
|
|
674
|
-
perform @SCHEMA@.class_rows_admit(p_class, p_tenant, array['READ', 'WRITE', 'DELETE'],
|
|
675
|
-
format('класс %s меняет предков, алиас или derivedBy', p_class));
|
|
652
|
+
perform @SCHEMA@.class_rows_admit(p_class, p_tenant, array['READ', 'WRITE', 'DELETE'], 'reparent');
|
|
676
653
|
end $$;
|
|
677
654
|
|
|
678
655
|
-- Право актора на все строки класса p_class и каждого его потомка арендатора p_tenant: по каждой операции p_ops —
|
|
679
|
-
-- без условий, иначе acl_denied
|
|
656
|
+
-- без условий, иначе acl_denied «<что меняется>: нужно право <op> на все строки класса <класс>». Что меняется — p_what,
|
|
657
|
+
-- по нему ключ каталога (план 2.0, этап 3, п. 3): reparent, history, tighten; иное — готовый текст. Сам класс первым,
|
|
680
658
|
-- потомки — по глубине и имени; потомков у арендаторов класса System план сессии System не видит — они пропускаются.
|
|
681
|
-
-- Зовут class_acl_admit (смена
|
|
682
|
-
--
|
|
659
|
+
-- Зовут class_acl_admit (смена предков и алиаса), ужесточение (class_evolve) и смена history — включение и выключение
|
|
660
|
+
-- журнала (class_admit; доработки после точки Б).
|
|
683
661
|
create or replace function @SCHEMA@.class_rows_admit(p_class text, p_tenant uuid, p_ops text[], p_what text) returns void
|
|
684
662
|
language plpgsql stable
|
|
685
663
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
@@ -688,6 +666,7 @@ declare
|
|
|
688
666
|
c text;
|
|
689
667
|
op text;
|
|
690
668
|
chk jsonb;
|
|
669
|
+
p jsonb;
|
|
691
670
|
begin
|
|
692
671
|
for c in select p_class
|
|
693
672
|
union all
|
|
@@ -698,21 +677,27 @@ begin
|
|
|
698
677
|
foreach op in array p_ops loop
|
|
699
678
|
chk := @SCHEMA@.acl_check_data(c, op);
|
|
700
679
|
if not coalesce((chk->>'allow')::boolean, false) or chk ? 'filter' then
|
|
701
|
-
|
|
680
|
+
p := jsonb_build_object('cls', p_class, 'op', op, 'target', c);
|
|
681
|
+
case p_what
|
|
682
|
+
when 'reparent' then perform @SCHEMA@.lt_fail('sql-write.reparent_needs_all', p);
|
|
683
|
+
when 'history' then perform @SCHEMA@.lt_fail('sql-write.history_needs_all', p);
|
|
684
|
+
when 'tighten' then perform @SCHEMA@.lt_fail('sql-write.tighten_needs_all', p);
|
|
685
|
+
else perform @SCHEMA@.lt_fail('sql-write.change_needs_all', jsonb_build_object('what', p_what, 'op', op, 'target', c));
|
|
686
|
+
end case;
|
|
702
687
|
end if;
|
|
703
688
|
end loop;
|
|
704
689
|
end loop;
|
|
705
690
|
end $$;
|
|
706
691
|
|
|
707
|
-
-- Есть ли в журнале версии строк класса p_name арендатора p_tenant
|
|
708
|
-
--
|
|
692
|
+
-- Есть ли в журнале версии строк класса p_name арендатора p_tenant (доработки после точки Б: удаление и пересоздание
|
|
693
|
+
-- класса, остаток Н2 ревью №2). У класса System — строки любого арендатора, кроме
|
|
709
694
|
-- арендаторов со своим классом с тем же именем: их строки — строки своего класса, как их резолвит class_row (ревью
|
|
710
695
|
-- безопасности №3: история такого класса не мешает администратору создать и удалить класс System)
|
|
711
696
|
create or replace function @SCHEMA@.class_has_history(p_name text, p_tenant uuid) returns boolean
|
|
712
697
|
language sql stable
|
|
713
698
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
714
699
|
return exists (select 1 from @SCHEMA@.log l
|
|
715
|
-
where l.class = p_name
|
|
700
|
+
where l.class = p_name
|
|
716
701
|
and (l.tenant = p_tenant
|
|
717
702
|
or p_tenant = '@SYSTEM@'::uuid
|
|
718
703
|
and l.tenant <> all (array(select x.tenant from @SCHEMA@.entity x
|
|
@@ -738,8 +723,8 @@ return exists (select 1 from @SCHEMA@.log t
|
|
|
738
723
|
and @SCHEMA@.class_has_history(t.data->>'name', t.tenant));
|
|
739
724
|
|
|
740
725
|
-- Проверки строки Class сверх метасхемы (этап 1, п. 6): признак system, имя системного класса,
|
|
741
|
-
-- имена связей против методов цепочки, однозначность имени конца во всей схеме,
|
|
742
|
-
-- алиас и право на смену
|
|
726
|
+
-- имена связей и хабов против методов цепочки, однозначность имени конца во всей схеме,
|
|
727
|
+
-- алиас и право на смену предков и алиаса (ревью безопасности №2).
|
|
743
728
|
create or replace function @SCHEMA@.class_admit(p_data jsonb, p_tenant uuid, out data jsonb, out validator jsonpath)
|
|
744
729
|
language plpgsql volatile
|
|
745
730
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
@@ -748,25 +733,19 @@ as $$
|
|
|
748
733
|
declare
|
|
749
734
|
e record;
|
|
750
735
|
o record;
|
|
751
|
-
dname text;
|
|
752
|
-
drole text;
|
|
753
736
|
tc @SCHEMA@.entity;
|
|
754
737
|
prevd jsonb;
|
|
755
|
-
src text;
|
|
756
|
-
chk jsonb;
|
|
757
|
-
m jsonb;
|
|
758
|
-
dkid text;
|
|
759
738
|
begin
|
|
760
739
|
if p_tenant <> '@SYSTEM@'::uuid then
|
|
761
740
|
if (p_data->>'name') = any (array['Class', 'Account', 'member', 'Resource', 'rule']) then
|
|
762
|
-
perform @SCHEMA@.lt_fail('
|
|
741
|
+
perform @SCHEMA@.lt_fail('sql-write.system_name_reserved', jsonb_build_object('name', p_data->>'name'));
|
|
763
742
|
end if;
|
|
764
743
|
if coalesce((p_data->>'system')::boolean, false) then
|
|
765
|
-
perform @SCHEMA@.lt_fail('
|
|
744
|
+
perform @SCHEMA@.lt_fail('sql-write.system_flag');
|
|
766
745
|
end if;
|
|
767
746
|
if exists (select 1 from @SCHEMA@.entity s
|
|
768
747
|
where s.id = @SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array[p_data->>'name']) and s.class = 'Class') then
|
|
769
|
-
perform @SCHEMA@.lt_fail('
|
|
748
|
+
perform @SCHEMA@.lt_fail('sql-write.system_name_taken', jsonb_build_object('name', p_data->>'name'));
|
|
770
749
|
end if;
|
|
771
750
|
end if;
|
|
772
751
|
-- родитель — под разделяемой блокировкой своего семейства (хвост ревью доработок после 1.2.0): удаление класса берёт
|
|
@@ -779,7 +758,12 @@ begin
|
|
|
779
758
|
end if;
|
|
780
759
|
select c.data, c.validator into data, validator from @SCHEMA@.class_prepare(p_data, p_tenant) c;
|
|
781
760
|
if data->>'kind' = 'link' and (data->>'name') = any (@SCHEMA@.reserved_names()) then
|
|
782
|
-
perform @SCHEMA@.lt_fail('
|
|
761
|
+
perform @SCHEMA@.lt_fail('sql-write.link_name_reserved', jsonb_build_object('name', data->>'name'));
|
|
762
|
+
end if;
|
|
763
|
+
-- и хаб (план 2.0, этап 4, п. 2): шаг с именем метода цепочки или корня db до класса не дойдёт. Прежние такие хабы
|
|
764
|
+
-- остаются (up() называет их в предупреждении), но их строку Class база больше не пишет — ни создание, ни правку
|
|
765
|
+
if data->>'kind' = 'hub' and (data->>'name') = any (@SCHEMA@.reserved_names()) then
|
|
766
|
+
perform @SCHEMA@.lt_fail('sql-write.hub_name_reserved', jsonb_build_object('name', data->>'name'));
|
|
783
767
|
end if;
|
|
784
768
|
-- алиас — второе имя класса и для правил прав (ревью безопасности №2): совпади он с именем другого класса (в том
|
|
785
769
|
-- числе Class и системных) или с чужим алиасом арендатора и System, правила одного класса действовали бы на строки
|
|
@@ -789,107 +773,36 @@ begin
|
|
|
789
773
|
or exists (select 1 from @SCHEMA@.entity x
|
|
790
774
|
where x.class = 'Class' and x.tenant in (p_tenant, '@SYSTEM@'::uuid) and x.data->>'name' <> data->>'name'
|
|
791
775
|
and (x.data->>'name' = data->>'alias' or x.data->>'alias' = data->>'alias'))) then
|
|
792
|
-
perform @SCHEMA@.lt_fail('
|
|
793
|
-
data->>'alias', data->>'name'));
|
|
776
|
+
perform @SCHEMA@.lt_fail('sql-write.alias_taken', jsonb_build_object('alias', data->>'alias', 'cls', data->>'name'));
|
|
794
777
|
end if;
|
|
795
778
|
-- и наоборот: имя класса — не чужой алиас арендатора и System, иначе правила, написанные для нового класса, действовали
|
|
796
779
|
-- бы и на строки класса с этим алиасом
|
|
797
780
|
if exists (select 1 from @SCHEMA@.entity x
|
|
798
781
|
where x.class = 'Class' and x.tenant in (p_tenant, '@SYSTEM@'::uuid) and x.data->>'name' <> data->>'name'
|
|
799
782
|
and x.data->>'alias' = data->>'name') then
|
|
800
|
-
perform @SCHEMA@.lt_fail('
|
|
783
|
+
perform @SCHEMA@.lt_fail('sql-write.name_is_alias', jsonb_build_object('cls', data->>'name'));
|
|
801
784
|
end if;
|
|
802
|
-
--
|
|
803
|
-
-- арендатора, без прав чтения, поэтому строки попадают в агрегат только с ведома того, кто читает их все. Пути
|
|
804
|
-
-- владельца (сид, class_evolve, rekey_class) сюда не заходят. Описание до правки — для сравнения (нет — новый класс)
|
|
785
|
+
-- описание до правки — для сравнения (нет — новый класс). Пути владельца (сид, class_evolve) сюда не заходят
|
|
805
786
|
select x.data into prevd from @SCHEMA@.entity x
|
|
806
787
|
where x.class = 'Class' and x.tenant = p_tenant and x.data->>'name' = data->>'name';
|
|
807
|
-
--
|
|
808
|
-
-- проектор берёт арендатора в участники по его классам с пометкой и пишет туда итоги по всем строкам источников,
|
|
809
|
-
-- поэтому пользователь с правом WRITE на Class, заведя такой класс сам, читал бы итоги по строкам, которые ему читать
|
|
810
|
-
-- запрещено. Пометка своя или унаследованная, до правки или после; право letopis.projector — у администраторской роли
|
|
811
|
-
-- подключения и по правилам System (системный администратор — маска letopis.*)
|
|
812
|
-
if (data->'resolved' ? 'derivedBy' or coalesce(prevd->'resolved' ? 'derivedBy', false))
|
|
813
|
-
and not @SCHEMA@.letopis_allowed('letopis.projector') then
|
|
814
|
-
perform @SCHEMA@.lt_fail('acl_denied', format('класс %s производный: его описание задаёт только администратор (право letopis.projector)',
|
|
815
|
-
data->>'name'));
|
|
816
|
-
end if;
|
|
817
|
-
-- предок производного класса (ревью безопасности №3): правка предка пересчитывает описание потомка (class_evolve) —
|
|
818
|
-
-- поля, ключ, history, — поэтому класс с производными потомками (своего арендатора, у класса System — всех, кроме
|
|
819
|
-
-- арендаторов со своим одноимённым классом: их потомки наследуют свой класс) меняет тоже только администратор
|
|
820
|
-
if prevd is not null and not @SCHEMA@.letopis_allowed('letopis.projector') then
|
|
821
|
-
select x.data->>'name' into dkid from @SCHEMA@.entity x
|
|
822
|
-
where x.class = 'Class' and x.data->>'name' <> data->>'name'
|
|
823
|
-
and (x.tenant = p_tenant
|
|
824
|
-
or p_tenant = '@SYSTEM@'::uuid
|
|
825
|
-
and x.tenant <> all (array(select y.tenant from @SCHEMA@.entity y
|
|
826
|
-
where y.class = 'Class' and y.data->>'name' = data->>'name' and y.tenant <> '@SYSTEM@'::uuid)))
|
|
827
|
-
and x.data->'resolved'->'ancestors' ? (data->>'name') and x.data->'resolved' ? 'derivedBy'
|
|
828
|
-
order by x.data->>'name' limit 1;
|
|
829
|
-
if dkid is not null then
|
|
830
|
-
perform @SCHEMA@.lt_fail('acl_denied', format('класс %s — предок производного класса %s: его описание задаёт только администратор (право letopis.projector)',
|
|
831
|
-
data->>'name', dkid));
|
|
832
|
-
end if;
|
|
833
|
-
end if;
|
|
834
|
-
-- агрегат развёрнутого описания (свой или унаследованный), новый или изменённый против прежнего — описание
|
|
835
|
-
-- агрегата, новый потомок цели, смена parent, — задаёт только тот, кто без условий читает все строки каждого
|
|
836
|
-
-- класса-источника: класса of и его потомков (agg_classes: классы арендатора, у класса System — классы System).
|
|
837
|
-
-- Неизменённый агрегат не проверяется
|
|
838
|
-
if data->'resolved' ? 'aggregate' then
|
|
839
|
-
for e in select key, value from jsonb_each(data->'resolved'->'aggregate') order by key loop
|
|
840
|
-
continue when prevd->'resolved'->'aggregate'->e.key is not distinct from e.value;
|
|
841
|
-
-- класс-источник должен уже быть (ревью безопасности №3, /security-review): пока его нет, проверять право READ не
|
|
842
|
-
-- на чем, а когда класс заведёт другой пользователь, проверялись бы только его права — и описавший агрегат читал бы
|
|
843
|
-
-- в истории цели вклад каждой строки источника. Поэтому источник описывают раньше агрегата
|
|
844
|
-
if cardinality(@SCHEMA@.agg_classes(e.value->>'of', p_tenant, p_tenant = '@SYSTEM@'::uuid)) = 0 then
|
|
845
|
-
perform @SCHEMA@.lt_fail('invalid_class', format('агрегат %s.%s: класса-источника %s и его потомков %s нет — источник описывают раньше агрегата',
|
|
846
|
-
data->>'name', e.key, e.value->>'of', case when p_tenant = '@SYSTEM@'::uuid then 'в System' else 'у арендатора' end));
|
|
847
|
-
end if;
|
|
848
|
-
-- сам класс of первым, потомки — по имени
|
|
849
|
-
foreach src in array array(select n from unnest(@SCHEMA@.agg_classes(e.value->>'of', p_tenant, p_tenant = '@SYSTEM@'::uuid)) n
|
|
850
|
-
order by n = (e.value->>'of') desc, n) loop
|
|
851
|
-
chk := @SCHEMA@.acl_check_data(src, 'READ');
|
|
852
|
-
if not coalesce((chk->>'allow')::boolean, false) or chk ? 'filter' then
|
|
853
|
-
perform @SCHEMA@.lt_fail('acl_denied', format('агрегат %s.%s: нужно право READ на все строки класса %s', data->>'name', e.key, src));
|
|
854
|
-
end if;
|
|
855
|
-
end loop;
|
|
856
|
-
end loop;
|
|
857
|
-
end if;
|
|
858
|
-
-- класс становится источником агрегата (новая метка aggregatedBy — смена parent): в агрегат войдут его строки и строки
|
|
859
|
-
-- потомков — нужно право READ без условий на них до правки, по прежним предкам (здесь), и после (agg_source_admit в
|
|
860
|
-
-- триггере после оператора; новый класс проверяется только там: до вставки план прав его не видит)
|
|
861
|
-
m := @SCHEMA@.agg_new_mark(prevd->'resolved', data->'resolved');
|
|
862
|
-
if m is not null and prevd is not null then
|
|
863
|
-
perform @SCHEMA@.agg_source_admit(data->>'name', m);
|
|
864
|
-
for src in select x.data->>'name' from @SCHEMA@.entity x
|
|
865
|
-
where x.class = 'Class' and x.tenant = p_tenant and x.data->>'name' <> data->>'name'
|
|
866
|
-
and x.data->'resolved'->'ancestors' ? (data->>'name')
|
|
867
|
-
order by jsonb_array_length(x.data->'resolved'->'ancestors'), x.data->>'name' loop
|
|
868
|
-
perform @SCHEMA@.agg_source_admit(src, m);
|
|
869
|
-
end loop;
|
|
870
|
-
end if;
|
|
871
|
-
-- правка меняет предков, алиас или derivedBy (ревью безопасности №2): строки класса и потомков попали бы под правила
|
|
788
|
+
-- правка меняет предков или алиас (ревью безопасности №2): строки класса и потомков попали бы под правила
|
|
872
789
|
-- других имён — нужно право READ, WRITE и DELETE без условий на все их строки. У нового класса строк ещё нет
|
|
873
790
|
if prevd is not null
|
|
874
791
|
and (prevd->'resolved'->'ancestors' is distinct from data->'resolved'->'ancestors'
|
|
875
|
-
or prevd->>'alias' is distinct from data->>'alias'
|
|
876
|
-
or prevd->'resolved'->'derivedBy' is distinct from data->'resolved'->'derivedBy') then
|
|
792
|
+
or prevd->>'alias' is distinct from data->>'alias') then
|
|
877
793
|
perform @SCHEMA@.class_acl_admit(data->>'name', p_tenant);
|
|
878
794
|
end if;
|
|
879
795
|
-- алиас класса, у которого удалённые потомки хранят историю строк (ревью безопасности №3): правила находят их старые
|
|
880
|
-
-- версии и по алиасам предков, а надгробие потомка их не записывает — алиас меняется
|
|
796
|
+
-- версии и по алиасам предков, а надгробие потомка их не записывает — алиас не меняется
|
|
881
797
|
if prevd is not null and prevd->>'alias' is distinct from data->>'alias'
|
|
882
798
|
and @SCHEMA@.class_orphan_history(data->>'name', p_tenant) then
|
|
883
|
-
perform @SCHEMA@.lt_fail('
|
|
884
|
-
data->>'name'));
|
|
799
|
+
perform @SCHEMA@.lt_fail('sql-write.alias_orphan_history', jsonb_build_object('cls', data->>'name'));
|
|
885
800
|
end if;
|
|
886
|
-
--
|
|
887
|
-
--
|
|
888
|
-
--
|
|
889
|
-
-- строки класса и его потомков своего арендатора — как trimHistory требует их на каждую цель
|
|
801
|
+
-- history (доработки после точки Б): history: false выключает журнал. Одним правом WRITE на Class автор правки
|
|
802
|
+
-- останавливал бы аудит строк, на которые у него нет прав, поэтому смена развёрнутой history существующего класса
|
|
803
|
+
-- требует права READ и DELETE без условий на все строки класса и его потомков своего арендатора
|
|
890
804
|
if prevd is not null and prevd->'resolved'->'history' is distinct from data->'resolved'->'history' then
|
|
891
|
-
perform @SCHEMA@.class_rows_admit(data->>'name', p_tenant, array['READ', 'DELETE'],
|
|
892
|
-
format('класс %s меняет политику хранения истории', data->>'name'));
|
|
805
|
+
perform @SCHEMA@.class_rows_admit(data->>'name', p_tenant, array['READ', 'DELETE'], 'history');
|
|
893
806
|
end if;
|
|
894
807
|
for e in select key, value from jsonb_each(coalesce(data->'resolved'->'ends', '{}'::jsonb)) loop
|
|
895
808
|
select x.data->>'name' as name into o
|
|
@@ -900,52 +813,9 @@ begin
|
|
|
900
813
|
and @SCHEMA@.class_set(x.data->'resolved'->'ends'->e.key->'class') <> @SCHEMA@.class_set(e.value->'class')
|
|
901
814
|
limit 1;
|
|
902
815
|
if o.name is not null then
|
|
903
|
-
perform @SCHEMA@.lt_fail('
|
|
904
|
-
end if;
|
|
905
|
-
end loop;
|
|
906
|
-
-- на строки производного класса (план 1.1, §2.4) не ссылаются: проектор удаляет и строит их заново.
|
|
907
|
-
-- Классы целей — под разделяемой блокировкой семейства, а класс, который становится производным, — под
|
|
908
|
-
-- исключительной: описание со ссылкой и пометка derivedBy не проходят параллельно мимо друг друга
|
|
909
|
-
for e in select key, value from jsonb_each(coalesce(data->'resolved'->'ends', '{}'::jsonb)) loop
|
|
910
|
-
for tc in select c.* from jsonb_array_elements_text(@SCHEMA@.class_set(e.value->'class')) n(name)
|
|
911
|
-
cross join lateral @SCHEMA@.class_row(n.name, p_tenant) c where c.id is not null order by c.id loop
|
|
912
|
-
perform @SCHEMA@.family_lock(tc, false);
|
|
913
|
-
end loop;
|
|
914
|
-
dname := null;
|
|
915
|
-
select x.data->>'name' into dname
|
|
916
|
-
from @SCHEMA@.entity x
|
|
917
|
-
where x.class = 'Class' and x.tenant in (p_tenant, '@SYSTEM@'::uuid) and x.data->>'name' <> data->>'name'
|
|
918
|
-
and @SCHEMA@.class_set(e.value->'class') ? (x.data->>'name') and x.data->'resolved' ? 'derivedBy'
|
|
919
|
-
limit 1;
|
|
920
|
-
if dname is null and data->'resolved' ? 'derivedBy' and @SCHEMA@.class_set(e.value->'class') ? (data->>'name') then
|
|
921
|
-
dname := data->>'name';
|
|
922
|
-
end if;
|
|
923
|
-
if dname is not null then
|
|
924
|
-
perform @SCHEMA@.lt_fail('derived_class', format('конец %s ведёт на производный класс %s', e.key, dname));
|
|
816
|
+
perform @SCHEMA@.lt_fail('sql-write.end_other_class', jsonb_build_object('end', e.key, 'cls', o.name));
|
|
925
817
|
end if;
|
|
926
818
|
end loop;
|
|
927
|
-
if data->'resolved' ? 'derivedBy' then
|
|
928
|
-
tc := @SCHEMA@.class_row(data->>'name', p_tenant);
|
|
929
|
-
if tc.id is not null then
|
|
930
|
-
perform @SCHEMA@.family_lock(tc, true);
|
|
931
|
-
end if;
|
|
932
|
-
-- класс становится производным, а на него или его потомков уже ссылаются концы других классов (класс
|
|
933
|
-
-- System видят все арендаторы — его ссылки ищутся во всех)
|
|
934
|
-
select x.data->>'name', k.key into dname, drole
|
|
935
|
-
from @SCHEMA@.entity x
|
|
936
|
-
cross join lateral jsonb_each(coalesce(x.data->'resolved'->'ends', '{}'::jsonb)) k
|
|
937
|
-
where x.class = 'Class' and (x.tenant in (p_tenant, '@SYSTEM@'::uuid) or p_tenant = '@SYSTEM@'::uuid)
|
|
938
|
-
and x.data->>'name' <> data->>'name'
|
|
939
|
-
and @SCHEMA@.class_set(k.value->'class') ?| (array[data->>'name'] || array(
|
|
940
|
-
select y.data->>'name' from @SCHEMA@.entity y
|
|
941
|
-
where y.class = 'Class' and (y.tenant in (p_tenant, '@SYSTEM@'::uuid) or p_tenant = '@SYSTEM@'::uuid)
|
|
942
|
-
and y.data->'resolved'->'ancestors' ? (data->>'name')))
|
|
943
|
-
limit 1;
|
|
944
|
-
if dname is not null then
|
|
945
|
-
perform @SCHEMA@.lt_fail('derived_class', format('класс %s не может стать производным: на него ссылается конец %s класса %s',
|
|
946
|
-
data->>'name', drole, dname));
|
|
947
|
-
end if;
|
|
948
|
-
end if;
|
|
949
819
|
-- строки-замки семейства (family_lock) — заранее: первая запись в новое семейство их не вставляет
|
|
950
820
|
perform @SCHEMA@.family_rows(p_tenant, data);
|
|
951
821
|
end $$;
|
|
@@ -968,98 +838,81 @@ declare
|
|
|
968
838
|
begin
|
|
969
839
|
select * into c from @SCHEMA@.ctx();
|
|
970
840
|
if c.account is null then
|
|
971
|
-
perform @SCHEMA@.lt_fail('no_session'
|
|
841
|
+
perform @SCHEMA@.lt_fail('sql-write.no_session');
|
|
972
842
|
end if;
|
|
973
843
|
NEW.tenant := coalesce(NEW.tenant, c.tenant);
|
|
974
844
|
if NEW.tenant <> c.tenant then
|
|
975
|
-
perform @SCHEMA@.lt_fail('
|
|
845
|
+
perform @SCHEMA@.lt_fail('sql-write.foreign_tenant');
|
|
976
846
|
end if;
|
|
977
847
|
-- аккаунты (с их категориями) — только в System: их пишет системный администратор (этап 5)
|
|
978
848
|
if NEW.class = 'Account' and NEW.tenant <> '@SYSTEM@'::uuid then
|
|
979
|
-
perform @SCHEMA@.lt_fail('
|
|
849
|
+
perform @SCHEMA@.lt_fail('sql-write.account_tenant');
|
|
980
850
|
end if;
|
|
981
851
|
cls := @SCHEMA@.class_row(NEW.class, NEW.tenant);
|
|
982
852
|
if cls.id is null then
|
|
983
|
-
perform @SCHEMA@.lt_fail('
|
|
853
|
+
perform @SCHEMA@.lt_fail('sql-write.class_not_found', jsonb_build_object('cls', NEW.class));
|
|
984
854
|
end if;
|
|
985
855
|
-- разделяемая блокировка корня семейства, затем описание перечитывается под ней (класс могли удалить, пока ждали)
|
|
986
856
|
perform @SCHEMA@.family_lock(cls, false);
|
|
987
857
|
cls := @SCHEMA@.class_row(NEW.class, NEW.tenant);
|
|
988
858
|
if cls.id is null then
|
|
989
|
-
perform @SCHEMA@.lt_fail('
|
|
859
|
+
perform @SCHEMA@.lt_fail('sql-write.class_deleted', jsonb_build_object('cls', NEW.class));
|
|
990
860
|
end if;
|
|
991
861
|
res := cls.data->'resolved';
|
|
992
862
|
if coalesce((res->>'abstract')::boolean, false) then
|
|
993
|
-
perform @SCHEMA@.lt_fail('
|
|
994
|
-
end if;
|
|
995
|
-
if res ? 'derivedBy' then
|
|
996
|
-
perform @SCHEMA@.lt_fail('derived_class', format('класс %s производный: его строки пишет проектор %s', NEW.class, res->>'derivedBy'));
|
|
863
|
+
perform @SCHEMA@.lt_fail('sql-write.class_abstract', jsonb_build_object('cls', NEW.class));
|
|
997
864
|
end if;
|
|
998
865
|
if NEW.class = 'Class' then
|
|
999
866
|
select a.data, a.validator into NEW.data, NEW.validator from @SCHEMA@.class_admit(coalesce(NEW.data, '{}'::jsonb), NEW.tenant) a;
|
|
1000
867
|
else
|
|
1001
|
-
-- агрегатное поле считает база (план 1.1, §2.4): переданное значение — отказ, новое — 0 до умолчаний и
|
|
1002
|
-
-- проверки; итог из живых источников ставит триггер после оператора
|
|
1003
|
-
if res ? 'aggregate' then
|
|
1004
|
-
if coalesce(NEW.data, '{}'::jsonb) ?| @SCHEMA@.agg_fields(res) then
|
|
1005
|
-
perform @SCHEMA@.lt_fail('aggregate_field', format('поля %s класса %s считает база (aggregate) — их не передают',
|
|
1006
|
-
array_to_string(array(select x from unnest(@SCHEMA@.agg_fields(res)) x where NEW.data ? x), ', '), NEW.class));
|
|
1007
|
-
end if;
|
|
1008
|
-
NEW.data := @SCHEMA@.agg_zero(res, coalesce(NEW.data, '{}'::jsonb));
|
|
1009
|
-
end if;
|
|
1010
868
|
NEW.data := @SCHEMA@.apply_defaults(res->'schema', coalesce(NEW.data, '{}'::jsonb));
|
|
1011
869
|
bad := @SCHEMA@.validate_data(res->'rules', cls.validator, res->'vars', NEW.data);
|
|
1012
870
|
if bad <> '[]'::jsonb then
|
|
1013
|
-
perform @SCHEMA@.lt_fail('invalid_data',
|
|
871
|
+
perform @SCHEMA@.lt_fail('sql-write.invalid_data', jsonb_build_object('cls', NEW.class), jsonb_build_object('issues', bad));
|
|
1014
872
|
end if;
|
|
1015
873
|
NEW.validator := null;
|
|
1016
874
|
end if;
|
|
1017
875
|
-- цели проверяет и блокирует триггер после оператора (targets_bad) — один раз на оператор
|
|
1018
876
|
select * into lk from @SCHEMA@.links_shape(res, NEW.links);
|
|
1019
|
-
if lk.
|
|
1020
|
-
perform @SCHEMA@.lt_fail(lk.
|
|
877
|
+
if lk.key is not null then
|
|
878
|
+
perform @SCHEMA@.lt_fail(lk.key, lk.params);
|
|
1021
879
|
end if;
|
|
1022
880
|
NEW.links := lk.links;
|
|
1023
881
|
if NEW.class = 'member' and NEW.links->>'Tenant' is distinct from NEW.tenant::text then
|
|
1024
|
-
perform @SCHEMA@.lt_fail('
|
|
882
|
+
perform @SCHEMA@.lt_fail('sql-write.member_tenant');
|
|
1025
883
|
end if;
|
|
1026
884
|
kid := @SCHEMA@.key_id(res, NEW.class, NEW.tenant, NEW.data, NEW.links);
|
|
1027
885
|
if kid is null then
|
|
1028
886
|
if NEW.id is not null then
|
|
1029
|
-
perform @SCHEMA@.lt_fail('id_from_db',
|
|
887
|
+
perform @SCHEMA@.lt_fail('sql-write.id_from_db', jsonb_build_object('cls', NEW.class));
|
|
1030
888
|
end if;
|
|
1031
889
|
NEW.id := @SCHEMA@.v5_id(NEW.tenant, NEW.class, array[gen_random_uuid()::text]);
|
|
1032
890
|
else
|
|
1033
891
|
if NEW.id is not null and NEW.id <> kid then
|
|
1034
|
-
perform @SCHEMA@.lt_fail('id_mismatch',
|
|
892
|
+
perform @SCHEMA@.lt_fail('sql-write.id_mismatch', jsonb_build_object('cls', NEW.class), jsonb_build_object('expected', kid));
|
|
1035
893
|
end if;
|
|
1036
894
|
NEW.id := kid;
|
|
1037
895
|
end if;
|
|
1038
|
-
-- воскрешение: номер версии
|
|
1039
|
-
--
|
|
1040
|
-
|
|
1041
|
-
select l.rev, l.hash, l.at, l.op, l.data into last from @SCHEMA@.log l where l.id = NEW.id order by l.rev desc limit 1 for update;
|
|
896
|
+
-- воскрешение: номер версии продолжается от последней записи журнала; запись блокируется — параллельный restore того
|
|
897
|
+
-- же id ждёт (этап 6)
|
|
898
|
+
select l.rev, l.at, l.op, l.data into last from @SCHEMA@.log l where l.id = NEW.id order by l.rev desc limit 1 for update;
|
|
1042
899
|
-- класс с историей строк в журнале (остаток Н2 ревью №2; доработки после точки Б): правила прав находят строки, и
|
|
1043
|
-
-- старые версии тоже, по именам класса, предков и алиасов — класс с другим parent
|
|
900
|
+
-- старые версии тоже, по именам класса, предков и алиасов — класс с другим parent или alias подставил бы
|
|
1044
901
|
-- старую историю под чужие правила. Поэтому, если журнал арендатора хранит версии строк класса с этим именем, класс
|
|
1045
902
|
-- создаётся только заново и с тем же описанием, что в надгробии его строки Class. Надгробия нет (история осталась от
|
|
1046
|
-
-- класса System, удалённого раньше, или строку Class стёрли мимо триггеров) —
|
|
903
|
+
-- класса System, удалённого раньше, или строку Class стёрли мимо триггеров) — отказ
|
|
1047
904
|
-- Так же — если историю хранят удалённые потомки класса (их правила находят старые версии и по алиасу предка), и
|
|
1048
|
-
--
|
|
1049
|
-
-- history в resolved — значение по умолчанию true)
|
|
905
|
+
-- history прежняя (ревью безопасности №3; у описаний без ключа history в resolved — значение по умолчанию true)
|
|
1050
906
|
if NEW.class = 'Class'
|
|
1051
907
|
and (@SCHEMA@.class_has_history(NEW.data->>'name', NEW.tenant) or @SCHEMA@.class_orphan_history(NEW.data->>'name', NEW.tenant))
|
|
1052
908
|
and not (coalesce(last.op = 'delete', false)
|
|
1053
909
|
and last.data->'resolved'->'ancestors' is not distinct from NEW.data->'resolved'->'ancestors'
|
|
1054
910
|
and last.data->>'alias' is not distinct from NEW.data->>'alias'
|
|
1055
|
-
and last.data->'resolved'->'derivedBy' is not distinct from NEW.data->'resolved'->'derivedBy'
|
|
1056
911
|
and coalesce(last.data->'resolved'->'history', 'true'::jsonb) is not distinct from coalesce(NEW.data->'resolved'->'history', 'true'::jsonb)) then
|
|
1057
|
-
perform @SCHEMA@.lt_fail('
|
|
1058
|
-
NEW.data->>'name'));
|
|
912
|
+
perform @SCHEMA@.lt_fail('sql-write.history_recreate', jsonb_build_object('cls', NEW.data->>'name'));
|
|
1059
913
|
end if;
|
|
1060
914
|
ts := @SCHEMA@.now_ts();
|
|
1061
915
|
NEW.rev := coalesce(last.rev, 0) + 1;
|
|
1062
|
-
NEW.prev_hash := last.hash;
|
|
1063
916
|
NEW.at := greatest(ts, last.at + interval '1 microsecond');
|
|
1064
917
|
NEW.created_at := NEW.at;
|
|
1065
918
|
NEW.cv := cls.rev;
|
|
@@ -1068,13 +921,12 @@ begin
|
|
|
1068
921
|
elsif NEW.owner not in (NEW.tenant, c.account)
|
|
1069
922
|
and not exists (select 1 from @SCHEMA@.entity a where a.id = NEW.owner and a.class = 'Account' and a.tenant = '@SYSTEM@'::uuid) then
|
|
1070
923
|
-- явный владелец — существующий аккаунт; разрешает ли его условие ACL, решает политика вставки
|
|
1071
|
-
perform @SCHEMA@.lt_fail('
|
|
924
|
+
perform @SCHEMA@.lt_fail('sql-write.owner_not_found');
|
|
1072
925
|
end if;
|
|
1073
926
|
NEW.author := c.author;
|
|
1074
927
|
NEW.agent := c.agent;
|
|
1075
928
|
NEW.op := 'create';
|
|
1076
929
|
NEW.reason := nullif(current_setting('letopis.reason', true), '');
|
|
1077
|
-
NEW.moved := null;
|
|
1078
930
|
NEW.tags := coalesce(NEW.tags, '{}');
|
|
1079
931
|
return NEW;
|
|
1080
932
|
end $$;
|
|
@@ -1087,118 +939,72 @@ declare
|
|
|
1087
939
|
c record;
|
|
1088
940
|
cls @SCHEMA@.entity;
|
|
1089
941
|
res jsonb;
|
|
1090
|
-
old_res jsonb;
|
|
1091
942
|
bad jsonb;
|
|
1092
943
|
lk record;
|
|
1093
944
|
kid uuid;
|
|
1094
|
-
op text := 'update';
|
|
1095
|
-
inc jsonb;
|
|
1096
945
|
begin
|
|
1097
946
|
select * into c from @SCHEMA@.ctx();
|
|
1098
947
|
if c.account is null then
|
|
1099
|
-
perform @SCHEMA@.lt_fail('no_session'
|
|
948
|
+
perform @SCHEMA@.lt_fail('sql-write.no_session');
|
|
1100
949
|
end if;
|
|
1101
950
|
if NEW.id <> OLD.id or NEW.tenant <> OLD.tenant or NEW.owner <> OLD.owner or NEW.created_at <> OLD.created_at then
|
|
1102
|
-
perform @SCHEMA@.lt_fail('
|
|
951
|
+
perform @SCHEMA@.lt_fail('sql-write.immutable_columns');
|
|
1103
952
|
end if;
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
-- строки прав не получаются сменой класса: аккаунт с категориями — только в System (ревью этапа 5)
|
|
1108
|
-
if NEW.class <> OLD.class and (NEW.class in ('Account', 'member', 'Resource', 'rule') or OLD.class in ('Account', 'member', 'Resource', 'rule')) then
|
|
1109
|
-
perform @SCHEMA@.lt_fail('reclass_denied', 'класс системной строки не меняется');
|
|
953
|
+
-- класс строки неизменен (план 2.0, редакция 2): смены класса нет ни правкой, ни функцией
|
|
954
|
+
if NEW.class <> OLD.class then
|
|
955
|
+
perform @SCHEMA@.lt_fail('sql-write.class_immutable', jsonb_build_object('cls', OLD.class));
|
|
1110
956
|
end if;
|
|
1111
957
|
cls := @SCHEMA@.class_row(NEW.class, OLD.tenant);
|
|
1112
958
|
if cls.id is null then
|
|
1113
|
-
perform @SCHEMA@.lt_fail('
|
|
959
|
+
perform @SCHEMA@.lt_fail('sql-write.class_not_found', jsonb_build_object('cls', NEW.class));
|
|
1114
960
|
end if;
|
|
1115
961
|
perform @SCHEMA@.family_lock(cls, false);
|
|
1116
962
|
cls := @SCHEMA@.class_row(NEW.class, OLD.tenant);
|
|
1117
963
|
if cls.id is null then
|
|
1118
|
-
perform @SCHEMA@.lt_fail('
|
|
964
|
+
perform @SCHEMA@.lt_fail('sql-write.class_deleted', jsonb_build_object('cls', NEW.class));
|
|
1119
965
|
end if;
|
|
1120
966
|
res := cls.data->'resolved';
|
|
1121
967
|
if coalesce((res->>'abstract')::boolean, false) then
|
|
1122
|
-
perform @SCHEMA@.lt_fail('
|
|
1123
|
-
end if;
|
|
1124
|
-
if res ? 'derivedBy' or (NEW.class <> OLD.class and (@SCHEMA@.class_row(OLD.class, OLD.tenant)).data->'resolved' ? 'derivedBy') then
|
|
1125
|
-
perform @SCHEMA@.lt_fail('derived_class', format('класс %s производный: его строки пишет проектор',
|
|
1126
|
-
case when res ? 'derivedBy' then NEW.class else OLD.class end));
|
|
1127
|
-
end if;
|
|
1128
|
-
if NEW.class <> OLD.class then
|
|
1129
|
-
-- смена класса (этап 1, п. 9): только read committed, только для классов без ключа
|
|
1130
|
-
if current_setting('transaction_isolation') <> 'read committed' then
|
|
1131
|
-
perform @SCHEMA@.lt_fail('isolation_level', 'смена класса разрешена только в read committed');
|
|
1132
|
-
end if;
|
|
1133
|
-
old_res := (@SCHEMA@.class_row(OLD.class, OLD.tenant)).data->'resolved';
|
|
1134
|
-
if jsonb_array_length(coalesce(old_res->'key', '[]'::jsonb)) > 0 or jsonb_array_length(coalesce(res->'key', '[]'::jsonb)) > 0 then
|
|
1135
|
-
perform @SCHEMA@.lt_fail('use_reclass', format('у класса %s или %s есть ключ: id меняется, используйте reclass()', OLD.class, NEW.class));
|
|
1136
|
-
end if;
|
|
1137
|
-
-- обычный update берёт слабую блокировку; смене класса нужна for update, чтобы дождаться
|
|
1138
|
-
-- транзакций, которые как раз пишут ссылку на эту строку
|
|
1139
|
-
perform 1 from @SCHEMA@.entity x where x.id = OLD.id for update;
|
|
1140
|
-
-- строка уходит из своего класса — как удаление из него: нужно право DELETE на прежнюю строку (доработки после
|
|
1141
|
-
-- точки Б; иначе с одним WRITE строку переносили в класс, где есть DELETE, и удаляли там)
|
|
1142
|
-
if not @SCHEMA@.acl_row_ok(@SCHEMA@.acl_plan('DELETE'), OLD.class, OLD.id, OLD.owner, OLD.tenant, OLD.tags, OLD.data, OLD.links) then
|
|
1143
|
-
perform @SCHEMA@.lt_fail('acl_denied', format('нет права DELETE на строку класса %s: смена класса убирает её из класса', OLD.class));
|
|
1144
|
-
end if;
|
|
1145
|
-
inc := @SCHEMA@.incoming_conflicts(OLD.id, NEW.class, OLD.tenant);
|
|
1146
|
-
if inc <> '[]'::jsonb then
|
|
1147
|
-
perform @SCHEMA@.lt_fail('reclass_denied', format('роли ссылающихся строк не принимают класс %s', NEW.class),
|
|
1148
|
-
@SCHEMA@.visible_detail(inc, c.tenant));
|
|
1149
|
-
end if;
|
|
1150
|
-
op := 'reclass';
|
|
968
|
+
perform @SCHEMA@.lt_fail('sql-write.class_abstract', jsonb_build_object('cls', NEW.class));
|
|
1151
969
|
end if;
|
|
1152
970
|
if NEW.class = 'Class' then
|
|
1153
971
|
select a.data, a.validator into NEW.data, NEW.validator from @SCHEMA@.class_admit(NEW.data, OLD.tenant) a;
|
|
1154
972
|
else
|
|
1155
|
-
-- агрегатное поле (план 1.1, §2.4): изменить его нельзя, без поля (замена данных целиком) — прежнее значение;
|
|
1156
|
-
-- при смене класса — прежнее число или 0, итог посчитает триггер после оператора
|
|
1157
|
-
if res ? 'aggregate' then
|
|
1158
|
-
NEW.data := @SCHEMA@.agg_guard(res, OLD.data, NEW.data, NEW.class <> OLD.class);
|
|
1159
|
-
end if;
|
|
1160
973
|
NEW.data := @SCHEMA@.apply_defaults(res->'schema', NEW.data);
|
|
1161
974
|
bad := @SCHEMA@.validate_data(res->'rules', cls.validator, res->'vars', NEW.data);
|
|
1162
975
|
if bad <> '[]'::jsonb then
|
|
1163
|
-
perform @SCHEMA@.lt_fail('invalid_data',
|
|
976
|
+
perform @SCHEMA@.lt_fail('sql-write.invalid_data', jsonb_build_object('cls', NEW.class), jsonb_build_object('issues', bad));
|
|
1164
977
|
end if;
|
|
1165
978
|
NEW.validator := null;
|
|
1166
979
|
end if;
|
|
1167
980
|
select * into lk from @SCHEMA@.links_shape(res, NEW.links);
|
|
1168
|
-
if lk.
|
|
1169
|
-
perform @SCHEMA@.lt_fail(lk.
|
|
981
|
+
if lk.key is not null then
|
|
982
|
+
perform @SCHEMA@.lt_fail(lk.key, lk.params);
|
|
1170
983
|
end if;
|
|
1171
984
|
NEW.links := lk.links;
|
|
1172
985
|
if NEW.class = 'member' and NEW.links->>'Tenant' is distinct from OLD.tenant::text then
|
|
1173
|
-
perform @SCHEMA@.lt_fail('
|
|
986
|
+
perform @SCHEMA@.lt_fail('sql-write.member_tenant');
|
|
1174
987
|
end if;
|
|
1175
988
|
kid := @SCHEMA@.key_id(res, NEW.class, OLD.tenant, NEW.data, NEW.links);
|
|
1176
989
|
if kid is not null and kid <> OLD.id then
|
|
1177
|
-
perform @SCHEMA@.lt_fail('
|
|
990
|
+
perform @SCHEMA@.lt_fail('sql-write.key_changed', jsonb_build_object('cls', NEW.class));
|
|
1178
991
|
end if;
|
|
1179
992
|
-- ничего не изменилось: строка возвращается как есть, версии нет, оператор считает её затронутой
|
|
1180
|
-
if NEW.
|
|
993
|
+
if NEW.data = OLD.data and NEW.links = OLD.links and NEW.tags = OLD.tags then
|
|
1181
994
|
return OLD;
|
|
1182
995
|
end if;
|
|
1183
996
|
if NEW.class = 'Class' and not @SCHEMA@.class_change_safe(OLD.data->'resolved', NEW.data->'resolved') then
|
|
1184
997
|
-- ужесточение: исключительная блокировка корня семейства (и нового корня при смене родителя)
|
|
1185
998
|
perform @SCHEMA@.family_lock(OLD, true);
|
|
1186
999
|
perform @SCHEMA@.family_lock(NEW, true);
|
|
1187
|
-
-- смена агрегатов — ещё семейства источников (план 1.1, §2.4): операция, зафиксированная до пересчёта, в него
|
|
1188
|
-
-- войдёт, а ожидающая допишет вклад уже по новому описанию
|
|
1189
|
-
if OLD.data->'resolved'->'aggregate' is distinct from NEW.data->'resolved'->'aggregate' then
|
|
1190
|
-
perform @SCHEMA@.agg_lock_sources(OLD.data->'resolved', NEW.data->'resolved', OLD.tenant);
|
|
1191
|
-
end if;
|
|
1192
1000
|
end if;
|
|
1193
1001
|
NEW.rev := OLD.rev + 1;
|
|
1194
|
-
NEW.prev_hash := OLD.hash;
|
|
1195
1002
|
NEW.at := greatest(@SCHEMA@.now_ts(), OLD.at + interval '1 microsecond');
|
|
1196
1003
|
NEW.cv := cls.rev;
|
|
1197
1004
|
NEW.author := c.author;
|
|
1198
1005
|
NEW.agent := c.agent;
|
|
1199
|
-
NEW.op :=
|
|
1006
|
+
NEW.op := 'update';
|
|
1200
1007
|
NEW.reason := nullif(current_setting('letopis.reason', true), '');
|
|
1201
|
-
NEW.moved := null;
|
|
1202
1008
|
return NEW;
|
|
1203
1009
|
end $$;
|
|
1204
1010
|
|
|
@@ -1217,13 +1023,11 @@ begin
|
|
|
1217
1023
|
bad := @SCHEMA@.targets_bad((select jsonb_agg(jsonb_build_object('class', n.class, 'tenant', n.tenant, 'links', n.links))
|
|
1218
1024
|
from added n where n.links <> '{}'::jsonb), true, @SCHEMA@.acl_actor('READ'));
|
|
1219
1025
|
end if;
|
|
1220
|
-
if bad
|
|
1221
|
-
perform @SCHEMA@.lt_fail('
|
|
1222
|
-
elsif bad is not null then
|
|
1223
|
-
perform @SCHEMA@.lt_fail('target_not_found', format('цель конца %s не найдена', bad));
|
|
1026
|
+
if bad is not null then
|
|
1027
|
+
perform @SCHEMA@.lt_fail('sql-write.end_target_missing', jsonb_build_object('end', bad));
|
|
1224
1028
|
end if;
|
|
1225
|
-
insert into @SCHEMA@.log (id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason
|
|
1226
|
-
select n.id, n.rev, n.class, n.cv, n.tenant, n.owner, n.links, n.data, n.tags, n.at, n.author, n.agent, n.op, n.reason
|
|
1029
|
+
insert into @SCHEMA@.log (id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason)
|
|
1030
|
+
select n.id, n.rev, n.class, n.cv, n.tenant, n.owner, n.links, n.data, n.tags, n.at, n.author, n.agent, n.op, n.reason
|
|
1227
1031
|
from added n
|
|
1228
1032
|
join (select k.class, k.tenant from (select distinct a.class, a.tenant from added a) k
|
|
1229
1033
|
where @SCHEMA@.history_on(k.class, k.tenant)) h on h.class = n.class and h.tenant = n.tenant
|
|
@@ -1234,19 +1038,6 @@ begin
|
|
|
1234
1038
|
if sc && array['rule', 'Resource', 'member', 'Account', 'Class'] then
|
|
1235
1039
|
perform @SCHEMA@.acl_bump();
|
|
1236
1040
|
end if;
|
|
1237
|
-
if 'Class' = any (sc) then
|
|
1238
|
-
-- новый класс — источник агрегата (ревью коммита этапа 4б): нужно право READ без условий на все его строки; план
|
|
1239
|
-
-- прав после acl_bump видит новый класс
|
|
1240
|
-
perform @SCHEMA@.agg_source_admit(n.data->>'name', @SCHEMA@.agg_new_mark(null, n.data->'resolved'))
|
|
1241
|
-
from added n where n.class = 'Class' and n.data->'resolved' ? 'aggregatedBy';
|
|
1242
|
-
-- и описавшему агрегат (хвост ревью доработок после 1.2.0): заведя потомка источника, читающий всё пользователь
|
|
1243
|
-
-- иначе открыл бы его строки тому, кто описал агрегат без права на них
|
|
1244
|
-
perform @SCHEMA@.agg_author_admit(n.data->>'name', n.tenant, null, n.data->'resolved')
|
|
1245
|
-
from added n where n.class = 'Class' and n.data->'resolved' ? 'aggregatedBy';
|
|
1246
|
-
-- новая цель агрегатов (план 1.1, §2.4): метки её источников (aggregatedBy) — заново
|
|
1247
|
-
perform @SCHEMA@.agg_mark(n.tenant, @SCHEMA@.agg_of(n.data->'resolved'))
|
|
1248
|
-
from added n where n.class = 'Class' and n.data->'resolved' ? 'aggregate';
|
|
1249
|
-
end if;
|
|
1250
1041
|
return null;
|
|
1251
1042
|
end $$;
|
|
1252
1043
|
|
|
@@ -1265,31 +1056,26 @@ declare
|
|
|
1265
1056
|
cid uuid;
|
|
1266
1057
|
bad text;
|
|
1267
1058
|
begin
|
|
1268
|
-
-- цели проверяются у строк, у которых изменились концы
|
|
1059
|
+
-- цели проверяются у строк, у которых изменились концы
|
|
1269
1060
|
if exists (select 1 from new_rows n join old_rows o on o.id = n.id and o.rev <> n.rev
|
|
1270
|
-
where n.links <> '{}'::jsonb and
|
|
1061
|
+
where n.links <> '{}'::jsonb and n.links <> o.links) then
|
|
1271
1062
|
bad := @SCHEMA@.targets_bad((select jsonb_agg(jsonb_build_object('class', n.class, 'tenant', n.tenant, 'links', n.links))
|
|
1272
1063
|
from new_rows n join old_rows o on o.id = n.id and o.rev <> n.rev
|
|
1273
|
-
where n.links <> '{}'::jsonb and
|
|
1064
|
+
where n.links <> '{}'::jsonb and n.links <> o.links), true, @SCHEMA@.acl_actor('READ'));
|
|
1274
1065
|
end if;
|
|
1275
|
-
if bad
|
|
1276
|
-
perform @SCHEMA@.lt_fail('
|
|
1277
|
-
elsif bad is not null then
|
|
1278
|
-
perform @SCHEMA@.lt_fail('target_not_found', format('цель конца %s не найдена', bad));
|
|
1066
|
+
if bad is not null then
|
|
1067
|
+
perform @SCHEMA@.lt_fail('sql-write.end_target_missing', jsonb_build_object('end', bad));
|
|
1279
1068
|
end if;
|
|
1280
|
-
insert into @SCHEMA@.log (id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason
|
|
1281
|
-
select n.id, n.rev, n.class, n.cv, n.tenant, n.owner, n.links, n.data, n.tags, n.at, n.author, n.agent, n.op, n.reason
|
|
1282
|
-
@SCHEMA@.change_paths(o.data, o.links, o.tags, o.owner, o.class, n.data, n.links, n.tags, n.owner, n.class)
|
|
1069
|
+
insert into @SCHEMA@.log (id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason)
|
|
1070
|
+
select n.id, n.rev, n.class, n.cv, n.tenant, n.owner, n.links, n.data, n.tags, n.at, n.author, n.agent, n.op, n.reason
|
|
1283
1071
|
from new_rows n
|
|
1284
1072
|
join old_rows o on o.id = n.id and o.rev <> n.rev
|
|
1285
1073
|
join (select k.class, k.tenant from (select distinct a.class, a.tenant from new_rows a) k
|
|
1286
1074
|
where @SCHEMA@.history_on(k.class, k.tenant)) h on h.class = n.class and h.tenant = n.tenant
|
|
1287
1075
|
order by n.id; -- порядок seq не зависит от плана: правка многих строк пишет версии в порядке id
|
|
1288
|
-
-- сигнал — по
|
|
1289
|
-
-- кэше других процессов устарели бы (в 1.0.1 сигнал шёл только по новому)
|
|
1076
|
+
-- сигнал — по паре «класс, арендатор» (класс и арендатор строки неизменны)
|
|
1290
1077
|
select array_agg(k.class), array_agg(k.tenant) into sc, st
|
|
1291
|
-
from (select distinct
|
|
1292
|
-
cross join lateral (values (n.class, n.tenant), (o.class, o.tenant)) v(class, tenant)) k;
|
|
1078
|
+
from (select distinct n.class, n.tenant from new_rows n join old_rows o on o.id = n.id and o.rev <> n.rev) k;
|
|
1293
1079
|
perform @SCHEMA@.signal(sc, st);
|
|
1294
1080
|
if sc && array['rule', 'Resource', 'member', 'Account', 'Class'] then
|
|
1295
1081
|
perform @SCHEMA@.acl_bump();
|
|
@@ -1298,10 +1084,6 @@ begin
|
|
|
1298
1084
|
where n.class = 'Class' order by n.id loop
|
|
1299
1085
|
select * into o_row from old_rows x where x.id = cid;
|
|
1300
1086
|
select * into n_row from new_rows x where x.id = cid;
|
|
1301
|
-
-- класс становится источником агрегата (смена parent; ревью коммита этапа 4б): право READ без условий на его строки
|
|
1302
|
-
-- и после правки — по новым предкам (до правки проверил class_admit)
|
|
1303
|
-
perform @SCHEMA@.agg_source_admit(n_row.data->>'name', @SCHEMA@.agg_new_mark(o_row.data->'resolved', n_row.data->'resolved'));
|
|
1304
|
-
perform @SCHEMA@.agg_author_admit(n_row.data->>'name', n_row.tenant, o_row.data->'resolved', n_row.data->'resolved');
|
|
1305
1087
|
perform @SCHEMA@.class_evolve(o_row, n_row);
|
|
1306
1088
|
end loop;
|
|
1307
1089
|
-- отключённый аккаунт: сессии гаснут (этап 5, п. 2), каждая — сигнал кэшу
|
|
@@ -1323,15 +1105,7 @@ declare
|
|
|
1323
1105
|
classes text[] := array[n.data->>'name'];
|
|
1324
1106
|
conflicts jsonb;
|
|
1325
1107
|
c record;
|
|
1326
|
-
-- агрегаты (план 1.1, §2.4): источники, чьи метки пересчитываются; цели, поле которых считается заново
|
|
1327
|
-
agg_names text[] := '{}';
|
|
1328
|
-
agg_own text[] := '{}';
|
|
1329
|
-
agg_other text[] := @SCHEMA@.agg_marked(o.data->'resolved', n.data->'resolved');
|
|
1330
1108
|
begin
|
|
1331
|
-
if o.data->'resolved'->'aggregate' is distinct from n.data->'resolved'->'aggregate' then
|
|
1332
|
-
agg_names := @SCHEMA@.agg_of(o.data->'resolved') || @SCHEMA@.agg_of(n.data->'resolved');
|
|
1333
|
-
agg_own := agg_own || (n.data->>'name');
|
|
1334
|
-
end if;
|
|
1335
1109
|
-- потомки: описание resolved и validator пересчитываются, каждый пересчёт — версия класса в журнале
|
|
1336
1110
|
for d in select x.* from @SCHEMA@.entity x
|
|
1337
1111
|
where x.class = 'Class' and x.id <> n.id
|
|
@@ -1342,43 +1116,18 @@ begin
|
|
|
1342
1116
|
if prep.data is distinct from d.data or prep.validator::text is distinct from d.validator::text then
|
|
1343
1117
|
perform @SCHEMA@.sys_update(d.id, prep.data, d.links, prep.validator, 'update');
|
|
1344
1118
|
end if;
|
|
1345
|
-
-- потомок становится источником вместе с классом (смена parent; ревью коммита этапа 4б) — то же право READ на его
|
|
1346
|
-
-- строки по новым предкам (до правки проверил class_admit). class_evolve зовёт только триггер правки приложения;
|
|
1347
|
-
-- потомков класса System у арендаторов план прав сессии System не видит, их строки считают агрегаты System для
|
|
1348
|
-
-- целей того же арендатора
|
|
1349
|
-
if d.tenant = n.tenant then
|
|
1350
|
-
perform @SCHEMA@.agg_source_admit(d.data->>'name', @SCHEMA@.agg_new_mark(d.data->'resolved', prep.data->'resolved'));
|
|
1351
|
-
end if;
|
|
1352
|
-
-- и описавшему агрегат — в объёме описания (agg_author_admit сам пропускает класс арендатора под агрегатом System)
|
|
1353
|
-
perform @SCHEMA@.agg_author_admit(d.data->>'name', d.tenant, d.data->'resolved', prep.data->'resolved');
|
|
1354
1119
|
classes := classes || (d.data->>'name');
|
|
1355
|
-
if d.data->'resolved'->'aggregate' is distinct from prep.data->'resolved'->'aggregate' then
|
|
1356
|
-
agg_names := agg_names || @SCHEMA@.agg_of(d.data->'resolved') || @SCHEMA@.agg_of(prep.data->'resolved');
|
|
1357
|
-
agg_own := agg_own || (d.data->>'name');
|
|
1358
|
-
end if;
|
|
1359
|
-
agg_other := agg_other || @SCHEMA@.agg_marked(d.data->'resolved', prep.data->'resolved');
|
|
1360
1120
|
end loop;
|
|
1361
|
-
-- агрегаты семейства изменились: метки источников заново и поле у всех целей — из живых источников (строки
|
|
1362
|
-
-- проверит ужесточение ниже); у источника сменились предки — заново цели, которые его считают
|
|
1363
|
-
if cardinality(agg_names) > 0 then
|
|
1364
|
-
perform @SCHEMA@.agg_mark(n.tenant, agg_names);
|
|
1365
|
-
end if;
|
|
1366
|
-
if cardinality(agg_own) > 0 then
|
|
1367
|
-
perform @SCHEMA@.agg_refresh(agg_own, n.tenant, false);
|
|
1368
|
-
end if;
|
|
1369
|
-
if cardinality(agg_other) > 0 then
|
|
1370
|
-
perform @SCHEMA@.agg_refresh(agg_other, n.tenant, true);
|
|
1371
|
-
end if;
|
|
1372
1121
|
if not @SCHEMA@.class_change_safe(o.data->'resolved', n.data->'resolved') then
|
|
1373
1122
|
-- ужесточение проверяет все строки семейства, в том числе невидимые актору, и отвечает их числом: пробное
|
|
1374
1123
|
-- ужесточение (maximum, enum, pattern…) было оракулом по чужим строкам (доработки после точки Б). Нужно право READ
|
|
1375
1124
|
-- без условий на все строки класса и потомков своего арендатора; строки других арендаторов (класс System) —
|
|
1376
1125
|
-- уровень установки, их число остаётся в others
|
|
1377
|
-
perform @SCHEMA@.class_rows_admit(n.data->>'name', n.tenant, array['READ'],
|
|
1126
|
+
perform @SCHEMA@.class_rows_admit(n.data->>'name', n.tenant, array['READ'], 'tighten');
|
|
1378
1127
|
conflicts := @SCHEMA@.family_conflicts(classes, n.tenant, o.data->'resolved'->'ancestors' is distinct from n.data->'resolved'->'ancestors');
|
|
1379
1128
|
if conflicts <> '[]'::jsonb then
|
|
1380
1129
|
select * into c from @SCHEMA@.ctx();
|
|
1381
|
-
perform @SCHEMA@.lt_fail('
|
|
1130
|
+
perform @SCHEMA@.lt_fail('sql-write.tightening_mismatch', jsonb_build_object('cls', n.data->>'name'),
|
|
1382
1131
|
@SCHEMA@.visible_detail(conflicts, c.tenant));
|
|
1383
1132
|
end if;
|
|
1384
1133
|
end if;
|
|
@@ -1391,7 +1140,7 @@ set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
|
1391
1140
|
as $$
|
|
1392
1141
|
begin
|
|
1393
1142
|
if current_setting('transaction_isolation') <> 'read committed' then
|
|
1394
|
-
perform @SCHEMA@.lt_fail('
|
|
1143
|
+
perform @SCHEMA@.lt_fail('sql-write.delete_isolation');
|
|
1395
1144
|
end if;
|
|
1396
1145
|
return null;
|
|
1397
1146
|
end $$;
|
|
@@ -1401,14 +1150,14 @@ end $$;
|
|
|
1401
1150
|
-- после него, поэтому сессии гаснут с сигналом session:<хэш> здесь: процессы сбрасывают кэш ответов этих сессий.
|
|
1402
1151
|
-- Срабатывает для всех ролей, в том числе владельца: аккаунты удаляют и функции владельца — каскад trg_entity_ad и
|
|
1403
1152
|
-- account_purge. Строка Account у System не удаляется: ей принадлежат служебные строки secret (KEY — ключ прав и
|
|
1404
|
-
-- сигналов, LOCK —
|
|
1153
|
+
-- сигналов, LOCK — строки-замки), которые каскад стёр бы, а без них установка не пишет.
|
|
1405
1154
|
create or replace function @SCHEMA@.trg_entity_bd_account() returns trigger
|
|
1406
1155
|
language plpgsql security definer
|
|
1407
1156
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
1408
1157
|
as $$
|
|
1409
1158
|
begin
|
|
1410
1159
|
if OLD.id = '@SYSTEM@'::uuid then
|
|
1411
|
-
perform @SCHEMA@.lt_fail('
|
|
1160
|
+
perform @SCHEMA@.lt_fail('sql-write.system_account_delete');
|
|
1412
1161
|
end if;
|
|
1413
1162
|
perform @SCHEMA@.auth_kill(OLD.id, null);
|
|
1414
1163
|
return OLD;
|
|
@@ -1447,10 +1196,10 @@ begin
|
|
|
1447
1196
|
for r in select x.* from @SCHEMA@.entity x where x.owner = p_user and x.owner <> x.tenant and x.tenant = p_tenant
|
|
1448
1197
|
order by x.id for update loop
|
|
1449
1198
|
update @SCHEMA@.entity x
|
|
1450
|
-
set owner = p_tenant, rev = x.rev + 1,
|
|
1199
|
+
set owner = p_tenant, rev = x.rev + 1,
|
|
1451
1200
|
at = greatest(@SCHEMA@.now_ts(), x.at + interval '1 microsecond'),
|
|
1452
1201
|
author = coalesce(c.author, '@SYSTEM@'::uuid), agent = c.agent, op = 'update',
|
|
1453
|
-
reason = nullif(current_setting('letopis.reason', true), '')
|
|
1202
|
+
reason = nullif(current_setting('letopis.reason', true), '')
|
|
1454
1203
|
where x.id = r.id
|
|
1455
1204
|
returning x.* into r;
|
|
1456
1205
|
perform @SCHEMA@.log_version(r);
|
|
@@ -1497,13 +1246,7 @@ begin
|
|
|
1497
1246
|
dplan := @SCHEMA@.acl_plan('DELETE');
|
|
1498
1247
|
wplan := @SCHEMA@.acl_plan('WRITE');
|
|
1499
1248
|
if exists (select 1 from @SCHEMA@.entity x join gone g on g.id = x.id) then
|
|
1500
|
-
perform @SCHEMA@.lt_fail('recreated_in_statement'
|
|
1501
|
-
end if;
|
|
1502
|
-
-- строки производного класса удаляет только проектор (каскад функции владельца сюда не доходит)
|
|
1503
|
-
select d.class into denied from (select distinct g.class, g.tenant from gone g) d
|
|
1504
|
-
where (@SCHEMA@.class_row(d.class, d.tenant)).data->'resolved' ? 'derivedBy' limit 1;
|
|
1505
|
-
if denied is not null then
|
|
1506
|
-
perform @SCHEMA@.lt_fail('derived_class', format('класс %s производный: его строки удаляет проектор', denied));
|
|
1249
|
+
perform @SCHEMA@.lt_fail('sql-write.recreated_in_statement');
|
|
1507
1250
|
end if;
|
|
1508
1251
|
-- удаление класса (хвост ревью безопасности доработок после 1.2.0): проверки ниже — нет строк, потомков и истории —
|
|
1509
1252
|
-- не видят строку, которую писатель ещё не зафиксировал, и она осталась бы без класса, а с ней история удалённого
|
|
@@ -1512,25 +1255,7 @@ begin
|
|
|
1512
1255
|
for cl in select g.* from gone g where g.class = 'Class' order by @SCHEMA@.family_name(g.tenant, g.data), g.id loop
|
|
1513
1256
|
perform @SCHEMA@.family_lock(cl, true);
|
|
1514
1257
|
end loop;
|
|
1515
|
-
for r in select g.id, g.tenant, g.data->>'name' as name
|
|
1516
|
-
-- производный класс удаляет только администратор (class_admit)
|
|
1517
|
-
if r.derived and not @SCHEMA@.letopis_allowed('letopis.projector') then
|
|
1518
|
-
perform @SCHEMA@.lt_fail('acl_denied', format('класс %s производный: его описание задаёт только администратор (право letopis.projector)', r.name));
|
|
1519
|
-
end if;
|
|
1520
|
-
-- класс-источник агрегата (автоматическое ревью коммита 5290be1): пока его называет агрегат класса того же
|
|
1521
|
-
-- арендатора, класс не удаляется — иначе класс с этим именем завёл бы потом другой пользователь, и право READ
|
|
1522
|
-
-- описавшего агрегат на его строки уже не проверить (класс of проверяется только при описании агрегата). Агрегаты
|
|
1523
|
-
-- классов System описывает системный администратор: он читает всё
|
|
1524
|
-
if r.tenant <> '@SYSTEM@'::uuid then
|
|
1525
|
-
select x.data->>'name' || '.' || a.key into denied
|
|
1526
|
-
from @SCHEMA@.entity x cross join lateral jsonb_each(x.data->'resolved'->'aggregate') a
|
|
1527
|
-
where x.class = 'Class' and x.tenant = r.tenant and x.data->'resolved' ? 'aggregate' and a.value->>'of' = r.name
|
|
1528
|
-
order by 1 limit 1;
|
|
1529
|
-
if denied is not null then
|
|
1530
|
-
perform @SCHEMA@.lt_fail('invalid_class', format('класс %s — источник агрегата %s: сначала уберите агрегат из описания цели',
|
|
1531
|
-
r.name, denied));
|
|
1532
|
-
end if;
|
|
1533
|
-
end if;
|
|
1258
|
+
for r in select g.id, g.tenant, g.data->>'name' as name from gone g where g.class = 'Class' loop
|
|
1534
1259
|
-- у класса System — строки и потомки любого арендатора, кроме арендаторов со своим одноимённым классом: их строки и
|
|
1535
1260
|
-- потомки относятся к своему классу (class_row; ревью безопасности №3)
|
|
1536
1261
|
own := case when r.tenant = '@SYSTEM@'::uuid
|
|
@@ -1541,24 +1266,21 @@ begin
|
|
|
1541
1266
|
where x.class = r.name and (x.tenant = r.tenant or r.tenant = '@SYSTEM@'::uuid and x.tenant <> all (own)))
|
|
1542
1267
|
or exists (select 1 from @SCHEMA@.entity x where x.class = 'Class' and x.data->>'parent' = r.name
|
|
1543
1268
|
and (x.tenant = r.tenant or r.tenant = '@SYSTEM@'::uuid and x.tenant <> all (own))) then
|
|
1544
|
-
perform @SCHEMA@.lt_fail('
|
|
1269
|
+
perform @SCHEMA@.lt_fail('sql-write.class_not_empty', jsonb_build_object('cls', r.name));
|
|
1545
1270
|
end if;
|
|
1546
1271
|
-- и история строк (остаток Н2; доработки после точки Б): удалённый класс с историей, созданный заново с другими
|
|
1547
|
-
-- предками или алиасом, подставил бы её под чужие правила —
|
|
1272
|
+
-- предками или алиасом, подставил бы её под чужие правила — класс с историей строк не удаляется
|
|
1548
1273
|
if @SCHEMA@.class_has_history(r.name, r.tenant) then
|
|
1549
|
-
perform @SCHEMA@.lt_fail('
|
|
1274
|
+
perform @SCHEMA@.lt_fail('sql-write.class_has_history', jsonb_build_object('cls', r.name));
|
|
1550
1275
|
end if;
|
|
1551
1276
|
end loop;
|
|
1552
|
-
-- удалена цель агрегатов (план 1.1, §2.4): метки её источников — уже без неё
|
|
1553
|
-
perform @SCHEMA@.agg_mark(g.tenant, @SCHEMA@.agg_of(g.data->'resolved'))
|
|
1554
|
-
from gone g where g.class = 'Class' and g.data->'resolved' ? 'aggregate';
|
|
1555
1277
|
v_author := coalesce(c.author, '@SYSTEM@'::uuid);
|
|
1556
1278
|
-- надгробия: последний снимок следующей версией с видом delete — одной вставкой на уровень, в порядке id: без
|
|
1557
1279
|
-- order by порядок seq (а с ним итог delete() и время at) зависел от плана соединения и менялся от прогона к
|
|
1558
1280
|
-- прогону; время now_ts() (volatile) планировщик вычисляет уже после сортировки
|
|
1559
|
-
insert into @SCHEMA@.log (id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason
|
|
1281
|
+
insert into @SCHEMA@.log (id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason)
|
|
1560
1282
|
select g.id, g.rev + 1, g.class, g.cv, g.tenant, g.owner, g.links, g.data, g.tags,
|
|
1561
|
-
greatest(@SCHEMA@.now_ts(), g.at + interval '1 microsecond'), v_author, c.agent, 'delete', v_reason
|
|
1283
|
+
greatest(@SCHEMA@.now_ts(), g.at + interval '1 microsecond'), v_author, c.agent, 'delete', v_reason
|
|
1562
1284
|
from gone g
|
|
1563
1285
|
join (select d.class, d.tenant from (select distinct x.class, x.tenant from gone x) d
|
|
1564
1286
|
where @SCHEMA@.history_on(d.class, d.tenant)) h on h.class = g.class and h.tenant = g.tenant
|
|
@@ -1589,9 +1311,9 @@ begin
|
|
|
1589
1311
|
where x.id in (select y.id from @SCHEMA@.entity y where y.id = any (casc) order by y.id for update)
|
|
1590
1312
|
returning x.*
|
|
1591
1313
|
), t as (
|
|
1592
|
-
insert into @SCHEMA@.log (id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason
|
|
1314
|
+
insert into @SCHEMA@.log (id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason)
|
|
1593
1315
|
select d.id, d.rev + 1, d.class, d.cv, d.tenant, d.owner, d.links, d.data, d.tags,
|
|
1594
|
-
greatest(@SCHEMA@.now_ts(), d.at + interval '1 microsecond'), v_author, c.agent, 'delete', v_reason
|
|
1316
|
+
greatest(@SCHEMA@.now_ts(), d.at + interval '1 microsecond'), v_author, c.agent, 'delete', v_reason
|
|
1595
1317
|
from d
|
|
1596
1318
|
join (select dd.class, dd.tenant from (select distinct x.class, x.tenant from d x) dd
|
|
1597
1319
|
where @SCHEMA@.history_on(dd.class, dd.tenant)) h on h.class = d.class and h.tenant = d.tenant
|
|
@@ -1605,8 +1327,10 @@ begin
|
|
|
1605
1327
|
-- класс отказавшей строки называется, только если актор её видит (ревью безопасности №3)
|
|
1606
1328
|
if drows is not null then
|
|
1607
1329
|
denied := @SCHEMA@.visible_class(drows);
|
|
1608
|
-
|
|
1609
|
-
|
|
1330
|
+
if denied is not null then
|
|
1331
|
+
perform @SCHEMA@.lt_fail('sql-write.cascade_delete_denied', jsonb_build_object('cls', denied));
|
|
1332
|
+
end if;
|
|
1333
|
+
perform @SCHEMA@.lt_fail('sql-write.cascade_delete_denied_hidden');
|
|
1610
1334
|
end if;
|
|
1611
1335
|
mems := mems || lvl_mems;
|
|
1612
1336
|
classes := classes || lvl_classes;
|
|
@@ -1614,9 +1338,10 @@ begin
|
|
|
1614
1338
|
-- unset: снять конец, новая версия
|
|
1615
1339
|
for r in select x.* from @SCHEMA@.entity x where x.id = any (unsets) order by x.id for update loop
|
|
1616
1340
|
if not @SCHEMA@.acl_row_ok(wplan, r.class, r.id, r.owner, r.tenant, r.tags, r.data, r.links) then
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1341
|
+
if @SCHEMA@.visible_class(jsonb_build_array(to_jsonb(r))) is not null then
|
|
1342
|
+
perform @SCHEMA@.lt_fail('sql-write.unset_write_denied', jsonb_build_object('cls', r.class));
|
|
1343
|
+
end if;
|
|
1344
|
+
perform @SCHEMA@.lt_fail('sql-write.unset_write_denied_hidden');
|
|
1620
1345
|
end if;
|
|
1621
1346
|
newlinks := '{}'::jsonb;
|
|
1622
1347
|
for k in select key, value from jsonb_each(r.links) loop
|
|
@@ -1647,7 +1372,7 @@ begin
|
|
|
1647
1372
|
select coalesce(jsonb_agg(jsonb_build_object('id', x.id, 'class', x.class, 'tenant', x.tenant)), '[]'::jsonb) into blockers
|
|
1648
1373
|
from (select x.id, x.class, x.tenant from @SCHEMA@.entity x where x.ends && all_ids order by x.id limit 1000) x;
|
|
1649
1374
|
if blockers <> '[]'::jsonb then
|
|
1650
|
-
perform @SCHEMA@.lt_fail('delete_restricted', '
|
|
1375
|
+
perform @SCHEMA@.lt_fail('sql-write.delete_restricted', '{}'::jsonb,
|
|
1651
1376
|
@SCHEMA@.visible_detail(blockers, c.tenant));
|
|
1652
1377
|
end if;
|
|
1653
1378
|
return null;
|
|
@@ -1717,9 +1442,7 @@ create or replace trigger entity_bi before insert on @SCHEMA@.entity
|
|
|
1717
1442
|
for each row when (current_user <> '@OWNER_NAME@') execute function @SCHEMA@.trg_entity_bi();
|
|
1718
1443
|
create or replace trigger entity_bu before update on @SCHEMA@.entity
|
|
1719
1444
|
for each row when (current_user <> '@OWNER_NAME@') execute function @SCHEMA@.trg_entity_bu();
|
|
1720
|
-
|
|
1721
|
-
drop trigger if exists entity_ai on @SCHEMA@.entity;
|
|
1722
|
-
create trigger entity_ai after insert on @SCHEMA@.entity referencing new table as added
|
|
1445
|
+
create or replace trigger entity_ai after insert on @SCHEMA@.entity referencing new table as added
|
|
1723
1446
|
for each statement when (current_user <> '@OWNER_NAME@') execute function @SCHEMA@.trg_entity_ai();
|
|
1724
1447
|
create or replace trigger entity_au after update on @SCHEMA@.entity referencing old table as old_rows new table as new_rows
|
|
1725
1448
|
for each statement when (current_user <> '@OWNER_NAME@') execute function @SCHEMA@.trg_entity_au();
|
|
@@ -1739,192 +1462,6 @@ alter table @SCHEMA@.entity enable always trigger entity_bd;
|
|
|
1739
1462
|
alter table @SCHEMA@.entity enable always trigger entity_bd_account;
|
|
1740
1463
|
alter table @SCHEMA@.entity enable always trigger entity_ad;
|
|
1741
1464
|
|
|
1742
|
-
-- ========================= смена класса с ключом =========================
|
|
1743
|
-
|
|
1744
|
-
-- Замена id цели во всех концах.
|
|
1745
|
-
create or replace function @SCHEMA@.links_replace(p_links jsonb, p_old uuid, p_new uuid) returns jsonb
|
|
1746
|
-
language sql immutable parallel safe
|
|
1747
|
-
return (
|
|
1748
|
-
select coalesce(jsonb_object_agg(k.key, case jsonb_typeof(k.value)
|
|
1749
|
-
when 'array' then coalesce((select jsonb_agg(case when x = p_old::text then to_jsonb(p_new::text) else to_jsonb(x) end)
|
|
1750
|
-
from jsonb_array_elements_text(k.value) x), '[]'::jsonb)
|
|
1751
|
-
else case when k.value #>> '{}' = p_old::text then to_jsonb(p_new::text) else k.value end end), '{}'::jsonb)
|
|
1752
|
-
from jsonb_each(p_links) k
|
|
1753
|
-
);
|
|
1754
|
-
|
|
1755
|
-
-- Перенос строки на новый id (смена класса с ключом, смена ключа): новая строка продолжает журнал
|
|
1756
|
-
-- нового id и ссылается на старый (moved), ссылки переводятся — если переведённый конец входит
|
|
1757
|
-
-- в ключ ссылающейся строки, её id тоже меняется, и перевод идёт дальше рекурсивно; старая строка
|
|
1758
|
-
-- удаляется с надгробием, которое ссылается на новый id. Проверки делает вызывающая функция; p_plan — план WRITE
|
|
1759
|
-
-- актора (reclass, rekey, rekey_class): каждая строка, чьи ссылки перевод переписывает, на всех уровнях рекурсии должна
|
|
1760
|
-
-- им проходить (доработки после точки Б: прежде проверялся только первый уровень); null — путь владельца.
|
|
1761
|
-
-- Агрегаты (план 1.1, §2.4): перенос — несколько операторов (новая строка, перевод ссылок, удаление старой), и
|
|
1762
|
-
-- триггер агрегатов на это время только копит их вклады (agg_hold_begin), а в конце применяет одним итогом: сумма
|
|
1763
|
-
-- не проходит через промежуточные значения, новая цель считается целиком из живых источников.
|
|
1764
|
-
drop function if exists @SCHEMA@.move_row(@SCHEMA@.entity, uuid, text, jsonb, jsonb, text);
|
|
1765
|
-
create or replace function @SCHEMA@.move_row(o @SCHEMA@.entity, p_new_id uuid, p_class text, p_data jsonb, p_links jsonb, p_op text,
|
|
1766
|
-
p_plan jsonb default null)
|
|
1767
|
-
returns @SCHEMA@.entity
|
|
1768
|
-
language plpgsql volatile
|
|
1769
|
-
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
1770
|
-
as $$
|
|
1771
|
-
declare
|
|
1772
|
-
c record;
|
|
1773
|
-
last record;
|
|
1774
|
-
ts timestamptz;
|
|
1775
|
-
n @SCHEMA@.entity;
|
|
1776
|
-
r @SCHEMA@.entity;
|
|
1777
|
-
nl jsonb;
|
|
1778
|
-
rid uuid;
|
|
1779
|
-
held boolean := @SCHEMA@.agg_hold_begin('acc');
|
|
1780
|
-
begin
|
|
1781
|
-
select * into c from @SCHEMA@.ctx();
|
|
1782
|
-
select l.rev, l.hash, l.at into last from @SCHEMA@.log l where l.id = p_new_id order by l.rev desc limit 1 for update;
|
|
1783
|
-
ts := greatest(@SCHEMA@.now_ts(), last.at + interval '1 microsecond');
|
|
1784
|
-
insert into @SCHEMA@.entity (id, rev, class, cv, tenant, owner, links, data, tags, at, created_at, author, agent, op, reason, moved, prev_hash, validator)
|
|
1785
|
-
values (p_new_id, coalesce(last.rev, 0) + 1, p_class, coalesce((@SCHEMA@.class_row(p_class, o.tenant)).rev, 0), o.tenant, o.owner,
|
|
1786
|
-
p_links, p_data, o.tags, ts, ts, coalesce(c.author, '@SYSTEM@'::uuid), c.agent, p_op,
|
|
1787
|
-
nullif(current_setting('letopis.reason', true), ''), o.id, last.hash, null)
|
|
1788
|
-
returning * into n;
|
|
1789
|
-
perform @SCHEMA@.log_version(n);
|
|
1790
|
-
for r in select x.* from @SCHEMA@.entity x where x.ends @> array[o.id] and x.id <> p_new_id order by x.id for update loop
|
|
1791
|
-
-- строки производного класса пишет только проектор: перевод их ссылок мимо него оставил бы проекцию
|
|
1792
|
-
-- несогласованной (ревью безопасности №3, №3)
|
|
1793
|
-
if (@SCHEMA@.class_row(r.class, r.tenant)).data->'resolved' ? 'derivedBy' then
|
|
1794
|
-
perform @SCHEMA@.lt_fail('derived_class',
|
|
1795
|
-
case when p_plan is null or @SCHEMA@.visible_class(jsonb_build_array(to_jsonb(r))) is not null
|
|
1796
|
-
then format('на строку ссылается строка производного класса %s: её пишет проектор', r.class)
|
|
1797
|
-
else 'на строку ссылается строка производного класса: её пишет проектор' end);
|
|
1798
|
-
end if;
|
|
1799
|
-
if p_plan is not null and not @SCHEMA@.acl_row_ok(p_plan, r.class, r.id, r.owner, r.tenant, r.tags, r.data, r.links) then
|
|
1800
|
-
perform @SCHEMA@.lt_fail('acl_denied', 'нет права WRITE на ссылающиеся строки');
|
|
1801
|
-
end if;
|
|
1802
|
-
nl := @SCHEMA@.links_replace(r.links, o.id, p_new_id);
|
|
1803
|
-
rid := coalesce(@SCHEMA@.key_id((@SCHEMA@.class_row(r.class, r.tenant)).data->'resolved', r.class, r.tenant, r.data, nl), r.id);
|
|
1804
|
-
if rid <> r.id then
|
|
1805
|
-
perform @SCHEMA@.move_row(r, rid, r.class, r.data, nl, 'rekey', p_plan);
|
|
1806
|
-
else
|
|
1807
|
-
perform @SCHEMA@.sys_update(r.id, r.data, nl, r.validator, 'update');
|
|
1808
|
-
end if;
|
|
1809
|
-
end loop;
|
|
1810
|
-
delete from @SCHEMA@.entity x where x.id = o.id returning x.* into r;
|
|
1811
|
-
-- надгробие всегда вида delete (по нему срез на момент отличает удалённый id), moved — новый id
|
|
1812
|
-
perform @SCHEMA@.tombstone(r, 'delete', p_new_id);
|
|
1813
|
-
if held then
|
|
1814
|
-
perform @SCHEMA@.agg_hold_end(true);
|
|
1815
|
-
end if;
|
|
1816
|
-
return n;
|
|
1817
|
-
end $$;
|
|
1818
|
-
|
|
1819
|
-
-- Смена класса (этап 1, п. 9): у класса с ключом id вычисляется заново для нового класса, ссылки
|
|
1820
|
-
-- переводятся в той же транзакции, журнал связывает старый и новый id. У класса без ключа id
|
|
1821
|
-
-- сохраняется. Только read committed; на строку сначала берётся for update.
|
|
1822
|
-
create or replace function @SCHEMA@.reclass(p_id uuid, p_class text, p_data jsonb default null) returns uuid
|
|
1823
|
-
language plpgsql volatile security definer
|
|
1824
|
-
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
1825
|
-
as $$
|
|
1826
|
-
declare
|
|
1827
|
-
c record;
|
|
1828
|
-
o @SCHEMA@.entity;
|
|
1829
|
-
cls @SCHEMA@.entity;
|
|
1830
|
-
res jsonb;
|
|
1831
|
-
d jsonb;
|
|
1832
|
-
bad jsonb;
|
|
1833
|
-
lk record;
|
|
1834
|
-
nid uuid;
|
|
1835
|
-
inc jsonb;
|
|
1836
|
-
rd jsonb;
|
|
1837
|
-
wplan jsonb;
|
|
1838
|
-
denied text;
|
|
1839
|
-
n_denied int;
|
|
1840
|
-
begin
|
|
1841
|
-
select * into c from @SCHEMA@.ctx();
|
|
1842
|
-
if c.account is null then
|
|
1843
|
-
perform @SCHEMA@.lt_fail('no_session', 'нет действующей сессии');
|
|
1844
|
-
end if;
|
|
1845
|
-
if current_setting('transaction_isolation') <> 'read committed' then
|
|
1846
|
-
perform @SCHEMA@.lt_fail('isolation_level', 'смена класса разрешена только в read committed');
|
|
1847
|
-
end if;
|
|
1848
|
-
rd := @SCHEMA@.acl_actor('READ');
|
|
1849
|
-
wplan := @SCHEMA@.acl_plan('WRITE');
|
|
1850
|
-
select x.* into o from @SCHEMA@.entity x where x.id = p_id and x.tenant = c.tenant for update;
|
|
1851
|
-
if o.id is null or not @SCHEMA@.row_visible(o.tenant, o.class, o.id, o.owner, o.tags, o.data, o.links, c.tenant, c.account, rd->'plan') then
|
|
1852
|
-
perform @SCHEMA@.lt_fail('target_not_found', 'цель не найдена');
|
|
1853
|
-
end if;
|
|
1854
|
-
if not @SCHEMA@.acl_row_ok(wplan, o.class, o.id, o.owner, o.tenant, o.tags, o.data, o.links) then
|
|
1855
|
-
perform @SCHEMA@.lt_fail('acl_denied', format('нет права WRITE на строку класса %s', o.class));
|
|
1856
|
-
end if;
|
|
1857
|
-
-- строка уходит из своего класса — как удаление из него: нужно право DELETE на прежнюю строку (доработки после
|
|
1858
|
-
-- точки Б; иначе с одним WRITE строку переносили в класс, где есть DELETE, и удаляли там)
|
|
1859
|
-
if not @SCHEMA@.acl_row_ok(@SCHEMA@.acl_plan('DELETE'), o.class, o.id, o.owner, o.tenant, o.tags, o.data, o.links) then
|
|
1860
|
-
perform @SCHEMA@.lt_fail('acl_denied', format('нет права DELETE на строку класса %s: смена класса убирает её из класса', o.class));
|
|
1861
|
-
end if;
|
|
1862
|
-
-- строки прав и классов функция не трогает
|
|
1863
|
-
if o.class in ('Class', 'Account', 'member', 'Resource', 'rule') or p_class in ('Class', 'Account', 'member', 'Resource', 'rule') then
|
|
1864
|
-
perform @SCHEMA@.lt_fail('reclass_denied', 'класс системной строки не меняется');
|
|
1865
|
-
end if;
|
|
1866
|
-
cls := @SCHEMA@.class_row(p_class, o.tenant);
|
|
1867
|
-
if cls.id is null then
|
|
1868
|
-
perform @SCHEMA@.lt_fail('invalid_class', format('класс %s не найден', p_class));
|
|
1869
|
-
end if;
|
|
1870
|
-
perform @SCHEMA@.family_lock(cls, false);
|
|
1871
|
-
cls := @SCHEMA@.class_row(p_class, o.tenant);
|
|
1872
|
-
if cls.id is null then
|
|
1873
|
-
perform @SCHEMA@.lt_fail('invalid_class', format('класс %s удалён', p_class));
|
|
1874
|
-
end if;
|
|
1875
|
-
res := cls.data->'resolved';
|
|
1876
|
-
if coalesce((res->>'abstract')::boolean, false) then
|
|
1877
|
-
perform @SCHEMA@.lt_fail('invalid_class', format('класс %s абстрактный', p_class));
|
|
1878
|
-
end if;
|
|
1879
|
-
if res ? 'derivedBy' or (@SCHEMA@.class_row(o.class, o.tenant)).data->'resolved' ? 'derivedBy' then
|
|
1880
|
-
perform @SCHEMA@.lt_fail('derived_class', 'строки производного класса пишет только его проектор');
|
|
1881
|
-
end if;
|
|
1882
|
-
d := coalesce(p_data, o.data);
|
|
1883
|
-
-- агрегатные поля нового класса считает база (план 1.1, §2.4): в данных их не передают; прежнее число остаётся
|
|
1884
|
-
-- до пересчёта, иначе 0
|
|
1885
|
-
if res ? 'aggregate' then
|
|
1886
|
-
if p_data ?| @SCHEMA@.agg_fields(res) then
|
|
1887
|
-
perform @SCHEMA@.lt_fail('aggregate_field', format('агрегатные поля класса %s считает база — в данных reclass их не передают', p_class));
|
|
1888
|
-
end if;
|
|
1889
|
-
d := @SCHEMA@.agg_zero(res, d);
|
|
1890
|
-
end if;
|
|
1891
|
-
d := @SCHEMA@.apply_defaults(res->'schema', d);
|
|
1892
|
-
bad := @SCHEMA@.validate_data(res->'rules', cls.validator, res->'vars', d);
|
|
1893
|
-
if bad <> '[]'::jsonb then
|
|
1894
|
-
perform @SCHEMA@.lt_fail('invalid_data', format('данные не проходят описание класса %s', p_class), jsonb_build_object('issues', bad));
|
|
1895
|
-
end if;
|
|
1896
|
-
select * into lk from @SCHEMA@.check_links(res, o.links, o.tenant, true, rd);
|
|
1897
|
-
if lk.code is not null then
|
|
1898
|
-
perform @SCHEMA@.lt_fail(lk.code, lk.msg);
|
|
1899
|
-
end if;
|
|
1900
|
-
inc := @SCHEMA@.incoming_conflicts(o.id, p_class, o.tenant);
|
|
1901
|
-
if inc <> '[]'::jsonb then
|
|
1902
|
-
perform @SCHEMA@.lt_fail('reclass_denied', format('роли ссылающихся строк не принимают класс %s', p_class),
|
|
1903
|
-
@SCHEMA@.visible_detail(inc, c.tenant));
|
|
1904
|
-
end if;
|
|
1905
|
-
nid := coalesce(@SCHEMA@.key_id(res, p_class, o.tenant, d, lk.links), o.id);
|
|
1906
|
-
if not @SCHEMA@.acl_row_ok(wplan, p_class, nid, o.owner, o.tenant, o.tags, d, lk.links) then
|
|
1907
|
-
perform @SCHEMA@.lt_fail('acl_denied', format('нет права WRITE на класс %s', p_class));
|
|
1908
|
-
end if;
|
|
1909
|
-
if nid <> o.id then
|
|
1910
|
-
-- перевод ссылок пишет ссылающиеся строки: на них тоже нужно право WRITE (глубже первого уровня — move_row);
|
|
1911
|
-
-- класс такой строки называется, только если актор её видит
|
|
1912
|
-
select min(x.class) filter (where @SCHEMA@.row_visible(x.tenant, x.class, x.id, x.owner, x.tags, x.data, x.links, c.tenant, c.account, rd->'plan')),
|
|
1913
|
-
count(*) into denied, n_denied from @SCHEMA@.entity x
|
|
1914
|
-
where x.ends @> array[o.id] and not @SCHEMA@.acl_row_ok(wplan, x.class, x.id, x.owner, x.tenant, x.tags, x.data, x.links);
|
|
1915
|
-
if n_denied > 0 then
|
|
1916
|
-
perform @SCHEMA@.lt_fail('acl_denied', case when denied is not null then format('нет права WRITE на ссылающиеся строки класса %s', denied)
|
|
1917
|
-
else 'нет права WRITE на ссылающиеся строки' end);
|
|
1918
|
-
end if;
|
|
1919
|
-
end if;
|
|
1920
|
-
if nid = o.id then
|
|
1921
|
-
perform @SCHEMA@.sys_update(o.id, d, lk.links, null, 'reclass', p_class);
|
|
1922
|
-
else
|
|
1923
|
-
perform @SCHEMA@.move_row(o, nid, p_class, d, lk.links, 'reclass', wplan);
|
|
1924
|
-
end if;
|
|
1925
|
-
return nid;
|
|
1926
|
-
end $$;
|
|
1927
|
-
|
|
1928
1465
|
-- ========================= merge, inc, links_patch =========================
|
|
1929
1466
|
|
|
1930
1467
|
-- Правка концов слотами (этап 4, п. 4): .Роль.set(), .unset(), а для множественных концов .add()
|
|
@@ -1972,7 +1509,10 @@ begin
|
|
|
1972
1509
|
end if;
|
|
1973
1510
|
res := jsonb_set(res, array[role], cur);
|
|
1974
1511
|
else
|
|
1975
|
-
raise exception using errcode = 'LT003', message = 'letopis: invalid_data:
|
|
1512
|
+
raise exception using errcode = 'LT003', message = 'letopis: invalid_data: '
|
|
1513
|
+
|| @SCHEMA@.lt_text('sql-write.links_patch_op', jsonb_build_object('op', coalesce(o->>'op', 'null'))),
|
|
1514
|
+
detail = jsonb_build_object('key', 'sql-write.links_patch_op', 'params', jsonb_build_object('op', coalesce(o->>'op', 'null')),
|
|
1515
|
+
'nodetail', true)::text;
|
|
1976
1516
|
end case;
|
|
1977
1517
|
end loop;
|
|
1978
1518
|
return res;
|
|
@@ -2013,39 +1553,46 @@ declare
|
|
|
2013
1553
|
cur jsonb := data;
|
|
2014
1554
|
v jsonb;
|
|
2015
1555
|
i int;
|
|
2016
|
-
fail text;
|
|
1556
|
+
fail text; -- ключ каталога отказа (sql-write.inc_*)
|
|
1557
|
+
p jsonb := '{}'::jsonb;
|
|
2017
1558
|
begin
|
|
2018
1559
|
if path is null or cardinality(path) = 0 or delta is null then
|
|
2019
|
-
fail := '
|
|
1560
|
+
fail := 'sql-write.inc_args';
|
|
2020
1561
|
else
|
|
2021
1562
|
for i in 1 .. cardinality(path) - 1 loop
|
|
2022
1563
|
if jsonb_typeof(cur) is distinct from 'object' or not cur ? path[i] then
|
|
2023
|
-
fail :=
|
|
1564
|
+
fail := 'sql-write.inc_path_missing';
|
|
2024
1565
|
exit;
|
|
2025
1566
|
end if;
|
|
2026
1567
|
cur := cur->path[i];
|
|
2027
1568
|
end loop;
|
|
2028
1569
|
if fail is null and jsonb_typeof(cur) is distinct from 'object' then
|
|
2029
|
-
fail :=
|
|
1570
|
+
fail := 'sql-write.inc_path_not_object';
|
|
2030
1571
|
end if;
|
|
2031
1572
|
if fail is null then
|
|
2032
1573
|
v := cur->path[cardinality(path)];
|
|
2033
1574
|
if v is null then
|
|
2034
1575
|
if start is null then
|
|
2035
|
-
fail :=
|
|
1576
|
+
fail := 'sql-write.inc_no_field';
|
|
2036
1577
|
else
|
|
2037
1578
|
return jsonb_set(data, path, to_jsonb(start + delta), true);
|
|
2038
1579
|
end if;
|
|
2039
1580
|
elsif jsonb_typeof(v) <> 'number' then
|
|
2040
|
-
fail :=
|
|
1581
|
+
fail := 'sql-write.inc_not_number';
|
|
2041
1582
|
else
|
|
2042
1583
|
return jsonb_set(data, path, to_jsonb((v #>> '{}')::numeric + delta));
|
|
2043
1584
|
end if;
|
|
2044
1585
|
end if;
|
|
2045
1586
|
end if;
|
|
2046
|
-
|
|
1587
|
+
-- путь — параметр всех текстов, кроме inc_args; нарушение в detail.issues — тот же русский текст с его key и params
|
|
1588
|
+
-- (их убирает fromDbError)
|
|
1589
|
+
if fail <> 'sql-write.inc_args' then
|
|
1590
|
+
p := jsonb_build_object('path', array_to_string(path, '.'));
|
|
1591
|
+
end if;
|
|
1592
|
+
raise exception using errcode = 'LT003', message = 'letopis: invalid_data: ' || @SCHEMA@.lt_text(fail, p),
|
|
2047
1593
|
detail = jsonb_build_object('issues', jsonb_build_array(jsonb_build_object(
|
|
2048
|
-
'path', to_jsonb(coalesce(path, '{}')), 'keyword', 'inc', 'message', fail)))
|
|
1594
|
+
'path', to_jsonb(coalesce(path, '{}')), 'keyword', 'inc', 'message', @SCHEMA@.lt_text(fail, p), 'key', fail, 'params', p)),
|
|
1595
|
+
'key', fail, 'params', p)::text;
|
|
2049
1596
|
end $$;
|
|
2050
1597
|
|
|
2051
1598
|
do $$
|
|
@@ -2054,8 +1601,7 @@ begin
|
|
|
2054
1601
|
foreach f in array array[
|
|
2055
1602
|
'now_ts()', 'lock_id(text)', 'lock_row(text)', 'lock_try(text)', 'family_name(uuid, jsonb)', 'family_rows(uuid, jsonb)', 'family_lock(@SCHEMA@.entity, boolean)', 'class_set(jsonb)', 'class_accepts(jsonb, text, uuid)', 'role_accepts(text, uuid, text, text, uuid)',
|
|
2056
1603
|
'apply_defaults(jsonb, jsonb)', 'links_shape(jsonb, jsonb)', 'targets_bad(jsonb, boolean, jsonb)', 'check_links(jsonb, jsonb, uuid, boolean, jsonb)', 'key_id(jsonb, text, uuid, jsonb, jsonb)',
|
|
2057
|
-
'log_version(@SCHEMA@.entity)', 'sys_update(uuid, jsonb, jsonb, jsonpath, text
|
|
2058
|
-
'links_replace(jsonb, uuid, uuid)', 'move_row(@SCHEMA@.entity, uuid, text, jsonb, jsonb, text, jsonb)', 'reclass(uuid, text, jsonb)',
|
|
1604
|
+
'log_version(@SCHEMA@.entity)', 'sys_update(uuid, jsonb, jsonb, jsonpath, text)', 'tombstone(@SCHEMA@.entity)',
|
|
2059
1605
|
'incoming_conflicts(uuid, text, uuid)', 'visible_detail(jsonb, uuid)', 'visible_class(jsonb)', 'class_change_safe(jsonb, jsonb)',
|
|
2060
1606
|
'family_conflicts(text[], uuid, boolean)', 'reserved_names()', 'class_acl_admit(text, uuid)', 'class_rows_admit(text, uuid, text[], text)', 'class_has_history(text, uuid)', 'class_orphan_history(text, uuid)', 'class_admit(jsonb, uuid)',
|
|
2061
1607
|
'trg_entity_bi()', 'trg_entity_bu()', 'trg_entity_ai()', 'trg_entity_au()', 'history_on(text, uuid)', 'signal_token(text, text, bytea)', 'signal_raw(text[])', 'signal(text[], uuid[])', 'class_evolve(@SCHEMA@.entity, @SCHEMA@.entity)',
|
|
@@ -2064,8 +1610,6 @@ begin
|
|
|
2064
1610
|
execute format('revoke all on function @SCHEMA@.%s from public', f);
|
|
2065
1611
|
end loop;
|
|
2066
1612
|
end $$;
|
|
2067
|
-
-- сырой SQL пишет теми же merge, inc и links_patch, что и
|
|
2068
|
-
-- до этапа 6 verify() был без параметров
|
|
2069
|
-
drop function if exists @SCHEMA@.verify();
|
|
1613
|
+
-- сырой SQL пишет теми же merge, inc и links_patch, что и библиотека
|
|
2070
1614
|
grant execute on function @SCHEMA@.merge(jsonb, jsonb), @SCHEMA@.inc(jsonb, text[], numeric, numeric), @SCHEMA@.links_patch(jsonb, jsonb),
|
|
2071
|
-
@SCHEMA@.reserved_names(), @SCHEMA@.
|
|
1615
|
+
@SCHEMA@.reserved_names(), @SCHEMA@.delete_preview(uuid[]) to @APP@;
|