letopis 1.0.1 → 1.2.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 +117 -10
- package/AGENT-CHEATSHEET.md +110 -8
- package/CHANGELOG.md +739 -0
- package/README.en.md +873 -68
- package/README.md +830 -66
- package/dist/admin.d.ts +24 -4
- package/dist/admin.js +39 -9
- package/dist/cache.d.ts +33 -9
- package/dist/cache.js +81 -32
- package/dist/chain.d.ts +29 -7
- package/dist/chain.js +61 -12
- package/dist/cli.js +59 -7
- package/dist/errors.js +4 -2
- package/dist/import.d.ts +6 -0
- package/dist/import.js +196 -175
- package/dist/index.d.ts +27 -5
- package/dist/index.js +191 -29
- package/dist/internal.d.ts +16 -0
- package/dist/internal.js +14 -0
- package/dist/load.d.ts +63 -0
- package/dist/load.js +331 -44
- package/dist/mcp.d.ts +28 -0
- package/dist/mcp.js +810 -0
- package/dist/model.d.ts +34 -0
- package/dist/model.js +17 -0
- package/dist/project.d.ts +309 -0
- package/dist/project.js +1097 -0
- package/dist/registry.d.ts +17 -0
- package/dist/registry.js +13 -0
- package/dist/signals.d.ts +73 -0
- package/dist/signals.js +169 -0
- package/dist/sql.d.ts +1 -1
- package/dist/sql.js +26 -7
- package/dist/tx.d.ts +9 -7
- package/dist/tx.js +15 -5
- package/dist/typed.d.ts +21 -6
- package/dist/types.d.ts +77 -4
- package/dist/types.js +8 -3
- package/dist/up.d.ts +16 -2
- package/dist/up.js +34 -4
- package/dist/uuid.js +19 -8
- package/dist/watch.d.ts +23 -2
- package/dist/watch.js +44 -7
- package/dist/write.d.ts +11 -2
- package/dist/write.js +57 -10
- package/llms.txt +9 -2
- package/package.json +13 -2
- package/sql/10-core.sql +13 -2
- package/sql/15-errors.sql +4 -1
- package/sql/40-class.sql +57 -2
- package/sql/60-write.sql +611 -82
- package/sql/70-read.sql +33 -8
- package/sql/75-history.sql +358 -0
- package/sql/80-auth.sql +7 -7
- package/sql/88-derived.sql +1154 -0
- package/sql/90-time.sql +134 -21
- package/sql/95-seed.system.sql +24 -6
- package/sql/99-revision.sql +1 -1
- package/sql/README.md +5 -1
- package/sql/migrate/0006-changed-paths.sql +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letopis",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Letopis (летопись): versioned entity store on PostgreSQL 18 — integrity, history, tenants and access control enforced by the database itself, dot-notation chains on top",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"postgres",
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
"./model": {
|
|
36
36
|
"types": "./dist/model.d.ts",
|
|
37
37
|
"import": "./dist/model.js"
|
|
38
|
+
},
|
|
39
|
+
"./mcp": {
|
|
40
|
+
"types": "./dist/mcp.d.ts",
|
|
41
|
+
"import": "./dist/mcp.js"
|
|
38
42
|
}
|
|
39
43
|
},
|
|
40
44
|
"engines": {
|
|
@@ -72,7 +76,8 @@
|
|
|
72
76
|
"peerDependencies": {
|
|
73
77
|
"zod": "^4",
|
|
74
78
|
"@electric-sql/pglite": "^0.5.8",
|
|
75
|
-
"@electric-sql/pglite-socket": "^0.2.11"
|
|
79
|
+
"@electric-sql/pglite-socket": "^0.2.11",
|
|
80
|
+
"@modelcontextprotocol/server": "^2.4.0"
|
|
76
81
|
},
|
|
77
82
|
"peerDependenciesMeta": {
|
|
78
83
|
"zod": {
|
|
@@ -83,13 +88,19 @@
|
|
|
83
88
|
},
|
|
84
89
|
"@electric-sql/pglite-socket": {
|
|
85
90
|
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"@modelcontextprotocol/server": {
|
|
93
|
+
"optional": true
|
|
86
94
|
}
|
|
87
95
|
},
|
|
88
96
|
"devDependencies": {
|
|
89
97
|
"@electric-sql/pglite": "^0.5.8",
|
|
90
98
|
"@electric-sql/pglite-socket": "^0.2.11",
|
|
99
|
+
"@modelcontextprotocol/client": "^2.4.0",
|
|
100
|
+
"@modelcontextprotocol/server": "^2.4.0",
|
|
91
101
|
"@types/node": "^22.20.5",
|
|
92
102
|
"letopis-0": "npm:letopis@0.21.0",
|
|
103
|
+
"letopis-1": "npm:letopis@1.0.1",
|
|
93
104
|
"tsx": "^4.23.15",
|
|
94
105
|
"typescript": "~5.9.3",
|
|
95
106
|
"zod": "4.6.5"
|
package/sql/10-core.sql
CHANGED
|
@@ -86,9 +86,16 @@ create table if not exists @SCHEMA@.log (
|
|
|
86
86
|
prev_hash bytea,
|
|
87
87
|
hash bytea generated always as (@SCHEMA@.version_hash(prev_hash,
|
|
88
88
|
@SCHEMA@.snapshot(id, rev, class, tenant, owner, links, data, tags, at, author, agent, op, reason, moved))) stored,
|
|
89
|
+
-- пути изменения против предыдущей версии (план 1.1, ревизия 6): производные, в снимок и хэш не входят;
|
|
90
|
+
-- null — не посчитано (создание, загрузчик, импорт), blame досчитывает сравнением
|
|
91
|
+
changed jsonb,
|
|
89
92
|
constraint log_id_rev unique (id, rev)
|
|
90
93
|
);
|
|
91
|
-
|
|
94
|
+
-- срез журнала по классу (asOf, withDeleted) — по арендатору (доработки после точки Б): цена запроса не растёт с
|
|
95
|
+
-- журналом других арендаторов с тем же именем класса. Прежний log_class_at (class, at) снимается при накате; на
|
|
96
|
+
-- большой установке новый индекс можно построить заранее — create index concurrently log_class_tenant_at …
|
|
97
|
+
create index if not exists log_class_tenant_at on @SCHEMA@.log (class, tenant, at);
|
|
98
|
+
drop index if exists @SCHEMA@.log_class_at;
|
|
92
99
|
create index if not exists log_tx_seq on @SCHEMA@.log (tx, seq);
|
|
93
100
|
-- versions({ follow: true }): переход по связи старого и нового id при reclass и rekey (этап 6)
|
|
94
101
|
create index if not exists log_moved on @SCHEMA@.log (moved) where moved is not null;
|
|
@@ -125,7 +132,11 @@ revoke all on @SCHEMA@.entity, @SCHEMA@.log, @SCHEMA@.secret from public, @APP@;
|
|
|
125
132
|
grant select, delete on @SCHEMA@.entity to @APP@;
|
|
126
133
|
grant insert (id, tenant, class, links, data, tags, owner) on @SCHEMA@.entity to @APP@;
|
|
127
134
|
grant update (class, links, data, tags) on @SCHEMA@.entity to @APP@;
|
|
128
|
-
|
|
135
|
+
-- журнал — без колонки changed (ревью №1 точки А плана 1.1): её пути описывают разницу с предыдущей версией, а та
|
|
136
|
+
-- читателю может быть не видна (условие ACL скрыло её и пропустило следующую); blame получает пути через
|
|
137
|
+
-- version_paths, которая проверяет видимость обеих версий (75-history.sql)
|
|
138
|
+
grant select (seq, tx, id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason, moved, prev_hash, hash)
|
|
139
|
+
on @SCHEMA@.log to @APP@;
|
|
129
140
|
alter table @SCHEMA@.entity enable row level security;
|
|
130
141
|
alter table @SCHEMA@.log enable row level security;
|
|
131
142
|
alter table @SCHEMA@.secret enable row level security;
|
package/sql/15-errors.sql
CHANGED
|
@@ -25,7 +25,10 @@ as $$
|
|
|
25
25
|
('LT017', 'delete_restricted', 'letopis: delete_restricted: удалению мешают ссылающиеся строки'),
|
|
26
26
|
('LT018', 'not_deleted', 'letopis: not_deleted: объект не удалён — restore и purge только для удалённых'),
|
|
27
27
|
('LT019', 'reset_disabled', 'letopis: reset_disabled: reset выключен в этой установке'),
|
|
28
|
-
('LT020', 'confirm_mismatch', 'letopis: confirm_mismatch: подтверждение не совпадает с именем схемы')
|
|
28
|
+
('LT020', 'confirm_mismatch', 'letopis: confirm_mismatch: подтверждение не совпадает с именем схемы'),
|
|
29
|
+
('LT021', 'derived_class', 'letopis: derived_class: строки производного класса пишет только его проектор'),
|
|
30
|
+
('LT022', 'aggregate_field', 'letopis: aggregate_field: агрегатное поле считает база'),
|
|
31
|
+
('LT023', 'projector_pending', 'letopis: projector_pending: проектор источника ещё не прочёл удаление')
|
|
29
32
|
$$;
|
|
30
33
|
|
|
31
34
|
create or replace function @SCHEMA@.lt_raise(p_code text, p_name text, p_msg text, p_detail jsonb default null)
|
package/sql/40-class.sql
CHANGED
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
-- key [<поле или роль>…] — из них вычисляется id (§2.3); [] — класс без ключа
|
|
13
13
|
-- history true | false | политика хранения (§2.11)
|
|
14
14
|
-- ownerDefault 'tenant' | 'actor'; cache { ttl }; meta {…} до 64 КБ
|
|
15
|
-
--
|
|
15
|
+
-- derivedBy имя проектора (план 1.1, §2.4): строки класса пишет только он; наследуется, потомок не меняет
|
|
16
|
+
-- aggregate { <поле>: { sum: <поле источника> | count: true, of: <класс-источник>, via: <роль>, where?: JSON Schema } }
|
|
17
|
+
-- агрегаты в базе (план 1.1, §2.4): поле держит триггер по живым строкам источника; наследуется,
|
|
18
|
+
-- потомок заменяет целиком
|
|
19
|
+
-- resolved развёрнутое описание с учётом предков — пишет триггер, присланное значение перезаписывается; у
|
|
20
|
+
-- источника агрегатов в нём ещё метка aggregatedBy — какие цели считают его строки (88-derived.sql)
|
|
16
21
|
-- Скомпилированная проверка данных класса лежит в колонке validator строки Class.
|
|
17
22
|
|
|
18
23
|
create or replace function @SCHEMA@.class_metaschema() returns jsonb
|
|
@@ -69,6 +74,23 @@ return $json${
|
|
|
69
74
|
"ownerDefault": {"type": "string", "enum": ["tenant", "actor"]},
|
|
70
75
|
"cache": {"type": "object"},
|
|
71
76
|
"meta": {"type": "object"},
|
|
77
|
+
"derivedBy": {"type": "string", "pattern": "^[a-z0-9][a-z0-9_.:-]{0,63}$"},
|
|
78
|
+
"aggregate": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"propertyNames": {"type": "string", "pattern": "^[^$]"},
|
|
81
|
+
"additionalProperties": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"required": ["of", "via"],
|
|
84
|
+
"additionalProperties": false,
|
|
85
|
+
"properties": {
|
|
86
|
+
"sum": {"type": "string", "pattern": "^[^$]"},
|
|
87
|
+
"count": {"type": "boolean", "enum": [true]},
|
|
88
|
+
"of": {"type": "string", "pattern": "^[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{0,62}$"},
|
|
89
|
+
"via": {"type": "string", "pattern": "^[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{0,62}$"},
|
|
90
|
+
"where": {"type": "object"}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
72
94
|
"resolved": {"type": "object"}
|
|
73
95
|
}
|
|
74
96
|
}$json$::jsonb;
|
|
@@ -114,6 +136,8 @@ declare
|
|
|
114
136
|
hist jsonb;
|
|
115
137
|
od text;
|
|
116
138
|
cache jsonb;
|
|
139
|
+
der text;
|
|
140
|
+
agg jsonb;
|
|
117
141
|
comp jsonb := '{}';
|
|
118
142
|
i int;
|
|
119
143
|
v jsonb;
|
|
@@ -143,6 +167,16 @@ begin
|
|
|
143
167
|
if v ? 'history' then hist := v->'history'; end if;
|
|
144
168
|
if v ? 'ownerDefault' then od := v->>'ownerDefault'; end if;
|
|
145
169
|
if v ? 'cache' then cache := v->'cache'; end if;
|
|
170
|
+
-- производный класс (план 1.1, §2.4): проектор задаёт предок, потомок его не меняет
|
|
171
|
+
if v ? 'derivedBy' then
|
|
172
|
+
if der is not null and der <> v->>'derivedBy' then
|
|
173
|
+
perform @SCHEMA@.lt_raise('LT010', 'invalid_class',
|
|
174
|
+
format('derivedBy класса %s (%s) не совпадает с предком (%s)', v->>'name', v->>'derivedBy', der));
|
|
175
|
+
end if;
|
|
176
|
+
der := v->>'derivedBy';
|
|
177
|
+
end if;
|
|
178
|
+
-- агрегаты (план 1.1, §2.4): потомок заменяет описание предка целиком, {} — снимает унаследованное
|
|
179
|
+
if v ? 'aggregate' then agg := v->'aggregate'; end if;
|
|
146
180
|
for m in select * from jsonb_each(coalesce(v->'meta', '{}')) loop
|
|
147
181
|
if m.value = 'null'::jsonb then meta := meta - m.key; else meta := meta || jsonb_build_object(m.key, m.value); end if;
|
|
148
182
|
end loop;
|
|
@@ -158,7 +192,9 @@ begin
|
|
|
158
192
|
'history', coalesce(hist, 'true'),
|
|
159
193
|
'ownerDefault', coalesce(od, 'tenant'),
|
|
160
194
|
'cache', cache,
|
|
161
|
-
|
|
195
|
+
-- ключи только у производных и у целей агрегатов: у описаний до 1.2 их нет, и сравнение «было — стало» их не задевает
|
|
196
|
+
'meta', meta) || case when der is not null then jsonb_build_object('derivedBy', der) else '{}'::jsonb end
|
|
197
|
+
|| case when agg is not null and agg <> '{}'::jsonb then jsonb_build_object('aggregate', agg) else '{}'::jsonb end;
|
|
162
198
|
end $$;
|
|
163
199
|
|
|
164
200
|
-- Проверка описания класса: метасхема, правила ключа (§2.3), правило unset (§2.10), размер meta.
|
|
@@ -243,6 +279,25 @@ begin
|
|
|
243
279
|
perform @SCHEMA@.lt_raise('LT010', 'invalid_class', format('history: неизвестный часовой пояс %s', res->'history'->>'tz'));
|
|
244
280
|
end if;
|
|
245
281
|
end if;
|
|
282
|
+
-- производный класс (план 1.1, §2.4): системным не бывает; его строки проектор удаляет и строит заново,
|
|
283
|
+
-- поэтому они не мешают удалять строки, на которые ссылаются (onDelete только cascade или unset)
|
|
284
|
+
if res ? 'derivedBy' then
|
|
285
|
+
if coalesce((data->>'system')::boolean, false) then
|
|
286
|
+
perform @SCHEMA@.lt_raise('LT010', 'invalid_class', 'системный класс не бывает производным');
|
|
287
|
+
end if;
|
|
288
|
+
for e in select key, value from jsonb_each(res->'ends') loop
|
|
289
|
+
if coalesce(e.value->>'onDelete', 'restrict') not in ('cascade', 'unset') then
|
|
290
|
+
perform @SCHEMA@.lt_raise('LT010', 'invalid_class',
|
|
291
|
+
format('конец %s производного класса — onDelete cascade или unset: производная строка не мешает удалять цель', e.key));
|
|
292
|
+
end if;
|
|
293
|
+
end loop;
|
|
294
|
+
end if;
|
|
295
|
+
-- агрегаты (план 1.1, §2.4; 88-derived.sql): цель — поля, источники, цикл, условия where; источник — метка
|
|
296
|
+
-- aggregatedBy и та же сверка против агрегатов, которые считают его строки (источник часто описывают после цели)
|
|
297
|
+
if res ? 'aggregate' then
|
|
298
|
+
res := @SCHEMA@.agg_prepare(res, data, p_tenant);
|
|
299
|
+
end if;
|
|
300
|
+
res := @SCHEMA@.agg_source(res, p_tenant);
|
|
246
301
|
select * into c from @SCHEMA@.compile_validator(res->'schema');
|
|
247
302
|
data := data || jsonb_build_object('resolved', res || jsonb_build_object('rules', c.rules, 'vars', c.vars));
|
|
248
303
|
validator := c.validator;
|