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.
Files changed (60) hide show
  1. package/AGENT-CHEATSHEET.en.md +117 -10
  2. package/AGENT-CHEATSHEET.md +110 -8
  3. package/CHANGELOG.md +739 -0
  4. package/README.en.md +873 -68
  5. package/README.md +830 -66
  6. package/dist/admin.d.ts +24 -4
  7. package/dist/admin.js +39 -9
  8. package/dist/cache.d.ts +33 -9
  9. package/dist/cache.js +81 -32
  10. package/dist/chain.d.ts +29 -7
  11. package/dist/chain.js +61 -12
  12. package/dist/cli.js +59 -7
  13. package/dist/errors.js +4 -2
  14. package/dist/import.d.ts +6 -0
  15. package/dist/import.js +196 -175
  16. package/dist/index.d.ts +27 -5
  17. package/dist/index.js +191 -29
  18. package/dist/internal.d.ts +16 -0
  19. package/dist/internal.js +14 -0
  20. package/dist/load.d.ts +63 -0
  21. package/dist/load.js +331 -44
  22. package/dist/mcp.d.ts +28 -0
  23. package/dist/mcp.js +810 -0
  24. package/dist/model.d.ts +34 -0
  25. package/dist/model.js +17 -0
  26. package/dist/project.d.ts +309 -0
  27. package/dist/project.js +1097 -0
  28. package/dist/registry.d.ts +17 -0
  29. package/dist/registry.js +13 -0
  30. package/dist/signals.d.ts +73 -0
  31. package/dist/signals.js +169 -0
  32. package/dist/sql.d.ts +1 -1
  33. package/dist/sql.js +26 -7
  34. package/dist/tx.d.ts +9 -7
  35. package/dist/tx.js +15 -5
  36. package/dist/typed.d.ts +21 -6
  37. package/dist/types.d.ts +77 -4
  38. package/dist/types.js +8 -3
  39. package/dist/up.d.ts +16 -2
  40. package/dist/up.js +34 -4
  41. package/dist/uuid.js +19 -8
  42. package/dist/watch.d.ts +23 -2
  43. package/dist/watch.js +44 -7
  44. package/dist/write.d.ts +11 -2
  45. package/dist/write.js +57 -10
  46. package/llms.txt +9 -2
  47. package/package.json +13 -2
  48. package/sql/10-core.sql +13 -2
  49. package/sql/15-errors.sql +4 -1
  50. package/sql/40-class.sql +57 -2
  51. package/sql/60-write.sql +611 -82
  52. package/sql/70-read.sql +33 -8
  53. package/sql/75-history.sql +358 -0
  54. package/sql/80-auth.sql +7 -7
  55. package/sql/88-derived.sql +1154 -0
  56. package/sql/90-time.sql +134 -21
  57. package/sql/95-seed.system.sql +24 -6
  58. package/sql/99-revision.sql +1 -1
  59. package/sql/README.md +5 -1
  60. 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.1",
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
- create index if not exists log_class_at on @SCHEMA@.log (class, at);
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
- grant select on @SCHEMA@.log to @APP@;
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
- -- resolved развёрнутое описание с учётом предков — пишет триггер, присланное значение перезаписывается
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
- 'meta', meta);
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;