letopis 0.16.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/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
- -- Сидится из schema.booking.v2.json (lib/sql/seed.booking.sql).
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'
@@ -28,6 +55,7 @@ CREATE TABLE IF NOT EXISTS "<SCHEMA-NAME>"."Schema" (
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,6 +162,7 @@ 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 (только проверки, никакой логики приложения).
@@ -140,6 +177,14 @@ CREATE INDEX IF NOT EXISTS entity_owner_idx ON "<SCHEMA-NAME>"."Entity" (owner
140
177
  -- полиморфный (его резолвит либа), лишние связи не проверяются.
141
178
  -- Tombstone-версии (deleted IS NOT NULL) не валидируются.
142
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
143
188
  CREATE OR REPLACE FUNCTION "<SCHEMA-NAME>".entity_check() RETURNS trigger
144
189
  LANGUAGE plpgsql AS $$
145
190
  DECLARE
@@ -184,6 +229,7 @@ BEGIN
184
229
 
185
230
  -- LINK: концы по Schema.links — jsonb-массив; элемент-объект = v2 (строгий жадный
186
231
  -- матчинг по порядку), элемент-строка = legacy-имя класса
232
+ -- START_BLOCK_ENTITY_CHECK_LINK
187
233
  IF s.category = 'LINK' THEN
188
234
  is_v2 := EXISTS (
189
235
  SELECT 1 FROM jsonb_array_elements(COALESCE(s.links, '[]'::jsonb)) le
@@ -223,6 +269,7 @@ BEGIN
223
269
  END IF;
224
270
  END IF;
225
271
 
272
+ -- END_BLOCK_ENTITY_CHECK_LINK
226
273
  RETURN NEW;
227
274
  END $$;
228
275
 
@@ -238,6 +285,13 @@ CREATE TRIGGER entity_check
238
285
  -- * вставка проходит entity_check (валидация класса/links).
239
286
  -- Работает и для голого SQL: UPDATE "<SCHEMA-NAME>"."Entity" SET data=… WHERE …
240
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
241
295
  CREATE OR REPLACE FUNCTION "<SCHEMA-NAME>".entity_update() RETURNS trigger
242
296
  LANGUAGE plpgsql AS $$
243
297
  DECLARE latest timestamptz;
@@ -270,6 +324,13 @@ CREATE TRIGGER entity_update
270
324
  -- * физического удаления не происходит никогда (очистка истории — retention-политики).
271
325
  -- Работает и для голого SQL: DELETE FROM "<SCHEMA-NAME>"."Entity" WHERE partition=… AND class=… AND id=…
272
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
273
334
  CREATE OR REPLACE FUNCTION "<SCHEMA-NAME>".entity_delete() RETURNS trigger
274
335
  LANGUAGE plpgsql AS $$
275
336
  DECLARE latest timestamptz;
@@ -310,6 +371,7 @@ CREATE TRIGGER entity_delete
310
371
  -- =============================================================================
311
372
 
312
373
  -- Аккаунты: пользователи и сервисы. Категории: System | User | Anonymous | Shadow…
374
+ -- START_BLOCK_AUTH_TABLES
313
375
  CREATE TABLE IF NOT EXISTS "<SCHEMA-NAME>"."Account" (
314
376
  id uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY,
315
377
  categories character varying[] DEFAULT ARRAY['User'::text] NOT NULL,
@@ -367,11 +429,19 @@ CREATE TABLE IF NOT EXISTS "<SCHEMA-NAME>"."Rule" (
367
429
  enabled boolean DEFAULT true NOT NULL,
368
430
  PRIMARY KEY (account, resource)
369
431
  );
432
+ -- END_BLOCK_AUTH_TABLES
370
433
 
371
434
  -- -----------------------------------------------------------------------------
372
435
  -- Триггер 4: realtime — факт каждой новой версии в pg_notify (канал = имя схемы).
373
436
  -- Payload лёгкий (без data): подписчик дочитывает нужное сам. Потребитель: db.watch().
374
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
375
445
  CREATE OR REPLACE FUNCTION "<SCHEMA-NAME>".entity_notify() RETURNS trigger
376
446
  LANGUAGE plpgsql AS $$
377
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
@@ -1,69 +1,111 @@
1
1
  -- =============================================================================
2
- -- Сид таблицы Schema из schema.booking.v2.json (источник правды — JSON).
3
- -- Файл сгенерирован: node lib/scripts/gen-seed.mjs — не редактировать вручную.
2
+ -- Сид таблицы Schema — ИСТОЧНИК ПРАВДЫ демо-домена booking (19 классов, 0.17).
3
+ -- Редактируется руками; накат: db/apply.mjs / up() (маркер <SCHEMA-NAME>).
4
4
  -- Идемпотентен (ON CONFLICT). Концы связей — Schema.links v2 (JSON-объекты в text[]).
5
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
6
29
 
7
30
  INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
8
- ('entity', 'Entity', 'Сущность', 'HUB', NULL, '{"id":{"type":"uuid","generate":7}}'::jsonb, '{"abstract":true,"description":"Абстрактный корень всех сущностей"}'::jsonb, '[]'::jsonb, 100, ARRAY['Entity']::text[])
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[])
9
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;
10
33
 
11
34
  INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
12
- ('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,"description":"Организация / точка / филиал — владелец каталога и расписаний; settings — вложенные настройки (пример структуры глубины 4)"}'::jsonb, '[]'::jsonb, 200, ARRAY['Org', 'Entity']::text[])
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[])
13
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;
14
37
 
15
38
  INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
16
- ('entity', 'Staff', 'Сотрудник', 'HUB', 'Entity', '{"name":"string","roles":{"type":"array","items":{"type":"enum","values":["owner","administrator","manager","master"]},"default":["master"]},"active":{"type":"boolean","default":true}}'::jsonb, '{"abstract":false,"description":"Человек: роли совмещаются (owner+master). Бронируемый ⇔ master ∈ roles. Владелец = первый сотрудник с owner."}'::jsonb, '[{"class":"Org","cardinality":1}]'::jsonb, 300, ARRAY['Staff', 'Entity']::text[])
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[])
17
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;
18
41
 
19
42
  INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
20
- ('entity', 'Folder', 'Папка', 'HUB', 'Entity', '{"name":"string","kind":{"type":"enum","values":["services","masters"],"default":"services"}}'::jsonb, '{"abstract":false,"description":"Папка-категория; вложенность — ссылкой на родительскую Папку в links"}'::jsonb, '[{"class":"Org","cardinality":1},{"class":"Folder","optional":true,"cardinality":1}]'::jsonb, 400, ARRAY['Folder', 'Entity']::text[])
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[])
21
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;
22
45
 
