letopis 0.13.0 → 0.18.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/CHANGELOG.md +203 -3
- package/README.md +825 -602
- package/dist/acl.js +62 -0
- package/dist/auth.d.ts +7 -0
- package/dist/auth.js +126 -0
- package/dist/chain.d.ts +44 -16
- package/dist/chain.js +309 -26
- package/dist/index.d.ts +7 -0
- package/dist/index.js +38 -0
- package/dist/ops.js +45 -0
- package/dist/schema.js +169 -4
- package/dist/sessions.js +66 -0
- package/dist/sql.d.ts +23 -3
- package/dist/sql.js +161 -25
- package/dist/tables.js +37 -0
- package/dist/tx.js +36 -0
- package/dist/types.d.ts +40 -4
- package/dist/types.js +46 -0
- package/dist/up.js +83 -0
- package/dist/uuid.d.ts +6 -0
- package/dist/uuid.js +68 -0
- package/dist/write.d.ts +12 -5
- package/dist/write.js +359 -95
- package/docker/Dockerfile +23 -0
- package/docker/start.sh +14 -0
- package/package.json +1 -1
- package/sql/ddl.sql +124 -12
- package/sql/seed.auth.sql +27 -0
- package/sql/seed.booking.sql +64 -17
package/dist/ops.js
CHANGED
|
@@ -2,6 +2,44 @@
|
|
|
2
2
|
* Операторы фильтров. Symbol-tagged объекты — не пересекаются с данными.
|
|
3
3
|
* Касты по типам полей делает sql.ts на основе Schema.attributes.
|
|
4
4
|
*/
|
|
5
|
+
//
|
|
6
|
+
// FILE: lib/src/ops.ts
|
|
7
|
+
// VERSION: 1.0.0
|
|
8
|
+
// START_MODULE_CONTRACT
|
|
9
|
+
// PURPOSE: Билдеры операторов фильтра, создающие узлы Op для DSL фильтров.
|
|
10
|
+
// SCOPE: type guard isOp + конструкторы операторов сравнения / списков / строк / массивов / наличия / логики (ne…or) поверх локальной фабрики op().
|
|
11
|
+
// DEPENDS: M-TYPES
|
|
12
|
+
// LINKS: M-OPS, V-M-OPS
|
|
13
|
+
// ROLE: RUNTIME
|
|
14
|
+
// MAP_MODE: EXPORTS
|
|
15
|
+
// END_MODULE_CONTRACT
|
|
16
|
+
//
|
|
17
|
+
// START_MODULE_MAP
|
|
18
|
+
// op - (локальный) фабрика узла Op: { [OP]: name, args }.
|
|
19
|
+
// isOp - type guard: значение является узлом Op (содержит символ OP).
|
|
20
|
+
// ne - ≠ (IS DISTINCT FROM).
|
|
21
|
+
// gt - >.
|
|
22
|
+
// gte - ≥.
|
|
23
|
+
// lt - <.
|
|
24
|
+
// lte - ≤.
|
|
25
|
+
// between - a ≤ x ≤ b.
|
|
26
|
+
// inList - значение из списка (IN).
|
|
27
|
+
// like - LIKE (шаблон с % и _).
|
|
28
|
+
// ilike - ILIKE (без регистра).
|
|
29
|
+
// starts - начинается с (LIKE s%).
|
|
30
|
+
// ends - заканчивается на (LIKE %s).
|
|
31
|
+
// has - массив-поле содержит значение.
|
|
32
|
+
// hasAny - массив-поле содержит хотя бы одно из.
|
|
33
|
+
// hasAll - массив-поле содержит все.
|
|
34
|
+
// exists - поле присутствует (true) / отсутствует (false) в data.
|
|
35
|
+
// isNull - поле NULL или отсутствует.
|
|
36
|
+
// not - НЕ-условие по полю (скаляр → «не равно»).
|
|
37
|
+
// or - ИЛИ на уровне фильтра шага.
|
|
38
|
+
// END_MODULE_MAP
|
|
39
|
+
//
|
|
40
|
+
// START_CHANGE_SUMMARY
|
|
41
|
+
// LAST_CHANGE: [v1.0.0 - Documented existing module: reverse-engineered contract + markup]
|
|
42
|
+
// END_CHANGE_SUMMARY
|
|
5
43
|
import { OP } from './types.js';
|
|
6
44
|
const op = (name, ...args) => ({ [OP]: name, args });
|
|
7
45
|
export const isOp = (v) => typeof v === 'object' && v !== null && OP in v;
|
|
@@ -39,5 +77,12 @@ export const exists = (yes = true) => op('exists', yes);
|
|
|
39
77
|
export const isNull = () => op('isNull');
|
|
40
78
|
/** НЕ-условие по полю: not(ilike('%тест%')); скаляр → «не равно» */
|
|
41
79
|
export const not = (v) => op('not', v);
|
|
80
|
+
// START_CONTRACT: or
|
|
81
|
+
// PURPOSE: Логическое ИЛИ на уровне фильтра шага — объединяет несколько фильтров-объектов в один узел Op.
|
|
82
|
+
// INPUTS: { filters: Record<string, unknown>[] - варьируемое число фильтров-объектов (каждый — свой набор условий) }
|
|
83
|
+
// OUTPUTS: { Op - узел { [OP]: 'or', args: [filters] } }
|
|
84
|
+
// SIDE_EFFECTS: none (чистая фабрика через op())
|
|
85
|
+
// LINKS: M-OPS, V-M-OPS
|
|
86
|
+
// END_CONTRACT: or
|
|
42
87
|
/** ИЛИ на уровне фильтра шага: db.Запись(or({status:'created'}, {status:'confirmed'})) */
|
|
43
88
|
export const or = (...filters) => op('or', filters);
|
package/dist/schema.js
CHANGED
|
@@ -2,9 +2,32 @@
|
|
|
2
2
|
* Загрузка таблицы Schema → реестр классов:
|
|
3
3
|
* - резолв имени по id и alias;
|
|
4
4
|
* - цепочка ancestors (walk по ancestor, если в БД не заполнена);
|
|
5
|
+
* - attributes НАСЛЕДУЮТСЯ по цепочке (потомок поверх предка; поле = замена правила
|
|
6
|
+
* целиком) — включая правило id (генерация v4/v5/v7); links НЕ наследуются;
|
|
5
7
|
* - скомпилированный fastest-validator per class;
|
|
6
8
|
* - карта типов полей (для SQL-кастов фильтров).
|
|
7
9
|
*/
|
|
10
|
+
//
|
|
11
|
+
// FILE: lib/src/schema.ts
|
|
12
|
+
// VERSION: 1.0.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.0.0 - Documented existing module: reverse-engineered contract + markup]
|
|
30
|
+
// END_CHANGE_SUMMARY
|
|
8
31
|
import { createRequire } from 'node:module';
|
|
9
32
|
const Validator = createRequire(import.meta.url)('fastest-validator');
|
|
10
33
|
const v = new Validator({ useNewCustomCheckerFunction: true });
|
|
@@ -20,6 +43,13 @@ export class Registry {
|
|
|
20
43
|
find(name) {
|
|
21
44
|
return this.byName.get(name);
|
|
22
45
|
}
|
|
46
|
+
// START_CONTRACT: Registry.resolve
|
|
47
|
+
// PURPOSE: Вернуть класс по id или alias, иначе бросить ошибку со списком известных.
|
|
48
|
+
// INPUTS: { name: string - id или alias класса }
|
|
49
|
+
// OUTPUTS: { ClassDef - найденный класс }
|
|
50
|
+
// SIDE_EFFECTS: none
|
|
51
|
+
// LINKS: M-SCHEMA, V-M-SCHEMA
|
|
52
|
+
// END_CONTRACT: Registry.resolve
|
|
23
53
|
/** Класс по id или alias; иначе понятная ошибка со списком. */
|
|
24
54
|
resolve(name) {
|
|
25
55
|
const def = this.byName.get(name);
|
|
@@ -33,6 +63,13 @@ export class Registry {
|
|
|
33
63
|
return this.byName.has(name);
|
|
34
64
|
}
|
|
35
65
|
}
|
|
66
|
+
// START_CONTRACT: fieldTypeOf
|
|
67
|
+
// PURPOSE: Свести attribute-спеку fastest-validator (строка/массив/объект) к FieldType.
|
|
68
|
+
// INPUTS: { attr: unknown - правило поля из attributes }
|
|
69
|
+
// OUTPUTS: { FieldType - вид поля (number/date/boolean/string/array/record/object/any) }
|
|
70
|
+
// SIDE_EFFECTS: none (рекурсивно по вложенным props/value)
|
|
71
|
+
// LINKS: M-SCHEMA, V-M-SCHEMA, type-FieldType
|
|
72
|
+
// END_CONTRACT: fieldTypeOf
|
|
36
73
|
/** Тип поля из fastest-validator DSL — для каста в SQL. */
|
|
37
74
|
export function fieldTypeOf(attr) {
|
|
38
75
|
if (typeof attr === 'string') {
|
|
@@ -88,7 +125,84 @@ export function fieldTypeOf(attr) {
|
|
|
88
125
|
}
|
|
89
126
|
return { kind: 'any' };
|
|
90
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Элемент Schema.links: объект-конец v2 (jsonb) либо legacy-строка 'Org'
|
|
130
|
+
* (включая JSON-текст в text[] у старых схем). Возвращает [конец, isV2].
|
|
131
|
+
*/
|
|
132
|
+
// START_CONTRACT: parseEnd
|
|
133
|
+
// PURPOSE: Разобрать один элемент Schema.links в LinkEnd (объект-конец v2 или legacy-строка).
|
|
134
|
+
// INPUTS: { cls: string - id класса; raw: string|object - сырой конец из jsonb/text[] }
|
|
135
|
+
// OUTPUTS: { [LinkEnd, boolean] - конец и признак v2-формы (для strictEnds) }
|
|
136
|
+
// SIDE_EFFECTS: none
|
|
137
|
+
// ERRORS: malformed link end (bad JSON); link end without classes
|
|
138
|
+
// LINKS: M-SCHEMA, V-M-SCHEMA
|
|
139
|
+
// END_CONTRACT: parseEnd
|
|
140
|
+
function parseEnd(cls, raw) {
|
|
141
|
+
let o;
|
|
142
|
+
if (typeof raw === 'string') {
|
|
143
|
+
if (!raw.trimStart().startsWith('{'))
|
|
144
|
+
return [{ classes: [raw] }, false];
|
|
145
|
+
try {
|
|
146
|
+
o = JSON.parse(raw);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
throw new Error(`letopis: class "${cls}" has malformed link end (bad JSON): ${raw}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
o = raw;
|
|
154
|
+
}
|
|
155
|
+
const classes = o.classes ?? (o.class ? [o.class] : []);
|
|
156
|
+
if (!classes.length || classes.some((c) => typeof c !== 'string' || !c)) {
|
|
157
|
+
throw new Error(`letopis: class "${cls}" has link end without classes: ${JSON.stringify(raw)}`);
|
|
158
|
+
}
|
|
159
|
+
return [{ classes, optional: o.optional === true, cardinality: o.cardinality }, true];
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* attributes.id: правило валидации + (объектом) спецификация генерации.
|
|
163
|
+
* generate/from — ключи letopis, вырезаются из правила перед компиляцией валидатора.
|
|
164
|
+
*/
|
|
165
|
+
// START_CONTRACT: parseIdGen
|
|
166
|
+
// PURPOSE: Отделить спецификацию генерации id (generate/from) от правила валидации attributes.id.
|
|
167
|
+
// INPUTS: { cls: string - id класса; attributes: object - слитые attributes класса }
|
|
168
|
+
// OUTPUTS: { { idGen: IdGen; attrs: object } - версия генерации id и очищенное правило }
|
|
169
|
+
// SIDE_EFFECTS: none
|
|
170
|
+
// ERRORS: generate must be 4|5|7; generate:5 needs "from"; from is for generate:5 only
|
|
171
|
+
// LINKS: M-SCHEMA, V-M-SCHEMA
|
|
172
|
+
// END_CONTRACT: parseIdGen
|
|
173
|
+
function parseIdGen(cls, attributes) {
|
|
174
|
+
const raw = attributes.id;
|
|
175
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
176
|
+
return { idGen: { version: 4 }, attrs: attributes };
|
|
177
|
+
}
|
|
178
|
+
const { generate, from, ...rule } = raw;
|
|
179
|
+
const attrs = { ...attributes, id: rule };
|
|
180
|
+
if (generate === undefined)
|
|
181
|
+
return { idGen: { version: 4 }, attrs };
|
|
182
|
+
if (generate !== 4 && generate !== 5 && generate !== 7) {
|
|
183
|
+
throw new Error(`letopis: class "${cls}" — attributes.id.generate must be 4 | 5 | 7, got ${JSON.stringify(generate)}`);
|
|
184
|
+
}
|
|
185
|
+
if (generate === 5) {
|
|
186
|
+
if (!Array.isArray(from) || !from.length || from.some((f) => typeof f !== 'string' || !f)) {
|
|
187
|
+
throw new Error(`letopis: class "${cls}" — id generate:5 needs "from": non-empty string[] (link ends / data fields)`);
|
|
188
|
+
}
|
|
189
|
+
return { idGen: { version: 5, from: from }, attrs };
|
|
190
|
+
}
|
|
191
|
+
if (from !== undefined) {
|
|
192
|
+
throw new Error(`letopis: class "${cls}" — attributes.id.from is for generate:5 only`);
|
|
193
|
+
}
|
|
194
|
+
return { idGen: { version: generate }, attrs };
|
|
195
|
+
}
|
|
196
|
+
// START_CONTRACT: buildDef
|
|
197
|
+
// PURPOSE: Собрать ClassDef из строки Schema — цепочка ancestors, наследование attributes, id-gen, компиляция check, типы полей, link-ends.
|
|
198
|
+
// INPUTS: { row: SchemaRow - строка класса; byId: Map<string,SchemaRow> - все классы партиции }
|
|
199
|
+
// OUTPUTS: { ClassDef - полностью разрешённое определение класса }
|
|
200
|
+
// SIDE_EFFECTS: none (компилирует fastest-validator)
|
|
201
|
+
// ERRORS: id v5 can't depend on optional end; id "from" source is neither end nor field
|
|
202
|
+
// LINKS: M-SCHEMA, V-M-SCHEMA, type-ClassDef
|
|
203
|
+
// END_CONTRACT: buildDef
|
|
91
204
|
function buildDef(row, byId) {
|
|
205
|
+
// START_BLOCK_INHERIT_ANCESTORS
|
|
92
206
|
// цепочка наследования: из БД или walk по ancestor
|
|
93
207
|
let ancestors = row.ancestors ?? [];
|
|
94
208
|
if (!ancestors.length) {
|
|
@@ -101,18 +215,57 @@ function buildDef(row, byId) {
|
|
|
101
215
|
cur = byId.get(cur)?.ancestor ?? null;
|
|
102
216
|
}
|
|
103
217
|
}
|
|
218
|
+
// attributes НАСЛЕДУЮТСЯ по цепочке ancestor: потомок ПОВЕРХ предка, переопределение
|
|
219
|
+
// поля — замена правила ЦЕЛИКОМ (не слияние объекта-правила). links не наследуются.
|
|
220
|
+
const merged = {};
|
|
221
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
222
|
+
Object.assign(merged, byId.get(ancestors[i])?.attributes ?? {});
|
|
223
|
+
}
|
|
224
|
+
// END_BLOCK_INHERIT_ANCESTORS
|
|
225
|
+
// attributes.id: спецификация генерации (generate/from) отделяется от правила валидации
|
|
226
|
+
const { idGen, attrs } = parseIdGen(row.id, merged);
|
|
104
227
|
// fastest-validator: валидируем {id, ...data}; СТРОГО — лишние поля запрещены
|
|
105
|
-
const compiled = v.compile({ ...
|
|
228
|
+
const compiled = v.compile({ ...attrs, $$strict: true });
|
|
106
229
|
const check = (data) => {
|
|
107
230
|
const res = compiled(data);
|
|
108
231
|
return res === true ? true : res;
|
|
109
232
|
};
|
|
110
233
|
const fieldTypes = new Map();
|
|
111
|
-
for (const [field, attr] of Object.entries(
|
|
234
|
+
for (const [field, attr] of Object.entries(attrs)) {
|
|
112
235
|
if (field.startsWith('$$'))
|
|
113
236
|
continue;
|
|
114
237
|
fieldTypes.set(field, fieldTypeOf(attr));
|
|
115
238
|
}
|
|
239
|
+
let strictEnds = false;
|
|
240
|
+
// jsonb-массив; text[] у legacy-схем даёт string[]; jsonb-строка "[…]" (двойная
|
|
241
|
+
// сериализация внешних писателей) — распарсить
|
|
242
|
+
let rawLinks = row.links ?? [];
|
|
243
|
+
if (typeof rawLinks === 'string')
|
|
244
|
+
rawLinks = JSON.parse(rawLinks);
|
|
245
|
+
const ends = rawLinks.map((raw) => {
|
|
246
|
+
const [end, v2] = parseEnd(row.id, raw);
|
|
247
|
+
if (v2)
|
|
248
|
+
strictEnds = true;
|
|
249
|
+
return end;
|
|
250
|
+
});
|
|
251
|
+
// START_BLOCK_ID_V5_VALIDATE
|
|
252
|
+
// id v5: каждый источник from — обязательный конец links (класс или полное имя союза
|
|
253
|
+
// 'Service|Complex') ЛИБО поле data
|
|
254
|
+
if (idGen.version === 5) {
|
|
255
|
+
for (const f of idGen.from) {
|
|
256
|
+
const end = ends.find((e) => e.classes.includes(f) || e.classes.join('|') === f);
|
|
257
|
+
if (end) {
|
|
258
|
+
if (end.optional) {
|
|
259
|
+
throw new Error(`letopis: class "${row.id}" — id (uuid v5) can't depend on optional end "${f}"`);
|
|
260
|
+
}
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (f !== 'id' && f in attrs)
|
|
264
|
+
continue;
|
|
265
|
+
throw new Error(`letopis: class "${row.id}" — id "from" source "${f}" is neither a link end nor a data field`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// END_BLOCK_ID_V5_VALIDATE
|
|
116
269
|
return {
|
|
117
270
|
id: row.id,
|
|
118
271
|
alias: row.alias,
|
|
@@ -120,8 +273,10 @@ function buildDef(row, byId) {
|
|
|
120
273
|
ancestor: row.ancestor,
|
|
121
274
|
ancestors,
|
|
122
275
|
descendants: row.descendants ?? [], // считает триггер schema_lineage
|
|
123
|
-
attributes:
|
|
124
|
-
links:
|
|
276
|
+
attributes: merged,
|
|
277
|
+
links: ends,
|
|
278
|
+
strictEnds,
|
|
279
|
+
idGen,
|
|
125
280
|
meta: row.meta ?? {},
|
|
126
281
|
abstract: row.meta?.abstract === true || row.meta?.abstract === 'true',
|
|
127
282
|
order: row.order,
|
|
@@ -129,13 +284,23 @@ function buildDef(row, byId) {
|
|
|
129
284
|
fieldTypes,
|
|
130
285
|
};
|
|
131
286
|
}
|
|
287
|
+
// START_CONTRACT: loadRegistry
|
|
288
|
+
// PURPOSE: Прочитать классы партиции из таблицы Schema и построить Registry со скомпилированными check.
|
|
289
|
+
// INPUTS: { sql: postgres.Sql; pgSchema: string; partition: string }
|
|
290
|
+
// OUTPUTS: { Promise<Registry> - реестр всех классов партиции }
|
|
291
|
+
// SIDE_EFFECTS: SELECT из "<pgSchema>"."Schema"
|
|
292
|
+
// ERRORS: schema has no classes for partition
|
|
293
|
+
// LINKS: M-SCHEMA, V-M-SCHEMA, M-DDL
|
|
294
|
+
// END_CONTRACT: loadRegistry
|
|
132
295
|
export async function loadRegistry(sql, pgSchema, partition) {
|
|
296
|
+
// START_BLOCK_LOAD_QUERY
|
|
133
297
|
const ident = `"${pgSchema.replace(/"/g, '""')}"`;
|
|
134
298
|
const rows = (await sql.unsafe(`SELECT id, alias, category, ancestor, attributes, links, meta, "order", ancestors, descendants
|
|
135
299
|
FROM ${ident}."Schema" WHERE partition = $1 ORDER BY category, "order"`, [partition]));
|
|
136
300
|
if (!rows.length) {
|
|
137
301
|
throw new Error(`letopis: schema "${pgSchema}" has no classes for partition "${partition}"`);
|
|
138
302
|
}
|
|
303
|
+
// END_BLOCK_LOAD_QUERY
|
|
139
304
|
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
140
305
|
const registry = new Registry();
|
|
141
306
|
for (const row of rows)
|
package/dist/sessions.js
CHANGED
|
@@ -3,16 +3,56 @@
|
|
|
3
3
|
* Клиент НЕ входит в зависимости — инжектируется пользователем (интерфейс ioredis-совместим).
|
|
4
4
|
* В store лежит только sha256-хэш токена: дамп Redis не раскрывает действующие токены.
|
|
5
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
|
|
6
31
|
import { createHash, randomBytes } from 'node:crypto';
|
|
7
32
|
const sha256 = (s) => createHash('sha256').update(s).digest('hex');
|
|
8
33
|
const accId = (a) => (typeof a === 'object' ? a.id : a);
|
|
9
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
|
|
10
42
|
export function makeSessions(store) {
|
|
11
43
|
const kTok = (h) => `sess:${h}`;
|
|
12
44
|
// индекс аккаунта без TTL: протухшие хэши безвредны, полная чистка — revokeAll
|
|
13
45
|
const kAcc = (id) => `sess:acc:${id}`;
|
|
14
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
|
|
15
54
|
async start(account, opts = {}) {
|
|
55
|
+
// START_BLOCK_START_MINT_AND_PERSIST
|
|
16
56
|
const id = accId(account);
|
|
17
57
|
const token = randomBytes(32).toString('hex');
|
|
18
58
|
const h = sha256(token);
|
|
@@ -20,13 +60,29 @@ export function makeSessions(store) {
|
|
|
20
60
|
const payload = { account: id, meta: opts.meta ?? {}, created: new Date().toISOString() };
|
|
21
61
|
await store.set(kTok(h), JSON.stringify(payload), 'EX', ttl);
|
|
22
62
|
await store.sadd(kAcc(id), h);
|
|
63
|
+
// END_BLOCK_START_MINT_AND_PERSIST
|
|
23
64
|
return token;
|
|
24
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
|
|
25
73
|
async check(token) {
|
|
26
74
|
const raw = await store.get(kTok(sha256(token)));
|
|
27
75
|
return raw ? JSON.parse(raw) : null;
|
|
28
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
|
|
29
84
|
async revoke(token) {
|
|
85
|
+
// START_BLOCK_REVOKE_LOOKUP_AND_DELETE
|
|
30
86
|
const h = sha256(token);
|
|
31
87
|
const raw = await store.get(kTok(h));
|
|
32
88
|
if (!raw)
|
|
@@ -35,14 +91,24 @@ export function makeSessions(store) {
|
|
|
35
91
|
await store.del(kTok(h));
|
|
36
92
|
await store.srem(kAcc(account), h);
|
|
37
93
|
return true;
|
|
94
|
+
// END_BLOCK_REVOKE_LOOKUP_AND_DELETE
|
|
38
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
|
|
39
103
|
async revokeAll(account) {
|
|
104
|
+
// START_BLOCK_REVOKE_ALL_PURGE
|
|
40
105
|
const id = accId(account);
|
|
41
106
|
const hs = await store.smembers(kAcc(id));
|
|
42
107
|
if (hs.length)
|
|
43
108
|
await store.del(...hs.map(kTok));
|
|
44
109
|
await store.del(kAcc(id));
|
|
45
110
|
return hs.length;
|
|
111
|
+
// END_BLOCK_REVOKE_ALL_PURGE
|
|
46
112
|
},
|
|
47
113
|
};
|
|
48
114
|
}
|
package/dist/sql.d.ts
CHANGED
|
@@ -55,11 +55,25 @@ export declare const retryDelay: (attempt: number) => Promise<void>;
|
|
|
55
55
|
* Служебные запросы (loadRegistry, tables, listen) через неё не идут.
|
|
56
56
|
*/
|
|
57
57
|
export declare function runQuery<T>(ctx: Ctx, text: string, params: unknown[], mode: QueryEvent['mode'], classes: string[]): Promise<T[]>;
|
|
58
|
+
/** Значение слота связи: id | вложенный план | null (снять конец). */
|
|
59
|
+
export type SlotValue = string | {
|
|
60
|
+
plan: Step[];
|
|
61
|
+
mods?: ChainMods;
|
|
62
|
+
} | null;
|
|
63
|
+
/** Уникальная метка узла пути (для pivot-возвратов; переживает нарезку плана). */
|
|
64
|
+
export declare const nextNodeKey: () => number;
|
|
58
65
|
export interface Step {
|
|
59
66
|
/** Имя, как вызвано в цепочке (алиас или id). */
|
|
60
67
|
name: string;
|
|
61
68
|
cls: ClassDef;
|
|
62
69
|
filter?: Filter;
|
|
70
|
+
/** Метка узла (есть у каждого реального шага). */
|
|
71
|
+
nodeKey?: number;
|
|
72
|
+
/**
|
|
73
|
+
* Pivot: шаг-ВОЗВРАТ к уже введённому узлу (повтор LINK-класса / entity(та же переменная)).
|
|
74
|
+
* Не создаёт узел; его фильтры дофильтровывают узел (AND); следующий шаг ветвится от узла.
|
|
75
|
+
*/
|
|
76
|
+
pivotKey?: number;
|
|
63
77
|
/** .alias(name) — ключ шага в путях. */
|
|
64
78
|
aliasKey?: string;
|
|
65
79
|
/** .tags(…) — фильтр по колонке tags: строка | string[] (все) | has/hasAny/hasAll. */
|
|
@@ -70,13 +84,17 @@ export interface Step {
|
|
|
70
84
|
ownerFilter?: string;
|
|
71
85
|
/** Внутреннее (write-цепочки): containment по links — контекст-связи. */
|
|
72
86
|
linksFilter?: Record<string, string>;
|
|
73
|
-
/**
|
|
74
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Слоты связей (`.Класс.set(target)` / `.Класс.unset()`): значения концов записываемой
|
|
89
|
+
* версии БЕЗ участия в фильтре целей. string — id; {plan} — вложенная цепочка (та же
|
|
90
|
+
* транзакция, ровно одна сущность класса конца); null — снять optional-конец.
|
|
91
|
+
*/
|
|
92
|
+
extraLinks?: Record<string, SlotValue>;
|
|
75
93
|
/** Внутреннее (enforceAcl): предикат WRITE/DELETE-правила — цели ищутся только среди своих. */
|
|
76
94
|
aclFilter?: Record<string, unknown>;
|
|
77
95
|
/** .deep(max): рекурсивный self-обход (дети любой глубины), только reverse того же класса. */
|
|
78
96
|
deepMax?: number;
|
|
79
|
-
/** Операция записи на шаге (.
|
|
97
|
+
/** Операция записи на шаге (.create/.update/.delete/.anonymize) — исполняется терминалом плана. */
|
|
80
98
|
op?: import('./types.js').PlanOp;
|
|
81
99
|
/** Внутреннее: шаг «те же сущности» (операция сразу после операции) — цель = строки старта. */
|
|
82
100
|
self?: boolean;
|
|
@@ -85,6 +103,8 @@ export declare const entityTable: (pgSchema: string) => string;
|
|
|
85
103
|
/** Тип листа data-пути по Schema: спуск через record (значение) и object (props). */
|
|
86
104
|
export declare function leafType(cls: ClassDef, path: string[]): FieldType | undefined;
|
|
87
105
|
type HopMode = 'forward' | 'reverse';
|
|
106
|
+
/** Класс target входит в какой-нибудь конец def (союзы учитываются). */
|
|
107
|
+
export declare function linksTo(def: ClassDef, target: string): boolean;
|
|
88
108
|
/** Правило обхода между шагами (по реестру Schema). */
|
|
89
109
|
export declare function resolveHop(prev: ClassDef, next: ClassDef): HopMode;
|
|
90
110
|
export type ReadMode = 'paths' | 'rows' | 'ids' | 'count' | 'versions' | 'agg';
|