letopis 1.4.1 → 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 +5 -2
- package/AGENT-CHEATSHEET.md +5 -2
- package/CHANGELOG.md +140 -1
- package/README.en.md +66 -15
- package/README.md +62 -12
- package/dist/admin.js +11 -2
- package/dist/cache.js +3 -2
- package/dist/chain.d.ts +2 -1
- package/dist/chain.js +9 -3
- package/dist/cli.js +10 -2
- package/dist/errors.js +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +57 -9
- package/dist/internal.d.ts +7 -0
- package/dist/internal.js +5 -0
- package/dist/load.js +20 -7
- package/dist/mcp.js +4 -2
- package/dist/model.js +7 -1
- package/dist/project.d.ts +9 -0
- package/dist/project.js +45 -11
- package/dist/signals.d.ts +4 -4
- package/dist/signals.js +4 -4
- package/dist/sql.d.ts +6 -5
- package/dist/sql.js +100 -23
- package/dist/sync.js +11 -4
- package/dist/tx.d.ts +15 -0
- package/dist/tx.js +12 -3
- package/dist/types.d.ts +8 -1
- package/dist/up.js +5 -0
- package/dist/watch.d.ts +12 -0
- package/dist/watch.js +24 -7
- package/dist/write.js +23 -9
- package/package.json +8 -4
- package/sql/60-write.sql +9 -0
- package/sql/70-read.sql +13 -7
- package/sql/75-history.sql +106 -28
- package/sql/80-auth.sql +2 -1
- package/sql/88-derived.sql +2 -1
- package/sql/90-time.sql +106 -28
package/sql/90-time.sql
CHANGED
|
@@ -389,9 +389,12 @@ begin
|
|
|
389
389
|
end if;
|
|
390
390
|
end loop;
|
|
391
391
|
end if;
|
|
392
|
+
-- по классам: последняя версия id — любого класса, класс проверяется у неё (план 2.0, 7.1 №1)
|
|
392
393
|
for r in select distinct on (l.id) l.* from @SCHEMA@.log l
|
|
393
|
-
where l.tenant = c.tenant and (case when p_ids is not null then l.id = any (p_ids)
|
|
394
|
+
where l.tenant = c.tenant and (case when p_ids is not null then l.id = any (p_ids)
|
|
395
|
+
else l.id in (select x.id from @SCHEMA@.log x where x.tenant = c.tenant and x.class = any (p_classes)) end)
|
|
394
396
|
order by l.id, l.rev desc loop
|
|
397
|
+
continue when p_ids is null and not (r.class = any (p_classes));
|
|
395
398
|
continue when r.op in ('purge', 'reset')
|
|
396
399
|
or not @SCHEMA@.row_visible(r.tenant, r.class, r.id, r.owner, r.tags, r.data, r.links, c.tenant, c.account, rd->'plan');
|
|
397
400
|
if r.class in ('Class', 'Account', 'member', 'Resource', 'rule') then
|
|
@@ -797,7 +800,9 @@ end $$;
|
|
|
797
800
|
-- ========================= обслуживание =========================
|
|
798
801
|
|
|
799
802
|
-- Одна пачка прореживания журнала (§2.11): версии, которые не нужны действующей политике хранения
|
|
800
|
-
-- своего
|
|
803
|
+
-- своего класса — как его резолвит class_row: свой класс арендатора, иначе System; у арендатора со своим одноимённым
|
|
804
|
+
-- классом политика класса System не действует (план 2.0, 7.3 №1: класс System, заведённый позже, дублировал строки
|
|
805
|
+
-- журнала в отборе и стирал промежуточные версии). Не трогаются версия 1, текущая и версии особых видов (create, delete, reclass,
|
|
801
806
|
-- rekey, restore, purge, reset, trim); история удалённого объекта, надгробие которого старше
|
|
802
807
|
-- последнего яруса со сроком, стирается целиком. Пачка — до 100 объектов после курсора (meta
|
|
803
808
|
-- строки ключа: участки по порядку id), не больше p_limit версий; объект, не вошедший целиком,
|
|
@@ -827,32 +832,60 @@ begin
|
|
|
827
832
|
)
|
|
828
833
|
select array_agg(q.id order by q.id) into ids from (
|
|
829
834
|
select distinct l.id from @SCHEMA@.log l
|
|
830
|
-
join pol on pol.p is not null and pol.cls = l.class
|
|
835
|
+
join pol on pol.p is not null and pol.cls = l.class
|
|
836
|
+
and (pol.ct = l.tenant
|
|
837
|
+
or pol.ct = '@SYSTEM@'::uuid
|
|
838
|
+
and not exists (select 1 from @SCHEMA@.entity y
|
|
839
|
+
where y.class = 'Class' and y.tenant = l.tenant and y.data->>'name' = l.class))
|
|
831
840
|
where l.at < p_now - @SCHEMA@.history_iv(pol.p->>'all') and (cur is null or l.id > cur)
|
|
832
841
|
order by l.id limit 100) q;
|
|
833
842
|
if ids is null then
|
|
834
843
|
update @SCHEMA@.secret s set meta = s.meta - 'thin' where s.id = '@SYSTEM@'::uuid;
|
|
835
844
|
return jsonb_build_object('thinned', 0, 'more', false);
|
|
836
845
|
end if;
|
|
846
|
+
-- последняя запись каждого id пачки — for update: restore(), purge() и новая версия того же id берут её же (инвариант
|
|
847
|
+
-- M-TIME); отбор ниже видит то, что они успели зафиксировать, и не стирает историю объекта, который только что
|
|
848
|
+
-- восстановили (план 2.0, 7.3 №3)
|
|
849
|
+
perform 1 from @SCHEMA@.log l
|
|
850
|
+
where l.id = any (ids) and not exists (select 1 from @SCHEMA@.log z where z.id = l.id and z.rev > l.rev)
|
|
851
|
+
order by l.id
|
|
852
|
+
for update of l;
|
|
837
853
|
with pol as (
|
|
838
854
|
select x.data->>'name' as cls, x.tenant as ct, @SCHEMA@.history_policy(x.data->'resolved') as p
|
|
839
855
|
from @SCHEMA@.entity x where x.class = 'Class'
|
|
840
856
|
), v as (
|
|
841
|
-
|
|
857
|
+
-- все версии id, любого класса: последняя версия и следующая по времени — по всей цепочке (план 2.0, 7.3 №2: после
|
|
858
|
+
-- reclass версии прежнего класса без политики выпадали из отбора и переживали стирание истории удалённого)
|
|
859
|
+
select l.id, l.rev, l.at, l.op, l.class, l.tenant,
|
|
842
860
|
max(l.rev) over (partition by l.id) as last_rev,
|
|
843
861
|
first_value(l.op) over (partition by l.id order by l.rev desc) as last_op,
|
|
844
862
|
first_value(l.at) over (partition by l.id order by l.rev desc) as last_at,
|
|
863
|
+
first_value(l.class) over (partition by l.id order by l.rev desc) as last_class,
|
|
845
864
|
lead(l.at) over (partition by l.id order by l.rev) as next_at
|
|
846
865
|
from @SCHEMA@.log l
|
|
847
|
-
join pol on pol.p is not null and pol.cls = l.class and (pol.ct = l.tenant or pol.ct = '@SYSTEM@'::uuid)
|
|
848
866
|
where l.id = any (ids)
|
|
867
|
+
), vp as (
|
|
868
|
+
-- прореживание версии — по политике её класса; стирание удалённого целиком — по политике класса надгробия
|
|
869
|
+
select v.*, pv.p, coalesce(pv.p->>'tz', 'UTC') as tz, pl.p as lp, coalesce(pl.p->>'tz', 'UTC') as ltz
|
|
870
|
+
from v
|
|
871
|
+
left join pol pv on pv.cls = v.class
|
|
872
|
+
and (pv.ct = v.tenant
|
|
873
|
+
or pv.ct = '@SYSTEM@'::uuid
|
|
874
|
+
and not exists (select 1 from @SCHEMA@.entity y
|
|
875
|
+
where y.class = 'Class' and y.tenant = v.tenant and y.data->>'name' = v.class))
|
|
876
|
+
left join pol pl on pl.cls = v.last_class
|
|
877
|
+
and (pl.ct = v.tenant
|
|
878
|
+
or pl.ct = '@SYSTEM@'::uuid
|
|
879
|
+
and not exists (select 1 from @SCHEMA@.entity y
|
|
880
|
+
where y.class = 'Class' and y.tenant = v.tenant and y.data->>'name' = v.last_class))
|
|
849
881
|
)
|
|
850
|
-
select array_agg(
|
|
851
|
-
from
|
|
852
|
-
where (
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
and not
|
|
882
|
+
select array_agg(vp.id order by vp.id, vp.rev), array_agg(vp.rev order by vp.id, vp.rev) into vid, vrev
|
|
883
|
+
from vp
|
|
884
|
+
where (vp.last_op = 'delete' and vp.lp is not null
|
|
885
|
+
and timezone(vp.ltz, vp.last_at) <= timezone(vp.ltz, p_now) - @SCHEMA@.history_horizon(vp.lp))
|
|
886
|
+
or (vp.p is not null and vp.rev <> 1 and vp.rev <> vp.last_rev
|
|
887
|
+
and vp.op not in ('create', 'delete', 'reclass', 'rekey', 'restore', 'purge', 'reset', 'trim')
|
|
888
|
+
and not @SCHEMA@.history_keep(vp.p, timezone(vp.tz, vp.at), timezone(vp.tz, p_now), timezone(vp.tz, vp.next_at)));
|
|
856
889
|
total := coalesce(cardinality(vid), 0);
|
|
857
890
|
k := least(total, p_limit);
|
|
858
891
|
if k > 0 then
|
|
@@ -875,11 +908,15 @@ begin
|
|
|
875
908
|
return jsonb_build_object('thinned', k, 'more', more);
|
|
876
909
|
end $$;
|
|
877
910
|
|
|
878
|
-
-- Обслуживание (этап 6, п. 8): истёкшие и погашенные сессии и одноразовые коды, одна
|
|
879
|
-
-- прореживания журнала, заполнение очереди сигналов. Вызывает системный администратор, роль с
|
|
911
|
+
-- Обслуживание (этап 6, п. 8): истёкшие и погашенные сессии и одноразовые коды, строки-замки удалённых семейств, одна
|
|
912
|
+
-- пачка прореживания журнала, заполнение очереди сигналов. Вызывает системный администратор, роль с
|
|
880
913
|
-- правом letopis.maintain или администраторское подключение. p_now — момент «сейчас» для
|
|
881
|
-
-- прореживания, только при allow_reset (тесты): иначе одним вызовом можно было бы проредить всю историю.
|
|
882
|
-
|
|
914
|
+
-- прореживания, только при allow_reset (тесты): иначе одним вызовом можно было бы проредить всю историю. p_cleanup —
|
|
915
|
+
-- удалять ли истёкшие сессии, коды и строки-замки: библиотека передаёт true только в первой пачке вызова, иначе каждая
|
|
916
|
+
-- из ≤100 пачек заново просматривала бы secret (план 2.0, 7.3 №4); прежний вызов без третьего аргумента чистит, как раньше.
|
|
917
|
+
drop function if exists @SCHEMA@.maintain(timestamptz, int);
|
|
918
|
+
create or replace function @SCHEMA@.maintain(p_now timestamptz default null, p_limit int default 10000,
|
|
919
|
+
p_cleanup boolean default true) returns jsonb
|
|
883
920
|
language plpgsql volatile security definer
|
|
884
921
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
885
922
|
as $$
|
|
@@ -887,6 +924,7 @@ declare
|
|
|
887
924
|
ts timestamptz := clock_timestamp();
|
|
888
925
|
s int;
|
|
889
926
|
o int;
|
|
927
|
+
fam text := '@SCHEMA_NAME@:family:';
|
|
890
928
|
begin
|
|
891
929
|
if not @SCHEMA@.letopis_allowed('letopis.maintain') then
|
|
892
930
|
perform @SCHEMA@.lt_fail('acl_denied', 'нет права letopis.maintain');
|
|
@@ -902,14 +940,41 @@ begin
|
|
|
902
940
|
end if;
|
|
903
941
|
ts := p_now;
|
|
904
942
|
end if;
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
943
|
+
s := 0;
|
|
944
|
+
o := 0;
|
|
945
|
+
if coalesce(p_cleanup, true) then
|
|
946
|
+
with d as (delete from @SCHEMA@.secret x where x.kind = 'SESSION'
|
|
947
|
+
and (x.deleted_at is not null or x.expires_at <= clock_timestamp()) returning 1)
|
|
948
|
+
select count(*)::int into s from d;
|
|
949
|
+
-- коды всех видов, которые выпускает auth_issue_otp: OTP и OTP_<имя> (тот же шаблон вида), — использованные,
|
|
950
|
+
-- заменённые, исчерпавшие попытки (deleted_at) и истёкшие
|
|
951
|
+
with d as (delete from @SCHEMA@.secret x where x.kind ~ '^OTP(_[A-Z0-9]{1,27})?$'
|
|
952
|
+
and (x.deleted_at is not null or x.expires_at <= clock_timestamp()) returning 1)
|
|
953
|
+
select count(*)::int into o from d;
|
|
954
|
+
-- строки-замки семейств, чьего корня больше нет — удалён класс или его арендатор (план 2.0, 7.7 №8, долгий
|
|
955
|
+
-- прогон): их заранее заводит запись строки Class (family_rows), а сами они не уходили — заведение и удаление
|
|
956
|
+
-- классов копило их без предела. Корень жив — строка Class с этим именем у арендатора семейства (family_name;
|
|
957
|
+
-- в имени класса двоеточия нет). Занятую строку не трогаем (skip locked), а строку-замок семейства — только вместе
|
|
958
|
+
-- с её шлюзом или без шлюза: исключительная блокировка берёт шлюз первым, и строка-замок, до которой она ещё не
|
|
959
|
+
-- дошла, у неё не пропадёт. Пропавшую строку lock_row и family_lock заводят заново, если класс с этим именем
|
|
960
|
+
-- появится снова.
|
|
961
|
+
with stale as (
|
|
962
|
+
select l.id, l.meta->>'name' as n
|
|
963
|
+
from @SCHEMA@.secret l
|
|
964
|
+
where l.kind = 'LOCK' and l.account = '@SYSTEM@'::uuid and l.deleted_at is null
|
|
965
|
+
and left(l.meta->>'name', length(fam)) = fam
|
|
966
|
+
and not exists (
|
|
967
|
+
select 1 from @SCHEMA@.entity c
|
|
968
|
+
where c.class = 'Class'
|
|
969
|
+
and c.data->>'name' = split_part(substr(l.meta->>'name', length(fam) + 1), ':', 2)
|
|
970
|
+
and c.tenant::text = split_part(substr(l.meta->>'name', length(fam) + 1), ':', 1))
|
|
971
|
+
for update of l skip locked)
|
|
972
|
+
delete from @SCHEMA@.secret x using stale st
|
|
973
|
+
where x.id = st.id
|
|
974
|
+
and (st.n like '%:gate'
|
|
975
|
+
or exists (select 1 from stale g where g.n = st.n || ':gate')
|
|
976
|
+
or not exists (select 1 from @SCHEMA@.secret y where y.id = @SCHEMA@.lock_id(st.n || ':gate')));
|
|
977
|
+
end if;
|
|
913
978
|
return jsonb_build_object('sessions', s, 'codes', o, 'queue', pg_notification_queue_usage())
|
|
914
979
|
|| @SCHEMA@.thin_batch(ts, greatest(coalesce(p_limit, 10000), 1));
|
|
915
980
|
end $$;
|
|
@@ -969,8 +1034,12 @@ begin
|
|
|
969
1034
|
select jsonb_build_object('id', id, 'rev', want_rev, 'problem', 'разрыв: нет версий ' || want_rev || '…' || (rev - 1))
|
|
970
1035
|
from exp
|
|
971
1036
|
where rev > want_rev
|
|
972
|
-
and not exists (select 1 from pol where pol.cls = exp.class and
|
|
973
|
-
and
|
|
1037
|
+
and not exists (select 1 from pol where pol.cls = exp.class and exp.prev_at < ts - pol.all_iv
|
|
1038
|
+
and (pol.ct = exp.tenant
|
|
1039
|
+
or pol.ct = '@SYSTEM@'::uuid
|
|
1040
|
+
and not exists (select 1 from @SCHEMA@.entity y
|
|
1041
|
+
where y.class = 'Class' and y.tenant = exp.tenant
|
|
1042
|
+
and y.data->>'name' = exp.class)))
|
|
974
1043
|
union all
|
|
975
1044
|
select jsonb_build_object('id', id, 'rev', rev, 'problem', 'номер версии меньше ожидаемого после обрезки')
|
|
976
1045
|
from exp where rev < want_rev
|
|
@@ -1064,11 +1133,20 @@ as $$
|
|
|
1064
1133
|
declare
|
|
1065
1134
|
horizon xid8 := pg_snapshot_xmin(pg_current_snapshot());
|
|
1066
1135
|
lim interval;
|
|
1136
|
+
t uuid := @SCHEMA@.ctx_tenant();
|
|
1067
1137
|
begin
|
|
1068
1138
|
if p_at is not null then
|
|
1139
|
+
-- ярус all классов, которые видит подписка: арендатора сессии и System (кроме одноимённых своих); политика класса
|
|
1140
|
+
-- чужого арендатора с тем же именем курсор не просрочивает (план 2.0, 7.3 №1). Без арендатора в контексте
|
|
1141
|
+
-- (администраторское подключение видит всех) — все классы
|
|
1069
1142
|
select min(@SCHEMA@.history_iv(p.p->>'all')) into lim
|
|
1070
1143
|
from @SCHEMA@.entity x cross join lateral (select @SCHEMA@.history_policy(x.data->'resolved') as p) p
|
|
1071
|
-
where x.class = 'Class' and p.p is not null and (p_classes is null or x.data->>'name' = any (p_classes))
|
|
1144
|
+
where x.class = 'Class' and p.p is not null and (p_classes is null or x.data->>'name' = any (p_classes))
|
|
1145
|
+
and (t is null
|
|
1146
|
+
or x.tenant = t
|
|
1147
|
+
or x.tenant = '@SYSTEM@'::uuid
|
|
1148
|
+
and not exists (select 1 from @SCHEMA@.entity y
|
|
1149
|
+
where y.class = 'Class' and y.tenant = t and y.data->>'name' = x.data->>'name'));
|
|
1072
1150
|
if lim is not null and p_at < clock_timestamp() - lim then
|
|
1073
1151
|
raise exception using errcode = 'LT016',
|
|
1074
1152
|
message = 'letopis: cursor_expired: курсор подписки старше яруса all — события могли проредиться, перечитайте текущее состояние';
|
|
@@ -1156,7 +1234,7 @@ begin
|
|
|
1156
1234
|
'history_keep(jsonb, timestamp, timestamp, timestamp)', 'session_role()', 'admin_role()', 'letopis_allowed(text)', 'session_expires()', 'signal_mode()', 'signal_key()', 'class_stamp()',
|
|
1157
1235
|
'last_version_visible(uuid, uuid, jsonb)', 'restore(uuid)', 'projector_unread(uuid, uuid, int)', 'purge(uuid, boolean)', 'trim_history(uuid[], text[], timestamptz)',
|
|
1158
1236
|
'rekey(uuid, jsonb, jsonb)', 'rekey_class(text, jsonb)', 'account_purge(uuid)', 'reset_mark(uuid, text, uuid)',
|
|
1159
|
-
'reset(text, text, uuid, int)', 'thin_batch(timestamptz, int)', 'maintain(timestamptz, int)', 'verify(boolean)',
|
|
1237
|
+
'reset(text, text, uuid, int)', 'thin_batch(timestamptz, int)', 'maintain(timestamptz, int, boolean)', 'verify(boolean)',
|
|
1160
1238
|
'watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot, boolean)', 'watch_leaving(xid8, bigint, text[], int, pg_snapshot, xid8, bigint)', 'watch_head()'] loop
|
|
1161
1239
|
execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
|
|
1162
1240
|
execute format('revoke all on function @SCHEMA@.%s from public', f);
|
|
@@ -1165,7 +1243,7 @@ end $$;
|
|
|
1165
1243
|
-- приложению: функции владельца этапа 6 (проверяют права сами), подписка (под RLS) и чистые помощники политики
|
|
1166
1244
|
grant execute on function @SCHEMA@.restore(uuid), @SCHEMA@.purge(uuid, boolean), @SCHEMA@.trim_history(uuid[], text[], timestamptz),
|
|
1167
1245
|
@SCHEMA@.rekey(uuid, jsonb, jsonb), @SCHEMA@.rekey_class(text, jsonb), @SCHEMA@.account_purge(uuid),
|
|
1168
|
-
@SCHEMA@.reset(text, text, uuid, int), @SCHEMA@.maintain(timestamptz, int), @SCHEMA@.verify(boolean),
|
|
1246
|
+
@SCHEMA@.reset(text, text, uuid, int), @SCHEMA@.maintain(timestamptz, int, boolean), @SCHEMA@.verify(boolean),
|
|
1169
1247
|
@SCHEMA@.session_expires(), @SCHEMA@.watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot, boolean), @SCHEMA@.watch_leaving(xid8, bigint, text[], int, pg_snapshot, xid8, bigint), @SCHEMA@.watch_head(),
|
|
1170
1248
|
@SCHEMA@.history_default(), @SCHEMA@.history_policy(jsonb), @SCHEMA@.history_iv(text),
|
|
1171
1249
|
@SCHEMA@.signal_mode(), @SCHEMA@.signal_key(), @SCHEMA@.class_stamp() to @APP@;
|