letopis 3.0.0 → 3.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.
- package/AGENT-CHEATSHEET.md +12 -5
- package/CHANGELOG.md +40 -0
- package/README.md +202 -46
- package/api-contract.json +4 -0
- package/dist/acl.d.ts +4 -2
- package/dist/admin.d.ts +5 -0
- package/dist/admin.js +10 -5
- package/dist/auth.d.ts +29 -8
- package/dist/auth.js +54 -9
- package/dist/chain.d.ts +19 -13
- package/dist/chain.js +17 -7
- package/dist/cli.js +46 -16
- package/dist/errors.d.ts +4 -5
- package/dist/errors.js +6 -7
- package/dist/index.d.ts +24 -13
- package/dist/index.js +75 -24
- package/dist/load.d.ts +1 -1
- package/dist/log.js +23 -2
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +8 -5
- package/dist/messages/access.d.ts +10 -0
- package/dist/messages/access.js +13 -0
- package/dist/messages/api.d.ts +10 -0
- package/dist/messages/api.js +23 -10
- package/dist/messages/boot.d.ts +5 -0
- package/dist/messages/boot.js +7 -0
- package/dist/messages/codes.js +2 -2
- package/dist/messages/log.d.ts +8 -5
- package/dist/messages/log.js +8 -5
- package/dist/messages/mcp.js +7 -7
- package/dist/messages/model.d.ts +16 -0
- package/dist/messages/model.js +20 -2
- package/dist/messages/write.d.ts +5 -0
- package/dist/messages/write.js +22 -14
- package/dist/model.d.ts +6 -2
- package/dist/model.js +5 -1
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +6 -0
- package/dist/sync.d.ts +3 -4
- package/dist/sync.js +3 -4
- package/dist/typed.d.ts +6 -6
- package/dist/types.d.ts +14 -9
- package/dist/types.js +2 -2
- package/dist/up.d.ts +5 -2
- package/dist/up.js +14 -6
- package/dist/write.d.ts +13 -9
- package/dist/write.js +81 -12
- package/llms.txt +2 -1
- package/package.json +1 -1
- package/sql/60-write.sql +18 -6
package/dist/messages/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Неизвестное имя класса, алиаса или роли: агенту доступна не db.registry, а letopis_model. */
|
|
2
2
|
const NAME_HINT = 'проверить имя: похожие названы в тексте и в similar, классы, алиасы и концы — letopis_model';
|
|
3
|
-
/** Удаление сорвалось после
|
|
4
|
-
const PREVIEW_HINT = 'посмотреть
|
|
3
|
+
/** Удаление сорвалось после предпросмотра: предпросмотр — заново. */
|
|
4
|
+
const PREVIEW_HINT = 'посмотреть предпросмотр заново (delete без confirm) и повторить удаление с confirm: true';
|
|
5
5
|
/** Неверная опция createMcpServer. */
|
|
6
6
|
const OPTION_HINT = 'исправить опцию createMcpServer: допустимые значения названы в тексте';
|
|
7
7
|
export const mcp = {
|
|
@@ -300,7 +300,7 @@ export const mcp = {
|
|
|
300
300
|
// mcp.ts, write: код операции агента не совпал с превью (detail.operation — нынешний)
|
|
301
301
|
'mcp.delete_operation_changed': {
|
|
302
302
|
code: 'confirmation_required',
|
|
303
|
-
ru: 'letopis_write: операция изменилась после
|
|
303
|
+
ru: 'letopis_write: операция изменилась после предпросмотра — посмотрите предпросмотр заново',
|
|
304
304
|
hint: PREVIEW_HINT,
|
|
305
305
|
},
|
|
306
306
|
// mcp.ts, write: целей удаления не видно
|
|
@@ -308,10 +308,10 @@ export const mcp = {
|
|
|
308
308
|
code: 'target_not_found',
|
|
309
309
|
ru: 'letopis_write: удалять нечего — целей не видно',
|
|
310
310
|
},
|
|
311
|
-
// mcp.ts, write: в превью есть $action restrict (detail.preview)
|
|
311
|
+
// mcp.ts, write: в превью есть $action restrict или сводка скрытых строк $hidden restrict (detail.preview)
|
|
312
312
|
'mcp.delete_restricted': {
|
|
313
313
|
code: 'delete_restricted',
|
|
314
|
-
ru: 'letopis_write: удалению мешают строки с $action: restrict',
|
|
314
|
+
ru: 'letopis_write: удалению мешают строки с $action: restrict (или скрытые правами — сводка $hidden: restrict)',
|
|
315
315
|
},
|
|
316
316
|
// mcp.ts, confirmDelete (stateError): requestState не той операции, не того пользователя или подделан
|
|
317
317
|
'mcp.delete_foreign_answer': {
|
|
@@ -321,7 +321,7 @@ export const mcp = {
|
|
|
321
321
|
// mcp.ts, confirmDelete (stateError): срок requestState истёк
|
|
322
322
|
'mcp.delete_answer_expired': {
|
|
323
323
|
code: 'confirmation_required',
|
|
324
|
-
ru: 'letopis_write: удаление не выполнено — подтверждение устарело — посмотрите
|
|
324
|
+
ru: 'letopis_write: удаление не выполнено — подтверждение устарело — посмотрите предпросмотр и подтвердите удаление заново',
|
|
325
325
|
hint: PREVIEW_HINT,
|
|
326
326
|
},
|
|
327
327
|
// mcp.ts, confirmDelete: человек отказал или закрыл окно
|
|
@@ -337,7 +337,7 @@ export const mcp = {
|
|
|
337
337
|
// mcp.ts, write: повторное превью в транзакции разошлось с подтверждённым
|
|
338
338
|
'mcp.delete_data_changed': {
|
|
339
339
|
code: 'confirmation_required',
|
|
340
|
-
ru: 'letopis_write: данные изменились после подтверждения — посмотрите
|
|
340
|
+
ru: 'letopis_write: данные изменились после подтверждения — посмотрите предпросмотр заново',
|
|
341
341
|
hint: PREVIEW_HINT,
|
|
342
342
|
},
|
|
343
343
|
// mcp.ts, limitsOf: maxRows, maxBytes — не целое из диапазона
|
package/dist/messages/model.d.ts
CHANGED
|
@@ -244,6 +244,22 @@ export declare const model: {
|
|
|
244
244
|
code: "invalid_query";
|
|
245
245
|
ru: string;
|
|
246
246
|
};
|
|
247
|
+
'model.cli_unknown_flag': {
|
|
248
|
+
code: "invalid_query";
|
|
249
|
+
ru: string;
|
|
250
|
+
};
|
|
251
|
+
'model.cli_flag_needs_value': {
|
|
252
|
+
code: "invalid_query";
|
|
253
|
+
ru: string;
|
|
254
|
+
};
|
|
255
|
+
'model.cli_flag_no_value': {
|
|
256
|
+
code: "invalid_query";
|
|
257
|
+
ru: string;
|
|
258
|
+
};
|
|
259
|
+
'model.cli_unexpected_arg': {
|
|
260
|
+
code: "invalid_query";
|
|
261
|
+
ru: string;
|
|
262
|
+
};
|
|
247
263
|
'model.types_header': {
|
|
248
264
|
code: "invalid_query";
|
|
249
265
|
ru: string;
|
package/dist/messages/model.js
CHANGED
|
@@ -299,6 +299,24 @@ export const model = {
|
|
|
299
299
|
code: 'invalid_query',
|
|
300
300
|
ru: 'правило {kind} в {path} проверяется только в библиотеке — в базе его нет',
|
|
301
301
|
},
|
|
302
|
+
// cli.ts, main: ошибка разбора аргументов (node:util parseArgs) — текст по-русски вместо английского текста Node (lmsg,
|
|
303
|
+
// код не печатается; выход с кодом 2). flag — флаг, как его передали (--aply, -x); arg — лишний аргумент
|
|
304
|
+
'model.cli_unknown_flag': {
|
|
305
|
+
code: 'invalid_query',
|
|
306
|
+
ru: 'неизвестный флаг {flag}; см. letopis --help',
|
|
307
|
+
},
|
|
308
|
+
'model.cli_flag_needs_value': {
|
|
309
|
+
code: 'invalid_query',
|
|
310
|
+
ru: 'флагу {flag} нужно значение: {flag} <значение> или {flag}=<значение>; см. letopis --help',
|
|
311
|
+
},
|
|
312
|
+
'model.cli_flag_no_value': {
|
|
313
|
+
code: 'invalid_query',
|
|
314
|
+
ru: 'флаг {flag} не принимает значения — уберите «=…» после него; см. letopis --help',
|
|
315
|
+
},
|
|
316
|
+
'model.cli_unexpected_arg': {
|
|
317
|
+
code: 'invalid_query',
|
|
318
|
+
ru: 'лишний аргумент {arg}; см. letopis --help',
|
|
319
|
+
},
|
|
302
320
|
// sync.ts, generateTypes: пометка в первой строке файла типов (после «// ») — текст не для ошибки
|
|
303
321
|
'model.types_header': {
|
|
304
322
|
code: 'invalid_query',
|
|
@@ -308,7 +326,7 @@ export const model = {
|
|
|
308
326
|
// и валидатор letopis его не исполняют, данные записались бы как есть; rules — пути и виды через запятую
|
|
309
327
|
'model.transform_unsupported': {
|
|
310
328
|
code: 'invalid_class',
|
|
311
|
-
ru: 'преобразования Zod не
|
|
312
|
-
hint: '
|
|
329
|
+
ru: 'в модели есть преобразования Zod, а при записи их никто не исполняет — значение записалось бы как есть; поля с преобразованием: {rules}',
|
|
330
|
+
hint: 'уберите из модели .trim(), .toLowerCase(), .transform(), z.preprocess() и .pipe() и приводите значение в коде сервиса до записи (например, name.trim()); проверку без изменения значения дают .regex(), .min(), .refine()',
|
|
313
331
|
},
|
|
314
332
|
};
|
package/dist/messages/write.d.ts
CHANGED
|
@@ -172,6 +172,11 @@ export declare const write: {
|
|
|
172
172
|
ru: string;
|
|
173
173
|
hint: string;
|
|
174
174
|
};
|
|
175
|
+
'write.delete_restricted': {
|
|
176
|
+
code: "delete_restricted";
|
|
177
|
+
ru: string;
|
|
178
|
+
hint: string;
|
|
179
|
+
};
|
|
175
180
|
'write.link_in_data': {
|
|
176
181
|
code: "invalid_data";
|
|
177
182
|
ru: string;
|
package/dist/messages/write.js
CHANGED
|
@@ -62,7 +62,7 @@ export const write = {
|
|
|
62
62
|
// create() или upsert() на возврате к узлу пути
|
|
63
63
|
'write.create_on_pivot': {
|
|
64
64
|
code: 'invalid_query',
|
|
65
|
-
ru: '{verb}() на возврате к узлу — он ведёт к
|
|
65
|
+
ru: '{verb}() на возврате к узлу — он ведёт к существующему объекту, используйте update()',
|
|
66
66
|
},
|
|
67
67
|
// create() или upsert() после шага с фильтром, а не с id
|
|
68
68
|
'write.create_with_filter': {
|
|
@@ -100,7 +100,7 @@ export const write = {
|
|
|
100
100
|
// слот после глагола, который слотов не принимает
|
|
101
101
|
'write.slot_bad_verb': {
|
|
102
102
|
code: 'invalid_query',
|
|
103
|
-
ru: '
|
|
103
|
+
ru: 'шаг роли .{role} после {verb}() — шаги ролей бывают только у create(), update() и upsert()',
|
|
104
104
|
},
|
|
105
105
|
// write.ts:slotRole: имя слота — не роль записываемого класса и не класс реестра
|
|
106
106
|
'write.slot_unknown': {
|
|
@@ -115,22 +115,22 @@ export const write = {
|
|
|
115
115
|
// write.ts:slotRole: класс слота принимают несколько ролей
|
|
116
116
|
'write.slot_ambiguous': {
|
|
117
117
|
code: 'invalid_query',
|
|
118
|
-
ru: '
|
|
118
|
+
ru: 'шаг {name} у класса {cls} неоднозначен: роли {roles} — назовите роль',
|
|
119
119
|
},
|
|
120
120
|
// конец из пути: шаг подходит нескольким ролям
|
|
121
121
|
'write.end_ambiguous': {
|
|
122
122
|
code: 'invalid_query',
|
|
123
|
-
ru: 'конец класса {cls} для шага {step} неоднозначен: роли {roles} — назовите шаг ролью или задайте
|
|
123
|
+
ru: 'конец класса {cls} для шага {step} неоднозначен: роли {roles} — назовите шаг ролью или задайте конец шагом роли после глагола',
|
|
124
124
|
},
|
|
125
125
|
// конец из пути: шаг дал не одну сущность; count — сколько найдено
|
|
126
126
|
'write.end_not_single': {
|
|
127
127
|
code: 'invalid_query',
|
|
128
|
-
ru: 'шаг {step} должен дать ровно
|
|
128
|
+
ru: 'шаг {step} должен дать ровно один объект для конца {cls} (найдено {count})',
|
|
129
129
|
},
|
|
130
130
|
// слот .роль.set() одиночного конца — значение дало несколько сущностей
|
|
131
131
|
'write.slot_single_many': {
|
|
132
132
|
code: 'invalid_query',
|
|
133
|
-
ru: '
|
|
133
|
+
ru: 'шаг роли .{role}.set(): конец одиночный, а значение дало объектов: {count}',
|
|
134
134
|
},
|
|
135
135
|
// write.ts:existsError: create() — объект уже есть, id не вычислить
|
|
136
136
|
'write.exists': {
|
|
@@ -207,32 +207,40 @@ export const write = {
|
|
|
207
207
|
// restore({ expect }), delete({ expect }): expect — не список строк { id, rev }; verb — глагол
|
|
208
208
|
'write.bad_expect': {
|
|
209
209
|
code: 'invalid_query',
|
|
210
|
-
ru: '{verb}({ expect }): expect — список строк { id, rev } (строки
|
|
210
|
+
ru: '{verb}({ expect }): expect — список строк { id, rev } (строки ответа; у delete — и строки-сводки предпросмотра как есть)',
|
|
211
211
|
},
|
|
212
212
|
// delete({ expect }) без confirm: превью сравнивать не с чем
|
|
213
213
|
'write.expect_without_confirm': {
|
|
214
214
|
code: 'invalid_query',
|
|
215
|
-
ru: 'delete({ expect }) — только с { confirm: true }: без него delete() и есть
|
|
215
|
+
ru: 'delete({ expect }) — только с { confirm: true }: без него delete() и есть предпросмотр',
|
|
216
216
|
},
|
|
217
217
|
// delete({ confirm: true, expect }): превью в транзакции удаления не совпало с показанным; detail.preview — новое превью
|
|
218
218
|
'write.preview_changed': {
|
|
219
219
|
code: 'conflict',
|
|
220
|
-
ru: 'delete({ confirm: true, expect }):
|
|
221
|
-
hint: 'показать пользователю
|
|
220
|
+
ru: 'delete({ confirm: true, expect }): предпросмотр изменился с тех пор, как его показали — ничего не удалено',
|
|
221
|
+
hint: 'показать пользователю новый предпросмотр (err.detail.preview) и удалить с ним: delete({ confirm: true, expect: err.detail.preview })',
|
|
222
|
+
},
|
|
223
|
+
// write.ts, checkRestrict (3.0.1): сверенный предпросмотр delete({ confirm: true, expect }) содержит мешающие строки
|
|
224
|
+
// (restrict) — видимые или сводку скрытых правами; отказ до удаления. visible — число видимых, others — скрытых;
|
|
225
|
+
// detail — { rows: [{ id, class }], others }, как у отказа триггера (sql-write.delete_restricted)
|
|
226
|
+
'write.delete_restricted': {
|
|
227
|
+
code: 'delete_restricted',
|
|
228
|
+
ru: 'удалению мешают ссылающиеся строки: видимых {visible}, скрытых правами {others} — ничего не удалено',
|
|
229
|
+
hint: 'показать пользователю мешающие строки (err.detail.rows); скрытые правами (err.detail.others) видит только их владелец — обратиться к нему; удалить или перевести мешающие строки (или сменить у их конца onDelete) и заново показать предпросмотр delete()',
|
|
222
230
|
},
|
|
223
231
|
// write.ts, checkFullData и checkPatchEnds (3.0, вместо api.validation_failed_end): лишний ключ данных — конец класса
|
|
224
232
|
// (имя роли или класса на конце); keys — такие ключи, role — роль первого из них. Нарушения ValidationError дописывает
|
|
225
233
|
// к тексту сам
|
|
226
234
|
'write.link_in_data': {
|
|
227
235
|
code: 'invalid_data',
|
|
228
|
-
ru: '{verb}() у {cls}: {keys} — связь, а не поле данных; конец задают путь цепочки или
|
|
229
|
-
hint: 'конец (связь) — не поле данных: путь цепочки (db.Владелец(id).Класс().create(…)) или
|
|
236
|
+
ru: '{verb}() у {cls}: {keys} — связь, а не поле данных; конец задают путь цепочки или шаг роли после глагола: .{role}.set(id)',
|
|
237
|
+
hint: 'конец (связь) — не поле данных: путь цепочки (db.Владелец(id).Класс().create(…)) или шаг роли после глагола: db.Класс.create({ … }).Роль.set(id); у конца-списка — .Роль.add(id) / .Роль.remove(id), снять — .Роль.unset()',
|
|
230
238
|
},
|
|
231
239
|
// 3.0: delete({ confirm: true }) без expect и без all — удаление не сверено с показанным превью
|
|
232
240
|
'write.confirm_without_expect': {
|
|
233
241
|
code: 'invalid_query',
|
|
234
|
-
ru: 'delete({ confirm: true }) без { expect }: удаление не сверено с
|
|
235
|
-
hint: 'передайте expect из
|
|
242
|
+
ru: 'delete({ confirm: true }) без { expect }: удаление не сверено с предпросмотром, который видел пользователь — ничего не удалено',
|
|
243
|
+
hint: 'передайте expect из предпросмотра: const p = await db.Класс(id).delete().rows(); показать p пользователю; await db.Класс(id).delete({ confirm: true, expect: p }).rows(). Осознанно удалить текущий состав без сверки — delete({ confirm: true, all: true })',
|
|
236
244
|
},
|
|
237
245
|
// 3.0: restore() без expect и без all — и для явных id: последнее надгробие могло смениться с тех пор, как его видели
|
|
238
246
|
'write.restore_without_expect': {
|
package/dist/model.d.ts
CHANGED
|
@@ -29,7 +29,11 @@ type TargetName<T> = T extends string ? T : T extends {
|
|
|
29
29
|
type MinRequired<O> = O extends {
|
|
30
30
|
min: infer N;
|
|
31
31
|
} ? (N extends 0 | undefined ? false : number extends N ? boolean : true) : false;
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Конец класса: `one(X)`, `one(A, B)`, `many(X, { min, max })`. Параметры типа: `Many` — конец-список, `Required` —
|
|
34
|
+
* обязателен, `T` — имена классов целей (только для типов). `min`, `max`, `onDeleteRule`, `metaData` необязательны:
|
|
35
|
+
* не заданы — `undefined`.
|
|
36
|
+
*/
|
|
33
37
|
export declare class End<Many extends boolean = boolean, Required extends boolean = boolean, T extends string = string> {
|
|
34
38
|
readonly targets: readonly Target[];
|
|
35
39
|
readonly many: Many;
|
|
@@ -38,7 +42,7 @@ export declare class End<Many extends boolean = boolean, Required extends boolea
|
|
|
38
42
|
readonly max?: number;
|
|
39
43
|
readonly onDeleteRule?: OnDelete;
|
|
40
44
|
readonly metaData?: Record<string, unknown>;
|
|
41
|
-
/** Только для вывода типов: имена классов целей — строгие переходы и
|
|
45
|
+
/** Только для вывода типов: имена классов целей — строгие переходы и шаги ролей цепочек. */
|
|
42
46
|
readonly __targets?: T;
|
|
43
47
|
constructor(targets: readonly Target[], opts: {
|
|
44
48
|
many: Many;
|
package/dist/model.js
CHANGED
|
@@ -14,7 +14,11 @@ import { z } from 'zod';
|
|
|
14
14
|
import { lerr } from './errors.js';
|
|
15
15
|
import { normalizeSchema } from './jsonschema.js';
|
|
16
16
|
import { RESERVED_CLASS_NAMES } from './types.js';
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Конец класса: `one(X)`, `one(A, B)`, `many(X, { min, max })`. Параметры типа: `Many` — конец-список, `Required` —
|
|
19
|
+
* обязателен, `T` — имена классов целей (только для типов). `min`, `max`, `onDeleteRule`, `metaData` необязательны:
|
|
20
|
+
* не заданы — `undefined`.
|
|
21
|
+
*/
|
|
18
22
|
export class End {
|
|
19
23
|
targets;
|
|
20
24
|
many;
|
package/dist/registry.d.ts
CHANGED
|
@@ -41,6 +41,12 @@ interface ClassRow {
|
|
|
41
41
|
resolved: Record<string, unknown>;
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Реестр классов сессии — `db.registry`: описания классов арендатора и системных классов, как их видит подключение.
|
|
46
|
+
* Методы: `has(имя)` — есть ли класс или алиас; `find(имя)` — описание `ClassInfo` или `undefined`; `resolve(имя)` — то же,
|
|
47
|
+
* а неизвестное имя — `invalid_class`; `all()` — все классы; `family(имя)` — класс и его потомки; `leafType(класс, путь)` —
|
|
48
|
+
* тип поля по пути в данных; `load(строки)` — заменить содержимое (его зовёт само подключение, `db.refresh()`).
|
|
49
|
+
*/
|
|
44
50
|
export declare class Registry {
|
|
45
51
|
private byName;
|
|
46
52
|
private byAlias;
|
package/dist/registry.js
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { lerr } from './errors.js';
|
|
7
7
|
import { compileValidator } from './validate.js';
|
|
8
|
+
/**
|
|
9
|
+
* Реестр классов сессии — `db.registry`: описания классов арендатора и системных классов, как их видит подключение.
|
|
10
|
+
* Методы: `has(имя)` — есть ли класс или алиас; `find(имя)` — описание `ClassInfo` или `undefined`; `resolve(имя)` — то же,
|
|
11
|
+
* а неизвестное имя — `invalid_class`; `all()` — все классы; `family(имя)` — класс и его потомки; `leafType(класс, путь)` —
|
|
12
|
+
* тип поля по пути в данных; `load(строки)` — заменить содержимое (его зовёт само подключение, `db.refresh()`).
|
|
13
|
+
*/
|
|
8
14
|
export class Registry {
|
|
9
15
|
byName = new Map();
|
|
10
16
|
byAlias = new Map();
|
package/dist/sync.d.ts
CHANGED
|
@@ -27,10 +27,9 @@ export declare function syncModels(db: DbCore, models: readonly AnyModel[], opts
|
|
|
27
27
|
export declare function schemaToTs(s: JsonSchema | undefined, indent?: string): string;
|
|
28
28
|
/**
|
|
29
29
|
* Объявления `<Класс>Data` и `<Класс>Links` для классов реестра и `<Класс>Model` — имя, алиас, концы с классами целей
|
|
30
|
-
* и
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* запись каталога model.types_header.
|
|
30
|
+
* и предки, — а в конце список `Models`: строгие типы цепочек без моделей Zod, `connect<Models>()`. Классы арендатора
|
|
31
|
+
* System (системные и классы сидов пакета) — только с includeSystem (`letopis types --system`); без него системные
|
|
32
|
+
* классы даёт встроенный набор SystemModels. Первая строка — пометка «Сгенерировано letopis types — не править руками.»
|
|
34
33
|
*/
|
|
35
34
|
export declare function generateTypes(registry: Registry, opts?: {
|
|
36
35
|
includeSystem?: boolean;
|
package/dist/sync.js
CHANGED
|
@@ -166,10 +166,9 @@ export function schemaToTs(s, indent = '') {
|
|
|
166
166
|
}
|
|
167
167
|
/**
|
|
168
168
|
* Объявления `<Класс>Data` и `<Класс>Links` для классов реестра и `<Класс>Model` — имя, алиас, концы с классами целей
|
|
169
|
-
* и
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* запись каталога model.types_header.
|
|
169
|
+
* и предки, — а в конце список `Models`: строгие типы цепочек без моделей Zod, `connect<Models>()`. Классы арендатора
|
|
170
|
+
* System (системные и классы сидов пакета) — только с includeSystem (`letopis types --system`); без него системные
|
|
171
|
+
* классы даёт встроенный набор SystemModels. Первая строка — пометка «Сгенерировано letopis types — не править руками.»
|
|
173
172
|
*/
|
|
174
173
|
export function generateTypes(registry, opts = {}) {
|
|
175
174
|
const out = [`// ${lmsg('model.types_header')}`, ''];
|
package/dist/typed.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - переходы цепочки выводятся из концов, как в resolveHop (lib/src/sql.ts): от класса C — роли его семейства (C и
|
|
7
7
|
* потомков) и классы, связанные с семейством C концом в любую сторону (роль, имя которой совпало с именем класса,
|
|
8
8
|
* во время выполнения — шаг класса, так же и здесь);
|
|
9
|
-
* -
|
|
9
|
+
* - шаги ролей после глагола — только роли класса C: `set(id | строка | цепочка)`, `add` и `remove` — у множественного конца, `unset` —
|
|
10
10
|
* у необязательного;
|
|
11
11
|
* - поля фильтров, `or()` и агрегатов `sum`, `avg`, `min`, `max`, `countBy` — поля класса.
|
|
12
12
|
* Чего типы не знают: возврат к узлу связи из пути (pivot) от класса, с которым связь не связана концом, и роль чужого
|
|
@@ -214,11 +214,11 @@ export type StepsOf<M extends readonly AnyModelLike[]> = {
|
|
|
214
214
|
} & {
|
|
215
215
|
[K in AllOf<M> as AliasOf<K>]: StepFnOf<K, M>;
|
|
216
216
|
};
|
|
217
|
-
/** Значение
|
|
217
|
+
/** Значение шага роли одиночного конца (`.Роль.set(…)`): id, строка ответа или цепочка. */
|
|
218
218
|
type SlotOne = string | {
|
|
219
219
|
readonly id: string;
|
|
220
220
|
} | ChainRef;
|
|
221
|
-
/** Значение
|
|
221
|
+
/** Значение шага роли множественного конца (`.Роль.add(…)`, `.remove(…)`): ещё и список id или строк. */
|
|
222
222
|
type SlotMany = SlotOne | readonly (string | {
|
|
223
223
|
readonly id: string;
|
|
224
224
|
})[];
|
|
@@ -248,7 +248,7 @@ type HopsOf<C, M extends readonly AnyModelLike[], All = AllOf<M>> = {
|
|
|
248
248
|
type Retyped = 'paths' | 'rows' | 'first' | 'versions' | 'limit' | 'offset' | 'sort' | 'asOf' | 'after' | 'withDeleted' | 'deep' | 'exact' | 'alias' | 'tags' | 'owner' | 'create' | 'update' | 'upsert' | 'delete' | 'forUpdate' | 'restore' | 'cache' | 'sum' | 'avg' | 'min' | 'max' | 'countBy';
|
|
249
249
|
/**
|
|
250
250
|
* Цепочка с текущим классом C (у шага-роли с несколькими целями — их союз): терминалы отдают RowOf<C>, шаги — переходы
|
|
251
|
-
* из концов,
|
|
251
|
+
* из концов, шаги ролей после глагола (`.Роль.set(…)`) — роли C, агрегаты — поля C.
|
|
252
252
|
*/
|
|
253
253
|
export type TypedChain<C, M extends readonly AnyModelLike[]> = Omit<ChainCore, Retyped> & {
|
|
254
254
|
paths(): Promise<Path[]>;
|
|
@@ -291,8 +291,8 @@ export type TypedChain<C, M extends readonly AnyModelLike[]> = Omit<ChainCore, R
|
|
|
291
291
|
reason?: string;
|
|
292
292
|
}): TypedChain<C, M>;
|
|
293
293
|
/**
|
|
294
|
-
* Без опций —
|
|
295
|
-
* `owner()` или `limit()`); `{ confirm: true }` без `expect` — `invalid_query
|
|
294
|
+
* Без опций — предпросмотр. `{ confirm: true, expect: предпросмотр }` — удалить ровно показанное (цели сужены фильтром, id, `tags()`,
|
|
295
|
+
* `owner()` или `limit()`); `{ confirm: true }` без `expect` — `invalid_query`;
|
|
296
296
|
* осознанно удалить текущий состав без сверки — `{ confirm: true, all: true }`.
|
|
297
297
|
*/
|
|
298
298
|
delete(opts?: DeleteOptions): TypedChain<C, M>;
|
package/dist/types.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
/**
|
|
5
5
|
* Ревизия схемы, которую знает эта версия пакета, — номер версии SQL-части letopis (таблиц и функций в схеме установки).
|
|
6
|
-
* После `up()` у схемы та же ревизия (`r.revision`); схему отставшей ревизии
|
|
6
|
+
* После `up()` у схемы та же ревизия (`r.revision`); схему отставшей ревизии (того же поколения `v2`) `up()` обновляет только с
|
|
7
7
|
* `upgrade: true`, а `connect()` о ней предупреждает.
|
|
8
8
|
*/
|
|
9
9
|
export declare const DDL_REVISION = 11;
|
|
@@ -18,7 +18,7 @@ export declare const SCHEMA_VERSION = "v2";
|
|
|
18
18
|
*/
|
|
19
19
|
export declare const ACL_CLASS_NAMES: readonly string[];
|
|
20
20
|
/**
|
|
21
|
-
* Имена методов цепочки: модификаторы, терминалы, глаголы записи и
|
|
21
|
+
* Имена методов цепочки: модификаторы, терминалы, глаголы записи и шаги роли, а также `then`. После шага цепочки эти
|
|
22
22
|
* имена библиотека не ищет среди классов; все они входят в `RESERVED_CLASS_NAMES`.
|
|
23
23
|
*/
|
|
24
24
|
export declare const CHAIN_METHODS: readonly ["then", "entity", "paths", "rows", "first", "ids", "count", "limit", "offset", "sort", "asOf", "withDeleted", "deep", "exact", "sum", "avg", "min", "max", "countBy", "after", "versions", "create", "update", "upsert", "set", "unset", "add", "remove", "delete", "restore", "alias", "tags", "owner", "forUpdate", "cache"];
|
|
@@ -51,17 +51,22 @@ export interface Row<D = Record<string, unknown>, L = Record<string, string | st
|
|
|
51
51
|
/** Вид операции версии: create, update, delete, restore, … */
|
|
52
52
|
op: string;
|
|
53
53
|
reason?: string;
|
|
54
|
-
/** Надгробие:
|
|
54
|
+
/** Надгробие: удалённый объект (withDeleted) или версия удаления (versions). */
|
|
55
55
|
$deleted?: true;
|
|
56
|
-
/** Глубина узла при deep() (1 — прямой потомок); в
|
|
56
|
+
/** Глубина узла при deep() (1 — прямой потомок); в предпросмотре удаления — уровень каскада (0 — цель). */
|
|
57
57
|
$depth?: number;
|
|
58
58
|
/** Что сделал upsert. */
|
|
59
59
|
$upsert?: 'created' | 'updated' | 'unchanged';
|
|
60
60
|
/**
|
|
61
61
|
* Превью удаления (delete() без confirm): удалится, снимется конец или помешает (restrict). В ответе
|
|
62
|
-
* `restore({ expect, partial: true })` — `skip`: цель не восстановлена, причина — `$why`.
|
|
62
|
+
* `restore({ expect, partial: true })` — `skip`: цель не восстановлена, причина — `$why`. `hidden` — строка-сводка
|
|
63
|
+
* предпросмотра, а не объект: скрытые правами строки замыкания (что с ними будет — `$hidden`, сколько — `$count`).
|
|
63
64
|
*/
|
|
64
|
-
$action?: 'delete' | 'unset' | 'restrict' | 'skip';
|
|
65
|
+
$action?: 'delete' | 'unset' | 'restrict' | 'skip' | 'hidden';
|
|
66
|
+
/** Строка-сводка предпросмотра удаления (`$action: 'hidden'`): что будет со скрытыми правами строками. */
|
|
67
|
+
$hidden?: 'delete' | 'unset' | 'restrict';
|
|
68
|
+
/** Строка-сводка предпросмотра удаления (`$action: 'hidden'`): число скрытых правами строк. */
|
|
69
|
+
$count?: number;
|
|
65
70
|
/**
|
|
66
71
|
* Почему цель `restore({ expect, partial: true })` не восстановлена: `restored` — её уже восстановили, `reissued` — id
|
|
67
72
|
* занял новый объект с тем же ключом, `changed` — после показанного удаления её меняли (удалили снова и т. п.).
|
|
@@ -139,7 +144,7 @@ export interface QueryEvent {
|
|
|
139
144
|
}
|
|
140
145
|
/** Порог логирования: события этого уровня и выше; `'off'` — ничего. */
|
|
141
146
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off';
|
|
142
|
-
/** Событие логирования (каталог событий —
|
|
147
|
+
/** Событие логирования (каталог событий — `LOG_EVENTS`). Поля без значения в событии отсутствуют. */
|
|
143
148
|
export interface LogEvent {
|
|
144
149
|
/** Время события, ISO 8601 (UTC). */
|
|
145
150
|
ts: string;
|
|
@@ -150,9 +155,9 @@ export interface LogEvent {
|
|
|
150
155
|
op?: string;
|
|
151
156
|
/** Длительность операции, мс (с точностью до 0,1). */
|
|
152
157
|
ms?: number;
|
|
153
|
-
/** Арендатор сессии
|
|
158
|
+
/** Арендатор сессии подключения (db, db.as(), db.begin()). */
|
|
154
159
|
tenant?: string;
|
|
155
|
-
/** Аккаунт
|
|
160
|
+
/** Аккаунт подключения: аккаунт сессии или тот, от чьего имени действует db.as(). */
|
|
156
161
|
account?: string;
|
|
157
162
|
/** Метка транзакции db.begin() или db.transaction(). */
|
|
158
163
|
tx?: string;
|
package/dist/types.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
/**
|
|
5
5
|
* Ревизия схемы, которую знает эта версия пакета, — номер версии SQL-части letopis (таблиц и функций в схеме установки).
|
|
6
|
-
* После `up()` у схемы та же ревизия (`r.revision`); схему отставшей ревизии
|
|
6
|
+
* После `up()` у схемы та же ревизия (`r.revision`); схему отставшей ревизии (того же поколения `v2`) `up()` обновляет только с
|
|
7
7
|
* `upgrade: true`, а `connect()` о ней предупреждает.
|
|
8
8
|
*/
|
|
9
9
|
export const DDL_REVISION = 11;
|
|
@@ -21,7 +21,7 @@ export const ACL_CLASS_NAMES = ['rule', 'Resource', 'member', 'Account', 'Class'
|
|
|
21
21
|
// Методы цепочки — единственный источник зарезервированных имён (план, §2.14, этап 3, п. 10)
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
23
|
/**
|
|
24
|
-
* Имена методов цепочки: модификаторы, терминалы, глаголы записи и
|
|
24
|
+
* Имена методов цепочки: модификаторы, терминалы, глаголы записи и шаги роли, а также `then`. После шага цепочки эти
|
|
25
25
|
* имена библиотека не ищет среди классов; все они входят в `RESERVED_CLASS_NAMES`.
|
|
26
26
|
*/
|
|
27
27
|
export const CHAIN_METHODS = [
|
package/dist/up.d.ts
CHANGED
|
@@ -121,9 +121,11 @@ export interface UpOptions extends Omit<InstallOptions, 'schema' | 'upTo' | 'mig
|
|
|
121
121
|
*/
|
|
122
122
|
login?: LoginOptions;
|
|
123
123
|
}
|
|
124
|
-
/** Роль приложения up({ login }). */
|
|
124
|
+
/** Роль приложения up({ login }): `{ user, password }` (поле — `user`, не `name`). */
|
|
125
125
|
export interface LoginOptions {
|
|
126
|
+
/** Имя роли PostgreSQL для приложения, например `'shop_app'`: нет — up() её создаёт, есть — меняет пароль. */
|
|
126
127
|
user: string;
|
|
128
|
+
/** Пароль роли: войдёт в строку подключения `r.login.dsn`. */
|
|
127
129
|
password: string;
|
|
128
130
|
}
|
|
129
131
|
export interface UpResult extends InstallResult {
|
|
@@ -200,7 +202,8 @@ export interface TenantOptions {
|
|
|
200
202
|
* APIKEY, KEYSECRET, OTP, TOTP, KEY, LOCK); иначе invalid_data.
|
|
201
203
|
*/
|
|
202
204
|
kind?: string;
|
|
203
|
-
ttl
|
|
205
|
+
/** Срок сессии: как ttl у auth (число секунд, '15m', интервал PostgreSQL '15 minutes'); по умолчанию 7 суток. */
|
|
206
|
+
ttl?: string | number;
|
|
204
207
|
/** Роли членства (по умолчанию ['owner']); роли уже существующего членства повтор не меняет. */
|
|
205
208
|
roles?: string[];
|
|
206
209
|
};
|
package/dist/up.js
CHANGED
|
@@ -19,7 +19,7 @@ import postgres from 'postgres';
|
|
|
19
19
|
import { DDL_REVISION, RESERVED_CLASS_NAMES, SCHEMA_VERSION } from './types.js';
|
|
20
20
|
import { makeLog, maskDsn, traceSql, traced } from './log.js';
|
|
21
21
|
import { startPglite } from './pglite.js';
|
|
22
|
-
import { hashPassword } from './auth.js';
|
|
22
|
+
import { hashPassword, ttlGuard, ttlOf } from './auth.js';
|
|
23
23
|
import { checkOptions, lerr, lmsg } from './errors.js';
|
|
24
24
|
import { sqlCatalog } from './messages/index.js';
|
|
25
25
|
import { v5Id } from './uuid.js';
|
|
@@ -572,8 +572,15 @@ async function ensureLogin(sql, login, owner, app, dsn) {
|
|
|
572
572
|
const verb = r ? 'alter role %I login password %L' : 'create role %I login password %L';
|
|
573
573
|
const [{ q }] = await tx `select format(${verb}::text, ${login.user}::text, ${login.password}::text) as q`;
|
|
574
574
|
await tx.unsafe(q);
|
|
575
|
-
const [{ g }] = await tx `select format('grant %I to %I', ${app}::text, ${login.user}::text) as g`;
|
|
576
|
-
|
|
575
|
+
const [{ g, installer }] = await tx `select format('grant %I to %I', ${app}::text, ${login.user}::text) as g, current_user::text as installer`;
|
|
576
|
+
// PostgreSQL 16+: выдать роль может только тот, у кого на неё ADMIN OPTION (или суперпользователь) — понятный отказ
|
|
577
|
+
// вместо сырого 42501 «permission denied to grant role» (3.0.1)
|
|
578
|
+
await tx.unsafe(g).catch((e) => {
|
|
579
|
+
if (e?.code === '42501') {
|
|
580
|
+
throw lerr('boot.login_grant_denied', { installer, app, user: login.user }, { cause: e });
|
|
581
|
+
}
|
|
582
|
+
throw e;
|
|
583
|
+
});
|
|
577
584
|
return !r;
|
|
578
585
|
});
|
|
579
586
|
let url;
|
|
@@ -642,8 +649,9 @@ export async function createTenant(sql, schema, opts, owner = 'letopis_owner') {
|
|
|
642
649
|
// логин без крайних пробелов, как у auth.setPassword() и verifyPassword() (план 2.1, A12)
|
|
643
650
|
const login = u ? (typeof u.login === 'string' ? u.login.trim() : u.login) : '';
|
|
644
651
|
const user = u ? v5Id(SYSTEM_ID, 'Account', [`user:${login}`]) : null;
|
|
652
|
+
const ttl = u ? ttlOf(u.ttl, 'createTenant') : null;
|
|
645
653
|
const hash = u ? await hashPassword(u.password) : null;
|
|
646
|
-
return sql.begin(async (tx) => {
|
|
654
|
+
return ttlGuard(sql.begin(async (tx) => {
|
|
647
655
|
// под блокировкой схемы, как накат: up({ tenant }) нескольких экземпляров сервиса разом заводит арендатора один
|
|
648
656
|
// раз, остальные находят его по имени (без неё оба вставили бы ту же строку и один упал бы на ключе)
|
|
649
657
|
await lockSchema(tx, schema);
|
|
@@ -676,8 +684,8 @@ export async function createTenant(sql, schema, opts, owner = 'letopis_owner') {
|
|
|
676
684
|
await tx.unsafe(`insert into ${S}.secret (account, kind, ident, material, meta)
|
|
677
685
|
select $1::uuid, $2, $3, convert_to($4, 'UTF8'), '{"confirmed": true}'::jsonb
|
|
678
686
|
where not exists (select 1 from ${S}.secret where kind = $2 and casefold(ident) collate pg_unicode_fast = casefold($3) collate pg_unicode_fast and deleted_at is null)`, [user, kind, login, hash]);
|
|
679
|
-
token = (await tx.unsafe(`select ${S}.new_session($1::uuid, $2::interval, $3::jsonb) as t`, [user,
|
|
687
|
+
token = (await tx.unsafe(`select ${S}.new_session($1::uuid, $2::interval, $3::jsonb) as t`, [user, ttl ?? '7 days', { tenant }]))[0].t;
|
|
680
688
|
}
|
|
681
689
|
return { tenant, user, token, created };
|
|
682
|
-
});
|
|
690
|
+
}), u?.ttl, 'createTenant');
|
|
683
691
|
}
|
package/dist/write.d.ts
CHANGED
|
@@ -19,19 +19,23 @@ export declare function inc(by: number, opts?: {
|
|
|
19
19
|
}): IncMarker;
|
|
20
20
|
export declare const isInc: (v: unknown) => v is IncMarker;
|
|
21
21
|
/**
|
|
22
|
-
* Ожидаемая строка `expect` у `restore()` и `delete({ confirm: true })
|
|
23
|
-
*
|
|
22
|
+
* Ожидаемая строка `expect` у `restore()` и `delete({ confirm: true })`: строка ответа или её `id`, `rev` (и `$action`
|
|
23
|
+
* предпросмотра удаления). Лишние поля строки не мешают.
|
|
24
24
|
*/
|
|
25
25
|
export interface Expected {
|
|
26
26
|
id: string;
|
|
27
27
|
rev: number;
|
|
28
|
-
/** У строки
|
|
28
|
+
/** У строки предпросмотра — что с ней будет; строку ответа (с любым $action) можно передать как есть. */
|
|
29
29
|
$action?: Row['$action'];
|
|
30
|
+
/** Строка-сводка предпросмотра (`$action: 'hidden'`, 3.0.1): что будет со скрытыми правами строками. */
|
|
31
|
+
$hidden?: Row['$hidden'];
|
|
32
|
+
/** Строка-сводка предпросмотра: число скрытых правами строк. */
|
|
33
|
+
$count?: number;
|
|
30
34
|
}
|
|
31
35
|
/**
|
|
32
|
-
* Опции delete()
|
|
36
|
+
* Опции delete(): без confirm — предпросмотр; `{ confirm: true, expect }` — удалить ровно показанный предпросмотр;
|
|
33
37
|
* `{ confirm: true, all: true }` — осознанно удалить текущий состав целей без сверки (и без фильтра). `{ confirm: true }`
|
|
34
|
-
* без expect и all — invalid_query
|
|
38
|
+
* без expect и all — invalid_query.
|
|
35
39
|
*/
|
|
36
40
|
export type DeleteOptions = {
|
|
37
41
|
confirm?: false;
|
|
@@ -50,9 +54,9 @@ export type DeleteOptions = {
|
|
|
50
54
|
reason?: string;
|
|
51
55
|
};
|
|
52
56
|
/**
|
|
53
|
-
* Опции restore()
|
|
57
|
+
* Опции restore(): `{ expect }` — надгробия, которые видел пользователь (ответ delete() или withDeleted()), с
|
|
54
58
|
* `partial: true` — что можно; `{ all: true }` — осознанно вернуть все удалённые цели шага без сверки. Без expect и all —
|
|
55
|
-
* invalid_query
|
|
59
|
+
* invalid_query, и для явных id.
|
|
56
60
|
*/
|
|
57
61
|
export type RestoreOptions = {
|
|
58
62
|
expect: readonly Expected[];
|
|
@@ -98,7 +102,7 @@ export type PlanOp = ({
|
|
|
98
102
|
export declare function dropUndefined(data: Record<string, unknown>): Record<string, unknown>;
|
|
99
103
|
/** Причина изменения: строка до 1000 символов без NUL (её не принимает set_config). */
|
|
100
104
|
export declare function checkReason(v: unknown, where: string): string;
|
|
101
|
-
/** Значение
|
|
105
|
+
/** Значение шага роли: id, список id, строка ответа или ленивая цепочка (в той же транзакции). */
|
|
102
106
|
export type SlotValue = string | string[] | {
|
|
103
107
|
id: string;
|
|
104
108
|
} | null | {
|
|
@@ -111,7 +115,7 @@ export interface Slot {
|
|
|
111
115
|
op: 'set' | 'unset' | 'add' | 'remove';
|
|
112
116
|
value: SlotValue;
|
|
113
117
|
}
|
|
114
|
-
/** Строка ответа записи (результат upsert — $upsert,
|
|
118
|
+
/** Строка ответа записи (результат upsert — $upsert, предпросмотр удаления — $action — поля Row). */
|
|
115
119
|
export type WriteRow = Row;
|
|
116
120
|
export interface IncSpec {
|
|
117
121
|
path: string[];
|