letopis 0.21.0 → 1.0.1

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 +337 -0
  4. package/MIGRATION.md +190 -0
  5. package/README.en.md +1938 -0
  6. package/README.md +1493 -3469
  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 +362 -563
  17. package/dist/cli.d.ts +2 -0
  18. package/dist/cli.js +178 -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 -252
  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 +1372 -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 -352
  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 -685
  89. package/sql/seed.auth.sql +0 -73
package/dist/tables.js DELETED
@@ -1,248 +0,0 @@
1
- import { reservedNamesOf } from './types.js';
2
- const T = (ctx, name) => `"${ctx.pgSchema.replace(/"/g, '""')}"."${name}"`;
3
- // START_CONTRACT: where
4
- // PURPOSE: Собрать SQL WHERE из простых equality-условий, добавляя значения в params.
5
- // INPUTS: { conds: Cond[] - условия с генераторами текста; params: unknown[] - аккумулятор значений }
6
- // OUTPUTS: { string - " WHERE ..." или пустая строка }
7
- // SIDE_EFFECTS: пушит значения в params (мутация массива)
8
- // LINKS: M-TABLES, V-M-TABLES
9
- // END_CONTRACT: where
10
- /** WHERE из простых equality-условий (+спец-обработчики). */
11
- function where(conds, params) {
12
- if (!conds.length)
13
- return '';
14
- const parts = conds.map((c) => {
15
- params.push(c.value);
16
- return c.text(params.length);
17
- });
18
- return ' WHERE ' + parts.join(' AND ');
19
- }
20
- const eq = (col) => (n) => `"${col}" = $${n}`;
21
- // ---------------------------------------------------------------------------
22
- // START_CONTRACT: makeTables
23
- // PURPOSE: Построить фасад Tables (accounts/credentials/resources/rules) поверх Ctx.
24
- // INPUTS: { ctx: Ctx - runtime-контекст с sql и pgSchema }
25
- // OUTPUTS: { Tables - четыре API с find/get/set/delete }
26
- // SIDE_EFFECTS: каждый метод выполняет SELECT/INSERT/UPDATE/DELETE через ctx.sql
27
- // LINKS: M-TABLES, V-M-TABLES, M-DDL
28
- // END_CONTRACT: makeTables
29
- export function makeTables(ctx) {
30
- const run = (text, params) => ctx.sql.unsafe(text, params);
31
- // START_BLOCK_ACCOUNTS_API
32
- const accounts = {
33
- async find(f = {}) {
34
- const conds = [];
35
- if (f.id !== undefined)
36
- conds.push({ text: eq('id'), value: f.id });
37
- if (f.enabled !== undefined)
38
- conds.push({ text: eq('enabled'), value: f.enabled });
39
- if (f.category !== undefined)
40
- conds.push({ text: (n) => `$${n} = ANY(categories)`, value: f.category });
41
- const params = [];
42
- return run(`SELECT * FROM ${T(ctx, 'Account')}${where(conds, params)} ORDER BY created`, params);
43
- },
44
- async get(id) {
45
- return (await run(`SELECT * FROM ${T(ctx, 'Account')} WHERE id = $1`, [id]))[0] ?? null;
46
- },
47
- // START_CONTRACT: accounts.set
48
- // PURPOSE: INSERT (без id) либо UPDATE по id аккаунта; вернуть строку Account.
49
- // INPUTS: { a: Partial<Account> - поля categories/data/meta/avatar/enabled (+id для update) }
50
- // OUTPUTS: { Promise<Account> - созданная/обновлённая строка }
51
- // SIDE_EFFECTS: INSERT или UPDATE в таблице Account
52
- // ERRORS: account "<id>" not found (UPDATE не нашёл строку)
53
- // LINKS: M-TABLES, V-M-TABLES
54
- // END_CONTRACT: accounts.set
55
- async set(a) {
56
- if (a.id) {
57
- const sets = ['updated = now()'];
58
- const params = [];
59
- for (const col of ['categories', 'data', 'meta', 'avatar', 'enabled']) {
60
- if (a[col] !== undefined) {
61
- params.push(a[col]);
62
- sets.push(`"${col}" = $${params.length}`);
63
- }
64
- }
65
- params.push(a.id);
66
- const rows = await run(`UPDATE ${T(ctx, 'Account')} SET ${sets.join(', ')} WHERE id = $${params.length} RETURNING *`, params);
67
- if (!rows[0])
68
- throw new Error(`letopis: account "${a.id}" not found`);
69
- return rows[0];
70
- }
71
- const cols = [];
72
- const params = [];
73
- for (const col of ['categories', 'data', 'meta', 'avatar', 'enabled']) {
74
- if (a[col] !== undefined) {
75
- params.push(a[col]);
76
- cols.push(`"${col}"`);
77
- }
78
- }
79
- const ph = params.map((_, i) => `$${i + 1}`).join(', ');
80
- const rows = await run(cols.length
81
- ? `INSERT INTO ${T(ctx, 'Account')} (${cols.join(', ')}) VALUES (${ph}) RETURNING *`
82
- : `INSERT INTO ${T(ctx, 'Account')} DEFAULT VALUES RETURNING *`, params);
83
- return rows[0];
84
- },
85
- async delete(id) {
86
- const rows = await run(`DELETE FROM ${T(ctx, 'Account')} WHERE id = $1 RETURNING id`, [id]);
87
- return rows.length > 0;
88
- },
89
- // START_CONTRACT: accounts.purge
90
- // PURPOSE: Физический офбординг тенанта через purge_account(); гарды Owner/System, последний Owner, не-себя — до сноса, в одной tx.
91
- // INPUTS: { id: string - целевой аккаунт }
92
- // OUTPUTS: { Promise<boolean> - true если Account физически снесён }
93
- // SIDE_EFFECTS: физ. DELETE всех Entity аккаунта + Account (Credential FK-каскад) через SQL purge_account()
94
- // ERRORS: нет caller/ACL; вызов не Owner/System; снос последнего Owner; снос своего аккаунта
95
- // LINKS: M-TABLES, V-M-TABLES, M-DDL
96
- // END_CONTRACT: accounts.purge
97
- async purge(id) {
98
- if (!ctx.account) {
99
- throw new Error('letopis: accounts.purge requires an authenticated caller — call it from a scoped handle: ' +
100
- '(await db.as(caller)).accounts.purge(id)');
101
- }
102
- if (id === ctx.account) {
103
- throw new Error('letopis: accounts.purge cannot purge the calling account itself');
104
- }
105
- const schema = `"${ctx.pgSchema.replace(/"/g, '""')}"`;
106
- return ctx.sql.begin(async (tsql) => {
107
- const q = (t, p) => tsql.unsafe(t, p);
108
- // вызывающий обязан быть Owner/System
109
- const caller = await q(`SELECT categories FROM ${T(ctx, 'Account')} WHERE id = $1`, [ctx.account]);
110
- const cats = caller[0]?.categories ?? [];
111
- if (!cats.includes('Owner') && !cats.includes('System')) {
112
- throw new Error('letopis: accounts.purge is restricted to Owner/System accounts');
113
- }
114
- const tgt = await q(`SELECT categories FROM ${T(ctx, 'Account')} WHERE id = $1`, [id]);
115
- if (!tgt.length)
116
- return false;
117
- // нельзя снести последний enabled Owner → тенант без владельца (лок-аут)
118
- if ((tgt[0].categories ?? []).includes('Owner')) {
119
- const cnt = await q(`SELECT count(*)::text AS n FROM ${T(ctx, 'Account')} WHERE 'Owner' = ANY(categories) AND enabled`, []);
120
- if (Number(cnt[0]?.n ?? 0) <= 1) {
121
- throw new Error('letopis: refusing to purge the last enabled Owner (tenant lock-out)');
122
- }
123
- }
124
- const res = await q(`SELECT ${schema}.purge_account($1::uuid) AS id`, [id]);
125
- return res[0]?.id != null;
126
- });
127
- },
128
- };
129
- // END_BLOCK_ACCOUNTS_API
130
- // START_BLOCK_CREDENTIALS_API
131
- const credentials = {
132
- async find(f = {}) {
133
- const conds = [];
134
- if (f.id !== undefined)
135
- conds.push({ text: eq('id'), value: f.id });
136
- if (f.account !== undefined)
137
- conds.push({ text: eq('account'), value: f.account });
138
- if (f.category !== undefined)
139
- conds.push({ text: eq('category'), value: f.category });
140
- if (f.identifier !== undefined)
141
- conds.push({ text: eq('identifier'), value: f.identifier });
142
- if (f.confirmed !== undefined)
143
- conds.push({ text: eq('confirmed'), value: f.confirmed });
144
- const params = [];
145
- let sql = `SELECT * FROM ${T(ctx, 'Credential')}${where(conds, params)}`;
146
- if (!f.withDeleted)
147
- sql += conds.length ? ' AND deleted IS NULL' : ' WHERE deleted IS NULL';
148
- return run(sql + ' ORDER BY created', params);
149
- },
150
- // START_CONTRACT: credentials.set
151
- // PURPOSE: upsert креденшела по UNIQUE (account, category, identifier); повторный set воскрешает (deleted → NULL).
152
- // INPUTS: { c: { account, category, identifier, meta?, confirmed? } }
153
- // OUTPUTS: { Promise<Credential> - upsert-нутая строка }
154
- // SIDE_EFFECTS: INSERT ... ON CONFLICT DO UPDATE в таблице Credential
155
- // LINKS: M-TABLES, V-M-TABLES
156
- // END_CONTRACT: credentials.set
157
- async set(c) {
158
- const rows = await run(`INSERT INTO ${T(ctx, 'Credential')} (account, category, identifier, meta, confirmed)
159
- VALUES ($1, $2, $3, $4, $5)
160
- ON CONFLICT (account, category, identifier) DO UPDATE
161
- SET meta = EXCLUDED.meta, confirmed = EXCLUDED.confirmed, updated = now(), deleted = NULL
162
- RETURNING *`, [c.account, c.category, c.identifier, c.meta ?? {}, c.confirmed ?? true]);
163
- return rows[0];
164
- },
165
- async delete(id) {
166
- const rows = await run(`UPDATE ${T(ctx, 'Credential')} SET deleted = now(), updated = now() WHERE id = $1 AND deleted IS NULL RETURNING id`, [id]);
167
- return rows.length > 0;
168
- },
169
- };
170
- // END_BLOCK_CREDENTIALS_API
171
- // START_BLOCK_RESOURCES_API
172
- const resources = {
173
- async find(f = {}) {
174
- const conds = [];
175
- if (f.category !== undefined)
176
- conds.push({ text: eq('category'), value: f.category });
177
- const params = [];
178
- return run(`SELECT * FROM ${T(ctx, 'Resource')}${where(conds, params)} ORDER BY alias`, params);
179
- },
180
- async get(alias) {
181
- return (await run(`SELECT * FROM ${T(ctx, 'Resource')} WHERE alias = $1`, [alias]))[0] ?? null;
182
- },
183
- async set(r) {
184
- const rows = await run(`INSERT INTO ${T(ctx, 'Resource')} (alias, category, pattern, meta) VALUES ($1, $2, $3, $4)
185
- ON CONFLICT (alias) DO UPDATE SET category = EXCLUDED.category, pattern = EXCLUDED.pattern, meta = EXCLUDED.meta
186
- RETURNING *`, [r.alias, r.category, r.pattern ?? null, r.meta ?? null]);
187
- return rows[0];
188
- },
189
- async delete(alias) {
190
- const rows = await run(`DELETE FROM ${T(ctx, 'Resource')} WHERE alias = $1 RETURNING alias`, [alias]);
191
- return rows.length > 0;
192
- },
193
- };
194
- // END_BLOCK_RESOURCES_API
195
- // START_BLOCK_RULES_API
196
- const rules = {
197
- async find(f = {}) {
198
- const conds = [];
199
- if (f.account !== undefined)
200
- conds.push({ text: eq('account'), value: f.account });
201
- if (f.resource !== undefined)
202
- conds.push({ text: eq('resource'), value: f.resource });
203
- if (f.permission !== undefined)
204
- conds.push({ text: eq('permission'), value: f.permission });
205
- if (f.enabled !== undefined)
206
- conds.push({ text: eq('enabled'), value: f.enabled });
207
- const params = [];
208
- return run(`SELECT * FROM ${T(ctx, 'Rule')}${where(conds, params)} ORDER BY weight DESC NULLS LAST`, params);
209
- },
210
- async set(r) {
211
- const rows = await run(`INSERT INTO ${T(ctx, 'Rule')} (account, resource, permission, weight, meta, enabled)
212
- VALUES ($1, $2, $3, $4, $5, $6)
213
- ON CONFLICT (account, resource) DO UPDATE
214
- SET permission = EXCLUDED.permission, weight = EXCLUDED.weight, meta = EXCLUDED.meta, enabled = EXCLUDED.enabled
215
- RETURNING *`, [r.account, r.resource, r.permission, r.weight ?? null, r.meta ?? null, r.enabled ?? true]);
216
- return rows[0];
217
- },
218
- async delete(account, resource) {
219
- const rows = await run(`DELETE FROM ${T(ctx, 'Rule')} WHERE account = $1 AND resource = $2 RETURNING account`, [account, resource]);
220
- return rows.length > 0;
221
- },
222
- };
223
- // END_BLOCK_RULES_API
224
- // START_BLOCK_SCHEMA_API
225
- const schema = {
226
- async define(def) {
227
- // класс с именем, которое Proxy разбирает до резолва класса, был бы недостижим как шаг
228
- const clash = reservedNamesOf(def);
229
- if (clash.length) {
230
- throw new Error(`letopis: class name ${clash.map((n) => `"${n}"`).join(' / ')} is reserved by the chain API ` +
231
- `(a step under that name resolves to the method, not the class) — rename the ${clash.includes(def.id) ? 'id' : 'alias'}`);
232
- }
233
- // jsonb-значения через sql.json() — postgres.js кладёт как jsonb (не PG-массив/строку)
234
- const j = (v) => ctx.sql.json(v);
235
- await run(`INSERT INTO ${T(ctx, 'Schema')} (partition, id, alias, category, ancestor, attributes, meta, links, "order")
236
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
237
- ON CONFLICT (partition, id) DO UPDATE SET
238
- alias = EXCLUDED.alias, category = EXCLUDED.category, ancestor = EXCLUDED.ancestor,
239
- attributes = EXCLUDED.attributes, meta = EXCLUDED.meta, links = EXCLUDED.links, "order" = EXCLUDED."order"`, [
240
- ctx.partition, def.id, def.alias, def.category, def.ancestor ?? null,
241
- j(def.attributes ?? {}), j(def.meta ?? {}), j(def.links ?? []), def.order ?? 0,
242
- ]);
243
- await ctx.reload?.(); // подхватить новое определение сразу (registry + enforcer)
244
- },
245
- };
246
- // END_BLOCK_SCHEMA_API
247
- return { accounts, credentials, resources, rules, schema };
248
- }
package/docker/Dockerfile DELETED
@@ -1,40 +0,0 @@
1
- # Dev-контейнер letopis: TimescaleDB + Redis в одном контейнере (решение проекта).
2
- # Обычный путь — letopis.up() собирает и запускает сам. Руками:
3
- # docker build -t letopis-db lib/docker
4
- # docker run -d --name letopis-timescale -p 15432:5432 -p 16379:6379 \
5
- # -v letopis-pgdata:/var/lib/postgresql/data \
6
- # -e POSTGRES_PASSWORD=test -e POSTGRES_DB=letopis letopis-db
7
- # FILE: lib/docker/Dockerfile
8
- # VERSION: 1.0.0
9
- # START_MODULE_CONTRACT
10
- # PURPOSE: Dev-образ БД — TimescaleDB pg17 + Redis в одном контейнере (для letopis.up()).
11
- # SCOPE: базовый образ, установка redis, копирование start.sh, EXPOSE 5432/6379, entrypoint.
12
- # DEPENDS: none
13
- # LINKS: M-DOCKER, V-M-DOCKER
14
- # ROLE: CONFIG
15
- # MAP_MODE: SUMMARY
16
- # END_MODULE_CONTRACT
17
- #
18
- # START_MODULE_MAP
19
- # FROM timescale/timescaledb:latest-pg17 - база (PostgreSQL 17 + TimescaleDB)
20
- # apk add redis - Redis в тот же образ
21
- # ENTRYPOINT start-letopis.sh - запуск redis + postgres
22
- # END_MODULE_MAP
23
- #
24
- # START_CHANGE_SUMMARY
25
- # LAST_CHANGE: [v1.0.0 - Documented existing module: reverse-engineered contract + markup]
26
- # END_CHANGE_SUMMARY
27
-
28
- # START_BLOCK_IMAGE_BUILD
29
- FROM timescale/timescaledb:latest-pg17
30
-
31
- RUN apk add --no-cache redis
32
-
33
- COPY start.sh /usr/local/bin/start-letopis.sh
34
- RUN chmod +x /usr/local/bin/start-letopis.sh
35
-
36
- EXPOSE 5432 6379
37
-
38
- ENTRYPOINT ["/usr/local/bin/start-letopis.sh"]
39
- CMD ["postgres"]
40
- # END_BLOCK_IMAGE_BUILD
package/docker/start.sh DELETED
@@ -1,18 +0,0 @@
1
- #!/bin/sh
2
- # FILE: lib/docker/start.sh
3
- # VERSION: 1.0.0
4
- # START_MODULE_CONTRACT
5
- # PURPOSE: Entrypoint dev-контейнера — поднять Redis фоном, затем штатный postgres docker-entrypoint.
6
- # SCOPE: redis-server (daemonize) + exec docker-entrypoint.sh.
7
- # DEPENDS: none
8
- # LINKS: M-DOCKER, V-M-DOCKER
9
- # ROLE: SCRIPT
10
- # MAP_MODE: NONE
11
- # END_MODULE_CONTRACT
12
- #
13
- # START_CHANGE_SUMMARY
14
- # LAST_CHANGE: [v1.0.0 - Documented existing module: reverse-engineered contract + markup]
15
- # END_CHANGE_SUMMARY
16
- # Redis фоном (dev: без пароля и персиста — сессии эфемерны), затем штатный entrypoint postgres.
17
- redis-server --port 6379 --bind 0.0.0.0 --protected-mode no --save '' --appendonly no --daemonize yes
18
- exec docker-entrypoint.sh "$@"