letopis 0.20.3 → 1.0.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.
Files changed (89) hide show
  1. package/AGENT-CHEATSHEET.en.md +368 -0
  2. package/AGENT-CHEATSHEET.md +354 -0
  3. package/CHANGELOG.md +348 -0
  4. package/MIGRATION.md +190 -0
  5. package/README.en.md +1937 -0
  6. package/README.md +1493 -3466
  7. package/dist/acl.d.ts +26 -50
  8. package/dist/acl.js +22 -267
  9. package/dist/admin.d.ts +138 -0
  10. package/dist/admin.js +170 -0
  11. package/dist/auth.d.ts +120 -73
  12. package/dist/auth.js +121 -306
  13. package/dist/cache.d.ts +73 -0
  14. package/dist/cache.js +148 -0
  15. package/dist/chain.d.ts +124 -191
  16. package/dist/chain.js +369 -551
  17. package/dist/cli.d.ts +2 -0
  18. package/dist/cli.js +164 -0
  19. package/dist/demo/booking.d.ts +289 -0
  20. package/dist/demo/booking.js +159 -0
  21. package/dist/errors.d.ts +29 -0
  22. package/dist/errors.js +70 -0
  23. package/dist/import.d.ts +179 -0
  24. package/dist/import.js +792 -0
  25. package/dist/index.d.ts +172 -26
  26. package/dist/index.js +304 -178
  27. package/dist/jsonschema.d.ts +22 -0
  28. package/dist/jsonschema.js +167 -0
  29. package/dist/load.d.ts +76 -0
  30. package/dist/load.js +884 -0
  31. package/dist/model.d.ts +166 -0
  32. package/dist/model.js +224 -0
  33. package/dist/ops.d.ts +7 -6
  34. package/dist/ops.js +7 -51
  35. package/dist/pglite.d.ts +22 -0
  36. package/dist/pglite.js +45 -0
  37. package/dist/registry.d.ts +57 -0
  38. package/dist/registry.js +82 -0
  39. package/dist/sql.d.ts +59 -142
  40. package/dist/sql.js +568 -654
  41. package/dist/sync.d.ts +31 -0
  42. package/dist/sync.js +108 -0
  43. package/dist/tx.d.ts +129 -8
  44. package/dist/tx.js +300 -73
  45. package/dist/typed.d.ts +97 -0
  46. package/dist/typed.js +1 -0
  47. package/dist/types.d.ts +71 -250
  48. package/dist/types.js +27 -108
  49. package/dist/up.d.ts +140 -47
  50. package/dist/up.js +339 -267
  51. package/dist/uuid.d.ts +21 -6
  52. package/dist/uuid.js +48 -64
  53. package/dist/validate.d.ts +24 -0
  54. package/dist/validate.js +251 -0
  55. package/dist/watch.d.ts +62 -0
  56. package/dist/watch.js +168 -0
  57. package/dist/write.d.ts +117 -74
  58. package/dist/write.js +658 -720
  59. package/llms.txt +26 -0
  60. package/package.json +49 -19
  61. package/sql/10-core.sql +136 -0
  62. package/sql/15-errors.sql +60 -0
  63. package/sql/20-context.sql +153 -0
  64. package/sql/30-validate.sql +423 -0
  65. package/sql/40-class.sql +259 -0
  66. package/sql/50-acl.sql +539 -0
  67. package/sql/60-write.sql +1369 -0
  68. package/sql/70-read.sql +245 -0
  69. package/sql/80-auth.sql +827 -0
  70. package/sql/90-time.sql +957 -0
  71. package/sql/95-seed.system.sql +178 -0
  72. package/sql/99-revision.sql +3 -0
  73. package/sql/README.md +56 -0
  74. package/sql/seed.booking.sql +39 -112
  75. package/dist/schema.d.ts +0 -15
  76. package/dist/schema.js +0 -351
  77. package/dist/sessions.d.ts +0 -32
  78. package/dist/sessions.js +0 -114
  79. package/dist/tables.d.ts +0 -105
  80. package/dist/tables.js +0 -248
  81. package/docker/Dockerfile +0 -40
  82. package/docker/start.sh +0 -18
  83. package/scripts/check-docs.mjs +0 -375
  84. package/scripts/gen-api-contract.mjs +0 -226
  85. package/scripts/gen-types.mjs +0 -350
  86. package/scripts/release-notes.mjs +0 -76
  87. package/scripts/schema-sync.mjs +0 -185
  88. package/sql/ddl.sql +0 -600
  89. package/sql/seed.auth.sql +0 -73
