letopis 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT-CHEATSHEET.en.md +15 -7
- package/AGENT-CHEATSHEET.md +19 -11
- package/CHANGELOG.md +299 -1
- package/README.en.md +180 -62
- package/README.md +165 -55
- package/dist/acl.d.ts +11 -5
- package/dist/admin.d.ts +30 -14
- package/dist/admin.js +53 -18
- package/dist/auth.d.ts +61 -17
- package/dist/auth.js +16 -2
- package/dist/cache.d.ts +5 -1
- package/dist/cache.js +3 -2
- package/dist/chain.d.ts +92 -15
- package/dist/chain.js +56 -7
- package/dist/cli.js +16 -2
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +5 -5
- package/dist/import.js +7 -0
- package/dist/index.d.ts +73 -45
- package/dist/index.js +69 -14
- package/dist/internal.d.ts +7 -0
- package/dist/internal.js +5 -0
- package/dist/load.d.ts +10 -3
- package/dist/load.js +39 -11
- package/dist/mcp.d.ts +20 -11
- package/dist/mcp.js +8 -6
- package/dist/model.d.ts +4 -1
- package/dist/model.js +7 -1
- package/dist/ops.d.ts +58 -18
- package/dist/ops.js +58 -18
- package/dist/project.d.ts +39 -11
- package/dist/project.js +75 -15
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +4 -1
- package/dist/signals.d.ts +4 -4
- package/dist/signals.js +4 -4
- package/dist/sql.d.ts +8 -4
- package/dist/sql.js +110 -25
- package/dist/sync.js +11 -4
- package/dist/tx.d.ts +15 -0
- package/dist/tx.js +22 -4
- package/dist/types.d.ts +21 -7
- package/dist/types.js +13 -6
- package/dist/up.d.ts +48 -25
- package/dist/up.js +94 -41
- package/dist/validate.d.ts +9 -2
- package/dist/validate.js +19 -3
- package/dist/watch.d.ts +29 -5
- package/dist/watch.js +62 -13
- package/dist/write.d.ts +3 -2
- package/dist/write.js +52 -19
- package/package.json +8 -4
- package/sql/10-core.sql +6 -1
- package/sql/60-write.sql +47 -18
- package/sql/70-read.sql +13 -7
- package/sql/75-history.sql +106 -28
- package/sql/80-auth.sql +41 -3
- package/sql/88-derived.sql +3 -1
- package/sql/90-time.sql +118 -30
- package/sql/95-seed.system.sql +6 -3
- package/sql/99-revision.sql +1 -1
- package/sql/migrate/0007-secret-account-fk.sql +12 -0
package/sql/60-write.sql
CHANGED
|
@@ -472,8 +472,12 @@ as $$
|
|
|
472
472
|
declare
|
|
473
473
|
e @SCHEMA@.entity;
|
|
474
474
|
c record;
|
|
475
|
+
was text;
|
|
475
476
|
begin
|
|
476
477
|
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;
|
|
477
481
|
update @SCHEMA@.entity x
|
|
478
482
|
set class = coalesce(p_class, x.class), data = p_data, links = p_links, validator = p_validator,
|
|
479
483
|
rev = x.rev + 1, prev_hash = x.hash,
|
|
@@ -489,6 +493,11 @@ begin
|
|
|
489
493
|
select x.* into e from @SCHEMA@.entity x where x.id = p_id;
|
|
490
494
|
end if;
|
|
491
495
|
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;
|
|
492
501
|
return e;
|
|
493
502
|
end $$;
|
|
494
503
|
|
|
@@ -1217,7 +1226,8 @@ begin
|
|
|
1217
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, n.moved, n.prev_hash
|
|
1218
1227
|
from added n
|
|
1219
1228
|
join (select k.class, k.tenant from (select distinct a.class, a.tenant from added a) k
|
|
1220
|
-
where @SCHEMA@.history_on(k.class, k.tenant)) h on h.class = n.class and h.tenant = n.tenant
|
|
1229
|
+
where @SCHEMA@.history_on(k.class, k.tenant)) h on h.class = n.class and h.tenant = n.tenant
|
|
1230
|
+
order by n.at, n.id; -- порядок seq — порядок входа, а не плана соединения
|
|
1221
1231
|
-- сигнал — по паре «класс, арендатор»
|
|
1222
1232
|
select array_agg(k.class), array_agg(k.tenant) into sc, st from (select distinct a.class, a.tenant from added a) k;
|
|
1223
1233
|
perform @SCHEMA@.signal(sc, st);
|
|
@@ -1273,7 +1283,8 @@ begin
|
|
|
1273
1283
|
from new_rows n
|
|
1274
1284
|
join old_rows o on o.id = n.id and o.rev <> n.rev
|
|
1275
1285
|
join (select k.class, k.tenant from (select distinct a.class, a.tenant from new_rows a) k
|
|
1276
|
-
where @SCHEMA@.history_on(k.class, k.tenant)) h on h.class = n.class and h.tenant = n.tenant
|
|
1286
|
+
where @SCHEMA@.history_on(k.class, k.tenant)) h on h.class = n.class and h.tenant = n.tenant
|
|
1287
|
+
order by n.id; -- порядок seq не зависит от плана: правка многих строк пишет версии в порядке id
|
|
1277
1288
|
-- сигнал — по парам нового и прежнего класса: при смене класса без ключа ответы прежнего класса в
|
|
1278
1289
|
-- кэше других процессов устарели бы (в 1.0.1 сигнал шёл только по новому)
|
|
1279
1290
|
select array_agg(k.class), array_agg(k.tenant) into sc, st
|
|
@@ -1385,6 +1396,24 @@ begin
|
|
|
1385
1396
|
return null;
|
|
1386
1397
|
end $$;
|
|
1387
1398
|
|
|
1399
|
+
-- Перед удалением строки Account (ревизия 7, решение владельца). Строки secret аккаунта — сессии, хэши паролей и ключей,
|
|
1400
|
+
-- секреты TOTP, внешние учётные записи — уносит каскад внешнего ключа secret.account в конце оператора, раньше триггера
|
|
1401
|
+
-- после него, поэтому сессии гаснут с сигналом session:<хэш> здесь: процессы сбрасывают кэш ответов этих сессий.
|
|
1402
|
+
-- Срабатывает для всех ролей, в том числе владельца: аккаунты удаляют и функции владельца — каскад trg_entity_ad и
|
|
1403
|
+
-- account_purge. Строка Account у System не удаляется: ей принадлежат служебные строки secret (KEY — ключ прав и
|
|
1404
|
+
-- сигналов, LOCK — строки-замки, PROJECTOR — чекпоинты проекторов), которые каскад стёр бы, а без них установка не пишет.
|
|
1405
|
+
create or replace function @SCHEMA@.trg_entity_bd_account() returns trigger
|
|
1406
|
+
language plpgsql security definer
|
|
1407
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
1408
|
+
as $$
|
|
1409
|
+
begin
|
|
1410
|
+
if OLD.id = '@SYSTEM@'::uuid then
|
|
1411
|
+
perform @SCHEMA@.lt_fail('acl_denied', 'аккаунт System не удаляется: ему принадлежат служебные строки установки');
|
|
1412
|
+
end if;
|
|
1413
|
+
perform @SCHEMA@.auth_kill(OLD.id, null);
|
|
1414
|
+
return OLD;
|
|
1415
|
+
end $$;
|
|
1416
|
+
|
|
1388
1417
|
-- Действие со строкой, которая ссылается на удаляемые id (p_ids), по правилам её концов (p_ends):
|
|
1389
1418
|
-- cascade сильнее restrict, restrict сильнее unset; null — строка на них не ссылается. Общее правило
|
|
1390
1419
|
-- для триггера удаления и превью delete_preview() (этап 4, п. 6): превью не расходится с удалением.
|
|
@@ -1456,9 +1485,7 @@ declare
|
|
|
1456
1485
|
dplan jsonb;
|
|
1457
1486
|
wplan jsonb;
|
|
1458
1487
|
denied text;
|
|
1459
|
-
accs uuid[];
|
|
1460
1488
|
mems jsonb;
|
|
1461
|
-
lvl_accs uuid[];
|
|
1462
1489
|
lvl_mems jsonb;
|
|
1463
1490
|
drows jsonb;
|
|
1464
1491
|
own uuid[];
|
|
@@ -1526,19 +1553,21 @@ begin
|
|
|
1526
1553
|
perform @SCHEMA@.agg_mark(g.tenant, @SCHEMA@.agg_of(g.data->'resolved'))
|
|
1527
1554
|
from gone g where g.class = 'Class' and g.data->'resolved' ? 'aggregate';
|
|
1528
1555
|
v_author := coalesce(c.author, '@SYSTEM@'::uuid);
|
|
1529
|
-
-- надгробия: последний снимок следующей версией с видом delete — одной вставкой на
|
|
1556
|
+
-- надгробия: последний снимок следующей версией с видом delete — одной вставкой на уровень, в порядке id: без
|
|
1557
|
+
-- order by порядок seq (а с ним итог delete() и время at) зависел от плана соединения и менялся от прогона к
|
|
1558
|
+
-- прогону; время now_ts() (volatile) планировщик вычисляет уже после сортировки
|
|
1530
1559
|
insert into @SCHEMA@.log (id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason, moved, prev_hash, changed)
|
|
1531
1560
|
select g.id, g.rev + 1, g.class, g.cv, g.tenant, g.owner, g.links, g.data, g.tags,
|
|
1532
1561
|
greatest(@SCHEMA@.now_ts(), g.at + interval '1 microsecond'), v_author, c.agent, 'delete', v_reason, null, g.hash, '[]'::jsonb
|
|
1533
1562
|
from gone g
|
|
1534
1563
|
join (select d.class, d.tenant from (select distinct x.class, x.tenant from gone x) d
|
|
1535
|
-
where @SCHEMA@.history_on(d.class, d.tenant)) h on h.class = g.class and h.tenant = g.tenant
|
|
1564
|
+
where @SCHEMA@.history_on(d.class, d.tenant)) h on h.class = g.class and h.tenant = g.tenant
|
|
1565
|
+
order by g.id;
|
|
1536
1566
|
-- пары «класс, арендатор» для сигнала — строкой «класс арендатор» (пробела нет ни в имени, ни в uuid)
|
|
1537
1567
|
select coalesce(array_agg(g.id), '{}'), coalesce(array_agg(distinct g.class), '{}'),
|
|
1538
1568
|
coalesce(array_agg(distinct g.class || ' ' || g.tenant::text), '{}'),
|
|
1539
|
-
coalesce(array_agg(g.id) filter (where g.class = 'Account'), '{}'),
|
|
1540
1569
|
coalesce(jsonb_agg(jsonb_build_object('tenant', g.tenant, 'user', g.links->>'User')) filter (where g.class = 'member'), '[]'::jsonb)
|
|
1541
|
-
into lvl, classes, pairs,
|
|
1570
|
+
into lvl, classes, pairs, mems from gone g;
|
|
1542
1571
|
all_ids := lvl;
|
|
1543
1572
|
while cardinality(lvl) > 0 loop
|
|
1544
1573
|
-- кто ссылается на уровень: действие по правилам концов (общее с превью)
|
|
@@ -1566,20 +1595,19 @@ begin
|
|
|
1566
1595
|
from d
|
|
1567
1596
|
join (select dd.class, dd.tenant from (select distinct x.class, x.tenant from d x) dd
|
|
1568
1597
|
where @SCHEMA@.history_on(dd.class, dd.tenant)) h on h.class = d.class and h.tenant = d.tenant
|
|
1598
|
+
order by d.id -- надгробия уровня в порядке id, как и на уровне 0
|
|
1569
1599
|
)
|
|
1570
1600
|
select coalesce(array_agg(d.id), '{}'), coalesce(array_agg(distinct d.class), '{}'),
|
|
1571
1601
|
coalesce(array_agg(distinct d.class || ' ' || d.tenant::text), '{}'),
|
|
1572
1602
|
jsonb_agg(to_jsonb(d)) filter (where not @SCHEMA@.acl_row_ok(dplan, d.class, d.id, d.owner, d.tenant, d.tags, d.data, d.links)),
|
|
1573
|
-
coalesce(array_agg(d.id) filter (where d.class = 'Account'), '{}'),
|
|
1574
1603
|
coalesce(jsonb_agg(jsonb_build_object('tenant', d.tenant, 'user', d.links->>'User')) filter (where d.class = 'member'), '[]'::jsonb)
|
|
1575
|
-
into lvl, lvl_classes, lvl_pairs, drows,
|
|
1604
|
+
into lvl, lvl_classes, lvl_pairs, drows, lvl_mems from d;
|
|
1576
1605
|
-- класс отказавшей строки называется, только если актор её видит (ревью безопасности №3)
|
|
1577
1606
|
if drows is not null then
|
|
1578
1607
|
denied := @SCHEMA@.visible_class(drows);
|
|
1579
1608
|
perform @SCHEMA@.lt_fail('acl_denied', case when denied is not null then format('нет права DELETE на класс %s в каскаде удаления', denied)
|
|
1580
1609
|
else 'нет права DELETE на строки в каскаде удаления' end);
|
|
1581
1610
|
end if;
|
|
1582
|
-
accs := accs || lvl_accs;
|
|
1583
1611
|
mems := mems || lvl_mems;
|
|
1584
1612
|
classes := classes || lvl_classes;
|
|
1585
1613
|
pairs := pairs || lvl_pairs;
|
|
@@ -1605,14 +1633,11 @@ begin
|
|
|
1605
1633
|
end loop;
|
|
1606
1634
|
-- выход из членства (и удаление аккаунта, которое снимает его членства): строки арендатора, которыми
|
|
1607
1635
|
-- владел участник, переходят арендатору (§2.10) — новой версией
|
|
1608
|
-
for r in select distinct (m->>'tenant')::uuid as tenant, (m->>'user')::uuid as usr from jsonb_array_elements(mems) m loop
|
|
1636
|
+
for r in select distinct (m->>'tenant')::uuid as tenant, (m->>'user')::uuid as usr from jsonb_array_elements(mems) m order by 1, 2 loop
|
|
1609
1637
|
perform @SCHEMA@.owner_to_tenant(r.tenant, r.usr);
|
|
1610
1638
|
end loop;
|
|
1611
|
-
-- удалённый
|
|
1612
|
-
|
|
1613
|
-
perform @SCHEMA@.auth_kill(r.acc, null);
|
|
1614
|
-
update @SCHEMA@.secret x set deleted_at = clock_timestamp() where x.account = r.acc and x.deleted_at is null;
|
|
1615
|
-
end loop;
|
|
1639
|
+
-- удалённый аккаунт (ревизия 7): сессии погасил с сигналом триггер до удаления (trg_entity_bd_account), его строки
|
|
1640
|
+
-- secret стёр каскад внешнего ключа secret.account — идентификаторы входа свободны
|
|
1616
1641
|
perform @SCHEMA@.signal(array_agg(split_part(x.p, ' ', 1)), array_agg(split_part(x.p, ' ', 2)::uuid))
|
|
1617
1642
|
from (select distinct p from unnest(pairs) p) x;
|
|
1618
1643
|
if classes && array['rule', 'Resource', 'member', 'Account', 'Class'] then
|
|
@@ -1700,6 +1725,9 @@ create or replace trigger entity_au after update on @SCHEMA@.entity referencing
|
|
|
1700
1725
|
for each statement when (current_user <> '@OWNER_NAME@') execute function @SCHEMA@.trg_entity_au();
|
|
1701
1726
|
create or replace trigger entity_bd before delete on @SCHEMA@.entity
|
|
1702
1727
|
for each statement when (current_user <> '@OWNER_NAME@') execute function @SCHEMA@.trg_entity_bd();
|
|
1728
|
+
-- удаление аккаунта — для всех ролей, в том числе владельца (каскад trg_entity_ad, account_purge): ревизия 7
|
|
1729
|
+
create or replace trigger entity_bd_account before delete on @SCHEMA@.entity
|
|
1730
|
+
for each row when (OLD.class = 'Account') execute function @SCHEMA@.trg_entity_bd_account();
|
|
1703
1731
|
create or replace trigger entity_ad after delete on @SCHEMA@.entity referencing old table as gone
|
|
1704
1732
|
for each statement when (current_user <> '@OWNER_NAME@') execute function @SCHEMA@.trg_entity_ad();
|
|
1705
1733
|
-- в режиме репликации (session_replication_role = replica) триггеры тоже работают
|
|
@@ -1708,6 +1736,7 @@ alter table @SCHEMA@.entity enable always trigger entity_bu;
|
|
|
1708
1736
|
alter table @SCHEMA@.entity enable always trigger entity_ai;
|
|
1709
1737
|
alter table @SCHEMA@.entity enable always trigger entity_au;
|
|
1710
1738
|
alter table @SCHEMA@.entity enable always trigger entity_bd;
|
|
1739
|
+
alter table @SCHEMA@.entity enable always trigger entity_bd_account;
|
|
1711
1740
|
alter table @SCHEMA@.entity enable always trigger entity_ad;
|
|
1712
1741
|
|
|
1713
1742
|
-- ========================= смена класса с ключом =========================
|
|
@@ -2030,7 +2059,7 @@ begin
|
|
|
2030
2059
|
'incoming_conflicts(uuid, text, uuid)', 'visible_detail(jsonb, uuid)', 'visible_class(jsonb)', 'class_change_safe(jsonb, jsonb)',
|
|
2031
2060
|
'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)',
|
|
2032
2061
|
'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)',
|
|
2033
|
-
'trg_entity_bd()', 'trg_entity_ad()', 'owner_to_tenant(uuid, uuid)', 'ref_action(jsonb, jsonb, uuid[])', 'delete_preview(uuid[])', 'merge(jsonb, jsonb)', 'inc(jsonb, text[], numeric, numeric)', 'links_patch(jsonb, jsonb)'] loop
|
|
2062
|
+
'trg_entity_bd()', 'trg_entity_bd_account()', 'trg_entity_ad()', 'owner_to_tenant(uuid, uuid)', 'ref_action(jsonb, jsonb, uuid[])', 'delete_preview(uuid[])', 'merge(jsonb, jsonb)', 'inc(jsonb, text[], numeric, numeric)', 'links_patch(jsonb, jsonb)'] loop
|
|
2034
2063
|
execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
|
|
2035
2064
|
execute format('revoke all on function @SCHEMA@.%s from public', f);
|
|
2036
2065
|
end loop;
|
package/sql/70-read.sql
CHANGED
|
@@ -53,9 +53,11 @@ as $$
|
|
|
53
53
|
where r.class = any (p_classes) and r.at <= coalesce(p_at, 'infinity'::timestamptz)) c
|
|
54
54
|
cross join lateral (
|
|
55
55
|
select y.* from (select x.* from @SCHEMA@.log x where x.id = c.id offset 0) y
|
|
56
|
-
where y.
|
|
56
|
+
where y.at <= coalesce(p_at, 'infinity'::timestamptz)
|
|
57
57
|
order by y.rev desc limit 1) w
|
|
58
|
-
|
|
58
|
+
-- класс — у последней версии на момент, а не среди версий классов шага: после смены класса без смены id объект
|
|
59
|
+
-- не числится в прежнем классе (план 2.0, 7.1 №1)
|
|
60
|
+
where w.class = any (p_classes) and @SCHEMA@.link_ids(w.links) @> array[p_src]
|
|
59
61
|
$$;
|
|
60
62
|
|
|
61
63
|
-- id строк классов p_classes: по вложению в data (GIN entity_data), по целям концов (GIN entity_ends),
|
|
@@ -134,12 +136,16 @@ begin
|
|
|
134
136
|
else
|
|
135
137
|
-- арендатор id неизменен, записи purge и trim несут арендатора объекта: условие до distinct on не меняет, какая
|
|
136
138
|
-- версия id последняя (индекс log_class_tenant_at)
|
|
139
|
+
-- кандидаты — по классу (индекс log_class_tenant_at), последняя версия id на момент — любого класса, и класс
|
|
140
|
+
-- проверяется у неё: иначе после смены класса без смены id объект числился бы в прежнем классе (план 2.0, 7.1 №1)
|
|
137
141
|
return query select v.id, v.rev from (
|
|
138
|
-
select distinct
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
select distinct l.id from @SCHEMA@.log l
|
|
143
|
+
where l.class = any (p_classes) and l.tenant = any (v_tenants) and l.at <= coalesce(p_at, 'infinity'::timestamptz)) c
|
|
144
|
+
cross join lateral (
|
|
145
|
+
select y.id, y.rev, y.op, y.class, y.tenant, y.owner, y.tags, y.data, y.links
|
|
146
|
+
from @SCHEMA@.log y where y.id = c.id and y.at <= coalesce(p_at, 'infinity'::timestamptz)
|
|
147
|
+
order by y.rev desc limit 1) v
|
|
148
|
+
where v.class = any (p_classes) and (v.op = 'delete') = p_dead and v.op not in ('purge', 'trim', 'reset')
|
|
143
149
|
and (p_data is null or v.data @> p_data) and (p_ends is null or @SCHEMA@.link_ids(v.links) @> p_ends)
|
|
144
150
|
and @SCHEMA@.row_visible(v.tenant, v.class, v.id, v.owner, v.tags, v.data, v.links, c_tenant, c_account, c_plan);
|
|
145
151
|
-- свои членства в других арендаторах: версии, ссылавшиеся на аккаунт (GIN log_ends)
|
package/sql/75-history.sql
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
-- change_paths — пути для колонки changed (пишут триггер правки и log_version);
|
|
12
12
|
-- row_leaves — листья снимка строки (то, что объясняет blame);
|
|
13
13
|
-- version_paths — пути из changed тех версий, у которых предыдущая версия видна вызывающему;
|
|
14
|
+
-- history_segments — отрезки истории объекта по переносам id (follow и blame);
|
|
14
15
|
-- diff_versions — что изменилось между двумя версиями или моментами (jsonb в виде ответа diff());
|
|
15
16
|
-- blame — какая версия, кто, когда и почему последней изменила каждый лист (ответ blame()).
|
|
16
17
|
|
|
@@ -154,21 +155,79 @@ begin
|
|
|
154
155
|
if c_account is null then
|
|
155
156
|
return;
|
|
156
157
|
end if;
|
|
158
|
+
-- обе версии — поиском по (id, rev) на каждую пару: lateral с limit 1 не даёт планировщику соединить журнал хэшем.
|
|
159
|
+
-- Свой план вызова (первые пять на соединении) по фильтру видимости оценивал версии n в 2 строки и искал
|
|
160
|
+
-- предыдущую версию по одному id, проверяя номер фильтром, — квадрат от числа версий: blame по 1000 версиям
|
|
161
|
+
-- ~250 мс, по 10 000 — секунды (план 2.0, попутно с 7.1 №2)
|
|
157
162
|
return query
|
|
158
163
|
select n.id, n.rev, n.changed
|
|
159
164
|
from unnest(p_ids, p_revs) q(id, rev)
|
|
160
|
-
join
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
cross join lateral (select x.id, x.rev, x.changed from @SCHEMA@.log x
|
|
166
|
+
where x.id = q.id and x.rev = q.rev and x.changed is not null and x.op not in ('purge', 'trim', 'reset')
|
|
167
|
+
and @SCHEMA@.row_visible(x.tenant, x.class, x.id, x.owner, x.tags, x.data, x.links, c_tenant, c_account, c_plan)
|
|
168
|
+
limit 1) n
|
|
169
|
+
cross join lateral (select 1 from @SCHEMA@.log o
|
|
170
|
+
where o.id = n.id and o.rev = n.rev - 1 and o.op not in ('purge', 'trim', 'reset')
|
|
171
|
+
and @SCHEMA@.row_visible(o.tenant, o.class, o.id, o.owner, o.tags, o.data, o.links, c_tenant, c_account, c_plan)
|
|
172
|
+
limit 1) o;
|
|
173
|
+
end $$;
|
|
174
|
+
|
|
175
|
+
-- Отрезки истории объекта (план 2.0, 7.1 №2): версии id с номерами (lo, hi], hi null — до последней. Границы
|
|
176
|
+
-- отрезков на id — переносы: надгробие переноса (delete с moved) кончает отрезок, первая версия на новом id (reclass,
|
|
177
|
+
-- rekey с moved) начинает. Объект, заведённый на прежнем id после переноса, — другой отрезок: follow и blame его не
|
|
178
|
+
-- берут (прежде брали все версии всех связанных id). Начало — отрезок с версией p_rev объекта p_id (n = 0), дальше по
|
|
179
|
+
-- переносам назад (n < 0) и, если не p_back, вперёд (n > 0). Перенос пишется парой строк с moved друг на друга
|
|
180
|
+
-- (надгробие на старом id → новый id, первая версия на новом → старый): строки переноса id находит индекс log_moved
|
|
181
|
+
-- через парную строку, без прохода по версиям id; у объекта без переносов — один пустой поиск. Строка, чья пара стёрта
|
|
182
|
+
-- (история другой стороны удалена целиком или обрезана), границей не считается. Пара одного переноса — ближайшая по
|
|
183
|
+
-- seq (время у них может разойтись на микросекунды). Тот же обход строит versions({ follow }) в построителе
|
|
184
|
+
-- (lib/src/sql.ts:buildRead): меняются вместе. plpgsql, а не sql: план запроса кэшируется (тело функции на sql
|
|
185
|
+
-- планировалось на каждом вызове).
|
|
186
|
+
create or replace function @SCHEMA@.history_segments(p_id uuid, p_rev int, p_back boolean default false)
|
|
187
|
+
returns table (id uuid, lo int, hi int, n int)
|
|
188
|
+
language plpgsql stable
|
|
189
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
190
|
+
as $$
|
|
191
|
+
#variable_conflict use_column
|
|
192
|
+
begin
|
|
193
|
+
return query with recursive s(id, lo, hi, n) as (
|
|
194
|
+
select p_id, b.lo, b.hi, 0 from (
|
|
195
|
+
select greatest(coalesce(max(x.rev) filter (where x.op = 'delete' and x.rev < p_rev), 0),
|
|
196
|
+
coalesce(max(x.rev - 1) filter (where x.op <> 'delete' and x.rev <= p_rev), 0)) as lo,
|
|
197
|
+
least(min(x.rev) filter (where x.op = 'delete' and x.rev >= p_rev),
|
|
198
|
+
min(x.rev - 1) filter (where x.op <> 'delete' and x.rev > p_rev)) as hi
|
|
199
|
+
from @SCHEMA@.log m join @SCHEMA@.log x on x.moved = m.id and x.id = p_id where m.moved = p_id) b
|
|
200
|
+
union
|
|
201
|
+
select g.id, b.lo, b.hi, g.n
|
|
202
|
+
from s cross join lateral (
|
|
203
|
+
-- назад: первая версия отрезка — прибытие с прежнего id; прежний отрезок кончается надгробием переноса сюда
|
|
204
|
+
select a.moved as id, (select t.rev from @SCHEMA@.log t where t.id = a.moved and t.op = 'delete' and t.moved = s.id
|
|
205
|
+
order by abs(t.seq - a.seq) limit 1) as r, s.n - 1 as n
|
|
206
|
+
from @SCHEMA@.log a
|
|
207
|
+
where s.n <= 0 and a.id = s.id and a.rev = s.lo + 1 and a.moved is not null and a.op <> 'delete'
|
|
208
|
+
union all
|
|
209
|
+
-- вперёд: последняя версия отрезка — надгробие переноса; следующий отрезок начинается прибытием отсюда
|
|
210
|
+
select t.moved, (select a.rev from @SCHEMA@.log a where a.id = t.moved and a.op <> 'delete' and a.moved = s.id
|
|
211
|
+
order by abs(a.seq - t.seq) limit 1), s.n + 1
|
|
212
|
+
from @SCHEMA@.log t
|
|
213
|
+
where not p_back and s.n >= 0 and t.id = s.id and t.rev = s.hi and t.op = 'delete' and t.moved is not null
|
|
214
|
+
) g
|
|
215
|
+
cross join lateral (
|
|
216
|
+
select greatest(coalesce(max(x.rev) filter (where x.op = 'delete' and x.rev < g.r), 0),
|
|
217
|
+
coalesce(max(x.rev - 1) filter (where x.op <> 'delete' and x.rev <= g.r), 0)) as lo,
|
|
218
|
+
least(min(x.rev) filter (where x.op = 'delete' and x.rev >= g.r),
|
|
219
|
+
min(x.rev - 1) filter (where x.op <> 'delete' and x.rev > g.r)) as hi
|
|
220
|
+
from @SCHEMA@.log m join @SCHEMA@.log x on x.moved = m.id and x.id = g.id where m.moved = g.id) b
|
|
221
|
+
where g.r is not null and abs(g.n) <= 1000
|
|
222
|
+
)
|
|
223
|
+
select s.id, s.lo, s.hi, s.n from s;
|
|
166
224
|
end $$;
|
|
167
225
|
|
|
168
226
|
-- diff: что изменилось между двумя версиями объекта (ответ терминала diff()). «После» — номер версии
|
|
169
227
|
-- p_to, последняя версия на момент p_to_at или последняя вообще; «до» — номер p_from, последняя живая
|
|
170
228
|
-- версия на момент p_from_at (объекта не было — null, все пути added) или версия перед «после».
|
|
171
|
-
-- p_follow — через смену id (reclass, rekey) по колонке moved
|
|
229
|
+
-- p_follow — через смену id (reclass, rekey) по колонке moved: отрезки истории этого объекта (history_segments) от
|
|
230
|
+
-- версии p_to или последней версии p_id; надгробие переноса — не версия.
|
|
172
231
|
-- «После» — надгробие: to.deleted, изменения полей не перечисляются.
|
|
173
232
|
create or replace function @SCHEMA@.diff_versions(p_id uuid, p_from int default null, p_to int default null,
|
|
174
233
|
p_from_at timestamptz default null, p_to_at timestamptz default null, p_follow boolean default false)
|
|
@@ -181,21 +240,26 @@ set jit = off
|
|
|
181
240
|
as $$
|
|
182
241
|
declare
|
|
183
242
|
ids uuid[] := array[p_id];
|
|
243
|
+
los int[] := array[0];
|
|
244
|
+
his int[] := array[null::int];
|
|
245
|
+
multi boolean := false;
|
|
246
|
+
whole boolean := true;
|
|
184
247
|
t record;
|
|
185
248
|
f record;
|
|
186
249
|
has_f boolean;
|
|
187
250
|
begin
|
|
188
251
|
if p_follow then
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
union select l.id from @SCHEMA@.log l where l.moved = g.id) x)
|
|
195
|
-
select array_agg(g.id) into ids from g;
|
|
252
|
+
select array_agg(s.id), array_agg(s.lo), array_agg(s.hi) into ids, los, his
|
|
253
|
+
from (select distinct g.id, g.lo, g.hi from @SCHEMA@.history_segments(p_id, coalesce(p_to, (select max(l.rev) from @SCHEMA@.log l
|
|
254
|
+
where l.id = p_id and l.op not in ('purge', 'trim', 'reset')))) g) s;
|
|
255
|
+
multi := cardinality(ids) > (select count(distinct x) from unnest(ids) x);
|
|
256
|
+
whole := cardinality(ids) = 1 and los[1] = 0 and his[1] is null;
|
|
196
257
|
end if;
|
|
197
258
|
select l.id, l.rev, l.at, l.seq, l.op, l.class, l.data, l.links, l.tags, l.owner into t from @SCHEMA@.log l
|
|
198
259
|
where l.id = any (ids) and l.op not in ('purge', 'trim', 'reset') and not (p_follow and l.op = 'delete' and l.moved is not null)
|
|
260
|
+
and case when whole then true when multi then exists (select 1 from unnest(ids, los, his) s(id, lo, hi) where s.id = l.id and l.rev > s.lo
|
|
261
|
+
and l.rev <= coalesce(s.hi, 2147483647))
|
|
262
|
+
else l.rev > los[array_position(ids, l.id)] and l.rev <= coalesce(his[array_position(ids, l.id)], 2147483647) end
|
|
199
263
|
and (p_to is null or (l.id = p_id and l.rev = p_to)) and (p_to_at is null or l.at <= p_to_at)
|
|
200
264
|
order by l.at desc, l.seq desc limit 1;
|
|
201
265
|
if t.id is null then
|
|
@@ -208,6 +272,9 @@ begin
|
|
|
208
272
|
elsif p_from_at is not null then
|
|
209
273
|
select l.id, l.rev, l.at, l.op, l.class, l.data, l.links, l.tags, l.owner into f from @SCHEMA@.log l
|
|
210
274
|
where l.id = any (ids) and l.op not in ('purge', 'trim', 'reset') and not (p_follow and l.op = 'delete' and l.moved is not null)
|
|
275
|
+
and case when whole then true when multi then exists (select 1 from unnest(ids, los, his) s(id, lo, hi) where s.id = l.id and l.rev > s.lo
|
|
276
|
+
and l.rev <= coalesce(s.hi, 2147483647))
|
|
277
|
+
else l.rev > los[array_position(ids, l.id)] and l.rev <= coalesce(his[array_position(ids, l.id)], 2147483647) end
|
|
211
278
|
and l.at <= p_from_at
|
|
212
279
|
order by l.at desc, l.seq desc limit 1;
|
|
213
280
|
-- срез на момент: удалённого объекта тогда не было
|
|
@@ -215,6 +282,9 @@ begin
|
|
|
215
282
|
else
|
|
216
283
|
select l.id, l.rev, l.at, l.op, l.class, l.data, l.links, l.tags, l.owner into f from @SCHEMA@.log l
|
|
217
284
|
where l.id = any (ids) and l.op not in ('purge', 'trim', 'reset') and not (p_follow and l.op = 'delete' and l.moved is not null)
|
|
285
|
+
and case when whole then true when multi then exists (select 1 from unnest(ids, los, his) s(id, lo, hi) where s.id = l.id and l.rev > s.lo
|
|
286
|
+
and l.rev <= coalesce(s.hi, 2147483647))
|
|
287
|
+
else l.rev > los[array_position(ids, l.id)] and l.rev <= coalesce(his[array_position(ids, l.id)], 2147483647) end
|
|
218
288
|
and (l.at, l.seq) < (t.at, t.seq)
|
|
219
289
|
order by l.at desc, l.seq desc limit 1;
|
|
220
290
|
has_f := f.id is not null;
|
|
@@ -241,7 +311,9 @@ end $$;
|
|
|
241
311
|
-- blame: для каждого листа версии (последней на момент p_at или текущей) — версия, которая последней его
|
|
242
312
|
-- изменила, с автором, агентом, видом операции и причиной (ответ терминала blame()). Просматривается не
|
|
243
313
|
-- больше p_limit последних версий, назад через смену id (первая версия нового id указывает на прежний
|
|
244
|
-
-- колонкой moved)
|
|
314
|
+
-- колонкой moved) — по отрезкам истории этого объекта (history_segments): объект, что жил на этом id до переноса
|
|
315
|
+
-- или заведён на прежнем id после, — не его история; начало истории — первая версия самого раннего отрезка (у
|
|
316
|
+
-- объекта без переносов — версия 1). Пропуск номера версии (прореживание, обрезка, невидимая версия) — выпавшая версия могла
|
|
245
317
|
-- изменить лист и вернуть значение, поэтому лист точен (exact), только если от текущей версии до найденной
|
|
246
318
|
-- пропусков нет; лист, не менявшийся в просмотренной истории, принадлежит самой старой просмотренной версии —
|
|
247
319
|
-- exact, только если это версия 1 и пропусков нет вовсе. complete — история просмотрена целиком и без пропусков.
|
|
@@ -254,9 +326,12 @@ set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
|
254
326
|
set jit = off
|
|
255
327
|
as $$
|
|
256
328
|
declare
|
|
257
|
-
ids uuid[]
|
|
329
|
+
ids uuid[];
|
|
330
|
+
los int[];
|
|
331
|
+
his int[];
|
|
332
|
+
multi boolean;
|
|
333
|
+
whole boolean;
|
|
258
334
|
t record;
|
|
259
|
-
prev uuid;
|
|
260
335
|
lim int := greatest(coalesce(p_limit, 1000), 1);
|
|
261
336
|
res jsonb;
|
|
262
337
|
begin
|
|
@@ -264,12 +339,11 @@ begin
|
|
|
264
339
|
where l.id = p_id and l.op not in ('purge', 'trim', 'reset') and (p_at is null or l.at <= p_at)
|
|
265
340
|
order by l.rev desc limit 1;
|
|
266
341
|
if t.id is null then return null; end if;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
end loop;
|
|
342
|
+
-- отрезки от самого раннего: ids[1], los[1] — начало истории объекта
|
|
343
|
+
select array_agg(s.id order by s.n), array_agg(s.lo order by s.n), array_agg(s.hi order by s.n) into ids, los, his
|
|
344
|
+
from (select g.id, g.lo, g.hi, min(g.n) as n from @SCHEMA@.history_segments(p_id, t.rev, true) g group by 1, 2, 3) s;
|
|
345
|
+
multi := cardinality(ids) > (select count(distinct x) from unnest(ids) x);
|
|
346
|
+
whole := cardinality(ids) = 1 and los[1] = 0 and his[1] is null;
|
|
273
347
|
with v as (
|
|
274
348
|
-- соседняя более старая версия — окном (lead), а не соединением CTE с собой: соединение шло бы вложенным
|
|
275
349
|
-- циклом по квадрату числа версий
|
|
@@ -279,6 +353,9 @@ begin
|
|
|
279
353
|
lead(l.owner) over o as o_owner, lead(l.class) over o as o_class
|
|
280
354
|
from @SCHEMA@.log l
|
|
281
355
|
where l.id = any (ids) and l.op not in ('purge', 'trim', 'reset') and not (l.op = 'delete' and l.moved is not null)
|
|
356
|
+
and case when whole then true when multi then exists (select 1 from unnest(ids, los, his) s(id, lo, hi) where s.id = l.id and l.rev > s.lo
|
|
357
|
+
and l.rev <= coalesce(s.hi, 2147483647))
|
|
358
|
+
else l.rev > los[array_position(ids, l.id)] and l.rev <= coalesce(his[array_position(ids, l.id)], 2147483647) end
|
|
282
359
|
and (l.at, l.seq) <= (t.at, t.seq)
|
|
283
360
|
window o as (order by l.at desc, l.seq desc)
|
|
284
361
|
order by l.at desc, l.seq desc limit lim + 1
|
|
@@ -312,14 +389,15 @@ begin
|
|
|
312
389
|
from leaves lv join ch on lv.path[1:cardinality(ch.path)] = ch.path or ch.path[1:cardinality(lv.path)] = lv.path
|
|
313
390
|
group by lv.path
|
|
314
391
|
), oldest as (
|
|
315
|
-
|
|
392
|
+
-- first — самая старая версия окна и есть начало истории объекта
|
|
393
|
+
select w.n, w.rev, w.id = ids[1] and w.rev = los[1] + 1 as first from w order by w.n desc limit 1
|
|
316
394
|
), cut as (
|
|
317
395
|
select count(*) > lim as cut from v
|
|
318
396
|
), fields as (
|
|
319
397
|
select lv.path,
|
|
320
398
|
jsonb_build_object('path', to_jsonb(lv.path), 'rev', cv.rev, 'at', cv.at, 'author', cv.author, 'op', cv.op,
|
|
321
399
|
'exact', case when h.n is not null then not exists (select 1 from gaps g where g.n <= h.n)
|
|
322
|
-
else (select o.
|
|
400
|
+
else (select o.first from oldest o) and not (select c.cut from cut c) and not exists (select 1 from gaps) end)
|
|
323
401
|
|| case when cv.id <> p_id then jsonb_build_object('id', cv.id) else '{}'::jsonb end
|
|
324
402
|
|| case when cv.agent is not null then jsonb_build_object('agent', cv.agent) else '{}'::jsonb end
|
|
325
403
|
|| case when cv.reason is not null then jsonb_build_object('reason', cv.reason) else '{}'::jsonb end as obj
|
|
@@ -328,7 +406,7 @@ begin
|
|
|
328
406
|
join w cv on cv.n = coalesce(h.n, (select o.n from oldest o))
|
|
329
407
|
)
|
|
330
408
|
select jsonb_build_object('id', p_id, 'class', t.class, 'rev', t.rev,
|
|
331
|
-
'complete', (select o.
|
|
409
|
+
'complete', (select o.first from oldest o) and not (select c.cut from cut c) and not exists (select 1 from gaps),
|
|
332
410
|
'fields', coalesce((select jsonb_agg(f.obj order by f.path) from fields f), '[]'::jsonb))
|
|
333
411
|
|| case when t.op = 'delete' then '{"deleted": true}'::jsonb else '{}'::jsonb end
|
|
334
412
|
into res;
|
|
@@ -342,7 +420,7 @@ begin
|
|
|
342
420
|
'jsonb_changes(jsonb, jsonb, text[])',
|
|
343
421
|
'row_changes(jsonb, jsonb, text[], uuid, text, jsonb, jsonb, text[], uuid, text)',
|
|
344
422
|
'change_paths(jsonb, jsonb, text[], uuid, text, jsonb, jsonb, text[], uuid, text)',
|
|
345
|
-
'row_leaves(jsonb, jsonb, text[], uuid, text)', 'version_paths(uuid[], int[])',
|
|
423
|
+
'row_leaves(jsonb, jsonb, text[], uuid, text)', 'version_paths(uuid[], int[])', 'history_segments(uuid, int, boolean)',
|
|
346
424
|
'diff_versions(uuid, int, int, timestamptz, timestamptz, boolean)', 'blame(uuid, timestamptz, int)'] loop
|
|
347
425
|
execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
|
|
348
426
|
execute format('revoke all on function @SCHEMA@.%s from public', f);
|
|
@@ -354,5 +432,5 @@ grant execute on function @SCHEMA@.jsonb_changes(jsonb, jsonb, text[]),
|
|
|
354
432
|
@SCHEMA@.row_changes(jsonb, jsonb, text[], uuid, text, jsonb, jsonb, text[], uuid, text),
|
|
355
433
|
@SCHEMA@.change_paths(jsonb, jsonb, text[], uuid, text, jsonb, jsonb, text[], uuid, text),
|
|
356
434
|
@SCHEMA@.row_leaves(jsonb, jsonb, text[], uuid, text), @SCHEMA@.version_paths(uuid[], int[]),
|
|
357
|
-
@SCHEMA@.diff_versions(uuid, int, int, timestamptz, timestamptz, boolean),
|
|
435
|
+
@SCHEMA@.history_segments(uuid, int, boolean), @SCHEMA@.diff_versions(uuid, int, int, timestamptz, timestamptz, boolean),
|
|
358
436
|
@SCHEMA@.blame(uuid, timestamptz, int) to @APP@;
|
package/sql/80-auth.sql
CHANGED
|
@@ -755,7 +755,8 @@ declare
|
|
|
755
755
|
s record;
|
|
756
756
|
begin
|
|
757
757
|
if nullif(current_setting('letopis.account', true), '') is not null then
|
|
758
|
-
|
|
758
|
+
-- код из каталога базы (invalid_query в нём нет — lt_fail дал бы LT000 и сырую ошибку; план 2.0, 7.5 №3)
|
|
759
|
+
perform @SCHEMA@.lt_fail('acl_denied', 'auth.switch — для собственной сессии, не при имперсонации');
|
|
759
760
|
end if;
|
|
760
761
|
select x.id, x.account into s from @SCHEMA@.secret x
|
|
761
762
|
where x.kind = 'SESSION' and x.deleted_at is null and x.material = sha256(convert_to(coalesce(tok, ''), 'UTF8'))
|
|
@@ -795,6 +796,43 @@ begin
|
|
|
795
796
|
where m.class = 'member' and m.links->>'User' = c.account::text and @SCHEMA@.auth_enabled(m.tenant);
|
|
796
797
|
end $$;
|
|
797
798
|
|
|
799
|
+
-- Сведения о сессии по токену (db.auth.session): сервис, у которого есть токен пользователя (например, из
|
|
800
|
+
-- cookie), узнаёт, чья это сессия, жива ли она и в каком арендаторе работает, не подключаясь по этому токену.
|
|
801
|
+
-- Право auth.session, как у issue_session. Неверный, погашенный или истёкший токен и отключённый аккаунт — null:
|
|
802
|
+
-- причина не различается, как в ctx(). Сессия ищется только среди SESSION: ключ API или код с тем же sha256 не
|
|
803
|
+
-- подойдут. member — проверка членства из ctx() без исключения: членство в рабочем арендаторе цело и арендатор
|
|
804
|
+
-- включён; собственный арендатор — всегда true.
|
|
805
|
+
create or replace function @SCHEMA@.auth_session_info(p_token text) returns jsonb
|
|
806
|
+
language plpgsql stable security definer
|
|
807
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
808
|
+
as $$
|
|
809
|
+
declare
|
|
810
|
+
c record;
|
|
811
|
+
s record;
|
|
812
|
+
t uuid;
|
|
813
|
+
begin
|
|
814
|
+
select * into c from @SCHEMA@.ctx();
|
|
815
|
+
perform @SCHEMA@.auth_session_required(c.account);
|
|
816
|
+
perform @SCHEMA@.acl_require('auth.session');
|
|
817
|
+
select x.account, x.meta, x.expires_at into s from @SCHEMA@.secret x
|
|
818
|
+
where x.kind = 'SESSION' and x.deleted_at is null and x.material = sha256(convert_to(coalesce(p_token, ''), 'UTF8'))
|
|
819
|
+
and (x.expires_at is null or x.expires_at > clock_timestamp());
|
|
820
|
+
if s.account is null or not @SCHEMA@.auth_enabled(s.account) then
|
|
821
|
+
return null;
|
|
822
|
+
end if;
|
|
823
|
+
t := coalesce((s.meta->>'tenant')::uuid, s.account);
|
|
824
|
+
return jsonb_build_object(
|
|
825
|
+
'account', s.account,
|
|
826
|
+
'tenant', t,
|
|
827
|
+
'member', t = s.account or (
|
|
828
|
+
exists (select 1 from @SCHEMA@.entity x
|
|
829
|
+
where x.id = @SCHEMA@.v5_id(t, 'member', array[s.account::text, t::text]) and x.class = 'member' and x.tenant = t)
|
|
830
|
+
and @SCHEMA@.auth_enabled(t)),
|
|
831
|
+
'expiresAt', s.expires_at,
|
|
832
|
+
'loginAt', s.meta->'login_at',
|
|
833
|
+
'methods', coalesce(s.meta->'methods', '[]'::jsonb));
|
|
834
|
+
end $$;
|
|
835
|
+
|
|
798
836
|
do $$
|
|
799
837
|
declare f text;
|
|
800
838
|
begin
|
|
@@ -807,7 +845,7 @@ begin
|
|
|
807
845
|
'login_otp(text, text, text)', 'totp_code(bytea, bigint)', 'auth_enroll_totp(uuid)', 'auth_verify_totp(uuid, text, int)',
|
|
808
846
|
'auth_totp_enabled(uuid)', 'auth_link(uuid, text, text, jsonb, boolean)', 'auth_lookup(text, text, boolean)',
|
|
809
847
|
'auth_credentials(uuid)', 'auth_revoke_credential(uuid, boolean)', 'auth_set_flags(uuid, boolean, boolean)',
|
|
810
|
-
'auth_refresh(interval)', 'auth_logout()', 'auth_logout_all(uuid)', 'auth_switch(uuid)', 'auth_tenants()'] loop
|
|
848
|
+
'auth_refresh(interval)', 'auth_logout()', 'auth_logout_all(uuid)', 'auth_switch(uuid)', 'auth_tenants()', 'auth_session_info(text)'] loop
|
|
811
849
|
execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
|
|
812
850
|
execute format('revoke all on function @SCHEMA@.%s from public', f);
|
|
813
851
|
end loop;
|
|
@@ -824,4 +862,4 @@ grant execute on function
|
|
|
824
862
|
@SCHEMA@.auth_totp_enabled(uuid), @SCHEMA@.auth_link(uuid, text, text, jsonb, boolean), @SCHEMA@.auth_lookup(text, text, boolean),
|
|
825
863
|
@SCHEMA@.auth_credentials(uuid), @SCHEMA@.auth_revoke_credential(uuid, boolean), @SCHEMA@.auth_set_flags(uuid, boolean, boolean),
|
|
826
864
|
@SCHEMA@.auth_refresh(interval), @SCHEMA@.auth_logout(), @SCHEMA@.auth_logout_all(uuid), @SCHEMA@.auth_switch(uuid),
|
|
827
|
-
@SCHEMA@.auth_tenants() to @APP@;
|
|
865
|
+
@SCHEMA@.auth_tenants(), @SCHEMA@.auth_session_info(text) to @APP@;
|
package/sql/88-derived.sql
CHANGED
|
@@ -25,7 +25,8 @@ begin
|
|
|
25
25
|
for update;
|
|
26
26
|
if not found then
|
|
27
27
|
update @SCHEMA@.secret s set deleted_at = clock_timestamp()
|
|
28
|
-
where s.kind = 'PROJECTOR' and casefold(s.ident) = casefold(p_name)
|
|
28
|
+
where s.kind = 'PROJECTOR' and casefold(s.ident) collate pg_unicode_fast = casefold(p_name) collate pg_unicode_fast
|
|
29
|
+
and s.account <> '@SYSTEM@'::uuid and s.deleted_at is null;
|
|
29
30
|
insert into @SCHEMA@.secret (account, kind, ident, meta) values ('@SYSTEM@'::uuid, 'PROJECTOR', p_name, '{}'::jsonb);
|
|
30
31
|
return '{}'::jsonb;
|
|
31
32
|
end if;
|
|
@@ -272,6 +273,7 @@ begin
|
|
|
272
273
|
from g
|
|
273
274
|
join (select d.class, d.tenant from (select distinct y.class, y.tenant from g y) d
|
|
274
275
|
where @SCHEMA@.history_on(d.class, d.tenant)) h on h.class = g.class and h.tenant = g.tenant
|
|
276
|
+
order by g.id -- надгробия в порядке id, как в trg_entity_ad
|
|
275
277
|
)
|
|
276
278
|
select count(*)::int, coalesce(array_agg(distinct g.class || ' ' || g.tenant::text), '{}') into n, pairs from g;
|
|
277
279
|
if n > 0 then
|