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
package/dist/schema.js
DELETED
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Загрузка таблицы Schema → реестр классов:
|
|
3
|
-
* - резолв имени по id и alias;
|
|
4
|
-
* - цепочка ancestors (walk по ancestor, если в БД не заполнена);
|
|
5
|
-
* - attributes НАСЛЕДУЮТСЯ по цепочке (потомок поверх предка; поле = замена правила
|
|
6
|
-
* целиком) — включая правило id (генерация v4/v5/v7); links НЕ наследуются;
|
|
7
|
-
* - скомпилированный fastest-validator per class;
|
|
8
|
-
* - карта типов полей (для SQL-кастов фильтров).
|
|
9
|
-
*/
|
|
10
|
-
//
|
|
11
|
-
// FILE: lib/src/schema.ts
|
|
12
|
-
// VERSION: 1.1.0
|
|
13
|
-
// START_MODULE_CONTRACT
|
|
14
|
-
// PURPOSE: Строит реестр классов из таблицы Schema — резолв наследования/link-ends, типы полей, компиляция валидаторов.
|
|
15
|
-
// SCOPE: Registry (add/find/resolve/has), loadRegistry, fieldTypeOf; локальные parseEnd/parseIdGen/buildDef.
|
|
16
|
-
// DEPENDS: M-TYPES
|
|
17
|
-
// LINKS: M-SCHEMA, V-M-SCHEMA
|
|
18
|
-
// ROLE: RUNTIME
|
|
19
|
-
// MAP_MODE: EXPORTS
|
|
20
|
-
// END_MODULE_CONTRACT
|
|
21
|
-
//
|
|
22
|
-
// START_MODULE_MAP
|
|
23
|
-
// Registry - реестр классов (add/find/resolve/has, all)
|
|
24
|
-
// fieldTypeOf - attribute-спека fastest-validator → FieldType (для SQL-кастов)
|
|
25
|
-
// loadRegistry - загрузка Schema-строк партиции → Registry со скомпилированными check
|
|
26
|
-
// END_MODULE_MAP
|
|
27
|
-
//
|
|
28
|
-
// START_CHANGE_SUMMARY
|
|
29
|
-
// LAST_CHANGE: [v1.1.0 - loadRegistry: 42P01/3F000 перехватываются и заменяются адресной ошибкой
|
|
30
|
-
// (передано базовое имя вместо "vN.имя") со списком letopis-схем — паритет с валидацией up()]
|
|
31
|
-
// END_CHANGE_SUMMARY
|
|
32
|
-
import { createRequire } from 'node:module';
|
|
33
|
-
import { reservedNamesOf } from './types.js';
|
|
34
|
-
const Validator = createRequire(import.meta.url)('fastest-validator');
|
|
35
|
-
const v = new Validator({ useNewCustomCheckerFunction: true });
|
|
36
|
-
export class Registry {
|
|
37
|
-
byName = new Map();
|
|
38
|
-
all = [];
|
|
39
|
-
add(def) {
|
|
40
|
-
this.all.push(def);
|
|
41
|
-
this.byName.set(def.id, def);
|
|
42
|
-
this.byName.set(def.alias, def);
|
|
43
|
-
}
|
|
44
|
-
/** Класс по id или alias; undefined если нет. */
|
|
45
|
-
find(name) {
|
|
46
|
-
return this.byName.get(name);
|
|
47
|
-
}
|
|
48
|
-
// START_CONTRACT: Registry.resolve
|
|
49
|
-
// PURPOSE: Вернуть класс по id или alias, иначе бросить ошибку со списком известных.
|
|
50
|
-
// INPUTS: { name: string - id или alias класса }
|
|
51
|
-
// OUTPUTS: { ClassDef - найденный класс }
|
|
52
|
-
// SIDE_EFFECTS: none
|
|
53
|
-
// LINKS: M-SCHEMA, V-M-SCHEMA
|
|
54
|
-
// END_CONTRACT: Registry.resolve
|
|
55
|
-
/** Класс по id или alias; иначе понятная ошибка со списком. */
|
|
56
|
-
resolve(name) {
|
|
57
|
-
const def = this.byName.get(name);
|
|
58
|
-
if (!def) {
|
|
59
|
-
const names = this.all.map((d) => `${d.id}·${d.alias}`).join(', ');
|
|
60
|
-
throw new Error(`letopis: unknown class "${name}". Known: ${names}`);
|
|
61
|
-
}
|
|
62
|
-
return def;
|
|
63
|
-
}
|
|
64
|
-
has(name) {
|
|
65
|
-
return this.byName.has(name);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
// START_CONTRACT: fieldTypeOf
|
|
69
|
-
// PURPOSE: Свести attribute-спеку fastest-validator (строка/массив/объект) к FieldType.
|
|
70
|
-
// INPUTS: { attr: unknown - правило поля из attributes }
|
|
71
|
-
// OUTPUTS: { FieldType - вид поля (number/date/boolean/string/array/record/object/any) }
|
|
72
|
-
// SIDE_EFFECTS: none (рекурсивно по вложенным props/value)
|
|
73
|
-
// LINKS: M-SCHEMA, V-M-SCHEMA, type-FieldType
|
|
74
|
-
// END_CONTRACT: fieldTypeOf
|
|
75
|
-
/** Тип поля из fastest-validator DSL — для каста в SQL. */
|
|
76
|
-
export function fieldTypeOf(attr) {
|
|
77
|
-
if (typeof attr === 'string') {
|
|
78
|
-
const base = attr.split('|')[0].trim();
|
|
79
|
-
switch (base) {
|
|
80
|
-
case 'number':
|
|
81
|
-
return { kind: 'number' };
|
|
82
|
-
case 'date':
|
|
83
|
-
return { kind: 'date' };
|
|
84
|
-
case 'boolean':
|
|
85
|
-
return { kind: 'boolean' };
|
|
86
|
-
case 'array':
|
|
87
|
-
return { kind: 'array' };
|
|
88
|
-
case 'string':
|
|
89
|
-
case 'uuid':
|
|
90
|
-
case 'email':
|
|
91
|
-
case 'url':
|
|
92
|
-
case 'enum':
|
|
93
|
-
return { kind: 'string' };
|
|
94
|
-
default:
|
|
95
|
-
return { kind: 'any' };
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (Array.isArray(attr))
|
|
99
|
-
return attr.length ? fieldTypeOf(attr[0]) : { kind: 'any' };
|
|
100
|
-
if (typeof attr === 'object' && attr !== null) {
|
|
101
|
-
const t = attr.type;
|
|
102
|
-
if (t === 'record')
|
|
103
|
-
return { kind: 'record', value: fieldTypeOf(attr.value ?? 'any') };
|
|
104
|
-
if (t === 'object') {
|
|
105
|
-
// вложенная структура: {type:'object', props:{…}} — типы листьев любой глубины
|
|
106
|
-
const props = attr.props ?? attr.properties;
|
|
107
|
-
if (props && typeof props === 'object') {
|
|
108
|
-
const m = new Map();
|
|
109
|
-
for (const [k, v] of Object.entries(props))
|
|
110
|
-
m.set(k, fieldTypeOf(v));
|
|
111
|
-
return { kind: 'object', props: m };
|
|
112
|
-
}
|
|
113
|
-
return { kind: 'any' };
|
|
114
|
-
}
|
|
115
|
-
if (t === 'array')
|
|
116
|
-
return { kind: 'array' };
|
|
117
|
-
if (t === 'number')
|
|
118
|
-
return { kind: 'number' };
|
|
119
|
-
if (t === 'date')
|
|
120
|
-
return { kind: 'date' };
|
|
121
|
-
if (t === 'boolean')
|
|
122
|
-
return { kind: 'boolean' };
|
|
123
|
-
if (t === 'enum' || t === 'string' || t === 'uuid' || t === 'email' || t === 'url')
|
|
124
|
-
return { kind: 'string' };
|
|
125
|
-
if (typeof t === 'string')
|
|
126
|
-
return fieldTypeOf(t);
|
|
127
|
-
}
|
|
128
|
-
return { kind: 'any' };
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Элемент Schema.links: объект-конец v2 (jsonb) либо legacy-строка 'Org'
|
|
132
|
-
* (включая JSON-текст в text[] у старых схем). Возвращает [конец, isV2].
|
|
133
|
-
*/
|
|
134
|
-
// START_CONTRACT: parseEnd
|
|
135
|
-
// PURPOSE: Разобрать один элемент Schema.links в LinkEnd (объект-конец v2 или legacy-строка).
|
|
136
|
-
// INPUTS: { cls: string - id класса; raw: string|object - сырой конец из jsonb/text[] }
|
|
137
|
-
// OUTPUTS: { [LinkEnd, boolean] - конец и признак v2-формы (для strictEnds) }
|
|
138
|
-
// SIDE_EFFECTS: none
|
|
139
|
-
// ERRORS: malformed link end (bad JSON); link end without classes
|
|
140
|
-
// LINKS: M-SCHEMA, V-M-SCHEMA
|
|
141
|
-
// END_CONTRACT: parseEnd
|
|
142
|
-
function parseEnd(cls, raw) {
|
|
143
|
-
let o;
|
|
144
|
-
if (typeof raw === 'string') {
|
|
145
|
-
if (!raw.trimStart().startsWith('{'))
|
|
146
|
-
return [{ classes: [raw] }, false];
|
|
147
|
-
try {
|
|
148
|
-
o = JSON.parse(raw);
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
throw new Error(`letopis: class "${cls}" has malformed link end (bad JSON): ${raw}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
o = raw;
|
|
156
|
-
}
|
|
157
|
-
const classes = o.classes ?? (o.class ? [o.class] : []);
|
|
158
|
-
if (!classes.length || classes.some((c) => typeof c !== 'string' || !c)) {
|
|
159
|
-
throw new Error(`letopis: class "${cls}" has link end without classes: ${JSON.stringify(raw)}`);
|
|
160
|
-
}
|
|
161
|
-
return [{ classes, optional: o.optional === true, cardinality: o.cardinality }, true];
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* attributes.id: правило валидации + (объектом) спецификация генерации.
|
|
165
|
-
* generate/from — ключи letopis, вырезаются из правила перед компиляцией валидатора.
|
|
166
|
-
*/
|
|
167
|
-
// START_CONTRACT: parseIdGen
|
|
168
|
-
// PURPOSE: Отделить спецификацию генерации id (generate/from) от правила валидации attributes.id.
|
|
169
|
-
// INPUTS: { cls: string - id класса; attributes: object - слитые attributes класса }
|
|
170
|
-
// OUTPUTS: { { idGen: IdGen; attrs: object } - версия генерации id и очищенное правило }
|
|
171
|
-
// SIDE_EFFECTS: none
|
|
172
|
-
// ERRORS: generate must be 4|5|7; generate:5 needs "from"; from is for generate:5 only
|
|
173
|
-
// LINKS: M-SCHEMA, V-M-SCHEMA
|
|
174
|
-
// END_CONTRACT: parseIdGen
|
|
175
|
-
function parseIdGen(cls, attributes) {
|
|
176
|
-
const raw = attributes.id;
|
|
177
|
-
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
178
|
-
return { idGen: { version: 4 }, attrs: attributes };
|
|
179
|
-
}
|
|
180
|
-
const { generate, from, ...rule } = raw;
|
|
181
|
-
const attrs = { ...attributes, id: rule };
|
|
182
|
-
if (generate === undefined)
|
|
183
|
-
return { idGen: { version: 4 }, attrs };
|
|
184
|
-
if (generate !== 4 && generate !== 5 && generate !== 7) {
|
|
185
|
-
throw new Error(`letopis: class "${cls}" — attributes.id.generate must be 4 | 5 | 7, got ${JSON.stringify(generate)}`);
|
|
186
|
-
}
|
|
187
|
-
if (generate === 5) {
|
|
188
|
-
if (!Array.isArray(from) || !from.length || from.some((f) => typeof f !== 'string' || !f)) {
|
|
189
|
-
throw new Error(`letopis: class "${cls}" — id generate:5 needs "from": non-empty string[] (link ends / data fields)`);
|
|
190
|
-
}
|
|
191
|
-
return { idGen: { version: 5, from: from }, attrs };
|
|
192
|
-
}
|
|
193
|
-
if (from !== undefined) {
|
|
194
|
-
throw new Error(`letopis: class "${cls}" — attributes.id.from is for generate:5 only`);
|
|
195
|
-
}
|
|
196
|
-
return { idGen: { version: generate }, attrs };
|
|
197
|
-
}
|
|
198
|
-
// START_CONTRACT: buildDef
|
|
199
|
-
// PURPOSE: Собрать ClassDef из строки Schema — цепочка ancestors, наследование attributes, id-gen, компиляция check, типы полей, link-ends.
|
|
200
|
-
// INPUTS: { row: SchemaRow - строка класса; byId: Map<string,SchemaRow> - все классы партиции }
|
|
201
|
-
// OUTPUTS: { ClassDef - полностью разрешённое определение класса }
|
|
202
|
-
// SIDE_EFFECTS: none (компилирует fastest-validator)
|
|
203
|
-
// ERRORS: id v5 can't depend on optional end; id "from" source is neither end nor field
|
|
204
|
-
// LINKS: M-SCHEMA, V-M-SCHEMA, type-ClassDef
|
|
205
|
-
// END_CONTRACT: buildDef
|
|
206
|
-
function buildDef(row, byId) {
|
|
207
|
-
// START_BLOCK_INHERIT_ANCESTORS
|
|
208
|
-
// цепочка наследования: из БД или walk по ancestor
|
|
209
|
-
let ancestors = row.ancestors ?? [];
|
|
210
|
-
if (!ancestors.length) {
|
|
211
|
-
ancestors = [row.id];
|
|
212
|
-
let cur = row.ancestor;
|
|
213
|
-
const seen = new Set([row.id]);
|
|
214
|
-
while (cur && !seen.has(cur)) {
|
|
215
|
-
ancestors.push(cur);
|
|
216
|
-
seen.add(cur);
|
|
217
|
-
cur = byId.get(cur)?.ancestor ?? null;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
// attributes НАСЛЕДУЮТСЯ по цепочке ancestor: потомок ПОВЕРХ предка, переопределение
|
|
221
|
-
// поля — замена правила ЦЕЛИКОМ (не слияние объекта-правила). links не наследуются.
|
|
222
|
-
const merged = {};
|
|
223
|
-
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
224
|
-
Object.assign(merged, byId.get(ancestors[i])?.attributes ?? {});
|
|
225
|
-
}
|
|
226
|
-
// END_BLOCK_INHERIT_ANCESTORS
|
|
227
|
-
// attributes.id: спецификация генерации (generate/from) отделяется от правила валидации
|
|
228
|
-
const { idGen, attrs } = parseIdGen(row.id, merged);
|
|
229
|
-
// fastest-validator: валидируем {id, ...data}; СТРОГО — лишние поля запрещены
|
|
230
|
-
const compiled = v.compile({ ...attrs, $$strict: true });
|
|
231
|
-
const check = (data) => {
|
|
232
|
-
const res = compiled(data);
|
|
233
|
-
return res === true ? true : res;
|
|
234
|
-
};
|
|
235
|
-
const fieldTypes = new Map();
|
|
236
|
-
for (const [field, attr] of Object.entries(attrs)) {
|
|
237
|
-
if (field.startsWith('$$'))
|
|
238
|
-
continue;
|
|
239
|
-
fieldTypes.set(field, fieldTypeOf(attr));
|
|
240
|
-
}
|
|
241
|
-
let strictEnds = false;
|
|
242
|
-
// jsonb-массив; text[] у legacy-схем даёт string[]; jsonb-строка "[…]" (двойная
|
|
243
|
-
// сериализация внешних писателей) — распарсить
|
|
244
|
-
let rawLinks = row.links ?? [];
|
|
245
|
-
if (typeof rawLinks === 'string')
|
|
246
|
-
rawLinks = JSON.parse(rawLinks);
|
|
247
|
-
const ends = rawLinks.map((raw) => {
|
|
248
|
-
const [end, v2] = parseEnd(row.id, raw);
|
|
249
|
-
if (v2)
|
|
250
|
-
strictEnds = true;
|
|
251
|
-
return end;
|
|
252
|
-
});
|
|
253
|
-
// START_BLOCK_ID_V5_VALIDATE
|
|
254
|
-
// id v5: каждый источник from — обязательный конец links (класс или полное имя союза
|
|
255
|
-
// 'Service|Complex') ЛИБО поле data
|
|
256
|
-
if (idGen.version === 5) {
|
|
257
|
-
for (const f of idGen.from) {
|
|
258
|
-
const end = ends.find((e) => e.classes.includes(f) || e.classes.join('|') === f);
|
|
259
|
-
if (end) {
|
|
260
|
-
if (end.optional) {
|
|
261
|
-
throw new Error(`letopis: class "${row.id}" — id (uuid v5) can't depend on optional end "${f}"`);
|
|
262
|
-
}
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
if (f !== 'id' && f in attrs)
|
|
266
|
-
continue;
|
|
267
|
-
throw new Error(`letopis: class "${row.id}" — id "from" source "${f}" is neither a link end nor a data field`);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
// END_BLOCK_ID_V5_VALIDATE
|
|
271
|
-
return {
|
|
272
|
-
id: row.id,
|
|
273
|
-
alias: row.alias,
|
|
274
|
-
category: row.category,
|
|
275
|
-
ancestor: row.ancestor,
|
|
276
|
-
ancestors,
|
|
277
|
-
descendants: row.descendants ?? [], // считает триггер schema_lineage
|
|
278
|
-
attributes: merged,
|
|
279
|
-
links: ends,
|
|
280
|
-
strictEnds,
|
|
281
|
-
idGen,
|
|
282
|
-
meta: row.meta ?? {},
|
|
283
|
-
abstract: row.meta?.abstract === true || row.meta?.abstract === 'true',
|
|
284
|
-
order: row.order,
|
|
285
|
-
check,
|
|
286
|
-
fieldTypes,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
// START_CONTRACT: loadRegistry
|
|
290
|
-
// PURPOSE: Прочитать классы партиции из таблицы Schema и построить Registry со скомпилированными check.
|
|
291
|
-
// INPUTS: { sql: postgres.Sql; pgSchema: string; partition: string }
|
|
292
|
-
// OUTPUTS: { Promise<Registry> - реестр всех классов партиции }
|
|
293
|
-
// SIDE_EFFECTS: SELECT из "<pgSchema>"."Schema"; на 42P01/3F000 — доп. SELECT списка схем для подсказки
|
|
294
|
-
// ERRORS: schema has no classes for partition; schema has no "Schema" table (передано базовое имя вместо "vN.имя")
|
|
295
|
-
// LINKS: M-SCHEMA, V-M-SCHEMA, M-DDL
|
|
296
|
-
// END_CONTRACT: loadRegistry
|
|
297
|
-
/** Уже предупреждённые «схема:класс» — чтобы не повторять на каждый connect/reloadSchema. */
|
|
298
|
-
const warnedReserved = new Set();
|
|
299
|
-
export async function loadRegistry(sql, pgSchema, partition) {
|
|
300
|
-
// START_BLOCK_LOAD_QUERY
|
|
301
|
-
const ident = `"${pgSchema.replace(/"/g, '""')}"`;
|
|
302
|
-
let rows;
|
|
303
|
-
try {
|
|
304
|
-
rows = (await sql.unsafe(`SELECT id, alias, category, ancestor, attributes, links, meta, "order", ancestors, descendants
|
|
305
|
-
FROM ${ident}."Schema" WHERE partition = $1 ORDER BY category, "order"`, [partition]));
|
|
306
|
-
}
|
|
307
|
-
catch (e) {
|
|
308
|
-
// 42P01 undefined_table / 3F000 invalid_schema_name — типовая ошибка: передали БАЗОВОЕ имя
|
|
309
|
-
// ('booking') вместо полного с версией ('v1.booking'). Подсказываем вместо сырой ошибки PG.
|
|
310
|
-
const code = e.code;
|
|
311
|
-
if (code !== '42P01' && code !== '3F000')
|
|
312
|
-
throw e;
|
|
313
|
-
// перечисляем ТОЛЬКО схемы letopis (те, где есть таблица Schema), а не все namespace БД
|
|
314
|
-
const found = (await sql.unsafe(`SELECT table_schema AS s FROM information_schema.tables
|
|
315
|
-
WHERE table_name = 'Schema' ORDER BY 1`));
|
|
316
|
-
throw new Error(`letopis: schema "${pgSchema}" has no "Schema" table — connect({ schema }) takes the FULL ` +
|
|
317
|
-
`PG-schema name WITH the engine version ("v1.booking"); the library adds no prefix. ` +
|
|
318
|
-
`letopis schemas here: ${found.map((r) => r.s).join(', ') || '(none)'}. ` +
|
|
319
|
-
`Create one with up({ schema, version }) or db/apply.mjs --schema=… --version=…`);
|
|
320
|
-
}
|
|
321
|
-
if (!rows.length) {
|
|
322
|
-
throw new Error(`letopis: schema "${pgSchema}" has no classes for partition "${partition}"`);
|
|
323
|
-
}
|
|
324
|
-
// END_BLOCK_LOAD_QUERY
|
|
325
|
-
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
326
|
-
const registry = new Registry();
|
|
327
|
-
for (const row of rows)
|
|
328
|
-
registry.add(buildDef(row, byId));
|
|
329
|
-
// START_BLOCK_RESERVED_WARN
|
|
330
|
-
// Имя, перехватываемое Proxy до резолва класса, делает шаг недостижимым ПОД ЭТИМ именем
|
|
331
|
-
// (второе имя класса, если оно свободно, работает). Предупреждаем, а НЕ бросаем: демо-сид
|
|
332
|
-
// содержит класс id "link", и throw уронил бы up() на штатной схеме.
|
|
333
|
-
// Один раз на (схема, класс) за процесс: loadRegistry зовётся на каждый connect и
|
|
334
|
-
// reloadSchema — иначе штатный прогон утонул бы в повторах.
|
|
335
|
-
for (const def of registry.all) {
|
|
336
|
-
const clash = reservedNamesOf(def);
|
|
337
|
-
if (!clash.length)
|
|
338
|
-
continue;
|
|
339
|
-
const seen = `${pgSchema}:${def.id}`;
|
|
340
|
-
if (warnedReserved.has(seen))
|
|
341
|
-
continue;
|
|
342
|
-
warnedReserved.add(seen);
|
|
343
|
-
const free = [def.id, def.alias].filter((n) => !clash.includes(n));
|
|
344
|
-
console.warn(`letopis: class name ${clash.map((n) => `"${n}"`).join(' / ')} is reserved by the chain API — ` +
|
|
345
|
-
(free.length
|
|
346
|
-
? `use ${free.map((n) => `"${n}"`).join(' / ')} for chain steps`
|
|
347
|
-
: 'this class is unreachable as a chain step; rename it'));
|
|
348
|
-
}
|
|
349
|
-
// END_BLOCK_RESERVED_WARN
|
|
350
|
-
return registry;
|
|
351
|
-
}
|
package/dist/sessions.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/** Минимальный срез Redis-клиента (ioredis подходит как есть). */
|
|
2
|
-
export interface SessionStore {
|
|
3
|
-
set(key: string, value: string, mode: 'EX', ttlSec: number): Promise<unknown>;
|
|
4
|
-
get(key: string): Promise<string | null>;
|
|
5
|
-
del(...keys: string[]): Promise<unknown>;
|
|
6
|
-
sadd(key: string, ...members: string[]): Promise<unknown>;
|
|
7
|
-
srem(key: string, ...members: string[]): Promise<unknown>;
|
|
8
|
-
smembers(key: string): Promise<string[]>;
|
|
9
|
-
expire(key: string, ttlSec: number): Promise<unknown>;
|
|
10
|
-
}
|
|
11
|
-
export interface Session {
|
|
12
|
-
account: string;
|
|
13
|
-
meta: Record<string, unknown>;
|
|
14
|
-
created: string;
|
|
15
|
-
}
|
|
16
|
-
export interface Sessions {
|
|
17
|
-
/** Новая сессия → токен (отдаётся один раз; в Redis — только его sha256). Default ttl 7 суток. */
|
|
18
|
-
start(account: string | {
|
|
19
|
-
id: string;
|
|
20
|
-
}, opts?: {
|
|
21
|
-
ttlSec?: number;
|
|
22
|
-
meta?: Record<string, unknown>;
|
|
23
|
-
}): Promise<string>;
|
|
24
|
-
/** null — нет/просрочена/отозвана. */
|
|
25
|
-
check(token: string): Promise<Session | null>;
|
|
26
|
-
revoke(token: string): Promise<boolean>;
|
|
27
|
-
/** Гасит все сессии аккаунта, возвращает сколько было в индексе. */
|
|
28
|
-
revokeAll(account: string | {
|
|
29
|
-
id: string;
|
|
30
|
-
}): Promise<number>;
|
|
31
|
-
}
|
|
32
|
-
export declare function makeSessions(store: SessionStore): Sessions;
|
package/dist/sessions.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Сессии во внешнем KV (Redis): db.auth.sessions(store).
|
|
3
|
-
* Клиент НЕ входит в зависимости — инжектируется пользователем (интерфейс ioredis-совместим).
|
|
4
|
-
* В store лежит только sha256-хэш токена: дамп Redis не раскрывает действующие токены.
|
|
5
|
-
*/
|
|
6
|
-
//
|
|
7
|
-
// FILE: lib/src/sessions.ts
|
|
8
|
-
// VERSION: 1.0.0
|
|
9
|
-
// START_MODULE_CONTRACT
|
|
10
|
-
// PURPOSE: Жизненный цикл сессий поверх инжектируемого Redis-совместимого хранилища (start/check/revoke/revokeAll).
|
|
11
|
-
// SCOPE: интерфейсы SessionStore/Session/Sessions + фабрика makeSessions с операциями старта, проверки и отзыва сессий; токены хранятся как sha256 под ключами sess:* / sess:acc:* с TTL.
|
|
12
|
-
// DEPENDS: none
|
|
13
|
-
// LINKS: M-SESSIONS, V-M-SESSIONS
|
|
14
|
-
// ROLE: RUNTIME
|
|
15
|
-
// MAP_MODE: EXPORTS
|
|
16
|
-
// END_MODULE_CONTRACT
|
|
17
|
-
//
|
|
18
|
-
// START_MODULE_MAP
|
|
19
|
-
// SessionStore - минимальный срез Redis-клиента (set/get/del/sadd/srem/smembers/expire).
|
|
20
|
-
// Session - полезная нагрузка сессии: account, meta, created.
|
|
21
|
-
// Sessions - контракт сервиса сессий: start/check/revoke/revokeAll.
|
|
22
|
-
// sha256 - (локальный) hex-хэш строки по sha256.
|
|
23
|
-
// accId - (локальный) нормализует account: string | { id } → id.
|
|
24
|
-
// DEFAULT_TTL - (локальный) TTL по умолчанию: 7 суток в секундах.
|
|
25
|
-
// makeSessions - фабрика: связывает store и возвращает объект Sessions.
|
|
26
|
-
// END_MODULE_MAP
|
|
27
|
-
//
|
|
28
|
-
// START_CHANGE_SUMMARY
|
|
29
|
-
// LAST_CHANGE: [v1.0.0 - Documented existing module: reverse-engineered contract + markup]
|
|
30
|
-
// END_CHANGE_SUMMARY
|
|
31
|
-
import { createHash, randomBytes } from 'node:crypto';
|
|
32
|
-
const sha256 = (s) => createHash('sha256').update(s).digest('hex');
|
|
33
|
-
const accId = (a) => (typeof a === 'object' ? a.id : a);
|
|
34
|
-
const DEFAULT_TTL = 7 * 24 * 3600;
|
|
35
|
-
// START_CONTRACT: makeSessions
|
|
36
|
-
// PURPOSE: Фабрика сервиса сессий: связывает инжектируемый store и возвращает объект Sessions.
|
|
37
|
-
// INPUTS: { store: SessionStore - Redis-совместимое хранилище (инжектируется пользователем) }
|
|
38
|
-
// OUTPUTS: { Sessions - объект с методами start/check/revoke/revokeAll }
|
|
39
|
-
// SIDE_EFFECTS: none при создании; возвращённые методы читают/пишут store (ключи sess:* / sess:acc:*)
|
|
40
|
-
// LINKS: M-SESSIONS, V-M-SESSIONS
|
|
41
|
-
// END_CONTRACT: makeSessions
|
|
42
|
-
export function makeSessions(store) {
|
|
43
|
-
const kTok = (h) => `sess:${h}`;
|
|
44
|
-
// индекс аккаунта без TTL: протухшие хэши безвредны, полная чистка — revokeAll
|
|
45
|
-
const kAcc = (id) => `sess:acc:${id}`;
|
|
46
|
-
return {
|
|
47
|
-
// START_CONTRACT: start
|
|
48
|
-
// PURPOSE: Создать новую сессию и вернуть одноразовый токен (в store — только его sha256).
|
|
49
|
-
// INPUTS: { account: string | { id: string } - аккаунт, opts?: { ttlSec?: number; meta?: Record<string, unknown> } - TTL (default 7 суток) и метаданные }
|
|
50
|
-
// OUTPUTS: { Promise<string> - секретный токен (32 байта hex, отдаётся один раз) }
|
|
51
|
-
// SIDE_EFFECTS: store.set(sess:<h>, payload, EX ttl) + store.sadd(sess:acc:<id>, h)
|
|
52
|
-
// LINKS: M-SESSIONS, V-M-SESSIONS
|
|
53
|
-
// END_CONTRACT: start
|
|
54
|
-
async start(account, opts = {}) {
|
|
55
|
-
// START_BLOCK_START_MINT_AND_PERSIST
|
|
56
|
-
const id = accId(account);
|
|
57
|
-
const token = randomBytes(32).toString('hex');
|
|
58
|
-
const h = sha256(token);
|
|
59
|
-
const ttl = opts.ttlSec ?? DEFAULT_TTL;
|
|
60
|
-
const payload = { account: id, meta: opts.meta ?? {}, created: new Date().toISOString() };
|
|
61
|
-
await store.set(kTok(h), JSON.stringify(payload), 'EX', ttl);
|
|
62
|
-
await store.sadd(kAcc(id), h);
|
|
63
|
-
// END_BLOCK_START_MINT_AND_PERSIST
|
|
64
|
-
return token;
|
|
65
|
-
},
|
|
66
|
-
// START_CONTRACT: check
|
|
67
|
-
// PURPOSE: Проверить токен и вернуть данные сессии либо null.
|
|
68
|
-
// INPUTS: { token: string - секретный токен }
|
|
69
|
-
// OUTPUTS: { Promise<Session | null> - null = нет / просрочена / отозвана }
|
|
70
|
-
// SIDE_EFFECTS: store.get(sess:<sha256(token)>) (только чтение)
|
|
71
|
-
// LINKS: M-SESSIONS, V-M-SESSIONS
|
|
72
|
-
// END_CONTRACT: check
|
|
73
|
-
async check(token) {
|
|
74
|
-
const raw = await store.get(kTok(sha256(token)));
|
|
75
|
-
return raw ? JSON.parse(raw) : null;
|
|
76
|
-
},
|
|
77
|
-
// START_CONTRACT: revoke
|
|
78
|
-
// PURPOSE: Отозвать одну сессию по токену.
|
|
79
|
-
// INPUTS: { token: string - секретный токен }
|
|
80
|
-
// OUTPUTS: { Promise<boolean> - true если сессия была и удалена, false если её нет }
|
|
81
|
-
// SIDE_EFFECTS: store.del(sess:<h>) + store.srem(sess:acc:<account>, h)
|
|
82
|
-
// LINKS: M-SESSIONS, V-M-SESSIONS
|
|
83
|
-
// END_CONTRACT: revoke
|
|
84
|
-
async revoke(token) {
|
|
85
|
-
// START_BLOCK_REVOKE_LOOKUP_AND_DELETE
|
|
86
|
-
const h = sha256(token);
|
|
87
|
-
const raw = await store.get(kTok(h));
|
|
88
|
-
if (!raw)
|
|
89
|
-
return false;
|
|
90
|
-
const { account } = JSON.parse(raw);
|
|
91
|
-
await store.del(kTok(h));
|
|
92
|
-
await store.srem(kAcc(account), h);
|
|
93
|
-
return true;
|
|
94
|
-
// END_BLOCK_REVOKE_LOOKUP_AND_DELETE
|
|
95
|
-
},
|
|
96
|
-
// START_CONTRACT: revokeAll
|
|
97
|
-
// PURPOSE: Погасить все сессии аккаунта и очистить его индекс.
|
|
98
|
-
// INPUTS: { account: string | { id: string } - аккаунт }
|
|
99
|
-
// OUTPUTS: { Promise<number> - сколько хэшей было в индексе аккаунта }
|
|
100
|
-
// SIDE_EFFECTS: store.del(все sess:<h>) + store.del(sess:acc:<id>)
|
|
101
|
-
// LINKS: M-SESSIONS, V-M-SESSIONS
|
|
102
|
-
// END_CONTRACT: revokeAll
|
|
103
|
-
async revokeAll(account) {
|
|
104
|
-
// START_BLOCK_REVOKE_ALL_PURGE
|
|
105
|
-
const id = accId(account);
|
|
106
|
-
const hs = await store.smembers(kAcc(id));
|
|
107
|
-
if (hs.length)
|
|
108
|
-
await store.del(...hs.map(kTok));
|
|
109
|
-
await store.del(kAcc(id));
|
|
110
|
-
return hs.length;
|
|
111
|
-
// END_BLOCK_REVOKE_ALL_PURGE
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
}
|
package/dist/tables.d.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Фасады служебных таблиц auth/ACL: Account, Credential, Resource, Rule.
|
|
3
|
-
* Обычные таблицы (UPDATE/DELETE разрешены) — цепочки/версии тут не применяются.
|
|
4
|
-
* Доступны как db.accounts / db.credentials / db.resources / db.rules (и в транзакции).
|
|
5
|
-
*/
|
|
6
|
-
import type { Ctx } from './sql.js';
|
|
7
|
-
import type { Account, Credential, Resource, Rule } from './types.js';
|
|
8
|
-
export interface AccountsApi {
|
|
9
|
-
/** Фильтры: id, enabled, category (строка — вхождение в categories). */
|
|
10
|
-
find(f?: {
|
|
11
|
-
id?: string;
|
|
12
|
-
enabled?: boolean;
|
|
13
|
-
category?: string;
|
|
14
|
-
}): Promise<Account[]>;
|
|
15
|
-
get(id: string): Promise<Account | null>;
|
|
16
|
-
/** insert (без id) | update по id. jsonb-поля мержатся на уровне значения целиком. */
|
|
17
|
-
set(a: Partial<Account>): Promise<Account>;
|
|
18
|
-
/** Физический DELETE; Credential снесётся FK-каскадом. */
|
|
19
|
-
delete(id: string): Promise<boolean>;
|
|
20
|
-
/**
|
|
21
|
-
* Полный физический офбординг тенанта (необратимо): все Entity (account|owner=id) + сам Account
|
|
22
|
-
* (Credential — FK-каскад), через серверную purge_account(). Предохранители: только Owner/System-вызов;
|
|
23
|
-
* нельзя снести последний enabled Owner (лок-аут) и свой аккаунт. → true если Account снесён.
|
|
24
|
-
*/
|
|
25
|
-
purge(id: string): Promise<boolean>;
|
|
26
|
-
}
|
|
27
|
-
export interface CredentialsApi {
|
|
28
|
-
/** deleted IS NULL по умолчанию; withDeleted: true — включая удалённые. */
|
|
29
|
-
find(f?: {
|
|
30
|
-
id?: string;
|
|
31
|
-
account?: string;
|
|
32
|
-
category?: string;
|
|
33
|
-
identifier?: string;
|
|
34
|
-
confirmed?: boolean;
|
|
35
|
-
withDeleted?: boolean;
|
|
36
|
-
}): Promise<Credential[]>;
|
|
37
|
-
/** upsert по UNIQUE (account, category, identifier); повторный set воскрешает (deleted → NULL). */
|
|
38
|
-
set(c: {
|
|
39
|
-
account: string;
|
|
40
|
-
category: string;
|
|
41
|
-
identifier: string;
|
|
42
|
-
meta?: Record<string, unknown>;
|
|
43
|
-
confirmed?: boolean;
|
|
44
|
-
}): Promise<Credential>;
|
|
45
|
-
/** Мягкое удаление: deleted = now(). */
|
|
46
|
-
delete(id: string): Promise<boolean>;
|
|
47
|
-
}
|
|
48
|
-
export interface ResourcesApi {
|
|
49
|
-
find(f?: {
|
|
50
|
-
category?: string;
|
|
51
|
-
}): Promise<Resource[]>;
|
|
52
|
-
get(alias: string): Promise<Resource | null>;
|
|
53
|
-
/** upsert по alias. */
|
|
54
|
-
set(r: {
|
|
55
|
-
alias: string;
|
|
56
|
-
category: string;
|
|
57
|
-
pattern?: Record<string, unknown> | null;
|
|
58
|
-
meta?: Record<string, unknown> | null;
|
|
59
|
-
}): Promise<Resource>;
|
|
60
|
-
/** Физический DELETE; Rule на этот ресурс снесутся FK-каскадом. */
|
|
61
|
-
delete(alias: string): Promise<boolean>;
|
|
62
|
-
}
|
|
63
|
-
export interface RulesApi {
|
|
64
|
-
find(f?: {
|
|
65
|
-
account?: string;
|
|
66
|
-
resource?: string;
|
|
67
|
-
permission?: string;
|
|
68
|
-
enabled?: boolean;
|
|
69
|
-
}): Promise<Rule[]>;
|
|
70
|
-
/** upsert по PK (account, resource). Оба конца — Resource.alias (FK). */
|
|
71
|
-
set(r: {
|
|
72
|
-
account: string;
|
|
73
|
-
resource: string;
|
|
74
|
-
permission: string;
|
|
75
|
-
weight?: number | null;
|
|
76
|
-
meta?: Record<string, unknown> | null;
|
|
77
|
-
enabled?: boolean;
|
|
78
|
-
}): Promise<Rule>;
|
|
79
|
-
delete(account: string, resource: string): Promise<boolean>;
|
|
80
|
-
}
|
|
81
|
-
export interface SchemaApi {
|
|
82
|
-
/**
|
|
83
|
-
* Upsert определения класса в таблицу Schema (ON CONFLICT (partition,id) DO UPDATE) + reloadSchema().
|
|
84
|
-
* Правка «простым SQL», подхватывается сразу без реконнекта. attributes — DSL fastest-validator
|
|
85
|
-
* (валидирует либа при чтении реестра); ancestors/descendants считает триггер schema_lineage.
|
|
86
|
-
*/
|
|
87
|
-
define(def: {
|
|
88
|
-
id: string;
|
|
89
|
-
alias: string;
|
|
90
|
-
category: 'HUB' | 'LINK';
|
|
91
|
-
ancestor?: string | null;
|
|
92
|
-
attributes?: Record<string, unknown>;
|
|
93
|
-
meta?: Record<string, unknown>;
|
|
94
|
-
links?: unknown[];
|
|
95
|
-
order?: number;
|
|
96
|
-
}): Promise<void>;
|
|
97
|
-
}
|
|
98
|
-
export interface Tables {
|
|
99
|
-
accounts: AccountsApi;
|
|
100
|
-
credentials: CredentialsApi;
|
|
101
|
-
resources: ResourcesApi;
|
|
102
|
-
rules: RulesApi;
|
|
103
|
-
schema: SchemaApi;
|
|
104
|
-
}
|
|
105
|
-
export declare function makeTables(ctx: Ctx): Tables;
|