@@ -0,0 +1,178 @@
1
+ -- letopis 1.0 — системный сид (план, этап 1, п. 2; этап 5, п. 8).
2
+ -- System с постоянным id (как в 0.21), системные классы Class, Account, member, Resource, rule,
3
+ -- общие группы и системные права, сервисный аккаунт с ключом APIKEY. Сид пишет установщик от роли
4
+ -- владельца: триггеры для владельца не срабатывают, поэтому версию в журнал и служебные колонки сид
5
+ -- заполняет сам (§2.7). Ресурсы API конкретного продукта сюда не входят.
6
+
7
+ -- Запись первой версии строки от имени владельца: entity + журнал.
8
+ create or replace function @SCHEMA@.sys_create(p_id uuid, p_class text, p_tenant uuid, p_links jsonb, p_data jsonb)
9
+ returns uuid
10
+ language plpgsql volatile
11
+ set search_path = pg_catalog, @SCHEMA@, pg_temp
12
+ as $$
13
+ declare
14
+ now_ts timestamptz := clock_timestamp();
15
+ d jsonb := coalesce(p_data, '{}');
16
+ vld jsonpath;
17
+ begin
18
+ if exists (select 1 from @SCHEMA@.entity where id = p_id) then
19
+ return p_id;
20
+ end if;
21
+ if p_class = 'Class' then
22
+ select c.data, c.validator into d, vld from @SCHEMA@.class_prepare(d, p_tenant) c;
23
+ end if;
24
+ insert into @SCHEMA@.entity (id, rev, class, tenant, owner, links, data, at, created_at, author, op, validator)
25
+ values (p_id, 1, p_class, p_tenant, p_tenant, coalesce(p_links, '{}'), d, now_ts, now_ts, '@SYSTEM@', 'create', vld);
26
+ insert into @SCHEMA@.log (id, rev, class, tenant, owner, links, data, tags, at, author, op)
27
+ values (p_id, 1, p_class, p_tenant, p_tenant, coalesce(p_links, '{}'), d, '{}', now_ts, '@SYSTEM@', 'create');
28
+ if p_class in ('rule', 'Resource', 'member', 'Account', 'Class') then
29
+ perform @SCHEMA@.acl_bump();
30
+ end if;
31
+ return p_id;
32
+ end $$;
33
+ alter function @SCHEMA@.sys_create(uuid, text, uuid, jsonb, jsonb) owner to @OWNER@;
34
+ revoke all on function @SCHEMA@.sys_create(uuid, text, uuid, jsonb, jsonb) from public;
35
+
36
+ -- Ресурс и правило System от имени владельца (сид, тестовая обвязка).
37
+ create or replace function @SCHEMA@.sys_resource(p_tenant uuid, p_alias text, p_category text, p_pattern jsonb) returns uuid
38
+ language sql volatile
39
+ set search_path = pg_catalog, @SCHEMA@, pg_temp
40
+ return @SCHEMA@.sys_create(@SCHEMA@.v5_id(p_tenant, 'Resource', array[p_alias]), 'Resource', p_tenant, '{}',
41
+ jsonb_strip_nulls(jsonb_build_object('alias', p_alias, 'category', p_category, 'pattern', p_pattern)));
42
+
43
+ create or replace function @SCHEMA@.sys_rule(p_tenant uuid, p_group uuid, p_object uuid, p_permission text, p_weight int,
44
+ p_extra jsonb default '{}') returns uuid
45
+ language sql volatile
46
+ set search_path = pg_catalog, @SCHEMA@, pg_temp
47
+ return @SCHEMA@.sys_create(@SCHEMA@.v5_id(p_tenant, 'rule', array[p_group::text, p_object::text]), 'rule', p_tenant,
48
+ jsonb_build_object('Group', p_group, 'Object', p_object),
49
+ jsonb_build_object('permission', p_permission, 'weight', p_weight, 'enabled', true) || coalesce(p_extra, '{}'));
50
+
51
+ alter function @SCHEMA@.sys_resource(uuid, text, text, jsonb) owner to @OWNER@;
52
+ alter function @SCHEMA@.sys_rule(uuid, uuid, uuid, text, int, jsonb) owner to @OWNER@;
53
+ revoke all on function @SCHEMA@.sys_resource(uuid, text, text, jsonb), @SCHEMA@.sys_rule(uuid, uuid, uuid, text, int, jsonb) from public;
54
+
55
+ -- Системный сид целиком (установка и reset({ all }), этап 6): ключ подписи и счётчик прав, System,
56
+ -- системные классы, общие группы и системные права (этап 5, п. 8). Роль admin — роль членства в
57
+ -- System: системные администраторы работают в арендаторе System. Права auth.* — сервису
58
+ -- (имперсонация, выпуск сессий, креды), letopis.* (verify, reset, maintain) — только системному
59
+ -- администратору. Повторный вызов ничего не меняет.
60
+ create or replace function @SCHEMA@.seed_system() returns void
61
+ language plpgsql volatile
62
+ set search_path = pg_catalog, @SCHEMA@, pg_temp
63
+ as $$
64
+ declare
65
+ sys uuid := '@SYSTEM@'::uuid;
66
+ g_any uuid;
67
+ g_svc uuid;
68
+ g_admin uuid;
69
+ o_api uuid;
70
+ o_auth uuid;
71
+ o_letopis uuid;
72
+ op text;
73
+ begin
74
+ -- Ключ подписи пропуска прав и счётчик изменений прав (50-acl.sql: acl_token, acl_bump).
75
+ insert into @SCHEMA@.secret (id, account, kind, material, meta)
76
+ values ('@SYSTEM@'::uuid, '@SYSTEM@'::uuid, 'KEY', @PGCRYPTO@.gen_random_bytes(32), '{"v": 0}')
77
+ on conflict (id) do nothing;
78
+
79
+ perform @SCHEMA@.sys_create('@SYSTEM@'::uuid, 'Account', '@SYSTEM@'::uuid, '{}',
80
+ '{"name": "System", "categories": ["System"], "enabled": true}');
81
+
82
+ -- Строка Class, описывающая саму себя, и системные классы. Полная метасхема и развёрнутое
83
+ -- описание resolved — в 40-class.sql (этап 1, п. 6).
84
+ perform @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['Class']), 'Class', '@SYSTEM@'::uuid, '{}',
85
+ '{"name": "Class", "kind": "hub", "key": ["name"], "system": true,
86
+ "fields": {"properties": {"name": {"type": "string"}}, "required": ["name"]}}');
87
+ perform @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['Account']), 'Class', '@SYSTEM@'::uuid, '{}',
88
+ '{"name": "Account", "kind": "hub", "key": [], "system": true,
89
+ "fields": {"properties": {"name": {"type": "string"}, "categories": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
90
+ "enabled": {"type": "boolean", "default": true}}, "required": ["name"]}}');
91
+ -- Членство (этап 5, п. 4): пользователь в арендаторе с ролями; одно членство на пару. Строка лежит
92
+ -- в арендаторе, в которого вступают (конец Tenant равен арендатору строки).
93
+ perform @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['member']), 'Class', '@SYSTEM@'::uuid, '{}',
94
+ '{"name": "member", "kind": "link", "key": ["User", "Tenant"], "system": true,
95
+ "ends": {"User": {"class": "Account", "required": true, "onDelete": "cascade"},
96
+ "Tenant": {"class": "Account", "required": true, "onDelete": "cascade"}},
97
+ "fields": {"properties": {"roles": {"type": "array", "items": {"type": "string"}, "uniqueItems": true, "default": []}}}}');
98
+ -- Ресурс ACL (этап 5, п. 5): группа, эндпоинт или объект строк — см. 50-acl.sql. Маски шаблона —
99
+ -- только строки: маска не строкой в 0.21 совпадала со всеми (группа {"roles": ["staff"]} — с любым аккаунтом).
100
+ perform @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['Resource']), 'Class', '@SYSTEM@'::uuid, '{}',
101
+ '{"name": "Resource", "kind": "hub", "key": ["alias"], "system": true,
102
+ "fields": {"properties": {"alias": {"type": "string", "minLength": 1},
103
+ "category": {"type": "string", "enum": ["ACCOUNT", "API", "READ", "WRITE", "DELETE"]},
104
+ "pattern": {"type": "object",
105
+ "properties": {"categories": {"type": "string"}, "roles": {"type": "string"},
106
+ "endpoint": {"type": "string"}, "class": {"type": "string"}}},
107
+ "description": {"type": "string"}},
108
+ "required": ["alias", "category"]}}');
109
+ -- Правило ACL: группа → объект, allow | deny, вес; одно правило на пару.
110
+ perform @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['rule']), 'Class', '@SYSTEM@'::uuid, '{}',
111
+ '{"name": "rule", "kind": "link", "key": ["Group", "Object"], "system": true,
112
+ "ends": {"Group": {"class": "Resource", "required": true, "onDelete": "cascade"},
113
+ "Object": {"class": "Resource", "required": true, "onDelete": "cascade"}},
114
+ "fields": {"properties": {"permission": {"type": "string", "enum": ["allow", "deny"]},
115
+ "weight": {"type": "integer", "default": 0}, "enabled": {"type": "boolean", "default": true},
116
+ "code": {"type": "integer"}, "message": {"type": "string"}, "description": {"type": "string"}},
117
+ "required": ["permission"]}}');
118
+
119
+ perform @SCHEMA@.sys_resource(sys, 'anonymous:ACCOUNT', 'ACCOUNT', '{"categories": "{Anonymous,Shadow}"}');
120
+ perform @SCHEMA@.sys_resource(sys, 'authenticated:ACCOUNT', 'ACCOUNT', '{"categories": "!{Anonymous,Shadow}"}');
121
+ g_any := @SCHEMA@.sys_resource(sys, 'any:ACCOUNT', 'ACCOUNT', null);
122
+ g_svc := @SCHEMA@.sys_resource(sys, 'service:ACCOUNT', 'ACCOUNT', '{"categories": "{Service}"}');
123
+ g_admin := @SCHEMA@.sys_resource(sys, 'admin:ACCOUNT', 'ACCOUNT', '{"roles": "{admin}"}');
124
+ o_api := @SCHEMA@.sys_resource(sys, 'any:API', 'API', null);
125
+ o_auth := @SCHEMA@.sys_resource(sys, 'auth:API', 'API', '{"endpoint": "auth.*"}');
126
+ o_letopis := @SCHEMA@.sys_resource(sys, 'letopis:API', 'API', '{"endpoint": "letopis.*"}');
127
+ perform @SCHEMA@.sys_rule(sys, g_any, o_api, 'deny', 0, '{"code": 403, "message": "Доступ запрещён по умолчанию"}');
128
+ perform @SCHEMA@.sys_rule(sys, g_svc, o_auth, 'allow', 100);
129
+ perform @SCHEMA@.sys_rule(sys, g_admin, o_auth, 'allow', 100);
130
+ perform @SCHEMA@.sys_rule(sys, g_admin, o_letopis, 'allow', 100);
131
+ foreach op in array array['READ', 'WRITE', 'DELETE'] loop
132
+ perform @SCHEMA@.sys_rule(sys, g_admin, @SCHEMA@.sys_resource(sys, 'all:' || op, op, null), 'allow', 100);
133
+ end loop;
134
+ end $$;
135
+ alter function @SCHEMA@.seed_system() owner to @OWNER@;
136
+ revoke all on function @SCHEMA@.seed_system() from public;
137
+
138
+ select @SCHEMA@.seed_system();
139
+
140
+ -- Правило «аутентифицированным можно всё» в арендаторе (тестовая обвязка, бенчи, первичная настройка
141
+ -- арендатора): группа authenticated и объекты all:READ, all:WRITE, all:DELETE из System, правило —
142
+ -- в самом арендаторе, поэтому действует только на его строки.
143
+ create or replace function @SCHEMA@.sys_grant_all(p_tenant uuid) returns void
144
+ language plpgsql volatile
145
+ set search_path = pg_catalog, @SCHEMA@, pg_temp
146
+ as $$
147
+ declare
148
+ op text;
149
+ begin
150
+ foreach op in array array['READ', 'WRITE', 'DELETE'] loop
151
+ perform @SCHEMA@.sys_rule(p_tenant, @SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Resource', array['authenticated:ACCOUNT']),
152
+ @SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Resource', array['all:' || op]), 'allow', 10);
153
+ end loop;
154
+ end $$;
155
+ alter function @SCHEMA@.sys_grant_all(uuid) owner to @OWNER@;
156
+ revoke all on function @SCHEMA@.sys_grant_all(uuid) from public;
157
+
158
+ -- Сервисный аккаунт и его ключ: вызывает установщик, ключ генерирует и возвращает он же.
159
+ create or replace function @SCHEMA@.bootstrap_service(p_key text) returns uuid
160
+ language plpgsql volatile
161
+ set search_path = pg_catalog, @SCHEMA@, pg_temp
162
+ as $$
163
+ declare
164
+ svc uuid;
165
+ begin
166
+ select id into svc from @SCHEMA@.entity
167
+ where class = 'Account' and tenant = '@SYSTEM@' and data->'categories' ? 'Service' limit 1;
168
+ if svc is null then
169
+ svc := @SCHEMA@.uuid_v5('@SYSTEM@'::uuid, @SCHEMA@.v5_name('Account', array[gen_random_uuid()::text]));
170
+ perform @SCHEMA@.sys_create(svc, 'Account', '@SYSTEM@'::uuid, '{}',
171
+ '{"name": "service", "categories": ["Service"], "enabled": true}');
172
+ end if;
173
+ insert into @SCHEMA@.secret (account, kind, ident, material)
174
+ values (svc, 'APIKEY', left(p_key, 12), sha256(convert_to(p_key, 'UTF8')));
175
+ return svc;
176
+ end $$;
177
+ alter function @SCHEMA@.bootstrap_service(text) owner to @OWNER@;
178
+ revoke all on function @SCHEMA@.bootstrap_service(text) from public;
@@ -0,0 +1,3 @@
1
+ -- letopis 1.0 — штамп ревизии движка (план, §2.2, этап 1, п. 11): последний оператор наката.
2
+ -- Номер совпадает с DDL_REVISION в lib/src/types.ts (проверяет check-docs).
3
+ comment on schema @SCHEMA@ is 'letopis ddl_revision=5';
package/sql/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # SQL ядра letopis 1.0
2
+
3
+ Файлы накатываются установщиком (`lib/src/up.ts`, функция `install`) по возрастанию номера; каждый можно применить повторно.
4
+ Спецификация — `reports/PLAN-LETOPIS-1.0.md`, §2 и этап 1.
5
+
6
+ ## Подстановки при накате
7
+
8
+ | Маркер | Что подставляется |
9
+ |---|---|
10
+ | `@SCHEMA@` | имя схемы в кавычках, например `"v2.salon"` |
11
+ | `@SCHEMA_NAME@` | имя схемы как строковый литерал без кавычек идентификатора, например `v2.salon` |
12
+ | `@OWNER@` | роль-владелец, по умолчанию `letopis_owner` |
13
+ | `@APP@` | роль приложения, по умолчанию `letopis_app` |
14
+ | `@PGCRYPTO@` | схема, где установлен pgcrypto (`pg_extension.extnamespace`) |
15
+ | `@SYSTEM@` | id системного арендатора System: `2eba6d0a-1edd-4bb6-a85a-a703dab49035` (как в 0.21) |
16
+ | `@ALLOW_RESET@` | `true` или `false` — константа установки |
17
+ | `@HISTORY_DEFAULT@` | политика хранения по умолчанию `up({ history })` литералом JSON или `'null'` — константа установки |
18
+ | `@RESERVED_NAMES@` | методы цепочки — `RESERVED_CLASS_NAMES` из `lib/src/types.ts`, литералами через запятую |
19
+
20
+ ## Колонки снимка версии
21
+
22
+ Одинаковы в `entity` и `log` (§2.2): `id`, `rev`, `class`, `tenant`, `owner`, `links`, `data`, `tags`, `at` (время версии),
23
+ `author`, `agent`, `op`, `reason`, `moved`. Хэш — генерируемая колонка: `sha256(prev_hash || snapshot(...))`, где
24
+ `snapshot()` — неизменяемая функция, время входит числом микросекунд.
25
+
26
+ ## Коды ошибок
27
+
28
+ Собственный класс SQLSTATE `LT`; текст — `letopis: <имя>: <сообщение>`, детали — JSON в `DETAIL`.
29
+ Источник истины — функция `error_catalog()` в `15-errors.sql`.
30
+
31
+ | Код | Имя | Когда |
32
+ |---|---|---|
33
+ | LT001 | `no_session` | нет действующей сессии |
34
+ | LT002 | `target_not_found` | цель конца не найдена (одинаково для несуществующей и чужой) |
35
+ | LT003 | `invalid_data` | данные не проходят описание класса; в деталях `issues` — путь, ключевое слово, сообщение |
36
+ | LT004 | `undeclared_end` | конец не объявлен в классе |
37
+ | LT005 | `tenant_denied` | запись в чужого арендатора |
38
+ | LT006 | `acl_denied` | отказ по правилам доступа |
39
+ | LT007 | `id_mismatch` | id не равен v5 от ключа; в деталях `expected` |
40
+ | LT008 | `id_from_db` | id класса без ключа выдаёт база |
41
+ | LT009 | `immutable_key` | ключевое поле, id, арендатор, владелец менять нельзя |
42
+ | LT010 | `invalid_class` | описание класса отклонено, класс не найден или абстрактный |
43
+ | LT011 | `use_reclass` | класс с ключом меняется функцией `reclass()` |
44
+ | LT012 | `reclass_denied` | роли ссылающихся строк не принимают новый класс; в деталях `rows`, `others` |
45
+ | LT013 | `tightening_conflict` | ужесточению мешают строки; в деталях `rows`, `others` |
46
+ | LT014 | `isolation_level` | удаление и смена класса — только в `read committed` |
47
+ | LT015 | `recreated_in_statement` | id удалён и создан заново одним оператором |
48
+ | LT016 | `cursor_expired` | курсор подписки старше яруса `all` (этап 6) |
49
+ | LT017 | `delete_restricted` | удалению мешают ссылающиеся строки (`onDelete: restrict`); в деталях `rows`, `others` |
50
+ | LT018 | `not_deleted` | `restore` и `purge` — только для удалённых объектов (этап 6) |
51
+ | LT019 | `reset_disabled` | флаг `allowReset` установки выключен: `reset` и `maintain(now)` недоступны (этап 6) |
52
+ | LT020 | `confirm_mismatch` | подтверждение сброса не совпадает с именем схемы (этап 6) |
53
+
54
+ ## Порядок файлов
55
+
56
+ `10-core` → `15-errors` → `20-context` → `30-validate` → `40-class` → `50-acl` → `60-write` → `70-read` → `80-auth` → `90-time` → `95-seed.system` → `99-revision`. Миграции между ревизиями — `migrate/NNNN-*.sql` (номер — ревизия, к которой ведёт), их применяет `up({ upgrade: true })` до повторного наката.
@@ -1,112 +1,39 @@
1
- -- =============================================================================
2
- -- Сид таблицы Schema — ИСТОЧНИК ПРАВДЫ демо-домена booking (20 классов: 12 HUB + 8 LINK).
3
- -- Редактируется руками; накат: db/apply.mjs / up() (маркер <SCHEMA-NAME>).
4
- -- Идемпотентен (ON CONFLICT). Концы связей — Schema.links v2 (JSON-объекты в text[]).
5
- -- =============================================================================
6
- --
7
- -- FILE: lib/sql/seed.booking.sql
8
- -- VERSION: 1.0.1
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); число в заголовке файла сверяет scripts/check-docs.mjs
22
- -- END_MODULE_MAP
23
- --
24
- -- START_CHANGE_SUMMARY
25
- -- LAST_CHANGE: [v1.0.1 - Docs-only: заголовок файла приведён к факту (20 классов: 12 HUB + 8 LINK);
26
- -- число теперь проверяется автоматически. INSERT-ы не менялись]
27
- -- END_CHANGE_SUMMARY
28
- --
29
- -- START_BLOCK_SEED_HUB_CLASSES
30
-
31
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
32
- ('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[])
33
- 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
-
35
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
36
- ('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[])
37
- 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
-
39
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
40
- ('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[])
41
- 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
-
43
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
44
- ('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[])
45
- 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
-
47
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
48
- ('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[])
49
- 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
-
51
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
52
- ('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[])
53
- 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
-
55
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
56
- ('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[])
57
- 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
-
59
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
60
- ('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[])
61
- 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
-
63
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
64
- ('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[])
65
- 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
-
67
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
68
- ('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[])
69
- 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;
70
-
71
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
72
- ('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[])
73
- 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;
74
-
75
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
76
- ('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[])
77
- 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;
78
-
79
- -- END_BLOCK_SEED_HUB_CLASSES
80
- -- START_BLOCK_SEED_LINK_CLASSES
81
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
82
- ('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[])
83
- 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;
84
-
85
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
86
- ('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[])
87
- 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;
88
-
89
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
90
- ('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[])
91
- 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;
92
-
93
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
94
- ('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[])
95
- 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;
96
-
97
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
98
- ('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[])
99
- 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;
100
-
101
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
102
- ('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[])
103
- 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;
104
-
105
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
106
- ('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[])
107
- 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;
108
-
109
- INSERT INTO "<SCHEMA-NAME>"."Schema" (partition, id, alias, category, ancestor, attributes, meta, links, "order", ancestors) VALUES
110
- ('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[])
111
- 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;
112
- -- END_BLOCK_SEED_LINK_CLASSES
1
+ -- letopis 1.0 — демо-домен booking (план, этап 2, п. 8). Сгенерировано из lib/src/demo/booking.ts
2
+ -- командой `npm run seed:booking`; руками не править. Классы — системного арендатора, пишет владелец.
3
+
4
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['Org']), 'Class', '@SYSTEM@'::uuid, '{}',
5
+ $json${"name":"Org","kind":"hub","fields":{"properties":{"name":{"type":"string"},"timezone":{"default":"Europe/Moscow","type":"string"},"address":{"type":"string"},"phone":{"type":"string"},"active":{"default":true,"type":"boolean"},"settings":{"type":"object","properties":{"booking":{"type":"object","properties":{"deposit":{"type":"object","properties":{"amount":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991},"currency":{"default":"RUB","type":"string","enum":["RUB","USD","EUR","AED"]}},"additionalProperties":false},"autoconfirm":{"default":false,"type":"boolean"}},"additionalProperties":false}},"additionalProperties":false}},"required":["name"]},"alias":"Организация","meta":{"description":"Организация — арендатор: владелец каталога, локаций и расписаний; settings — вложенные настройки (глубина 4)","icon":"mdi:domain","color":"text-blue-600","order":200}}$json$::jsonb);
6
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['Person']), 'Class', '@SYSTEM@'::uuid, '{}',
7
+ $json${"name":"Person","kind":"hub","fields":{"properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"]},"abstract":true,"alias":"Контрагент","ends":{"Org":{"class":"Org","required":true}},"meta":{"description":"Абстрактный контрагент: имя и телефон; наследуют Мастер и Клиент","icon":"mdi:account-outline","color":"text-blue-500","order":300}}$json$::jsonb);
8
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['Staff']), 'Class', '@SYSTEM@'::uuid, '{}',
9
+ $json${"name":"Staff","kind":"hub","fields":{"properties":{"photo":{"type":"string"},"description":{"type":"string"},"specialization":{"type":"string"}}},"parent":"Person","alias":"Мастер","meta":{"description":"Мастер — бронируемый исполнитель; локации — связь адрес, доступность — связь окно","icon":"mdi:account-star","color":"text-indigo-500","order":400}}$json$::jsonb);
10
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['Customer']), 'Class', '@SYSTEM@'::uuid, '{}',
11
+ $json${"name":"Customer","kind":"hub","fields":{"properties":{"notes":{"type":"string"},"preferred_contact":{"default":"phone","type":"string","enum":["phone","email","messenger"]}}},"parent":"Person","alias":"Клиент","meta":{"description":"Клиент — записывается на услуги","icon":"mdi:account","color":"text-green-500","order":500}}$json$::jsonb);
12
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['Location']), 'Class', '@SYSTEM@'::uuid, '{}',
13
+ $json${"name":"Location","kind":"hub","fields":{"properties":{"address":{"type":"string"},"coordinates":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"]},"description":{"type":"string"}},"required":["address"]},"alias":"Локация","ends":{"Org":{"class":"Org","required":true}},"meta":{"description":"Локация — точка обслуживания организации","icon":"mdi:map-marker","color":"text-red-500","order":600}}$json$::jsonb);
14
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['Element']), 'Class', '@SYSTEM@'::uuid, '{}',
15
+ $json${"name":"Element","kind":"hub","fields":{"properties":{"name":{"type":"string"},"description":{"type":"string"}},"required":["name"]},"abstract":true,"alias":"Элемент","ends":{"Org":{"class":"Org","required":true}},"key":["Org","name"],"meta":{"description":"Абстрактная каталожная единица (Услуга, Товар, Комплекс): имя уникально в организации; цена — связь цена","icon":"mdi:view-list","color":"text-teal-500","order":700}}$json$::jsonb);
16
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['Service']), 'Class', '@SYSTEM@'::uuid, '{}',
17
+ $json${"name":"Service","kind":"hub","fields":{"properties":{"duration":{"type":"number","minimum":0}}},"parent":"Element","alias":"Услуга","meta":{"description":"Услуга: длительность в минутах","icon":"mdi:spa","color":"text-purple-500","order":800}}$json$::jsonb);
18
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['Product']), 'Class', '@SYSTEM@'::uuid, '{}',
19
+ $json${"name":"Product","kind":"hub","fields":{"properties":{"sku":{"type":"string"},"unit":{"default":"шт","type":"string"}}},"parent":"Element","alias":"Товар","meta":{"description":"Товар: артикул и единица измерения","icon":"mdi:package","color":"text-blue-400","order":900}}$json$::jsonb);
20
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['Complex']), 'Class', '@SYSTEM@'::uuid, '{}',
21
+ $json${"name":"Complex","kind":"hub","fields":{"properties":{"cost":{"type":"number","minimum":0},"duration":{"type":"number","minimum":0}},"required":["cost","duration"]},"parent":"Element","alias":"Комплекс","meta":{"description":"Комплекс: собственные стоимость и длительность; состав — связь состав","icon":"mdi:package-variant-closed","color":"text-orange-500","order":1000}}$json$::jsonb);
22
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['Schedule']), 'Class', '@SYSTEM@'::uuid, '{}',
23
+ $json${"name":"Schedule","kind":"hub","fields":{"properties":{"name":{"type":"string"},"year":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"month":{"type":"integer","minimum":1,"maximum":12}},"required":["name","year","month"]},"alias":"Расписание","ends":{"Org":{"class":"Org","required":true}},"meta":{"description":"Месячное расписание организации; наполнение — связь окно","icon":"mdi:calendar","color":"text-cyan-500","order":1100}}$json$::jsonb);
24
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['Folder']), 'Class', '@SYSTEM@'::uuid, '{}',
25
+ $json${"name":"Folder","kind":"hub","fields":{"properties":{"name":{"type":"string"}},"required":["name"]},"alias":"Папка","ends":{"Org":{"class":"Org","required":true},"Parent":{"class":"Folder"}},"meta":{"description":"Папка-категория; вложенность — конец Parent на родительскую папку; наполнение — связь содержимое","icon":"mdi:folder","color":"text-yellow-500","order":1200}}$json$::jsonb);
26
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['address']), 'Class', '@SYSTEM@'::uuid, '{}',
27
+ $json${"name":"address","kind":"link","fields":{"properties":{"default":{"default":false,"type":"boolean"},"schedule_info":{"type":"string"}}},"alias":"адрес","ends":{"Staff":{"class":"Staff","required":true},"Location":{"class":"Location","required":true}},"key":["Staff","Location"],"meta":{"description":"Адрес: Мастер работает в Локации; default — основная точка мастера","icon":"mdi:link","color":"text-sky-500","order":200}}$json$::jsonb);
28
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['slot']), 'Class', '@SYSTEM@'::uuid, '{}',
29
+ $json${"name":"slot","kind":"link","fields":{"properties":{"start_datetime":{"type":"string","format":"date-time","pattern":"^(?:(?:[0-9][0-9][2468][048]|[0-9][0-9][13579][26]|[0-9][0-9]0[48]|[02468][048]00|[13579][26]00)-02-29|[0-9]{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12][0-9]|3[01])|(?:0[469]|11)-(?:0[1-9]|[12][0-9]|30)|(?:02)-(?:0[1-9]|1[0-9]|2[0-8])))T(?:(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\\.[0-9]{3}(?:Z))$"},"end_datetime":{"type":"string","format":"date-time","pattern":"^(?:(?:[0-9][0-9][2468][048]|[0-9][0-9][13579][26]|[0-9][0-9]0[48]|[02468][048]00|[13579][26]00)-02-29|[0-9]{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12][0-9]|3[01])|(?:0[469]|11)-(?:0[1-9]|[12][0-9]|30)|(?:02)-(?:0[1-9]|1[0-9]|2[0-8])))T(?:(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\\.[0-9]{3}(?:Z))$"}},"required":["start_datetime","end_datetime"]},"alias":"окно","ends":{"Staff":{"class":"Staff","required":true},"Location":{"class":"Location","required":true},"Schedule":{"class":"Schedule","required":true}},"key":["Staff","start_datetime"],"meta":{"description":"Окно доступности: Мастер × Локация × Расписание и интервал; мастер не задваивает время","icon":"mdi:clock-outline","color":"text-amber-500","order":300}}$json$::jsonb);
30
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['booking']), 'Class', '@SYSTEM@'::uuid, '{}',
31
+ $json${"name":"booking","kind":"link","fields":{"properties":{"notes":{"type":"string"}}},"parent":"slot","alias":"запись","ends":{"Item":{"class":"Element","required":true},"Customer":{"class":"Customer","onDelete":"unset"}},"meta":{"description":"Запись — наследник окна: бронь мастера на элемент каталога для клиента; ручная бронь — без клиента","icon":"mdi:calendar-check","color":"text-emerald-500","order":400}}$json$::jsonb);
32
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['content']), 'Class', '@SYSTEM@'::uuid, '{}',
33
+ $json${"name":"content","kind":"link","fields":{"properties":{"order":{"default":0,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}}},"alias":"содержимое","ends":{"Folder":{"class":"Folder","required":true},"Item":{"class":"Element","required":true}},"key":["Folder","Item"],"meta":{"description":"Содержимое папки: элемент может лежать в нескольких папках","icon":"mdi:link","color":"text-sky-500","order":500}}$json$::jsonb);
34
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['compo']), 'Class', '@SYSTEM@'::uuid, '{}',
35
+ $json${"name":"compo","kind":"link","fields":{"properties":{"order":{"default":0,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"quantity":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}}},"alias":"состав","ends":{"Complex":{"class":"Complex","required":true},"Part":{"class":["Service","Product"],"required":true}},"key":["Complex","Part"],"meta":{"description":"Состав комплекса: услуги и товары с порядком и количеством","icon":"mdi:link","color":"text-sky-500","order":600}}$json$::jsonb);
36
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['skill']), 'Class', '@SYSTEM@'::uuid, '{}',
37
+ $json${"name":"skill","kind":"link","fields":{"properties":{"level":{"default":"intermediate","type":"string","enum":["basic","intermediate","advanced","expert"]}}},"alias":"навык","ends":{"Staff":{"class":"Staff","required":true},"Service":{"class":"Service","required":true}},"key":["Staff","Service"],"meta":{"description":"Навык: Мастер умеет Услугу; умение комплекса вычислимо по составу","icon":"mdi:link","color":"text-sky-500","order":700}}$json$::jsonb);
38
+ select @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['price']), 'Class', '@SYSTEM@'::uuid, '{}',
39
+ $json${"name":"price","kind":"link","fields":{"properties":{"note":{"default":"базовая","type":"string"},"amounts":{"type":"object","propertyNames":{"type":"string","enum":["RUB","USD","EUR","AED"]},"additionalProperties":{"type":"integer","minimum":0,"maximum":9007199254740991}}},"required":["amounts","note"]},"alias":"цена","ends":{"Item":{"class":"Element","required":true}},"key":["Item","note"],"meta":{"description":"Вариант цены элемента: мультивалютные суммы и описание варианта","icon":"mdi:cash-multiple","color":"text-green-600","order":800}}$json$::jsonb);
package/dist/schema.d.ts DELETED
@@ -1,15 +0,0 @@
1
- import type { Sql } from 'postgres';
2
- import type { ClassDef, FieldType } from './types.js';
3
- export declare class Registry {
4
- private byName;
5
- readonly all: ClassDef[];
6
- add(def: ClassDef): void;
7
- /** Класс по id или alias; undefined если нет. */
8
- find(name: string): ClassDef | undefined;
9
- /** Класс по id или alias; иначе понятная ошибка со списком. */
10
- resolve(name: string): ClassDef;
11
- has(name: string): boolean;
12
- }
13
- /** Тип поля из fastest-validator DSL — для каста в SQL. */
14
- export declare function fieldTypeOf(attr: unknown): FieldType;
15
- export declare function loadRegistry(sql: Sql, pgSchema: string, partition: string): Promise<Registry>;