letopis 0.18.1 → 0.20.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/CHANGELOG.md +254 -0
- package/LICENSE +21 -0
- package/README.md +492 -103
- package/dist/acl.d.ts +7 -2
- package/dist/acl.js +3 -2
- package/dist/chain.d.ts +46 -0
- package/dist/chain.js +38 -3
- package/dist/index.d.ts +8 -3
- package/dist/index.js +50 -19
- package/dist/schema.js +48 -6
- package/dist/sql.d.ts +26 -1
- package/dist/sql.js +130 -38
- package/dist/tables.d.ts +24 -0
- package/dist/tables.js +64 -1
- package/dist/types.d.ts +34 -9
- package/dist/types.js +40 -3
- package/dist/up.js +15 -4
- package/dist/write.d.ts +9 -0
- package/dist/write.js +46 -6
- package/package.json +9 -3
- package/scripts/check-docs.mjs +338 -0
- package/scripts/gen-api-contract.mjs +221 -0
- package/scripts/gen-types.mjs +238 -42
- package/sql/ddl.sql +114 -2
- package/sql/seed.booking.sql +5 -4
package/sql/ddl.sql
CHANGED
|
@@ -321,7 +321,8 @@ CREATE TRIGGER entity_update
|
|
|
321
321
|
-- * бьёт только актуальную ЖИВУЮ строку (история и tombstone неприкосновенны);
|
|
322
322
|
-- * advisory-lock сериализует параллельные удаления сущности;
|
|
323
323
|
-- * каскад: DELETE живых зависимых (links ⊃ {класс: id}) → рекурсия этого же триггера;
|
|
324
|
-
-- * физического удаления не происходит никогда
|
|
324
|
+
-- * физического удаления не происходит никогда — КРОМЕ явного purge (см. ниже):
|
|
325
|
+
-- при SET LOCAL letopis.purge='on' триггер целиком пропускается (WHEN) → физический DELETE.
|
|
325
326
|
-- Работает и для голого SQL: DELETE FROM "<SCHEMA-NAME>"."Entity" WHERE partition=… AND class=… AND id=…
|
|
326
327
|
-- -----------------------------------------------------------------------------
|
|
327
328
|
-- START_CONTRACT: entity_delete
|
|
@@ -363,7 +364,94 @@ END $$;
|
|
|
363
364
|
DROP TRIGGER IF EXISTS entity_delete ON "<SCHEMA-NAME>"."Entity";
|
|
364
365
|
CREATE TRIGGER entity_delete
|
|
365
366
|
BEFORE DELETE ON "<SCHEMA-NAME>"."Entity"
|
|
366
|
-
FOR EACH ROW
|
|
367
|
+
FOR EACH ROW
|
|
368
|
+
-- purge-режим: при SET LOCAL letopis.purge='on' триггер НЕ срабатывает → физический DELETE.
|
|
369
|
+
-- Обычный путь (флаг не выставлен) видит NULL → IS DISTINCT FROM 'on' = true → триггер работает (tombstone).
|
|
370
|
+
WHEN (current_setting('letopis.purge', true) IS DISTINCT FROM 'on')
|
|
371
|
+
EXECUTE FUNCTION "<SCHEMA-NAME>".entity_delete();
|
|
372
|
+
|
|
373
|
+
-- -----------------------------------------------------------------------------
|
|
374
|
+
-- Физический hard-erase (purge). Вся логика — в БД (две функции); либа лишь зовёт purge():
|
|
375
|
+
-- * purge_closure(partition,class,ids[]) — (class,id) всего поддерева по links (вкл. tombstone);
|
|
376
|
+
-- единый источник замыкания (UNION отсекает циклы/диаманты).
|
|
377
|
+
-- * purge(partition,class,id[,dry]) RETURNS SETOF Entity — двухфазный снос корня + поддерева:
|
|
378
|
+
-- - ДВУХФАЗНОСТЬ: только если сущность уже логически удалена (актуальная версия — tombstone);
|
|
379
|
+
-- живую не трогает (0 строк);
|
|
380
|
+
-- - dry=true → превью (актуальные версии замыкания), БД не трогается;
|
|
381
|
+
-- - dry=false → SET LOCAL letopis.purge='on' отключает entity_delete (см. WHEN выше) → плоский DELETE
|
|
382
|
+
-- замыкания без вложенного DML (без TM_SelfModified, детерминированно), возвращает снесённое;
|
|
383
|
+
-- - флаг транзакционный (is_local) + явный сброс → дальше в той же tx снова tombstone; привилегий не требует.
|
|
384
|
+
-- Голый SQL: SELECT * FROM "<SCHEMA-NAME>".purge('entity','Order','X'); -- снос
|
|
385
|
+
-- SELECT * FROM "<SCHEMA-NAME>".purge('entity','Order','X', true); -- превью
|
|
386
|
+
-- -----------------------------------------------------------------------------
|
|
387
|
+
-- START_CONTRACT: purge_closure
|
|
388
|
+
-- PURPOSE: (class,id)-замыкание поддерева по links (вкл. tombstone) — что снесёт purge(); переиспользуется самой purge() (снос и dry-превью).
|
|
389
|
+
-- INPUTS: { p_partition text; p_class text; p_ids text[] }
|
|
390
|
+
-- OUTPUTS: { SETOF (class text, id text) - узлы замыкания }
|
|
391
|
+
-- SIDE_EFFECTS: none (STABLE)
|
|
392
|
+
-- LINKS: M-DDL, V-M-DDL, M-WRITE, M-SQL
|
|
393
|
+
-- END_CONTRACT: purge_closure
|
|
394
|
+
CREATE OR REPLACE FUNCTION "<SCHEMA-NAME>".purge_closure(p_partition text, p_class text, p_ids text[])
|
|
395
|
+
RETURNS TABLE(class text, id text) LANGUAGE sql STABLE AS $$
|
|
396
|
+
WITH RECURSIVE closure(cls, cid) AS (
|
|
397
|
+
SELECT p_class, x FROM unnest(p_ids) AS x
|
|
398
|
+
UNION -- не ALL → отсекает циклы/диаманты links
|
|
399
|
+
SELECT e.class, e.id
|
|
400
|
+
FROM "<SCHEMA-NAME>"."Entity" e
|
|
401
|
+
JOIN closure c ON e.links @> jsonb_build_object(c.cls, c.cid)
|
|
402
|
+
WHERE e.partition = p_partition
|
|
403
|
+
)
|
|
404
|
+
SELECT cls, cid FROM closure;
|
|
405
|
+
$$;
|
|
406
|
+
|
|
407
|
+
-- START_CONTRACT: purge
|
|
408
|
+
-- PURPOSE: Двухфазно физически стереть логически удалённый корень и всё поддерево (по links, все версии); dry=превью.
|
|
409
|
+
-- INPUTS: { p_partition text; p_class text; p_id text; p_dry boolean = false }
|
|
410
|
+
-- OUTPUTS: { SETOF Entity - снесённые (по одному на сущность, latest) либо превью (dry); пусто если не tombstone }
|
|
411
|
+
-- SIDE_EFFECTS: при dry=false — SET LOCAL letopis.purge (отключает entity_delete на tx) + физический DELETE; advisory-lock
|
|
412
|
+
-- LINKS: M-DDL, V-M-DDL, M-WRITE, M-TABLES
|
|
413
|
+
-- END_CONTRACT: purge
|
|
414
|
+
CREATE OR REPLACE FUNCTION "<SCHEMA-NAME>".purge(
|
|
415
|
+
p_partition text, p_class text, p_id text, p_dry boolean DEFAULT false)
|
|
416
|
+
RETURNS SETOF "<SCHEMA-NAME>"."Entity" LANGUAGE plpgsql AS $$
|
|
417
|
+
BEGIN
|
|
418
|
+
PERFORM pg_advisory_xact_lock(
|
|
419
|
+
hashtextextended(p_partition || '|' || p_class || '|' || p_id, 0));
|
|
420
|
+
|
|
421
|
+
-- двухфазность: hard-purge только ПОСЛЕ логического удаления — актуальная версия обязана быть tombstone.
|
|
422
|
+
-- Живую (не удалённую) сущность purge НЕ трогает (возвращает 0 строк).
|
|
423
|
+
IF NOT EXISTS (
|
|
424
|
+
SELECT 1 FROM "<SCHEMA-NAME>"."Entity" e
|
|
425
|
+
WHERE e.partition = p_partition AND e.class = p_class AND e.id = p_id AND e.deleted IS NOT NULL
|
|
426
|
+
AND e.updated = (SELECT max(updated) FROM "<SCHEMA-NAME>"."Entity"
|
|
427
|
+
WHERE partition = p_partition AND class = p_class AND id = p_id)
|
|
428
|
+
) THEN
|
|
429
|
+
RETURN;
|
|
430
|
+
END IF;
|
|
431
|
+
|
|
432
|
+
IF p_dry THEN
|
|
433
|
+
-- превью: актуальная версия каждого члена замыкания, БД не трогаем
|
|
434
|
+
RETURN QUERY
|
|
435
|
+
SELECT DISTINCT ON (e.class, e.id) e.* FROM "<SCHEMA-NAME>"."Entity" e
|
|
436
|
+
JOIN "<SCHEMA-NAME>".purge_closure(p_partition, p_class, ARRAY[p_id]) c
|
|
437
|
+
ON e.class = c.class AND e.id = c.id
|
|
438
|
+
WHERE e.partition = p_partition
|
|
439
|
+
ORDER BY e.class, e.id, e.updated DESC;
|
|
440
|
+
RETURN;
|
|
441
|
+
END IF;
|
|
442
|
+
|
|
443
|
+
PERFORM set_config('letopis.purge', 'on', true); -- SET LOCAL: entity_delete отключён на эту tx
|
|
444
|
+
RETURN QUERY
|
|
445
|
+
WITH del AS (
|
|
446
|
+
DELETE FROM "<SCHEMA-NAME>"."Entity" e
|
|
447
|
+
USING "<SCHEMA-NAME>".purge_closure(p_partition, p_class, ARRAY[p_id]) c
|
|
448
|
+
WHERE e.partition = p_partition AND e.class = c.class AND e.id = c.id -- триггер пропущен (WHEN)
|
|
449
|
+
RETURNING e.*
|
|
450
|
+
)
|
|
451
|
+
SELECT DISTINCT ON (class, id) * FROM del ORDER BY class, id, updated DESC;
|
|
452
|
+
PERFORM set_config('letopis.purge', '', true); -- сброс: дальше в той же tx снова tombstone
|
|
453
|
+
RETURN;
|
|
454
|
+
END $$;
|
|
367
455
|
|
|
368
456
|
-- =============================================================================
|
|
369
457
|
-- Служебные таблицы (auth/ACL) — перенесены из legacy-дампа 1:1.
|
|
@@ -431,6 +519,30 @@ CREATE TABLE IF NOT EXISTS "<SCHEMA-NAME>"."Rule" (
|
|
|
431
519
|
);
|
|
432
520
|
-- END_BLOCK_AUTH_TABLES
|
|
433
521
|
|
|
522
|
+
-- -----------------------------------------------------------------------------
|
|
523
|
+
-- purge_account(): полный физический офбординг тенанта — все Entity (account|owner) + сам Account.
|
|
524
|
+
-- Credential уходит FK-каскадом (ON DELETE CASCADE). Флаг letopis.purge отключает entity_delete на
|
|
525
|
+
-- снос Entity (см. WHEN). Права/предохранители (Owner-only, последний Owner, не-себя) — в либе
|
|
526
|
+
-- (accounts.purge, им нужен ctx/ACL). Возвращает id снесённого Account либо NULL.
|
|
527
|
+
-- -----------------------------------------------------------------------------
|
|
528
|
+
-- START_CONTRACT: purge_account
|
|
529
|
+
-- PURPOSE: Физически стереть тенанта: все Entity с account|owner = id + сам Account (Credential — FK-каскад).
|
|
530
|
+
-- INPUTS: { p_id uuid }
|
|
531
|
+
-- OUTPUTS: { uuid - id снесённого Account, либо NULL если не найден }
|
|
532
|
+
-- SIDE_EFFECTS: SET LOCAL letopis.purge (отключает entity_delete); физический DELETE Entity + Account
|
|
533
|
+
-- LINKS: M-DDL, V-M-DDL, M-TABLES
|
|
534
|
+
-- END_CONTRACT: purge_account
|
|
535
|
+
CREATE OR REPLACE FUNCTION "<SCHEMA-NAME>".purge_account(p_id uuid)
|
|
536
|
+
RETURNS uuid LANGUAGE plpgsql AS $$
|
|
537
|
+
DECLARE gone uuid;
|
|
538
|
+
BEGIN
|
|
539
|
+
PERFORM set_config('letopis.purge', 'on', true); -- SET LOCAL: entity_delete отключён (см. WHEN)
|
|
540
|
+
DELETE FROM "<SCHEMA-NAME>"."Entity" WHERE account = p_id OR owner = p_id; -- обе оси (иначе entity_owner_fk RESTRICT)
|
|
541
|
+
PERFORM set_config('letopis.purge', '', true);
|
|
542
|
+
DELETE FROM "<SCHEMA-NAME>"."Account" WHERE id = p_id RETURNING id INTO gone; -- Credential — FK-каскад
|
|
543
|
+
RETURN gone;
|
|
544
|
+
END $$;
|
|
545
|
+
|
|
434
546
|
-- -----------------------------------------------------------------------------
|
|
435
547
|
-- Триггер 4: realtime — факт каждой новой версии в pg_notify (канал = имя схемы).
|
|
436
548
|
-- Payload лёгкий (без data): подписчик дочитывает нужное сам. Потребитель: db.watch().
|
package/sql/seed.booking.sql
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
-- =============================================================================
|
|
2
|
-
-- Сид таблицы Schema — ИСТОЧНИК ПРАВДЫ демо-домена booking (
|
|
2
|
+
-- Сид таблицы Schema — ИСТОЧНИК ПРАВДЫ демо-домена booking (20 классов: 12 HUB + 8 LINK).
|
|
3
3
|
-- Редактируется руками; накат: db/apply.mjs / up() (маркер <SCHEMA-NAME>).
|
|
4
4
|
-- Идемпотентен (ON CONFLICT). Концы связей — Schema.links v2 (JSON-объекты в text[]).
|
|
5
5
|
-- =============================================================================
|
|
6
6
|
--
|
|
7
7
|
-- FILE: lib/sql/seed.booking.sql
|
|
8
|
-
-- VERSION: 1.0.
|
|
8
|
+
-- VERSION: 1.0.1
|
|
9
9
|
-- START_MODULE_CONTRACT
|
|
10
10
|
-- PURPOSE: Идемпотентный сид таблицы Schema — источник правды демо-домена booking (классы HUB + LINK).
|
|
11
11
|
-- SCOPE: 20 INSERT-ов классов (12 HUB: Entity/Org/Person/Staff/Customer/Location/Element/Service/Product/Complex/Schedule/Folder; 8 LINK: link/address/slot/booking/content/compo/skill/price).
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
-- START_MODULE_MAP
|
|
19
19
|
-- HUB-классы - сущности домена (наследование от Entity; id v5/v7)
|
|
20
20
|
-- LINK-классы - связи (концы Schema.links v2; id v5 из концов)
|
|
21
|
-
-- NB:
|
|
21
|
+
-- NB: 20 классов (12 HUB + 8 LINK); число в заголовке файла сверяет scripts/check-docs.mjs
|
|
22
22
|
-- END_MODULE_MAP
|
|
23
23
|
--
|
|
24
24
|
-- START_CHANGE_SUMMARY
|
|
25
|
-
-- LAST_CHANGE: [v1.0.
|
|
25
|
+
-- LAST_CHANGE: [v1.0.1 - Docs-only: заголовок файла приведён к факту (20 классов: 12 HUB + 8 LINK);
|
|
26
|
+
-- число теперь проверяется автоматически. INSERT-ы не менялись]
|
|
26
27
|
-- END_CHANGE_SUMMARY
|
|
27
28
|
--
|
|
28
29
|
-- START_BLOCK_SEED_HUB_CLASSES
|