letopis 1.1.0 → 1.2.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.en.md +110 -15
- package/AGENT-CHEATSHEET.md +103 -13
- package/CHANGELOG.md +577 -0
- package/README.en.md +769 -73
- package/README.md +732 -67
- package/dist/admin.d.ts +20 -4
- package/dist/admin.js +33 -9
- package/dist/cli.js +53 -4
- package/dist/errors.js +4 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.js +75 -17
- package/dist/internal.d.ts +16 -0
- package/dist/internal.js +14 -0
- package/dist/load.js +42 -11
- package/dist/mcp.d.ts +28 -0
- package/dist/mcp.js +810 -0
- package/dist/model.d.ts +34 -0
- package/dist/model.js +17 -0
- package/dist/project.d.ts +309 -0
- package/dist/project.js +1097 -0
- package/dist/registry.d.ts +17 -0
- package/dist/registry.js +13 -0
- package/dist/sql.js +9 -5
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/uuid.js +19 -8
- package/dist/watch.d.ts +12 -0
- package/dist/watch.js +20 -1
- package/dist/write.d.ts +1 -1
- package/dist/write.js +25 -5
- package/llms.txt +6 -2
- package/package.json +12 -2
- package/sql/10-core.sql +5 -1
- package/sql/15-errors.sql +4 -1
- package/sql/40-class.sql +57 -2
- package/sql/60-write.sql +515 -48
- package/sql/70-read.sql +33 -8
- package/sql/80-auth.sql +4 -4
- package/sql/88-derived.sql +1154 -0
- package/sql/90-time.sql +89 -12
- package/sql/95-seed.system.sql +12 -2
- package/sql/README.md +4 -1
package/sql/90-time.sql
CHANGED
|
@@ -213,6 +213,9 @@ begin
|
|
|
213
213
|
if coalesce((res->>'abstract')::boolean, false) then
|
|
214
214
|
perform @SCHEMA@.lt_fail('invalid_class', format('класс %s абстрактный', l.class));
|
|
215
215
|
end if;
|
|
216
|
+
if res ? 'derivedBy' then
|
|
217
|
+
perform @SCHEMA@.lt_fail('derived_class', format('класс %s производный: его строки пишет проектор %s', l.class, res->>'derivedBy'));
|
|
218
|
+
end if;
|
|
216
219
|
d := @SCHEMA@.apply_defaults(res->'schema', l.data);
|
|
217
220
|
bad := @SCHEMA@.validate_data(res->'rules', cls.validator, res->'vars', d);
|
|
218
221
|
if bad <> '[]'::jsonb then
|
|
@@ -239,6 +242,9 @@ begin
|
|
|
239
242
|
values (p_id, l.rev + 1, l.class, cls.rev, l.tenant, own, lk.links, d, l.tags, ts, ts, c.author, c.agent, 'restore',
|
|
240
243
|
nullif(current_setting('letopis.reason', true), ''), null, l.hash, null)
|
|
241
244
|
returning * into n;
|
|
245
|
+
-- агрегатные поля восстановленной цели триггер после вставки посчитал из живых источников в эту же версию, а не
|
|
246
|
+
-- взял из надгробия (план 1.1, §2.4): в журнал идёт строка таблицы
|
|
247
|
+
select x.* into n from @SCHEMA@.entity x where x.id = p_id;
|
|
242
248
|
perform @SCHEMA@.log_version(n);
|
|
243
249
|
if l.class in ('rule', 'Resource', 'member', 'Account') then
|
|
244
250
|
perform @SCHEMA@.acl_bump();
|
|
@@ -273,6 +279,24 @@ begin
|
|
|
273
279
|
if not @SCHEMA@.acl_row_ok(@SCHEMA@.acl_plan('DELETE'), l.class, l.id, l.owner, l.tenant, l.tags, l.data, l.links) then
|
|
274
280
|
perform @SCHEMA@.lt_fail('acl_denied', format('нет права DELETE на класс %s', l.class));
|
|
275
281
|
end if;
|
|
282
|
+
-- проектор источника, чей курсор ещё не прошёл надгробие (в том числе надгробие этой транзакции), об удалении не
|
|
283
|
+
-- узнал бы — вклад строки остался бы в итогах навсегда (ревью безопасности №3, №5). Проектор касается арендатора, у
|
|
284
|
+
-- которого есть его производный класс (derivedBy — имя проектора): брошенный проектор с удалёнными производными
|
|
285
|
+
-- классами purge не держит. Курсор «tx:seq~снимок»: транзакции, видимые в снимке, учла перестройка. Чекпоинт без from
|
|
286
|
+
-- (до 1.2.0) касается всех классов.
|
|
287
|
+
if exists (select 1 from @SCHEMA@.secret s
|
|
288
|
+
cross join lateral (select split_part(s.meta->>'cursor', '~', 1) as head,
|
|
289
|
+
nullif(split_part(s.meta->>'cursor', '~', 2), '') as snap) k
|
|
290
|
+
where s.kind = 'PROJECTOR' and s.account = '@SYSTEM@'::uuid and s.deleted_at is null
|
|
291
|
+
and coalesce(s.meta->>'cursor', '') <> ''
|
|
292
|
+
and exists (select 1 from @SCHEMA@.entity x where x.class = 'Class' and x.tenant = l.tenant
|
|
293
|
+
and x.data->'resolved'->>'derivedBy' = s.ident)
|
|
294
|
+
and (not s.meta ? 'from' or coalesce((@SCHEMA@.class_row(l.class, l.tenant)).data->'resolved'->'ancestors',
|
|
295
|
+
jsonb_build_array(l.class)) ?| array(select jsonb_array_elements_text(s.meta->'from')))
|
|
296
|
+
and (l.tx, l.seq) > (split_part(k.head, ':', 1)::xid8, split_part(k.head, ':', 2)::bigint)
|
|
297
|
+
and not coalesce(pg_visible_in_snapshot(l.tx, k.snap::pg_snapshot), false)) then
|
|
298
|
+
perform @SCHEMA@.lt_fail('projector_pending', 'проектор источника ещё не прочёл удаление: purge — после его run()');
|
|
299
|
+
end if;
|
|
276
300
|
select count(*)::int into n from @SCHEMA@.log x where x.id = p_id;
|
|
277
301
|
if not coalesce(p_confirm, false) then
|
|
278
302
|
return jsonb_build_object('id', p_id, 'class', l.class, 'versions', n, 'purged', false);
|
|
@@ -386,6 +410,7 @@ declare
|
|
|
386
410
|
lk record;
|
|
387
411
|
nid uuid;
|
|
388
412
|
denied text;
|
|
413
|
+
n_denied int;
|
|
389
414
|
begin
|
|
390
415
|
select * into c from @SCHEMA@.ctx();
|
|
391
416
|
if c.account is null then
|
|
@@ -410,9 +435,15 @@ begin
|
|
|
410
435
|
perform @SCHEMA@.family_lock(cls, false);
|
|
411
436
|
cls := @SCHEMA@.class_row(o.class, o.tenant);
|
|
412
437
|
res := cls.data->'resolved';
|
|
438
|
+
if res ? 'derivedBy' then
|
|
439
|
+
perform @SCHEMA@.lt_fail('derived_class', format('класс %s производный: его строки пишет проектор %s', o.class, res->>'derivedBy'));
|
|
440
|
+
end if;
|
|
413
441
|
if jsonb_array_length(coalesce(res->'key', '[]'::jsonb)) = 0 then
|
|
414
442
|
perform @SCHEMA@.lt_fail('invalid_class', format('у класса %s нет ключа: id выдаёт база, менять нечего', o.class));
|
|
415
443
|
end if;
|
|
444
|
+
if res ? 'aggregate' and coalesce(p_data, '{}'::jsonb) ?| @SCHEMA@.agg_fields(res) then
|
|
445
|
+
perform @SCHEMA@.lt_fail('aggregate_field', format('агрегатные поля класса %s считает база — в rekey их не передают', o.class));
|
|
446
|
+
end if;
|
|
416
447
|
d := @SCHEMA@.apply_defaults(res->'schema', @SCHEMA@.merge(o.data, coalesce(p_data, '{}'::jsonb)));
|
|
417
448
|
bad := @SCHEMA@.validate_data(res->'rules', cls.validator, res->'vars', d);
|
|
418
449
|
if bad <> '[]'::jsonb then
|
|
@@ -432,13 +463,16 @@ begin
|
|
|
432
463
|
end if;
|
|
433
464
|
return o.id;
|
|
434
465
|
end if;
|
|
435
|
-
-- перевод ссылок пишет ссылающиеся строки: на них тоже нужно право WRITE
|
|
436
|
-
|
|
466
|
+
-- перевод ссылок пишет ссылающиеся строки: на них тоже нужно право WRITE (глубже первого уровня — move_row); класс
|
|
467
|
+
-- такой строки называется, только если актор её видит (доработки после точки Б)
|
|
468
|
+
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')),
|
|
469
|
+
count(*) into denied, n_denied from @SCHEMA@.entity x
|
|
437
470
|
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);
|
|
438
|
-
if
|
|
439
|
-
perform @SCHEMA@.lt_fail('acl_denied', format('нет права WRITE на ссылающиеся строки класса %s', denied)
|
|
471
|
+
if n_denied > 0 then
|
|
472
|
+
perform @SCHEMA@.lt_fail('acl_denied', case when denied is not null then format('нет права WRITE на ссылающиеся строки класса %s', denied)
|
|
473
|
+
else 'нет права WRITE на ссылающиеся строки' end);
|
|
440
474
|
end if;
|
|
441
|
-
perform @SCHEMA@.move_row(o, nid, o.class, d, lk.links, 'rekey');
|
|
475
|
+
perform @SCHEMA@.move_row(o, nid, o.class, d, lk.links, 'rekey', wplan);
|
|
442
476
|
return nid;
|
|
443
477
|
end $$;
|
|
444
478
|
|
|
@@ -462,6 +496,7 @@ declare
|
|
|
462
496
|
nid uuid;
|
|
463
497
|
n int := 0;
|
|
464
498
|
conflicts jsonb;
|
|
499
|
+
dname text;
|
|
465
500
|
begin
|
|
466
501
|
select * into c from @SCHEMA@.ctx();
|
|
467
502
|
if c.account is null then
|
|
@@ -478,9 +513,26 @@ begin
|
|
|
478
513
|
if coalesce((cr.data->>'system')::boolean, false) then
|
|
479
514
|
perform @SCHEMA@.lt_fail('invalid_class', 'ключ системного класса не меняется');
|
|
480
515
|
end if;
|
|
516
|
+
-- строки производного класса (и его потомков) переносит только владелец: ключ меняют в описании, а строки
|
|
517
|
+
-- строит заново проектор (rebuild)
|
|
518
|
+
if cr.data->'resolved' ? 'derivedBy' then
|
|
519
|
+
perform @SCHEMA@.lt_fail('derived_class', format('класс %s производный: его строки пишет проектор %s', p_class, cr.data->'resolved'->>'derivedBy'));
|
|
520
|
+
end if;
|
|
521
|
+
-- и потомки: производный потомок непроизводного предка наследует его ключ, а его строки переносит только проектор
|
|
522
|
+
-- (доработки после точки Б)
|
|
523
|
+
select x.data->>'name' into dname from @SCHEMA@.entity x
|
|
524
|
+
where x.class = 'Class' and x.tenant = cr.tenant and x.id <> cr.id
|
|
525
|
+
and x.data->'resolved'->'ancestors' ? p_class and x.data->'resolved' ? 'derivedBy'
|
|
526
|
+
order by x.data->>'name' limit 1;
|
|
527
|
+
if dname is not null then
|
|
528
|
+
perform @SCHEMA@.lt_fail('derived_class', format('у класса %s производный потомок %s: его строки пишет проектор', p_class, dname));
|
|
529
|
+
end if;
|
|
481
530
|
if not @SCHEMA@.acl_row_ok(wplan, 'Class', cr.id, cr.owner, cr.tenant, cr.tags, cr.data, cr.links) then
|
|
482
531
|
perform @SCHEMA@.lt_fail('acl_denied', 'нет права WRITE на класс Class');
|
|
483
532
|
end if;
|
|
533
|
+
-- смена ключа проверяет все строки семейства и отвечает числом нарушающих — как ужесточение, только с правом READ без
|
|
534
|
+
-- условий на все строки класса и потомков (доработки после точки Б)
|
|
535
|
+
perform @SCHEMA@.class_rows_admit(p_class, cr.tenant, array['READ'], format('смена ключа класса %s', p_class));
|
|
484
536
|
perform @SCHEMA@.family_lock(cr, true);
|
|
485
537
|
select x.* into cr from @SCHEMA@.entity x where x.id = cr.id for update;
|
|
486
538
|
select p.data, p.validator into prep from @SCHEMA@.class_prepare(cr.data || jsonb_build_object('key', p_key), cr.tenant) p;
|
|
@@ -507,7 +559,7 @@ begin
|
|
|
507
559
|
res := (@SCHEMA@.class_row(r.class, r.tenant)).data->'resolved';
|
|
508
560
|
nid := coalesce(@SCHEMA@.key_id(res, r.class, r.tenant, r.data, r.links), r.id);
|
|
509
561
|
if nid <> r.id then
|
|
510
|
-
perform @SCHEMA@.move_row(r, nid, r.class, r.data, r.links, 'rekey');
|
|
562
|
+
perform @SCHEMA@.move_row(r, nid, r.class, r.data, r.links, 'rekey', wplan);
|
|
511
563
|
n := n + 1;
|
|
512
564
|
end if;
|
|
513
565
|
end loop;
|
|
@@ -631,6 +683,7 @@ declare
|
|
|
631
683
|
n int;
|
|
632
684
|
m int;
|
|
633
685
|
key text;
|
|
686
|
+
held boolean;
|
|
634
687
|
begin
|
|
635
688
|
if not @ALLOW_RESET@ then
|
|
636
689
|
perform @SCHEMA@.lt_fail('reset_disabled', 'reset выключен в этой установке (up({ allowReset }))');
|
|
@@ -657,9 +710,15 @@ begin
|
|
|
657
710
|
if not exists (select 1 from @SCHEMA@.entity a where a.id = t and a.class = 'Account' and a.tenant = '@SYSTEM@'::uuid) then
|
|
658
711
|
perform @SCHEMA@.lt_fail('target_not_found', 'арендатор не найден');
|
|
659
712
|
end if;
|
|
713
|
+
-- строки арендатора стираются пачками в любом порядке: агрегаты его целей (план 1.1, §2.4) не пересчитываются —
|
|
714
|
+
-- цели уходят вместе с источниками, а промежуточный итог пачки мог бы выйти за правило класса
|
|
715
|
+
held := @SCHEMA@.agg_hold_begin('off');
|
|
660
716
|
with d as (delete from @SCHEMA@.entity x where x.id in (
|
|
661
717
|
select y.id from @SCHEMA@.entity y where y.tenant = t limit coalesce(p_limit, 2147483647)) returning 1)
|
|
662
718
|
select count(*)::int into n from d;
|
|
719
|
+
if held then
|
|
720
|
+
perform @SCHEMA@.agg_hold_end(false);
|
|
721
|
+
end if;
|
|
663
722
|
with d as (delete from @SCHEMA@.log x where x.seq in (
|
|
664
723
|
select y.seq from @SCHEMA@.log y where y.tenant = t limit coalesce(p_limit, 2147483647)) returning 1)
|
|
665
724
|
select count(*)::int into m from d;
|
|
@@ -781,6 +840,11 @@ begin
|
|
|
781
840
|
if not @SCHEMA@.letopis_allowed('letopis.maintain') then
|
|
782
841
|
perform @SCHEMA@.lt_fail('acl_denied', 'нет права letopis.maintain');
|
|
783
842
|
end if;
|
|
843
|
+
-- один исполнитель: строка-замок до конца транзакции пачки (lock_try) — посторонняя роль её не займёт (ревью
|
|
844
|
+
-- безопасности №3, №2); занята — null, обслуживание продолжит держатель
|
|
845
|
+
if not @SCHEMA@.lock_try('@SCHEMA_NAME@:maintain') then
|
|
846
|
+
return null;
|
|
847
|
+
end if;
|
|
784
848
|
if p_now is not null then
|
|
785
849
|
if not @ALLOW_RESET@ then
|
|
786
850
|
perform @SCHEMA@.lt_fail('reset_disabled', 'maintain(now) — только в установке с allow_reset');
|
|
@@ -805,7 +869,8 @@ end $$;
|
|
|
805
869
|
-- прореживание — пропуск после версии старше яруса all класса, у которого есть (или была) политика
|
|
806
870
|
-- хранения — своя или умолчание установки (метка default_policy в строке ключа). p_data — ещё и все строки: данные по проверке класса, концы (цель существует, её
|
|
807
871
|
-- арендатор — тот же или System, класс принимается концом), арендатор и владелец существуют, id
|
|
808
|
-
-- класса с ключом — v5 от
|
|
872
|
+
-- класса с ключом — v5 от ключа, агрегатные поля равны итогу живых источников (план 1.1, §2.4; ремонт —
|
|
873
|
+
-- rebuild_aggregates). Возвращает находки и якорь — хэш всех голов цепочек. Только
|
|
809
874
|
-- системный администратор (право letopis.verify) или администраторское подключение.
|
|
810
875
|
create or replace function @SCHEMA@.verify(p_data boolean default false) returns jsonb
|
|
811
876
|
language plpgsql stable security definer
|
|
@@ -913,6 +978,11 @@ begin
|
|
|
913
978
|
from @SCHEMA@.entity k2
|
|
914
979
|
where k2.class = 'Class' and k2.data->>'name' = t.class and k2.tenant in (t.tenant, '@SYSTEM@'::uuid)
|
|
915
980
|
order by k2.tenant = '@SYSTEM@'::uuid limit 1), '{}')))
|
|
981
|
+
union all
|
|
982
|
+
select jsonb_build_object('id', a.id, 'class', a.class, 'field', a.field, 'stored', a.stored, 'expected', a.expected,
|
|
983
|
+
'problem', format('агрегат %s не равен итогу живых источников: в строке %s, по источникам %s — ремонт rebuildAggregates',
|
|
984
|
+
a.field, coalesce(a.stored::text, 'нет значения'), a.expected::text))
|
|
985
|
+
from @SCHEMA@.agg_issues() a
|
|
916
986
|
)
|
|
917
987
|
select coalesce(jsonb_agg(f), '[]'::jsonb) into data_issues from found;
|
|
918
988
|
end if;
|
|
@@ -930,8 +1000,10 @@ end $$;
|
|
|
930
1000
|
-- события курсора: старше яруса all какого-либо класса подписки — cursor_expired (события могли
|
|
931
1001
|
-- проредиться). p_classes null — все классы. p_snap — снимок момента подписки «now»: транзакции,
|
|
932
1002
|
-- видимые в нём (зафиксированные до подписки), не отдаются.
|
|
1003
|
+
-- p_leaving (проектор, ревью безопасности №3, №4): ещё смены класса без смены id (reclass без moved), уводящие строку из
|
|
1004
|
+
-- p_classes, — версия уже другого класса, а прошлая была в p_classes; иначе уход строки из семейства не виден.
|
|
933
1005
|
create or replace function @SCHEMA@.watch_read(p_tx xid8, p_seq bigint, p_at timestamptz, p_classes text[], p_limit int,
|
|
934
|
-
p_snap pg_snapshot default null)
|
|
1006
|
+
p_snap pg_snapshot default null, p_leaving boolean default false)
|
|
935
1007
|
returns setof @SCHEMA@.log
|
|
936
1008
|
language plpgsql stable
|
|
937
1009
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
@@ -953,7 +1025,11 @@ begin
|
|
|
953
1025
|
return query select l.seq, l.tx, l.id, l.rev, l.class, l.cv, l.tenant, l.owner, l.links, l.data, l.tags, l.at, l.author,
|
|
954
1026
|
l.agent, l.op, l.reason, l.moved, l.prev_hash, l.hash, null::jsonb
|
|
955
1027
|
from @SCHEMA@.log l
|
|
956
|
-
where (l.tx, l.seq) > (p_tx, p_seq) and l.tx < horizon
|
|
1028
|
+
where (l.tx, l.seq) > (p_tx, p_seq) and l.tx < horizon
|
|
1029
|
+
and (p_classes is null or l.class = any (p_classes)
|
|
1030
|
+
or (p_leaving and l.op = 'reclass' and l.moved is null
|
|
1031
|
+
and (select p.class from @SCHEMA@.log p where p.id = l.id and p.rev < l.rev and p.op not in ('purge', 'trim', 'reset')
|
|
1032
|
+
order by p.rev desc limit 1) = any (p_classes)))
|
|
957
1033
|
and (p_snap is null or not pg_visible_in_snapshot(l.tx, p_snap))
|
|
958
1034
|
order by l.tx, l.seq limit p_limit;
|
|
959
1035
|
end $$;
|
|
@@ -967,8 +1043,9 @@ set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
|
967
1043
|
as $$
|
|
968
1044
|
select pg_snapshot_xmin(s), 0::bigint, s from pg_current_snapshot() s
|
|
969
1045
|
$$;
|
|
970
|
-
-- до исправления подписки «now»: без
|
|
1046
|
+
-- до исправления подписки «now»: без снимка; до ревью безопасности №3 — без p_leaving
|
|
971
1047
|
drop function if exists @SCHEMA@.watch_read(xid8, bigint, timestamptz, text[], int);
|
|
1048
|
+
drop function if exists @SCHEMA@.watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot);
|
|
972
1049
|
|
|
973
1050
|
do $$
|
|
974
1051
|
declare f text;
|
|
@@ -979,7 +1056,7 @@ begin
|
|
|
979
1056
|
'last_version_visible(uuid, uuid, jsonb)', 'restore(uuid)', 'purge(uuid, boolean)', 'trim_history(uuid[], text[], timestamptz)',
|
|
980
1057
|
'rekey(uuid, jsonb, jsonb)', 'rekey_class(text, jsonb)', 'account_purge(uuid)', 'reset_mark(uuid, text, uuid)',
|
|
981
1058
|
'reset(text, text, uuid, int)', 'thin_batch(timestamptz, int)', 'maintain(timestamptz, int)', 'verify(boolean)',
|
|
982
|
-
'watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot)', 'watch_head()'] loop
|
|
1059
|
+
'watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot, boolean)', 'watch_head()'] loop
|
|
983
1060
|
execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
|
|
984
1061
|
execute format('revoke all on function @SCHEMA@.%s from public', f);
|
|
985
1062
|
end loop;
|
|
@@ -988,6 +1065,6 @@ end $$;
|
|
|
988
1065
|
grant execute on function @SCHEMA@.restore(uuid), @SCHEMA@.purge(uuid, boolean), @SCHEMA@.trim_history(uuid[], text[], timestamptz),
|
|
989
1066
|
@SCHEMA@.rekey(uuid, jsonb, jsonb), @SCHEMA@.rekey_class(text, jsonb), @SCHEMA@.account_purge(uuid),
|
|
990
1067
|
@SCHEMA@.reset(text, text, uuid, int), @SCHEMA@.maintain(timestamptz, int), @SCHEMA@.verify(boolean),
|
|
991
|
-
@SCHEMA@.session_expires(), @SCHEMA@.watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot), @SCHEMA@.watch_head(),
|
|
1068
|
+
@SCHEMA@.session_expires(), @SCHEMA@.watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot, boolean), @SCHEMA@.watch_head(),
|
|
992
1069
|
@SCHEMA@.history_default(), @SCHEMA@.history_policy(jsonb), @SCHEMA@.history_iv(text),
|
|
993
1070
|
@SCHEMA@.signal_mode(), @SCHEMA@.signal_key(), @SCHEMA@.class_stamp() to @APP@;
|
package/sql/95-seed.system.sql
CHANGED
|
@@ -21,13 +21,19 @@ begin
|
|
|
21
21
|
if p_class = 'Class' then
|
|
22
22
|
select c.data, c.validator into d, vld from @SCHEMA@.class_prepare(d, p_tenant) c;
|
|
23
23
|
end if;
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
-- журнал раньше строки: агрегатные поля новой цели триггер после вставки досчитывает в ту же версию — и в entity,
|
|
25
|
+
-- и в уже записанную версию журнала (план 1.1, §2.4)
|
|
26
26
|
insert into @SCHEMA@.log (id, rev, class, tenant, owner, links, data, tags, at, author, op)
|
|
27
27
|
values (p_id, 1, p_class, p_tenant, p_tenant, coalesce(p_links, '{}'), d, '{}', now_ts, '@SYSTEM@', 'create');
|
|
28
|
+
insert into @SCHEMA@.entity (id, rev, class, tenant, owner, links, data, at, created_at, author, op, validator)
|
|
29
|
+
values (p_id, 1, p_class, p_tenant, p_tenant, coalesce(p_links, '{}'), d, now_ts, now_ts, '@SYSTEM@', 'create', vld);
|
|
28
30
|
if p_class in ('rule', 'Resource', 'member', 'Account', 'Class') then
|
|
29
31
|
perform @SCHEMA@.acl_bump();
|
|
30
32
|
end if;
|
|
33
|
+
-- новая цель агрегатов: метки её источников — заново
|
|
34
|
+
if p_class = 'Class' and d->'resolved' ? 'aggregate' then
|
|
35
|
+
perform @SCHEMA@.agg_mark(p_tenant, @SCHEMA@.agg_of(d->'resolved'));
|
|
36
|
+
end if;
|
|
31
37
|
return p_id;
|
|
32
38
|
end $$;
|
|
33
39
|
alter function @SCHEMA@.sys_create(uuid, text, uuid, jsonb, jsonb) owner to @OWNER@;
|
|
@@ -139,6 +145,10 @@ begin
|
|
|
139
145
|
foreach op in array array['READ', 'WRITE', 'DELETE'] loop
|
|
140
146
|
perform @SCHEMA@.sys_rule(sys, g_admin, @SCHEMA@.sys_resource(sys, 'all:' || op, op, null), 'allow', 100);
|
|
141
147
|
end loop;
|
|
148
|
+
-- строки-замки (60-write.sql: lock_row) семейств всех классов, в том числе классов арендаторов, заведённых до 1.2, и
|
|
149
|
+
-- обслуживания — заранее: первое обращение под нагрузкой их не вставляет
|
|
150
|
+
perform @SCHEMA@.family_rows(c.tenant, c.data) from @SCHEMA@.entity c where c.class = 'Class';
|
|
151
|
+
perform @SCHEMA@.lock_row('@SCHEMA_NAME@:maintain');
|
|
142
152
|
end $$;
|
|
143
153
|
alter function @SCHEMA@.seed_system() owner to @OWNER@;
|
|
144
154
|
revoke all on function @SCHEMA@.seed_system() from public;
|
package/sql/README.md
CHANGED
|
@@ -51,7 +51,10 @@
|
|
|
51
51
|
| LT018 | `not_deleted` | `restore` и `purge` — только для удалённых объектов (этап 6) |
|
|
52
52
|
| LT019 | `reset_disabled` | флаг `allowReset` установки выключен: `reset` и `maintain(now)` недоступны (этап 6) |
|
|
53
53
|
| LT020 | `confirm_mismatch` | подтверждение сброса не совпадает с именем схемы (этап 6) |
|
|
54
|
+
| LT021 | `derived_class` | строки производного класса (`derivedBy`) пишет и удаляет только его проектор; на них не ссылаются концы (план 1.1, §2.4) |
|
|
55
|
+
| LT022 | `aggregate_field` | агрегатное поле (`aggregate`) считает база: приложение и сырой SQL его не передают и не меняют (план 1.1, §2.4) |
|
|
56
|
+
| LT023 | `projector_pending` | `purge` удалённого объекта, чьё надгробие ещё не прочёл проектор его класса (ревью безопасности №3, №5) |
|
|
54
57
|
|
|
55
58
|
## Порядок файлов
|
|
56
59
|
|
|
57
|
-
`10-core` → `15-errors` → `20-context` → `30-validate` → `40-class` → `50-acl` → `60-write` → `70-read` → `80-auth` → `90-time` → `95-seed.system` → `99-revision`. Миграции между ревизиями — `migrate/NNNN-*.sql` (номер — ревизия, к которой ведёт), их применяет `up({ upgrade: true })` до повторного наката.
|
|
60
|
+
`10-core` → `15-errors` → `20-context` → `30-validate` → `40-class` → `50-acl` → `60-write` → `70-read` → `75-history` → `80-auth` → `88-derived` → `90-time` → `95-seed.system` → `99-revision`. Миграции между ревизиями — `migrate/NNNN-*.sql` (номер — ревизия, к которой ведёт), их применяет `up({ upgrade: true })` до повторного наката.
|