23
46
  INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
24
- ('entity', 'Service', 'Услуга', 'HUB', 'Entity', '{"id":{"type":"uuid","generate":5,"from":["Org","name"]},"name":"string","description":"string|optional","duration":"number|integer|positive","price":{"type":"record","key":{"type":"enum","values":["RUB","USD","EUR","AED"]},"value":"number|integer|min:0","default":{"RUB":0}},"active":{"type":"boolean","default":true}}'::jsonb, '{"abstract":false,"description":"Услуга: длительность (мин) + цена; занимает столько подряд окон, сколько покрывает duration"}'::jsonb, '[{"class":"Org","cardinality":1},{"class":"Folder","optional":true,"cardinality":1}]'::jsonb, 500, ARRAY['Service', 'Entity']::text[])
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[])
25
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;
26
49
 
27
50
  INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
28
- ('entity', 'Complex', 'Комплекс', 'HUB', 'Entity', '{"id":{"type":"uuid","generate":5,"from":["Org","name"]},"name":"string","description":"string|optional","duration":"number|integer|positive","discount":"number|integer|default:0","price":{"type":"record","key":{"type":"enum","values":["RUB","USD","EUR","AED"]},"value":"number|integer|min:0","optional":true}}'::jsonb, '{"abstract":false,"description":"Комплекс: собственный duration; состав — позиция(Комплекс→Услуга); свободный текст цены — в description"}'::jsonb, '[{"class":"Org","cardinality":1},{"class":"Folder","optional":true,"cardinality":1}]'::jsonb, 600, ARRAY['Complex', 'Entity']::text[])
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[])
29
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;
30
53
 
31
54
  INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
32
- ('entity', 'Customer', 'Клиент', 'HUB', 'Entity', '{"name":"string|optional","contact":"string|optional","language":{"type":"enum","values":["ru","en"],"default":"ru"},"active":{"type":"boolean","default":true}}'::jsonb, '{"abstract":false,"description":"Клиент — записывается на услуги"}'::jsonb, '[{"class":"Org","cardinality":1}]'::jsonb, 700, ARRAY['Customer', 'Entity']::text[])
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[])
33
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;
34
57
 
35
58
  INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
36
- ('entity', 'Schedule', 'Расписание', 'HUB', 'Entity', '{"start":{"type":"date","convert":true},"end":{"type":"date","convert":true},"active":{"type":"boolean","default":true}}'::jsonb, '{"abstract":false,"description":"Расписание — период организации, содержит непересекающиеся Окна (инвариант проверяет приложение)"}'::jsonb, '[{"class":"Org","cardinality":1}]'::jsonb, 800, ARRAY['Schedule', 'Entity']::text[])
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[])
37
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;
38
61
 
39
62
  INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
40
- ('entity', 'Slot', 'Окно', 'HUB', 'Entity', '{"id":{"type":"uuid","generate":5,"from":["Schedule","start"]},"start":{"type":"date","convert":true},"end":{"type":"date","convert":true}}'::jsonb, '{"abstract":false,"description":"Окно — общий таймлайн произвольной длины внутри Расписания; статуса нет — занятость мастера хранит LINK занятость"}'::jsonb, '[{"class":"Schedule","cardinality":1}]'::jsonb, 900, ARRAY['Slot', 'Entity']::text[])
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[])
41
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;
42
65
 
