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.
- package/AGENT-CHEATSHEET.en.md +368 -0
- package/AGENT-CHEATSHEET.md +354 -0
- package/CHANGELOG.md +348 -0
- package/MIGRATION.md +190 -0
- package/README.en.md +1937 -0
- package/README.md +1493 -3466
- package/dist/acl.d.ts +26 -50
- package/dist/acl.js +22 -267
- package/dist/admin.d.ts +138 -0
- package/dist/admin.js +170 -0
- package/dist/auth.d.ts +120 -73
- package/dist/auth.js +121 -306
- package/dist/cache.d.ts +73 -0
- package/dist/cache.js +148 -0
- package/dist/chain.d.ts +124 -191
- package/dist/chain.js +369 -551
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +164 -0
- package/dist/demo/booking.d.ts +289 -0
- package/dist/demo/booking.js +159 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +70 -0
- package/dist/import.d.ts +179 -0
- package/dist/import.js +792 -0
- package/dist/index.d.ts +172 -26
- package/dist/index.js +304 -178
- package/dist/jsonschema.d.ts +22 -0
- package/dist/jsonschema.js +167 -0
- package/dist/load.d.ts +76 -0
- package/dist/load.js +884 -0
- package/dist/model.d.ts +166 -0
- package/dist/model.js +224 -0
- package/dist/ops.d.ts +7 -6
- package/dist/ops.js +7 -51
- package/dist/pglite.d.ts +22 -0
- package/dist/pglite.js +45 -0
- package/dist/registry.d.ts +57 -0
- package/dist/registry.js +82 -0
- package/dist/sql.d.ts +59 -142
- package/dist/sql.js +568 -654
- package/dist/sync.d.ts +31 -0
- package/dist/sync.js +108 -0
- package/dist/tx.d.ts +129 -8
- package/dist/tx.js +300 -73
- package/dist/typed.d.ts +97 -0
- package/dist/typed.js +1 -0
- package/dist/types.d.ts +71 -250
- package/dist/types.js +27 -108
- package/dist/up.d.ts +140 -47
- package/dist/up.js +339 -267
- package/dist/uuid.d.ts +21 -6
- package/dist/uuid.js +48 -64
- package/dist/validate.d.ts +24 -0
- package/dist/validate.js +251 -0
- package/dist/watch.d.ts +62 -0
- package/dist/watch.js +168 -0
- package/dist/write.d.ts +117 -74
- package/dist/write.js +658 -720
- package/llms.txt +26 -0
- package/package.json +49 -19
- package/sql/10-core.sql +136 -0
- package/sql/15-errors.sql +60 -0
- package/sql/20-context.sql +153 -0
- package/sql/30-validate.sql +423 -0
- package/sql/40-class.sql +259 -0
- package/sql/50-acl.sql +539 -0
- package/sql/60-write.sql +1369 -0
- package/sql/70-read.sql +245 -0
- package/sql/80-auth.sql +827 -0
- package/sql/90-time.sql +957 -0
- package/sql/95-seed.system.sql +178 -0
- package/sql/99-revision.sql +3 -0
- package/sql/README.md +56 -0
- package/sql/seed.booking.sql +39 -112
- package/dist/schema.d.ts +0 -15
- package/dist/schema.js +0 -351
- package/dist/sessions.d.ts +0 -32
- package/dist/sessions.js +0 -114
- package/dist/tables.d.ts +0 -105
- package/dist/tables.js +0 -248
- package/docker/Dockerfile +0 -40
- package/docker/start.sh +0 -18
- package/scripts/check-docs.mjs +0 -375
- package/scripts/gen-api-contract.mjs +0 -226
- package/scripts/gen-types.mjs +0 -350
- package/scripts/release-notes.mjs +0 -76
- package/scripts/schema-sync.mjs +0 -185
- package/sql/ddl.sql +0 -600
- 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;
|
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 })` до повторного наката.
|
package/sql/seed.booking.sql
CHANGED
|
@@ -1,112 +1,39 @@
|
|
|
1
|
-
--
|
|
2
|
-
--
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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>;
|