letopis 0.13.0 → 0.18.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 +203 -3
- package/README.md +825 -602
- package/dist/acl.js +62 -0
- package/dist/auth.d.ts +7 -0
- package/dist/auth.js +126 -0
- package/dist/chain.d.ts +44 -16
- package/dist/chain.js +309 -26
- package/dist/index.d.ts +7 -0
- package/dist/index.js +38 -0
- package/dist/ops.js +45 -0
- package/dist/schema.js +169 -4
- package/dist/sessions.js +66 -0
- package/dist/sql.d.ts +23 -3
- package/dist/sql.js +161 -25
- package/dist/tables.js +37 -0
- package/dist/tx.js +36 -0
- package/dist/types.d.ts +40 -4
- package/dist/types.js +46 -0
- package/dist/up.js +83 -0
- package/dist/uuid.d.ts +6 -0
- package/dist/uuid.js +68 -0
- package/dist/write.d.ts +12 -5
- package/dist/write.js +359 -95
- package/docker/Dockerfile +23 -0
- package/docker/start.sh +14 -0
- package/package.json +1 -1
- package/sql/ddl.sql +124 -12
- package/sql/seed.auth.sql +27 -0
- package/sql/seed.booking.sql +64 -17
package/docker/Dockerfile
CHANGED
|
@@ -4,6 +4,28 @@
|
|
|
4
4
|
# docker run -d --name letopis-timescale -p 15432:5432 -p 16379:6379 \
|
|
5
5
|
# -v letopis-pgdata:/var/lib/postgresql/data \
|
|
6
6
|
# -e POSTGRES_PASSWORD=test -e POSTGRES_DB=letopis letopis-db
|
|
7
|
+
# FILE: lib/docker/Dockerfile
|
|
8
|
+
# VERSION: 1.0.0
|
|
9
|
+
# START_MODULE_CONTRACT
|
|
10
|
+
# PURPOSE: Dev-образ БД — TimescaleDB pg17 + Redis в одном контейнере (для letopis.up()).
|
|
11
|
+
# SCOPE: базовый образ, установка redis, копирование start.sh, EXPOSE 5432/6379, entrypoint.
|
|
12
|
+
# DEPENDS: none
|
|
13
|
+
# LINKS: M-DOCKER, V-M-DOCKER
|
|
14
|
+
# ROLE: CONFIG
|
|
15
|
+
# MAP_MODE: SUMMARY
|
|
16
|
+
# END_MODULE_CONTRACT
|
|
17
|
+
#
|
|
18
|
+
# START_MODULE_MAP
|
|
19
|
+
# FROM timescale/timescaledb:latest-pg17 - база (PostgreSQL 17 + TimescaleDB)
|
|
20
|
+
# apk add redis - Redis в тот же образ
|
|
21
|
+
# ENTRYPOINT start-letopis.sh - запуск redis + postgres
|
|
22
|
+
# END_MODULE_MAP
|
|
23
|
+
#
|
|
24
|
+
# START_CHANGE_SUMMARY
|
|
25
|
+
# LAST_CHANGE: [v1.0.0 - Documented existing module: reverse-engineered contract + markup]
|
|
26
|
+
# END_CHANGE_SUMMARY
|
|
27
|
+
|
|
28
|
+
# START_BLOCK_IMAGE_BUILD
|
|
7
29
|
FROM timescale/timescaledb:latest-pg17
|
|
8
30
|
|
|
9
31
|
RUN apk add --no-cache redis
|
|
@@ -15,3 +37,4 @@ EXPOSE 5432 6379
|
|
|
15
37
|
|
|
16
38
|
ENTRYPOINT ["/usr/local/bin/start-letopis.sh"]
|
|
17
39
|
CMD ["postgres"]
|
|
40
|
+
# END_BLOCK_IMAGE_BUILD
|
package/docker/start.sh
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
+
# FILE: lib/docker/start.sh
|
|
3
|
+
# VERSION: 1.0.0
|
|
4
|
+
# START_MODULE_CONTRACT
|
|
5
|
+
# PURPOSE: Entrypoint dev-контейнера — поднять Redis фоном, затем штатный postgres docker-entrypoint.
|
|
6
|
+
# SCOPE: redis-server (daemonize) + exec docker-entrypoint.sh.
|
|
7
|
+
# DEPENDS: none
|
|
8
|
+
# LINKS: M-DOCKER, V-M-DOCKER
|
|
9
|
+
# ROLE: SCRIPT
|
|
10
|
+
# MAP_MODE: NONE
|
|
11
|
+
# END_MODULE_CONTRACT
|
|
12
|
+
#
|
|
13
|
+
# START_CHANGE_SUMMARY
|
|
14
|
+
# LAST_CHANGE: [v1.0.0 - Documented existing module: reverse-engineered contract + markup]
|
|
15
|
+
# END_CHANGE_SUMMARY
|
|
2
16
|
# Redis фоном (dev: без пароля и персиста — сессии эфемерны), затем штатный entrypoint postgres.
|
|
3
17
|
redis-server --port 6379 --bind 0.0.0.0 --protected-mode no --save '' --appendonly no --daemonize yes
|
|
4
18
|
exec docker-entrypoint.sh "$@"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letopis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Letopis (летопись): append-only versioned entity store on TimescaleDB with dot-notation chains — every change is a new row, history is first-class (asOf, versions, watch, cascade tombstones)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"timescaledb",
|
package/sql/ddl.sql
CHANGED
|
@@ -6,14 +6,41 @@
|
|
|
6
6
|
-- "<SCHEMA-NAME>" — маркер: letopis.up() / db/apply.mjs подставляет полное имя схемы vN.<имя>.
|
|
7
7
|
-- =============================================================================
|
|
8
8
|
|
|
9
|
+
-- FILE: lib/sql/ddl.sql
|
|
10
|
+
-- VERSION: 1.0.0
|
|
11
|
+
-- START_MODULE_CONTRACT
|
|
12
|
+
-- PURPOSE: Движок-хранилище PostgreSQL/TimescaleDB — таблицы, Entity-hypertable, индексы и триггеры целостности (валидация, версионирование, каскад-tombstone, lineage, notify).
|
|
13
|
+
-- SCOPE: таблицы Schema/Entity/Account/Credential/Resource/Rule + триггеры schema_lineage/entity_check/entity_update/entity_delete/entity_notify.
|
|
14
|
+
-- DEPENDS: none
|
|
15
|
+
-- LINKS: M-DDL, V-M-DDL
|
|
16
|
+
-- ROLE: RUNTIME
|
|
17
|
+
-- MAP_MODE: SUMMARY
|
|
18
|
+
-- END_MODULE_CONTRACT
|
|
19
|
+
--
|
|
20
|
+
-- START_MODULE_MAP
|
|
21
|
+
-- Schema - определения классов (HUB/LINK), наследование, attributes/links
|
|
22
|
+
-- Entity - append-only hypertable версий сущностей и связей
|
|
23
|
+
-- Account/Credential/Resource/Rule - служебные таблицы auth/ACL
|
|
24
|
+
-- schema_lineage - пересчёт ancestors/descendants (statement-level)
|
|
25
|
+
-- entity_check - валидация класса и концов LINK при INSERT
|
|
26
|
+
-- entity_update - UPDATE → вставка новой версии
|
|
27
|
+
-- entity_delete - DELETE → tombstone + рекурсивный каскад (advisory-lock)
|
|
28
|
+
-- entity_notify - pg_notify о новой версии (канал = имя схемы)
|
|
29
|
+
-- END_MODULE_MAP
|
|
30
|
+
--
|
|
31
|
+
-- START_CHANGE_SUMMARY
|
|
32
|
+
-- LAST_CHANGE: [v1.0.0 - Documented existing module: reverse-engineered contract + markup]
|
|
33
|
+
-- END_CHANGE_SUMMARY
|
|
34
|
+
|
|
9
35
|
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
|
10
36
|
|
|
11
37
|
CREATE SCHEMA IF NOT EXISTS "<SCHEMA-NAME>";
|
|
12
38
|
|
|
13
39
|
-- -----------------------------------------------------------------------------
|
|
14
40
|
-- Schema: классы предметной области (HUB — сущности, LINK — связи).
|
|
15
|
-
-- Сидится
|
|
41
|
+
-- Сидится демо-доменом booking: lib/sql/seed.booking.sql (источник правды).
|
|
16
42
|
-- -----------------------------------------------------------------------------
|
|
43
|
+
-- START_BLOCK_TABLE_SCHEMA
|
|
17
44
|
CREATE TABLE IF NOT EXISTS "<SCHEMA-NAME>"."Schema" (
|
|
18
45
|
partition text NOT NULL DEFAULT 'entity',
|
|
19
46
|
id text NOT NULL, -- англ. id класса: 'Staff', 'skill'
|
|
@@ -21,13 +48,14 @@ CREATE TABLE IF NOT EXISTS "<SCHEMA-NAME>"."Schema" (
|
|
|
21
48
|
category text NOT NULL CHECK (category IN ('HUB', 'LINK')),
|
|
22
49
|
ancestor text, -- прямой родитель (наследование)
|
|
23
50
|
attributes jsonb NOT NULL DEFAULT '{}', -- fastest-validator DSL (валидирует либа)
|
|
24
|
-
links
|
|
51
|
+
links jsonb NOT NULL DEFAULT '[]', -- МАССИВ ОБЪЕКТОВ-концов: {"class":…}/{"classes":[…],"optional":…,"cardinality":…} (союзы ролей, матчинг по порядку); legacy-элемент-строка "Org" ("Entity" = полиморфный)
|
|
25
52
|
meta jsonb NOT NULL DEFAULT '{}', -- {abstract?, description?}
|
|
26
53
|
"order" int NOT NULL DEFAULT 0,
|
|
27
54
|
ancestors text[] NOT NULL DEFAULT '{}', -- [self, parent, …, root] — считает триггер schema_lineage
|
|
28
55
|
descendants text[] NOT NULL DEFAULT '{}', -- все потомки (транзитивно) — считает триггер schema_lineage
|
|
29
56
|
PRIMARY KEY (partition, id)
|
|
30
57
|
);
|
|
58
|
+
-- END_BLOCK_TABLE_SCHEMA
|
|
31
59
|
|
|
32
60
|
-- -----------------------------------------------------------------------------
|
|
33
61
|
-- Триггер lineage: ancestors + descendants пересчитываются автоматически
|
|
@@ -35,6 +63,13 @@ CREATE TABLE IF NOT EXISTS "<SCHEMA-NAME>"."Schema" (
|
|
|
35
63
|
-- Рекурсивные CTE вверх и вниз; защита от циклов (depth ≤ 32) и от
|
|
36
64
|
-- саморекурсии триггера (pg_trigger_depth).
|
|
37
65
|
-- -----------------------------------------------------------------------------
|
|
66
|
+
-- START_CONTRACT: schema_lineage
|
|
67
|
+
-- PURPOSE: Пересчитать ancestors + descendants всех классов при изменении Schema (statement-level; защита от циклов depth≤32 и саморекурсии).
|
|
68
|
+
-- INPUTS: { trigger: AFTER INSERT/UPDATE(ancestor,id)/DELETE ON Schema }
|
|
69
|
+
-- OUTPUTS: { NULL - обновляет колонки ancestors/descendants }
|
|
70
|
+
-- SIDE_EFFECTS: UPDATE Schema.ancestors/descendants
|
|
71
|
+
-- LINKS: M-DDL, V-M-DDL
|
|
72
|
+
-- END_CONTRACT: schema_lineage
|
|
38
73
|
CREATE OR REPLACE FUNCTION "<SCHEMA-NAME>".schema_lineage() RETURNS trigger
|
|
39
74
|
LANGUAGE plpgsql AS $$
|
|
40
75
|
BEGIN
|
|
@@ -86,6 +121,7 @@ CREATE TRIGGER schema_lineage
|
|
|
86
121
|
-- Версия = строка; актуальная версия = max(updated); удаление = tombstone (deleted).
|
|
87
122
|
-- links = {"Класс": "id", ...} — произвольное число ссылок в одной строке.
|
|
88
123
|
-- -----------------------------------------------------------------------------
|
|
124
|
+
-- START_BLOCK_ENTITY_STORAGE
|
|
89
125
|
CREATE TABLE IF NOT EXISTS "<SCHEMA-NAME>"."Entity" (
|
|
90
126
|
partition text NOT NULL,
|
|
91
127
|
id text NOT NULL,
|
|
@@ -126,22 +162,41 @@ CREATE INDEX IF NOT EXISTS entity_data_idx ON "<SCHEMA-NAME>"."Entity" USING
|
|
|
126
162
|
CREATE INDEX IF NOT EXISTS entity_tags_idx ON "<SCHEMA-NAME>"."Entity" USING gin (tags);
|
|
127
163
|
CREATE INDEX IF NOT EXISTS entity_account_idx ON "<SCHEMA-NAME>"."Entity" (account);
|
|
128
164
|
CREATE INDEX IF NOT EXISTS entity_owner_idx ON "<SCHEMA-NAME>"."Entity" (owner);
|
|
165
|
+
-- END_BLOCK_ENTITY_STORAGE
|
|
129
166
|
|
|
130
167
|
-- -----------------------------------------------------------------------------
|
|
131
168
|
-- Триггер 1: целостность при INSERT (только проверки, никакой логики приложения).
|
|
132
169
|
-- * класс существует в Schema той же partition;
|
|
133
170
|
-- * класс не абстрактный;
|
|
134
171
|
-- * каждый ключ links — существующий класс, значение — строка (id);
|
|
135
|
-
-- * LINK
|
|
136
|
-
-- (
|
|
172
|
+
-- * LINK: концы из Schema.links.
|
|
173
|
+
-- Формат v2 (элемент — JSON-объект '{"class":…}' / '{"classes":[…]}'):
|
|
174
|
+
-- жадный матчинг по порядку объявления, optional-концы могут отсутствовать,
|
|
175
|
+
-- связи вне объявленных концов — ошибка.
|
|
176
|
+
-- Legacy (элемент — имя класса): конец обязателен по имени, 'Entity' —
|
|
177
|
+
-- полиморфный (его резолвит либа), лишние связи не проверяются.
|
|
137
178
|
-- Tombstone-версии (deleted IS NOT NULL) не валидируются.
|
|
138
179
|
-- -----------------------------------------------------------------------------
|
|
180
|
+
-- START_CONTRACT: entity_check
|
|
181
|
+
-- PURPOSE: Проверить при INSERT — класс существует и не абстрактный; ключи links — существующие классы со строковыми id; концы LINK (v2 жадный матчинг / legacy).
|
|
182
|
+
-- INPUTS: { trigger: BEFORE INSERT ON Entity (NEW) }
|
|
183
|
+
-- OUTPUTS: { NEW - либо EXCEPTION }
|
|
184
|
+
-- SIDE_EFFECTS: none (валидация; tombstone-версии пропускаются)
|
|
185
|
+
-- ERRORS: unknown class; class is abstract; links key not a class; link requires end; stray link
|
|
186
|
+
-- LINKS: M-DDL, V-M-DDL, M-WRITE
|
|
187
|
+
-- END_CONTRACT: entity_check
|
|
139
188
|
CREATE OR REPLACE FUNCTION "<SCHEMA-NAME>".entity_check() RETURNS trigger
|
|
140
189
|
LANGUAGE plpgsql AS $$
|
|
141
190
|
DECLARE
|
|
142
|
-
s
|
|
143
|
-
bad
|
|
144
|
-
k
|
|
191
|
+
s RECORD;
|
|
192
|
+
bad text;
|
|
193
|
+
k text;
|
|
194
|
+
used text[];
|
|
195
|
+
ej jsonb;
|
|
196
|
+
ecs text[];
|
|
197
|
+
hit text;
|
|
198
|
+
c2 text;
|
|
199
|
+
is_v2 boolean;
|
|
145
200
|
BEGIN
|
|
146
201
|
IF NEW.deleted IS NOT NULL THEN
|
|
147
202
|
RETURN NEW;
|
|
@@ -172,15 +227,49 @@ BEGIN
|
|
|
172
227
|
RAISE EXCEPTION 'ENTITY_CHECK: links key "%" is not a class or its value is not an id string', bad;
|
|
173
228
|
END IF;
|
|
174
229
|
|
|
175
|
-
-- LINK:
|
|
230
|
+
-- LINK: концы по Schema.links — jsonb-массив; элемент-объект = v2 (строгий жадный
|
|
231
|
+
-- матчинг по порядку), элемент-строка = legacy-имя класса
|
|
232
|
+
-- START_BLOCK_ENTITY_CHECK_LINK
|
|
176
233
|
IF s.category = 'LINK' THEN
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
234
|
+
is_v2 := EXISTS (
|
|
235
|
+
SELECT 1 FROM jsonb_array_elements(COALESCE(s.links, '[]'::jsonb)) le
|
|
236
|
+
WHERE jsonb_typeof(le.value) = 'object');
|
|
237
|
+
IF is_v2 THEN
|
|
238
|
+
used := ARRAY[]::text[];
|
|
239
|
+
FOR ej IN SELECT le.value FROM jsonb_array_elements(COALESCE(s.links, '[]'::jsonb)) le LOOP
|
|
240
|
+
ecs := COALESCE(
|
|
241
|
+
(SELECT array_agg(x) FROM jsonb_array_elements_text(ej->'classes') x),
|
|
242
|
+
ARRAY[ej->>'class']);
|
|
243
|
+
hit := NULL;
|
|
244
|
+
FOREACH c2 IN ARRAY ecs LOOP
|
|
245
|
+
IF (COALESCE(NEW.links, '{}'::jsonb) ? c2) AND NOT (c2 = ANY(used)) THEN
|
|
246
|
+
hit := c2;
|
|
247
|
+
EXIT;
|
|
248
|
+
END IF;
|
|
249
|
+
END LOOP;
|
|
250
|
+
IF hit IS NOT NULL THEN
|
|
251
|
+
used := used || hit;
|
|
252
|
+
ELSIF NOT COALESCE((ej->>'optional')::boolean, false) THEN
|
|
253
|
+
RAISE EXCEPTION 'ENTITY_CHECK: link "%" requires end "%"', NEW.class, array_to_string(ecs, '|');
|
|
254
|
+
END IF;
|
|
255
|
+
END LOOP;
|
|
256
|
+
SELECT e.key INTO bad
|
|
257
|
+
FROM jsonb_each(COALESCE(NEW.links, '{}'::jsonb)) e
|
|
258
|
+
WHERE NOT (e.key = ANY(used))
|
|
259
|
+
LIMIT 1;
|
|
260
|
+
IF bad IS NOT NULL THEN
|
|
261
|
+
RAISE EXCEPTION 'ENTITY_CHECK: link "%" has stray link "%" — not an end in Schema.links', NEW.class, bad;
|
|
180
262
|
END IF;
|
|
181
|
-
|
|
263
|
+
ELSE
|
|
264
|
+
FOR k IN SELECT le.value #>> '{}' FROM jsonb_array_elements(COALESCE(s.links, '[]'::jsonb)) le LOOP
|
|
265
|
+
IF k <> 'Entity' AND NOT (COALESCE(NEW.links, '{}'::jsonb) ? k) THEN
|
|
266
|
+
RAISE EXCEPTION 'ENTITY_CHECK: link "%" requires end "%"', NEW.class, k;
|
|
267
|
+
END IF;
|
|
268
|
+
END LOOP;
|
|
269
|
+
END IF;
|
|
182
270
|
END IF;
|
|
183
271
|
|
|
272
|
+
-- END_BLOCK_ENTITY_CHECK_LINK
|
|
184
273
|
RETURN NEW;
|
|
185
274
|
END $$;
|
|
186
275
|
|
|
@@ -196,6 +285,13 @@ CREATE TRIGGER entity_check
|
|
|
196
285
|
-- * вставка проходит entity_check (валидация класса/links).
|
|
197
286
|
-- Работает и для голого SQL: UPDATE "<SCHEMA-NAME>"."Entity" SET data=… WHERE …
|
|
198
287
|
-- -----------------------------------------------------------------------------
|
|
288
|
+
-- START_CONTRACT: entity_update
|
|
289
|
+
-- PURPOSE: Превратить UPDATE в INSERT новой версии — бьёт только актуальную строку, историю не трогает, updated монотонно растёт.
|
|
290
|
+
-- INPUTS: { trigger: BEFORE UPDATE ON Entity (OLD/NEW) }
|
|
291
|
+
-- OUTPUTS: { NULL - физический UPDATE отменён }
|
|
292
|
+
-- SIDE_EFFECTS: INSERT новой версии (проходит entity_check)
|
|
293
|
+
-- LINKS: M-DDL, V-M-DDL
|
|
294
|
+
-- END_CONTRACT: entity_update
|
|
199
295
|
CREATE OR REPLACE FUNCTION "<SCHEMA-NAME>".entity_update() RETURNS trigger
|
|
200
296
|
LANGUAGE plpgsql AS $$
|
|
201
297
|
DECLARE latest timestamptz;
|
|
@@ -228,6 +324,13 @@ CREATE TRIGGER entity_update
|
|
|
228
324
|
-- * физического удаления не происходит никогда (очистка истории — retention-политики).
|
|
229
325
|
-- Работает и для голого SQL: DELETE FROM "<SCHEMA-NAME>"."Entity" WHERE partition=… AND class=… AND id=…
|
|
230
326
|
-- -----------------------------------------------------------------------------
|
|
327
|
+
-- START_CONTRACT: entity_delete
|
|
328
|
+
-- PURPOSE: Превратить DELETE в tombstone + рекурсивный каскад по links; advisory-lock сериализует параллельные удаления; физического удаления нет.
|
|
329
|
+
-- INPUTS: { trigger: BEFORE DELETE ON Entity (OLD) }
|
|
330
|
+
-- OUTPUTS: { NULL - физическое удаление отменено }
|
|
331
|
+
-- SIDE_EFFECTS: INSERT tombstone; DELETE живых зависимых (рекурсия триггера); pg_advisory_xact_lock
|
|
332
|
+
-- LINKS: M-DDL, V-M-DDL, M-WRITE
|
|
333
|
+
-- END_CONTRACT: entity_delete
|
|
231
334
|
CREATE OR REPLACE FUNCTION "<SCHEMA-NAME>".entity_delete() RETURNS trigger
|
|
232
335
|
LANGUAGE plpgsql AS $$
|
|
233
336
|
DECLARE latest timestamptz;
|
|
@@ -268,6 +371,7 @@ CREATE TRIGGER entity_delete
|
|
|
268
371
|
-- =============================================================================
|
|
269
372
|
|
|
270
373
|
-- Аккаунты: пользователи и сервисы. Категории: System | User | Anonymous | Shadow…
|
|
374
|
+
-- START_BLOCK_AUTH_TABLES
|
|
271
375
|
CREATE TABLE IF NOT EXISTS "<SCHEMA-NAME>"."Account" (
|
|
272
376
|
id uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY,
|
|
273
377
|
categories character varying[] DEFAULT ARRAY['User'::text] NOT NULL,
|
|
@@ -325,11 +429,19 @@ CREATE TABLE IF NOT EXISTS "<SCHEMA-NAME>"."Rule" (
|
|
|
325
429
|
enabled boolean DEFAULT true NOT NULL,
|
|
326
430
|
PRIMARY KEY (account, resource)
|
|
327
431
|
);
|
|
432
|
+
-- END_BLOCK_AUTH_TABLES
|
|
328
433
|
|
|
329
434
|
-- -----------------------------------------------------------------------------
|
|
330
435
|
-- Триггер 4: realtime — факт каждой новой версии в pg_notify (канал = имя схемы).
|
|
331
436
|
-- Payload лёгкий (без data): подписчик дочитывает нужное сам. Потребитель: db.watch().
|
|
332
437
|
-- -----------------------------------------------------------------------------
|
|
438
|
+
-- START_CONTRACT: entity_notify
|
|
439
|
+
-- PURPOSE: Опубликовать факт новой версии в pg_notify (канал = имя схемы), лёгкий payload без data.
|
|
440
|
+
-- INPUTS: { trigger: AFTER INSERT ON Entity (NEW) }
|
|
441
|
+
-- OUTPUTS: { NULL }
|
|
442
|
+
-- SIDE_EFFECTS: pg_notify(канал = имя схемы)
|
|
443
|
+
-- LINKS: M-DDL, V-M-DDL, M-CHAIN
|
|
444
|
+
-- END_CONTRACT: entity_notify
|
|
333
445
|
CREATE OR REPLACE FUNCTION "<SCHEMA-NAME>".entity_notify() RETURNS trigger
|
|
334
446
|
LANGUAGE plpgsql AS $$
|
|
335
447
|
BEGIN
|
package/sql/seed.auth.sql
CHANGED
|
@@ -4,11 +4,35 @@
|
|
|
4
4
|
-- Идемпотентен (ON CONFLICT).
|
|
5
5
|
-- =============================================================================
|
|
6
6
|
|
|
7
|
+
-- FILE: lib/sql/seed.auth.sql
|
|
8
|
+
-- VERSION: 1.0.0
|
|
9
|
+
-- START_MODULE_CONTRACT
|
|
10
|
+
-- PURPOSE: Идемпотентный сид System-аккаунта и дефолтного набора ACL Resource/Rule для auth-эндпоинтов.
|
|
11
|
+
-- SCOPE: 1 Account (System), 16 Resource (subjects+API), 12 Rule (allow/deny с весами).
|
|
12
|
+
-- DEPENDS: none
|
|
13
|
+
-- LINKS: M-SEED-AUTH, V-M-SEED-AUTH
|
|
14
|
+
-- ROLE: CONFIG
|
|
15
|
+
-- MAP_MODE: SUMMARY
|
|
16
|
+
-- END_MODULE_CONTRACT
|
|
17
|
+
--
|
|
18
|
+
-- START_MODULE_MAP
|
|
19
|
+
-- Account - один System-аккаунт (фикс. id 2eba6d0a-…)
|
|
20
|
+
-- Resource - 16 ресурсов ACL (anonymous/authenticated/any, auth.*, payment/shop/billing/geo)
|
|
21
|
+
-- Rule - 12 правил allow/deny, вес 0..105
|
|
22
|
+
-- END_MODULE_MAP
|
|
23
|
+
--
|
|
24
|
+
-- START_CHANGE_SUMMARY
|
|
25
|
+
-- LAST_CHANGE: [v1.0.0 - Documented existing module: reverse-engineered contract + markup]
|
|
26
|
+
-- END_CHANGE_SUMMARY
|
|
27
|
+
|
|
28
|
+
-- START_BLOCK_SEED_ACCOUNT
|
|
7
29
|
-- Системный аккаунт (фиксированный id из дампа)
|
|
8
30
|
INSERT INTO "<SCHEMA-NAME>"."Account" (id, categories, data, meta, avatar, enabled) VALUES
|
|
9
31
|
('2eba6d0a-1edd-4bb6-a85a-a703dab49035', '{System}', '{"id": "System", "name": "System"}', '{}', 'https://i.pravatar.cc/128?img=28', true)
|
|
10
32
|
ON CONFLICT (id) DO NOTHING;
|
|
11
33
|
|
|
34
|
+
-- END_BLOCK_SEED_ACCOUNT
|
|
35
|
+
-- START_BLOCK_SEED_RESOURCES
|
|
12
36
|
-- Ресурсы: субъекты (категории аккаунтов) и объекты (API-эндпоинты)
|
|
13
37
|
INSERT INTO "<SCHEMA-NAME>"."Resource" (alias, category, pattern, meta) VALUES
|
|
14
38
|
('anonymous:ACCOUNT', 'ACCOUNT', '{"categories": "{Anonymous,Shadow}"}', '{"description": "Unauthenticated users (includes Anonymous and Shadow categories)"}'),
|
|
@@ -29,6 +53,8 @@ INSERT INTO "<SCHEMA-NAME>"."Resource" (alias, category, pattern, meta) VALUES
|
|
|
29
53
|
('geo:API', 'API', '{"endpoint": "v2.geo.*"}', NULL)
|
|
30
54
|
ON CONFLICT (alias) DO UPDATE SET category = EXCLUDED.category, pattern = EXCLUDED.pattern, meta = EXCLUDED.meta;
|
|
31
55
|
|
|
56
|
+
-- END_BLOCK_SEED_RESOURCES
|
|
57
|
+
-- START_BLOCK_SEED_RULES
|
|
32
58
|
-- Правила: субъект → объект → allow|deny (weight — приоритет)
|
|
33
59
|
INSERT INTO "<SCHEMA-NAME>"."Rule" (account, resource, permission, weight, meta, enabled) VALUES
|
|
34
60
|
('authenticated:ACCOUNT', 'auth.user.password:API', 'allow', 90, '{"description": "Users can change their own password"}', true),
|
|
@@ -44,3 +70,4 @@ INSERT INTO "<SCHEMA-NAME>"."Rule" (account, resource, permission, weight, meta,
|
|
|
44
70
|
('authenticated:ACCOUNT', 'billing:API', 'allow', 90, '{"description": "Users can access billing (uses ctx.meta.$account.id)"}', true),
|
|
45
71
|
('authenticated:ACCOUNT', 'geo:API', 'allow', 90, '{"description": "Users can access geo (uses ctx.meta.$account.id)"}', true)
|
|
46
72
|
ON CONFLICT (account, resource) DO UPDATE SET permission = EXCLUDED.permission, weight = EXCLUDED.weight, meta = EXCLUDED.meta, enabled = EXCLUDED.enabled;
|
|
73
|
+
-- END_BLOCK_SEED_RULES
|
package/sql/seed.booking.sql
CHANGED
|
@@ -1,64 +1,111 @@
|
|
|
1
1
|
-- =============================================================================
|
|
2
|
-
-- Сид таблицы Schema
|
|
3
|
-
--
|
|
2
|
+
-- Сид таблицы Schema — ИСТОЧНИК ПРАВДЫ демо-домена booking (19 классов, 0.17).
|
|
3
|
+
-- Редактируется руками; накат: db/apply.mjs / up() (маркер <SCHEMA-NAME>).
|
|
4
|
+
-- Идемпотентен (ON CONFLICT). Концы связей — Schema.links v2 (JSON-объекты в text[]).
|
|
4
5
|
-- =============================================================================
|
|
6
|
+
--
|
|
7
|
+
-- FILE: lib/sql/seed.booking.sql
|
|
8
|
+
-- VERSION: 1.0.0
|
|
9
|
+
-- START_MODULE_CONTRACT
|
|
10
|
+
-- PURPOSE: Идемпотентный сид таблицы Schema — источник правды демо-домена booking (классы HUB + LINK).
|
|
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).
|
|
12
|
+
-- DEPENDS: none
|
|
13
|
+
-- LINKS: M-SEED-BOOKING, V-M-SEED-BOOKING
|
|
14
|
+
-- ROLE: CONFIG
|
|
15
|
+
-- MAP_MODE: SUMMARY
|
|
16
|
+
-- END_MODULE_CONTRACT
|
|
17
|
+
--
|
|
18
|
+
-- START_MODULE_MAP
|
|
19
|
+
-- HUB-классы - сущности домена (наследование от Entity; id v5/v7)
|
|
20
|
+
-- LINK-классы - связи (концы Schema.links v2; id v5 из концов)
|
|
21
|
+
-- NB: фактически 20 классов (12 HUB + 8 LINK); заголовок файла говорит «19» — расхождение
|
|
22
|
+
-- END_MODULE_MAP
|
|
23
|
+
--
|
|
24
|
+
-- START_CHANGE_SUMMARY
|
|
25
|
+
-- LAST_CHANGE: [v1.0.0 - Documented existing module: reverse-engineered contract + markup]
|
|
26
|
+
-- END_CHANGE_SUMMARY
|
|
27
|
+
--
|
|
28
|
+
-- START_BLOCK_SEED_HUB_CLASSES
|
|
5
29
|
|
|
6
30
|
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
7
|
-
('entity', 'Entity', 'Сущность', 'HUB', NULL, '{"id":"
|
|
31
|
+
('entity', 'Entity', 'Сущность', 'HUB', NULL, '{"id":{"type":"uuid","generate":7}}'::jsonb, '{"abstract":true,"appearance":{"icon":{"name":"mdi:atom","color":"text-violet-500"}},"description":"Абстрактный корень всех сущностей; дефолт id v7 наследуют все классы без своего правила"}'::jsonb, '[]'::jsonb, 100, ARRAY['Entity']::text[])
|
|
8
32
|
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
9
33
|
|
|
10
34
|
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
11
|
-
('entity', 'Org', 'Организация', 'HUB', 'Entity', '{"
|
|
35
|
+
('entity', 'Org', 'Организация', 'HUB', 'Entity', '{"name":"string","timezone":{"type":"string","default":"Europe/Moscow"},"address":"string|optional","phone":"string|optional","active":{"type":"boolean","default":true},"settings":{"type":"object","optional":true,"strict":true,"props":{"booking":{"type":"object","optional":true,"strict":true,"props":{"deposit":{"type":"object","optional":true,"strict":true,"props":{"amount":"number|integer|min:0|default:0","currency":{"type":"enum","values":["RUB","USD","EUR","AED"],"default":"RUB"}}},"autoconfirm":"boolean|default:false"}}}}}'::jsonb, '{"abstract":false,"appearance":{"icon":{"name":"mdi:domain","color":"text-blue-600"}},"description":"Организация — арендатор (мультитенантность): владелец каталога, локаций и расписаний; settings — вложенные настройки (пример структуры глубины 4)"}'::jsonb, '[]'::jsonb, 200, ARRAY['Org', 'Entity']::text[])
|
|
12
36
|
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
13
37
|
|
|
14
38
|
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
15
|
-
('entity', '
|
|
39
|
+
('entity', 'Person', 'Контрагент', 'HUB', 'Entity', '{"name":"string","phone":"string"}'::jsonb, '{"abstract":true,"appearance":{"icon":{"name":"mdi:account-outline","color":"text-blue-500"}},"description":"Абстрактный контрагент: имя + телефон; наследуют Мастер и Клиент"}'::jsonb, '[]'::jsonb, 300, ARRAY['Person', 'Entity']::text[])
|
|
16
40
|
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
17
41
|
|
|
18
42
|
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
19
|
-
('entity', '
|
|
43
|
+
('entity', 'Staff', 'Мастер', 'HUB', 'Person', '{"photo":"string|optional","description":"string|optional","specialization":"string|optional"}'::jsonb, '{"abstract":false,"appearance":{"icon":{"name":"mdi:account-star","color":"text-indigo-500"}},"description":"Мастер — бронируемый исполнитель; name/phone наследует от Контрагента; локации — LINK адрес, доступность — LINK окно"}'::jsonb, '[{"class":"Org","cardinality":1}]'::jsonb, 400, ARRAY['Staff', 'Person', 'Entity']::text[])
|
|
20
44
|
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
21
45
|
|
|
22
46
|
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
23
|
-
('entity', '
|
|
47
|
+
('entity', 'Customer', 'Клиент', 'HUB', 'Person', '{"notes":"string|optional","preferred_contact":{"type":"enum","values":["phone","email","messenger"],"default":"phone"}}'::jsonb, '{"abstract":false,"appearance":{"icon":{"name":"mdi:account","color":"text-green-500"}},"description":"Клиент — записывается на услуги; name/phone наследует от Контрагента"}'::jsonb, '[{"class":"Org","cardinality":1}]'::jsonb, 500, ARRAY['Customer', 'Person', 'Entity']::text[])
|
|
24
48
|
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
25
49
|
|
|
26
50
|
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
27
|
-
('entity', '
|
|
51
|
+
('entity', 'Location', 'Локация', 'HUB', 'Entity', '{"address":"string","coordinates":{"type":"object","optional":true,"props":{"lat":"number","lng":"number"}},"description":"string|optional"}'::jsonb, '{"abstract":false,"appearance":{"icon":{"name":"mdi:map-marker","color":"text-red-500"}},"description":"Локация — точка обслуживания организации"}'::jsonb, '[{"class":"Org","cardinality":1}]'::jsonb, 600, ARRAY['Location', 'Entity']::text[])
|
|
28
52
|
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
29
53
|
|
|
30
54
|
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
31
|
-
('entity', '
|
|
55
|
+
('entity', 'Element', 'Элемент', 'HUB', 'Entity', '{"id":{"type":"uuid","generate":5,"from":["Org","name"]},"name":"string","description":"string|optional"}'::jsonb, '{"abstract":true,"appearance":{"icon":{"name":"mdi:view-list","color":"text-teal-500"}},"description":"Абстрактная каталожная единица (Услуга/Товар/Комплекс). Правило id v5(Org, name) объявлено здесь один раз — наследуется потомками (имя уникально в организации, classId различает классы); links не наследуются — конец Org каждый потомок объявляет сам. Цена — отдельный LINK «цена» (варианты по пожеланиям, мультивалюта)"}'::jsonb, '[{"class":"Org","cardinality":1}]'::jsonb, 700, ARRAY['Element', 'Entity']::text[])
|
|
32
56
|
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
33
57
|
|
|
34
58
|
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
35
|
-
('entity', '
|
|
59
|
+
('entity', 'Service', 'Услуга', 'HUB', 'Element', '{"duration":"number|min:0|optional"}'::jsonb, '{"abstract":false,"appearance":{"icon":{"name":"mdi:spa","color":"text-purple-500"}},"description":"Услуга: длительность (мин); name/id-правило наследует от Элемента; цена — LINK «цена»"}'::jsonb, '[{"class":"Org","cardinality":1}]'::jsonb, 800, ARRAY['Service', 'Element', 'Entity']::text[])
|
|
36
60
|
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
37
61
|
|
|
38
62
|
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
39
|
-
('entity', '
|
|
63
|
+
('entity', 'Product', 'Товар', 'HUB', 'Element', '{"sku":"string|optional","unit":{"type":"string","default":"шт"}}'::jsonb, '{"abstract":false,"appearance":{"icon":{"name":"mdi:package","color":"text-blue-400"}},"description":"Товар: артикул + единица измерения; name/id-правило наследует от Элемента; цена — LINK «цена»"}'::jsonb, '[{"class":"Org","cardinality":1}]'::jsonb, 900, ARRAY['Product', 'Element', 'Entity']::text[])
|
|
40
64
|
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
41
65
|
|
|
42
66
|
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
43
|
-
('entity', '
|
|
67
|
+
('entity', 'Complex', 'Комплекс', 'HUB', 'Element', '{"cost":"number|min:0","duration":"number|min:0"}'::jsonb, '{"abstract":false,"appearance":{"icon":{"name":"mdi:package-variant-closed","color":"text-orange-500"}},"description":"Комплекс: собственные cost/duration; состав — LINK состав (Комплекс ← Элемент); цена — LINK «цена»"}'::jsonb, '[{"class":"Org","cardinality":1}]'::jsonb, 1000, ARRAY['Complex', 'Element', 'Entity']::text[])
|
|
44
68
|
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
45
69
|
|
|
46
70
|
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
47
|
-
('entity', '
|
|
71
|
+
('entity', 'Schedule', 'Расписание', 'HUB', 'Entity', '{"name":"string","year":"number|integer","month":"number|integer|min:1|max:12"}'::jsonb, '{"abstract":false,"appearance":{"icon":{"name":"mdi:calendar","color":"text-cyan-500"}},"description":"Месячное расписание организации; наполнение — LINK окно"}'::jsonb, '[{"class":"Org","cardinality":1}]'::jsonb, 1100, ARRAY['Schedule', 'Entity']::text[])
|
|
48
72
|
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
49
73
|
|
|
50
74
|
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
51
|
-
('entity', '
|
|
75
|
+
('entity', 'Folder', 'Папка', 'HUB', 'Entity', '{"name":"string"}'::jsonb, '{"abstract":false,"appearance":{"icon":{"name":"mdi:folder","color":"text-yellow-500"}},"description":"Папка-категория; вложенность — HUB-конец на родительскую Папку; наполнение — LINK содержимое"}'::jsonb, '[{"class":"Org","cardinality":1},{"class":"Folder","optional":true,"cardinality":1}]'::jsonb, 1200, ARRAY['Folder', 'Entity']::text[])
|
|
52
76
|
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
53
77
|
|
|
78
|
+
-- END_BLOCK_SEED_HUB_CLASSES
|
|
79
|
+
-- START_BLOCK_SEED_LINK_CLASSES
|
|
54
80
|
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
55
|
-
('entity', '
|
|
81
|
+
('entity', 'link', 'связь', 'LINK', NULL, '{"id":{"type":"uuid","generate":7}}'::jsonb, '{"abstract":true,"appearance":{"icon":{"name":"mdi:link","color":"text-sky-500"}},"description":"Абстрактная связь; концы объявляются потомками в links (Schema.links v2: объекты-концы, только HUB-классы)"}'::jsonb, '[]'::jsonb, 100, ARRAY['link']::text[])
|
|
56
82
|
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
57
83
|
|
|
58
84
|
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
59
|
-
('entity', '
|
|
85
|
+
('entity', 'address', 'адрес', 'LINK', 'link', '{"id":{"type":"uuid","generate":5,"from":["Staff","Location"]},"default":"boolean|default:false","schedule_info":"string|optional"}'::jsonb, '{"abstract":false,"appearance":{"icon":{"name":"mdi:link","color":"text-sky-500"}},"description":"Адрес: Мастер работает в Локации; default — основная точка мастера"}'::jsonb, '[{"class":"Staff","cardinality":1},{"class":"Location","cardinality":1}]'::jsonb, 200, ARRAY['address', 'link']::text[])
|
|
60
86
|
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
61
87
|
|
|
62
88
|
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
63
|
-
('entity', '
|
|
89
|
+
('entity', 'slot', 'окно', 'LINK', 'link', '{"id":{"type":"uuid","generate":5,"from":["Staff","start_datetime"]},"start_datetime":{"type":"date","convert":true},"end_datetime":{"type":"date","convert":true}}'::jsonb, '{"abstract":false,"appearance":{"icon":{"name":"mdi:clock-outline","color":"text-amber-500"}},"description":"Окно доступности: Мастер × Локация × Расписание + интервал. id = v5(Мастер, старт) — мастер не задваивает время ни в одной локации"}'::jsonb, '[{"class":"Staff","cardinality":1},{"class":"Location","cardinality":1},{"class":"Schedule","cardinality":1}]'::jsonb, 300, ARRAY['slot', 'link']::text[])
|
|
64
90
|
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
91
|
+
|
|
92
|
+
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
93
|
+
('entity', 'booking', 'запись', 'LINK', 'slot', '{"notes":"string|optional"}'::jsonb, '{"abstract":false,"validation":"strict","appearance":{"icon":{"name":"mdi:calendar-check","color":"text-emerald-500"}},"description":"Запись — наследник окна (start/end_datetime и правило id v5(Мастер, старт) наследуются): бронь мастера на Элемент для Клиента. Ручная бронь — без Клиента, имя в notes. classId в v5-формуле различает окно и запись; двойная бронь того же времени мертва"}'::jsonb, '[{"class":"Staff","cardinality":1},{"class":"Location","cardinality":1},{"class":"Schedule","cardinality":1},{"classes":["Service","Product","Complex"],"cardinality":1},{"class":"Customer","optional":true,"cardinality":1}]'::jsonb, 400, ARRAY['booking', 'slot', 'link']::text[])
|
|
94
|
+
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
95
|
+
|
|
96
|
+
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
97
|
+
('entity', 'content', 'содержимое', 'LINK', 'link', '{"id":{"type":"uuid","generate":5,"from":["Folder","Service|Product|Complex"]},"order":"number|integer|default:0"}'::jsonb, '{"abstract":false,"appearance":{"icon":{"name":"mdi:link","color":"text-sky-500"}},"description":"Содержимое папки: Папка ← Элемент (+порядок); элемент может лежать в нескольких папках"}'::jsonb, '[{"class":"Folder","cardinality":1},{"classes":["Service","Product","Complex"],"cardinality":1}]'::jsonb, 500, ARRAY['content', 'link']::text[])
|
|
98
|
+
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
99
|
+
|
|
100
|
+
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
101
|
+
('entity', 'compo', 'состав', 'LINK', 'link', '{"id":{"type":"uuid","generate":5,"from":["Complex","Service|Product"]},"order":"number|integer|default:0","quantity":"number|integer|min:1|default:1"}'::jsonb, '{"abstract":false,"appearance":{"icon":{"name":"mdi:link","color":"text-sky-500"}},"description":"Состав комплекса: Комплекс ← Элемент (+порядок, +количество); в состав входят и услуги, и товары"}'::jsonb, '[{"class":"Complex","cardinality":1},{"classes":["Service","Product"],"cardinality":1}]'::jsonb, 600, ARRAY['compo', 'link']::text[])
|
|
102
|
+
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
103
|
+
|
|
104
|
+
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
105
|
+
('entity', 'skill', 'навык', 'LINK', 'link', '{"id":{"type":"uuid","generate":5,"from":["Staff","Service"]},"level":{"type":"enum","values":["basic","intermediate","advanced","expert"],"default":"intermediate"}}'::jsonb, '{"abstract":false,"appearance":{"icon":{"name":"mdi:link","color":"text-sky-500"}},"description":"Навык: Мастер умеет Услугу (+уровень); умение комплекса вычислимо по составу"}'::jsonb, '[{"class":"Staff","cardinality":1},{"class":"Service","cardinality":1}]'::jsonb, 700, ARRAY['skill', 'link']::text[])
|
|
106
|
+
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
107
|
+
|
|
108
|
+
INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
|
|
109
|
+
('entity', 'price', 'цена', 'LINK', 'link', '{"id":{"type":"uuid","generate":5,"from":["Service|Product|Complex","note"]},"note":{"type":"string","default":"базовая"},"amounts":{"type":"record","key":{"type":"enum","values":["RUB","USD","EUR","AED"]},"value":"number|integer|min:0"}}'::jsonb, '{"abstract":false,"appearance":{"icon":{"name":"mdi:cash-multiple","color":"text-green-600"}},"description":"Вариант цены каталожного элемента: мультивалютные суммы (amounts) + описание (note). У элемента несколько вариантов по пожеланиям клиента (note различает: базовая/с дизайном/…); id v5(элемент, note) — note не задан ⇒ дефолт «базовая»; правку одной валюты amounts мержит точечно"}'::jsonb, '[{"classes":["Service","Product","Complex"],"cardinality":1}]'::jsonb, 800, ARRAY['price', 'link']::text[])
|
|
110
|
+
ON CONFLICT (partition, id) DO UPDATE SET alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor, attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order", ancestors = EXCLUDED.ancestors;
|
|
111
|
+
-- END_BLOCK_SEED_LINK_CLASSES
|