43
66
  INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
44
- ('entity', 'Booking', 'Запись', 'HUB', 'Entity', '{"status":{"type":"enum","values":["created","confirmed","completed","cancelled","no_show"],"default":"created"},"source":{"type":"enum","values":["online","web","admin","telegram"],"default":"online"},"comment":"string|optional","total":{"type":"record","key":{"type":"enum","values":["RUB","USD","EUR","AED"]},"value":"number|integer|min:0","default":{"RUB":0}}}'::jsonb, '{"abstract":false,"description":"Запись клиента. Без мастера (мульти-мастер): мастера — на занятости/позиции. Ручная бронь по имени: без Customer, имя в comment. Дата приёма = start первого окна брони. history: полный аудит версиями"}'::jsonb, '[{"class":"Customer","optional":true,"cardinality":1}]'::jsonb, 1000, ARRAY['Booking', 'Entity']::text[])
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[])
45
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;
46
69
 
47
70
  INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
48
- ('entity', 'link', 'связь', 'LINK', NULL, '{"id":{"type":"uuid","generate":7}}'::jsonb, '{"abstract":true,"description":"Абстрактная связь; концы объявляются потомками в links (Schema.links v2: объекты-концы)"}'::jsonb, '[]'::jsonb, 100, ARRAY['link']::text[])
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[])
49
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;
50
73
 
51
74
  INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
52
- ('entity', 'item', 'позиция', 'LINK', 'link', '{"qty":"number|integer|min:1|default:1","price":{"type":"record","key":{"type":"enum","values":["RUB","USD","EUR","AED"]},"value":"number|integer|min:0","optional":true}}'::jsonb, '{"abstract":false,"description":"Позиция заказа: Запись × предмет [Услуга|Комплекс] × Сотрудник?. Снимок цены в price. Состав комплекса — отдельный LINK «состав» (compo)"}'::jsonb, '[{"classes":["Booking"],"cardinality":1},{"classes":["Service","Complex"],"cardinality":1},{"class":"Staff","optional":true,"cardinality":1}]'::jsonb, 200, ARRAY['item', 'link']::text[])
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[])
53
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;
54
77
 
78
+ -- END_BLOCK_SEED_HUB_CLASSES
79
+ -- START_BLOCK_SEED_LINK_CLASSES
55
80
  INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
56
- ('entity', 'compo', 'состав', 'LINK', 'link', '{"id":{"type":"uuid","generate":5,"from":["Complex","Service"]},"qty":"number|integer|min:1|default:1"}'::jsonb, '{"abstract":false,"description":"Состав комплекса: Комплекс ← Услуга (+qty). Раньше жил на позиции — вынесен отдельным классом"}'::jsonb, '[{"class":"Complex","cardinality":1},{"class":"Service","cardinality":1}]'::jsonb, 250, ARRAY['compo', 'link']::text[])
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[])
57
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;
58
83
 
59
84
  INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
60
- ('entity', 'skill', 'навык', 'LINK', 'link', '{"id":{"type":"uuid","generate":5,"from":["Staff","Service|Complex"]}}'::jsonb, '{"abstract":false,"description":"Навык: Сотрудник умеет (Услуга|Комплекс); при записи проверяется"}'::jsonb, '[{"class":"Staff","cardinality":1},{"classes":["Service","Complex"],"cardinality":1}]'::jsonb, 300, ARRAY['skill', 'link']::text[])
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[])
61
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;
62
87
 
63
88
  INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
64
- ('entity', 'shift', 'смена', 'LINK', 'link', '{"id":{"type":"uuid","generate":5,"from":["Schedule","Staff"]}}'::jsonb, '{"abstract":false,"description":"Ростер: сотрудник работает в расписании ⇒ по умолчанию доступен во всех его окнах"}'::jsonb, '[{"class":"Schedule","cardinality":1},{"class":"Staff","cardinality":1}]'::jsonb, 400, ARRAY['shift', 'link']::text[])
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[])
65
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;
66
91
 
67
92
  INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
68
- ('entity', 'busy', 'занятость', 'LINK', 'link', '{"id":{"type":"uuid","generate":5,"from":["Slot","Staff"]},"kind":{"type":"enum","values":["off","hold","booking"],"default":"booking"},"reason":"string|optional"}'::jsonb, '{"abstract":false,"description":"Негативная доступность: свободно = НЕТ живой занятости. off — мастер убран из окна; hold — оформление; booking — бронь (+ключ Запись в links). id = uuidv5(Окно, Сотрудник) — считает схема (attributes.id) — детерминирован против двойной брони"}'::jsonb, '[{"class":"Staff","cardinality":1},{"class":"Slot","cardinality":1},{"class":"Booking","optional":true,"cardinality":1}]'::jsonb, 500, ARRAY['busy', 'link']::text[])
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[])
69
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