letopis 2.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 +491 -414
- package/CHANGELOG.md +361 -0
- package/LICENSE +75 -21
- package/LICENSE-COMMERCIAL.md +49 -0
- package/README.md +969 -210
- package/api-contract.json +385 -0
- package/dist/acl.d.ts +7 -2
- package/dist/acl.js +19 -2
- package/dist/admin.d.ts +7 -1
- package/dist/admin.js +16 -9
- package/dist/auth.d.ts +47 -11
- package/dist/auth.js +129 -15
- package/dist/chain.d.ts +52 -25
- package/dist/chain.js +445 -64
- package/dist/cli.js +72 -20
- package/dist/errors.d.ts +23 -6
- package/dist/errors.js +52 -13
- package/dist/index.d.ts +32 -15
- package/dist/index.js +209 -68
- package/dist/internal.d.ts +2 -0
- package/dist/load.d.ts +10 -6
- package/dist/load.js +30 -11
- package/dist/log.d.ts +121 -0
- package/dist/log.js +654 -0
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +34 -16
- package/dist/messages/access.d.ts +10 -0
- package/dist/messages/access.js +13 -0
- package/dist/messages/api.d.ts +76 -0
- package/dist/messages/api.js +109 -7
- package/dist/messages/boot.d.ts +5 -0
- package/dist/messages/boot.js +11 -4
- package/dist/messages/codes.js +4 -4
- package/dist/messages/index.d.ts +5 -2
- package/dist/messages/index.js +5 -2
- package/dist/messages/log.d.ts +232 -0
- package/dist/messages/log.js +105 -0
- package/dist/messages/mcp.js +7 -7
- package/dist/messages/model.d.ts +40 -1
- package/dist/messages/model.js +52 -3
- package/dist/messages/query.d.ts +10 -0
- package/dist/messages/query.js +13 -0
- package/dist/messages/sql-core.d.ts +15 -1
- package/dist/messages/sql-core.js +20 -3
- package/dist/messages/sql-time.d.ts +15 -0
- package/dist/messages/sql-time.js +23 -0
- package/dist/messages/write.d.ts +52 -0
- package/dist/messages/write.js +77 -6
- package/dist/model.d.ts +10 -2
- package/dist/model.js +28 -6
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +6 -0
- package/dist/sql.d.ts +2 -0
- package/dist/sql.js +77 -25
- package/dist/sync.d.ts +4 -5
- package/dist/sync.js +53 -4
- package/dist/tx.d.ts +10 -1
- package/dist/tx.js +44 -2
- package/dist/typed.d.ts +18 -14
- package/dist/types.d.ts +91 -8
- package/dist/types.js +8 -3
- package/dist/up.d.ts +8 -5
- package/dist/up.js +71 -23
- package/dist/uuid.d.ts +8 -0
- package/dist/uuid.js +9 -1
- package/dist/validate.js +24 -7
- package/dist/watch.d.ts +5 -0
- package/dist/watch.js +13 -0
- package/dist/write.d.ts +84 -3
- package/dist/write.js +427 -40
- package/llms.txt +38 -17
- package/package.json +12 -6
- package/sql/20-context.sql +11 -3
- package/sql/30-validate.sql +51 -10
- package/sql/40-class.sql +21 -1
- package/sql/60-write.sql +18 -6
- package/sql/80-auth.sql +88 -17
- package/sql/90-time.sql +34 -6
- package/sql/99-revision.sql +5 -1
- package/sql/migrate/0009-restore-rev.tenants-name.sql +10 -0
- package/sql/migrate/0010-impersonation-no-account.sql +5 -0
- package/sql/migrate/0011-key-lock.otp-enabled.sql +7 -0
- package/sql/migrate/README.md +5 -1
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* letopis maintain --dsn … --schema v2.x [--token … | --api-key …]
|
|
14
14
|
* letopis mcp --dsn … --schema v2.x --token … [--write] — сервер MCP по stdio (план 1.1, §2.5); dsn, schema и
|
|
15
15
|
* token — ещё из LETOPIS_DSN, LETOPIS_SCHEMA, LETOPIS_TOKEN; только токен пользователя: --api-key — отказ
|
|
16
|
+
* --log[=уровень] — у любой команды (логирование 3.0): события JSON-строками в stderr, уровень debug|info|warn|error|off,
|
|
17
|
+
* без значения — info; без флага действует LETOPIS_LOG
|
|
16
18
|
* справка — USAGE (её же читает справочник сайта, guide/scripts/gen-api.ts)
|
|
17
19
|
*/
|
|
18
20
|
import { parseArgs } from 'node:util';
|
|
@@ -23,13 +25,16 @@ import { resolve } from 'node:path';
|
|
|
23
25
|
import { connect } from './index.js';
|
|
24
26
|
import { syncModels, generateTypes } from './sync.js';
|
|
25
27
|
import { lerr, lmsg } from './errors.js';
|
|
26
|
-
|
|
27
|
-
letopis
|
|
28
|
-
letopis
|
|
28
|
+
import { stderrLog } from './log.js';
|
|
29
|
+
const USAGE = `letopis sync <модуль> --dsn <строка> --schema <схема> (--token <токен> | --api-key <ключ>) [--apply | --check]
|
|
30
|
+
letopis types --dsn <строка> --schema <схема> (--token <токен> | --api-key <ключ>) [--out <файл>] [--system]
|
|
31
|
+
letopis load <файлы…> --dsn <строка> --schema <схема> (--token <токен> | --api-key <ключ>) [--class <Класс>]
|
|
29
32
|
[--duplicates error|first|last] [--existing error|skip|update] [--commit-every <N>] [--initial]
|
|
30
|
-
letopis maintain --dsn <строка> --schema
|
|
31
|
-
letopis mcp --dsn <строка> --schema
|
|
32
|
-
(или LETOPIS_DSN, LETOPIS_SCHEMA, LETOPIS_TOKEN; ключ сервиса --api-key не принимается)
|
|
33
|
+
letopis maintain --dsn <строка> --schema <схема> [--token <токен> | --api-key <ключ>]
|
|
34
|
+
letopis mcp --dsn <строка> --schema <схема> --token <токен пользователя> [--write]
|
|
35
|
+
(или LETOPIS_DSN, LETOPIS_SCHEMA, LETOPIS_TOKEN; ключ сервиса --api-key не принимается)
|
|
36
|
+
у любой команды: --log[=debug|info|warn|error|off] — события JSON-строками в stderr (без значения — info)
|
|
37
|
+
(<схема> — v2.<имя> или просто <имя>: shop — это v2.shop)`;
|
|
33
38
|
/** Команды CLI: остальное — USAGE с кодом 2 ещё до подключения. */
|
|
34
39
|
const COMMANDS = new Set(['sync', 'types', 'load', 'maintain', 'mcp']);
|
|
35
40
|
const isModel = (v) => typeof v === 'object' && v !== null && typeof v.describe === 'function' && typeof v.name === 'string';
|
|
@@ -38,17 +43,64 @@ async function loadModels(path) {
|
|
|
38
43
|
const mod = await import(pathToFileURL(resolve(path)).href);
|
|
39
44
|
return [...new Set(Object.values(mod).flatMap((v) => (Array.isArray(v) ? v : [v])).filter(isModel))];
|
|
40
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Флаг --log[=уровень] (логирование 3.0): вынимается из аргументов до parseArgs — у строкового флага parseArgs требует
|
|
48
|
+
* значение, а --log без него значит info.
|
|
49
|
+
*/
|
|
50
|
+
function logFlag(argv) {
|
|
51
|
+
let level;
|
|
52
|
+
const args = [];
|
|
53
|
+
for (const a of argv) {
|
|
54
|
+
if (a === '--log')
|
|
55
|
+
level = 'info';
|
|
56
|
+
else if (a.startsWith('--log='))
|
|
57
|
+
level = (a.slice('--log='.length) || 'info');
|
|
58
|
+
else
|
|
59
|
+
args.push(a);
|
|
60
|
+
}
|
|
61
|
+
return { args, log: level === undefined ? {} : { log: stderrLog, logLevel: level } };
|
|
62
|
+
}
|
|
63
|
+
/** Флаги CLI для parseArgs. */
|
|
64
|
+
const OPTIONS = {
|
|
65
|
+
dsn: { type: 'string' }, schema: { type: 'string' }, token: { type: 'string' }, 'api-key': { type: 'string' },
|
|
66
|
+
apply: { type: 'boolean' }, out: { type: 'string' }, help: { type: 'boolean' },
|
|
67
|
+
class: { type: 'string' }, duplicates: { type: 'string' }, existing: { type: 'string' }, 'commit-every': { type: 'string' },
|
|
68
|
+
initial: { type: 'boolean' }, write: { type: 'boolean' }, system: { type: 'boolean' }, check: { type: 'boolean' },
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Ошибка разбора аргументов parseArgs — русский текст каталога (английский текст Node — только для прочих ошибок);
|
|
72
|
+
* null — не ошибка разбора.
|
|
73
|
+
*/
|
|
74
|
+
function parseError(e) {
|
|
75
|
+
const code = e?.code;
|
|
76
|
+
const message = String(e?.message ?? '');
|
|
77
|
+
// имя флага или аргумент — первое в кавычках: «Unknown option '--aply'…», «Option '--dsn <value>' argument missing»
|
|
78
|
+
const quoted = (/'([^']*)'/.exec(message)?.[1] ?? '').split(' ')[0] ?? '';
|
|
79
|
+
if (code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION')
|
|
80
|
+
return lmsg('model.cli_unknown_flag', { flag: quoted });
|
|
81
|
+
if (code === 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE') {
|
|
82
|
+
// строковому флагу не дали значения или булевому дали
|
|
83
|
+
return lmsg(/does not take/i.test(message) ? 'model.cli_flag_no_value' : 'model.cli_flag_needs_value', { flag: quoted });
|
|
84
|
+
}
|
|
85
|
+
if (code === 'ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL')
|
|
86
|
+
return lmsg('model.cli_unexpected_arg', { arg: quoted });
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
41
89
|
export async function main(argv) {
|
|
42
|
-
const {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
options:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
90
|
+
const { args, log } = logFlag(argv);
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = parseArgs({ args, allowPositionals: true, options: OPTIONS });
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
const text = parseError(e);
|
|
97
|
+
if (text === null)
|
|
98
|
+
throw e;
|
|
99
|
+
// код выхода 1 — как у прочих ошибок до подключения (прежде — необработанное исключение parseArgs)
|
|
100
|
+
console.error(`letopis: ${text}`);
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
const { values, positionals } = parsed;
|
|
52
104
|
const [cmd, arg] = positionals;
|
|
53
105
|
// клиенты MCP передают строку подключения и токен окружением (план 1.1, §2.5) — только для этой команды
|
|
54
106
|
if (cmd === 'mcp') {
|
|
@@ -67,9 +119,9 @@ export async function main(argv) {
|
|
|
67
119
|
return cmd ? 2 : 0;
|
|
68
120
|
}
|
|
69
121
|
if (cmd === 'mcp')
|
|
70
|
-
return mcp(values);
|
|
122
|
+
return mcp(values, log);
|
|
71
123
|
// загрузчику нужно администраторское подключение (§2.6, п. 1)
|
|
72
|
-
const db = await connect({ dsn: values.dsn, schema: values.schema, token: values.token, apiKey: values['api-key'], max: 2,
|
|
124
|
+
const db = await connect({ dsn: values.dsn, schema: values.schema, token: values.token, apiKey: values['api-key'], max: 2, ...log,
|
|
73
125
|
// maintain работает и на администраторском подключении без сессии; классы со строками синхронизирует
|
|
74
126
|
// администраторское подключение с сессией арендатора
|
|
75
127
|
...(cmd === 'load' || cmd === 'maintain' || cmd === 'sync' ? { allowBypassRls: true } : {}) });
|
|
@@ -145,7 +197,7 @@ export async function main(argv) {
|
|
|
145
197
|
* грузятся только здесь: letopis --help и прочие команды работают без SDK. В stdout — только протокол (предупреждения
|
|
146
198
|
* connect() идут в stderr); процесс живёт, пока клиент не закроет stdin.
|
|
147
199
|
*/
|
|
148
|
-
async function mcp(v) {
|
|
200
|
+
async function mcp(v, log = {}) {
|
|
149
201
|
if (v['api-key'] !== undefined) {
|
|
150
202
|
console.error(lerr('model.mcp_api_key_refused').message);
|
|
151
203
|
return 2;
|
|
@@ -155,7 +207,7 @@ async function mcp(v) {
|
|
|
155
207
|
return 2;
|
|
156
208
|
}
|
|
157
209
|
const { createMcpServer } = await import('./mcp.js');
|
|
158
|
-
const db = await connect({ dsn: v.dsn, schema: v.schema, token: v.token, max: 4 });
|
|
210
|
+
const db = await connect({ dsn: v.dsn, schema: v.schema, token: v.token, max: 4, ...log });
|
|
159
211
|
try {
|
|
160
212
|
const opts = { write: v.write === true };
|
|
161
213
|
// учётные данные — до начала протокола: отказ виден кодом выхода и текстом в stderr
|
package/dist/errors.d.ts
CHANGED
|
@@ -8,13 +8,26 @@ export interface Issue {
|
|
|
8
8
|
}
|
|
9
9
|
/** Текст записи каталога с параметрами (params.similar — «— похожие: …»): сообщения нарушений, описания, отчёты CLI. */
|
|
10
10
|
export declare function lmsg(key: MsgKey, params?: Params): string;
|
|
11
|
+
/** Логгер, в который уходит предупреждение (lib/src/log.ts, Log): только то, что нужно lwarn. */
|
|
12
|
+
interface WarnSink {
|
|
13
|
+
readonly active: boolean;
|
|
14
|
+
warning(key: string | undefined, msg: string, detail?: Record<string, unknown>): void;
|
|
15
|
+
}
|
|
16
|
+
export declare function setEnvWarnSink(fn: () => WarnSink): void;
|
|
11
17
|
/**
|
|
12
18
|
* Предупреждение по записи каталога — «letopis: <текст>» в console.warn; once — ключ «один раз на процесс». Неизвестное
|
|
13
19
|
* поле фильтра, sort() и агрегата с 2.0 — уже не предупреждение, а отказ query.unknown_field (sql.ts, checkField).
|
|
20
|
+
* Логирование 3.0: задан логгер (opts.log подключения или LETOPIS_LOG) — событие warning в него вместо console.warn.
|
|
14
21
|
*/
|
|
15
22
|
export declare function lwarn(key: MsgKey, params?: Params, opts?: {
|
|
16
23
|
once?: string;
|
|
24
|
+
log?: WarnSink;
|
|
17
25
|
}): void;
|
|
26
|
+
/**
|
|
27
|
+
* Готовый текст предупреждения: в логгер (событие warning с ключом и параметрами), если он задан, иначе console.warn.
|
|
28
|
+
* Ошибка логгера предупреждение не теряет и операцию не прерывает.
|
|
29
|
+
*/
|
|
30
|
+
export declare function warnText(text: string, key?: string, log?: WarnSink, params?: Params): void;
|
|
18
31
|
export declare class LetopisError extends Error {
|
|
19
32
|
readonly code: string;
|
|
20
33
|
readonly detail: unknown;
|
|
@@ -46,7 +59,12 @@ export declare function similar(name: string, candidates: Iterable<string>, n?:
|
|
|
46
59
|
* 7.9 №6). known — все ключи опций: объект `satisfies Record<keyof Опции, true>`, пропуск ключа — ошибка типов.
|
|
47
60
|
*/
|
|
48
61
|
export declare function checkOptions(where: string, opts: unknown, known: Readonly<Record<string, true>>): void;
|
|
49
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Первое нарушение для текста ошибки (план 2.1, A8): «: <путь> — <сообщение>», при нескольких — ещё их число. Полный
|
|
64
|
+
* список — err.issues.
|
|
65
|
+
*/
|
|
66
|
+
export declare function issuesSuffix(issues: readonly Issue[]): string;
|
|
67
|
+
/** Данные не проходят описание класса (`invalid_data`); текст — по ключу (по умолчанию api.validation_failed) и первое нарушение. */
|
|
50
68
|
export declare class ValidationError extends LetopisError {
|
|
51
69
|
readonly issues: Issue[];
|
|
52
70
|
constructor(cls: string, issues: Issue[], msg?: {
|
|
@@ -66,10 +84,9 @@ export declare function issuesFromDb(issues: unknown): Issue[];
|
|
|
66
84
|
*/
|
|
67
85
|
export declare function findingsFromDb<T>(rows: readonly T[]): T[];
|
|
68
86
|
/**
|
|
69
|
-
* Ошибка базы → LetopisError (ValidationError для invalid_data с нарушениями)
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* ошибки возвращаются как есть: на SQLSTATE конфликта держатся повторы (tx.ts).
|
|
87
|
+
* Ошибка базы → LetopisError (ValidationError для invalid_data с нарушениями) — для ошибок сырого SQL (`db.sql`,
|
|
88
|
+
* `db.transaction()`): тот же код, ключ и подробности, что у цепочек. Подробности ошибки базы переходят в `detail`, ключ
|
|
89
|
+
* каталога и параметры — в `key` и `params`, текст — русский; без ключа — текст базы. Значение не того типа (22P02) —
|
|
90
|
+
* `invalid_query`. Повторяемый конфликт (40001, 40P01) и прочие ошибки возвращаются как есть.
|
|
74
91
|
*/
|
|
75
92
|
export declare function fromDbError(e: unknown): unknown;
|
package/dist/errors.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Ошибки letopis 1.0 (план, §7.1, этап 2, п. 10). Текст — «letopis: <код>: <сообщение>», как у ошибок
|
|
3
3
|
* базы; код — имя из каталога (`error_catalog()` в lib/sql/15-errors.sql) или код самой библиотеки.
|
|
4
4
|
*
|
|
5
|
-
* Каталог
|
|
5
|
+
* Каталог сообщений: ошибка по ключу — `lerr(ключ, параметры)`, текст —
|
|
6
6
|
* шаблон записи с параметрами. У каждой ошибки — подсказка «что делать» (`hint`) и ссылка на строку её кода в разделе
|
|
7
7
|
* «Ошибки» README на GitHub по тегу версии (`docs`). База собирает текст по ключу и параметрам из того же шаблона и
|
|
8
|
-
*
|
|
8
|
+
* передаёт ключ и параметры вместе с ошибкой, а fromDbError переносит их в поля ошибки (`key`, `params`); без ключа —
|
|
9
9
|
* текст базы как есть. У нарушений (validate_data) и строк отказа ужесточения key и params наружу не идут: issuesFromDb,
|
|
10
10
|
* findingsFromDb.
|
|
11
11
|
*/
|
|
@@ -30,9 +30,15 @@ export function lmsg(key, params) {
|
|
|
30
30
|
return textOf(key, params);
|
|
31
31
|
}
|
|
32
32
|
const warned = new Set();
|
|
33
|
+
/** Логгер по LETOPIS_LOG для предупреждений без подключения — ставит log.ts (без цикла импорта значений). */
|
|
34
|
+
let envSink = null;
|
|
35
|
+
export function setEnvWarnSink(fn) {
|
|
36
|
+
envSink = fn;
|
|
37
|
+
}
|
|
33
38
|
/**
|
|
34
39
|
* Предупреждение по записи каталога — «letopis: <текст>» в console.warn; once — ключ «один раз на процесс». Неизвестное
|
|
35
40
|
* поле фильтра, sort() и агрегата с 2.0 — уже не предупреждение, а отказ query.unknown_field (sql.ts, checkField).
|
|
41
|
+
* Логирование 3.0: задан логгер (opts.log подключения или LETOPIS_LOG) — событие warning в него вместо console.warn.
|
|
36
42
|
*/
|
|
37
43
|
export function lwarn(key, params, opts = {}) {
|
|
38
44
|
if (opts.once !== undefined) {
|
|
@@ -42,7 +48,24 @@ export function lwarn(key, params, opts = {}) {
|
|
|
42
48
|
warned.clear();
|
|
43
49
|
warned.add(opts.once);
|
|
44
50
|
}
|
|
45
|
-
|
|
51
|
+
warnText(`letopis: ${textOf(key, params)}`, key, opts.log, params);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Готовый текст предупреждения: в логгер (событие warning с ключом и параметрами), если он задан, иначе console.warn.
|
|
55
|
+
* Ошибка логгера предупреждение не теряет и операцию не прерывает.
|
|
56
|
+
*/
|
|
57
|
+
export function warnText(text, key, log, params) {
|
|
58
|
+
const sink = log?.active ? log : envSink?.();
|
|
59
|
+
if (sink?.active) {
|
|
60
|
+
try {
|
|
61
|
+
sink.warning(key, text, params && Object.keys(params).length ? { ...params } : undefined);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// логгер не принял — как без него
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
console.warn(text);
|
|
46
69
|
}
|
|
47
70
|
export class LetopisError extends Error {
|
|
48
71
|
code;
|
|
@@ -120,12 +143,24 @@ export function checkOptions(where, opts, known) {
|
|
|
120
143
|
throw lerr('api.unknown_option', { where, name: k, similar: similar(k, Object.keys(known)) });
|
|
121
144
|
}
|
|
122
145
|
}
|
|
123
|
-
/**
|
|
146
|
+
/**
|
|
147
|
+
* Первое нарушение для текста ошибки (план 2.1, A8): «: <путь> — <сообщение>», при нескольких — ещё их число. Полный
|
|
148
|
+
* список — err.issues.
|
|
149
|
+
*/
|
|
150
|
+
export function issuesSuffix(issues) {
|
|
151
|
+
const first = issues[0];
|
|
152
|
+
if (!first)
|
|
153
|
+
return '';
|
|
154
|
+
const path = Array.isArray(first.path) && first.path.length ? first.path.join('.') : textOf('api.validation_root', undefined);
|
|
155
|
+
const more = issues.length > 1 ? textOf('api.validation_more', { n: issues.length }) : '';
|
|
156
|
+
return `: ${textOf('api.validation_issue', { path, issue: first.message })}${more}`;
|
|
157
|
+
}
|
|
158
|
+
/** Данные не проходят описание класса (`invalid_data`); текст — по ключу (по умолчанию api.validation_failed) и первое нарушение. */
|
|
124
159
|
export class ValidationError extends LetopisError {
|
|
125
160
|
issues;
|
|
126
161
|
constructor(cls, issues, msg) {
|
|
127
162
|
const m = msg ?? { key: 'api.validation_failed', params: { cls } };
|
|
128
|
-
super('invalid_data', textOf(m.key, m.params), { issues }, m);
|
|
163
|
+
super('invalid_data', textOf(m.key, m.params) + issuesSuffix(issues), { issues }, m);
|
|
129
164
|
this.name = 'ValidationError';
|
|
130
165
|
this.issues = issues;
|
|
131
166
|
}
|
|
@@ -178,11 +213,10 @@ function parseDetail(detail) {
|
|
|
178
213
|
}
|
|
179
214
|
}
|
|
180
215
|
/**
|
|
181
|
-
* Ошибка базы → LetopisError (ValidationError для invalid_data с нарушениями)
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* ошибки возвращаются как есть: на SQLSTATE конфликта держатся повторы (tx.ts).
|
|
216
|
+
* Ошибка базы → LetopisError (ValidationError для invalid_data с нарушениями) — для ошибок сырого SQL (`db.sql`,
|
|
217
|
+
* `db.transaction()`): тот же код, ключ и подробности, что у цепочек. Подробности ошибки базы переходят в `detail`, ключ
|
|
218
|
+
* каталога и параметры — в `key` и `params`, текст — русский; без ключа — текст базы. Значение не того типа (22P02) —
|
|
219
|
+
* `invalid_query`. Повторяемый конфликт (40001, 40P01) и прочие ошибки возвращаются как есть.
|
|
186
220
|
*/
|
|
187
221
|
export function fromDbError(e) {
|
|
188
222
|
const err = e;
|
|
@@ -192,6 +226,10 @@ export function fromDbError(e) {
|
|
|
192
226
|
if (err.code === '42501' && err.routine === 'ExecWithCheckOptions' && !/USING/.test(err.message ?? '')) {
|
|
193
227
|
return lerr('api.row_check_denied', undefined, { cause: e });
|
|
194
228
|
}
|
|
229
|
+
// значение не того типа (22P02: «invalid input syntax for type uuid» и т. п.), которое не поймала проверка до запроса,
|
|
230
|
+
// — ошибка letopis, а не сырая ошибка PostgreSQL (план 2.1, A7); 40001 и 40P01 остаются как есть — на них держатся повторы
|
|
231
|
+
if (err.code === '22P02')
|
|
232
|
+
return lerr('api.db_bad_value', { text: err.message ?? '' }, { cause: e });
|
|
195
233
|
let name;
|
|
196
234
|
let detail;
|
|
197
235
|
if (err.code === 'XX000') {
|
|
@@ -225,9 +263,10 @@ export function fromDbError(e) {
|
|
|
225
263
|
const issues = detail?.issues;
|
|
226
264
|
if (name === 'invalid_data' && Array.isArray(issues)) {
|
|
227
265
|
const cls = msg ? String(msg.params.cls ?? '?') : /класса (\S+)/.exec(text)?.[1] ?? '?';
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
266
|
+
const list = issuesFromDb(issues);
|
|
267
|
+
const v = new ValidationError(cls, list, msg);
|
|
268
|
+
// текст — тот, что собрала база по шаблону записи, и первое нарушение (план 2.1, A8)
|
|
269
|
+
return Object.assign(v, { cause: e, ...(text ? { message: `letopis: invalid_data: ${text}${issuesSuffix(list)}` } : {}) });
|
|
231
270
|
}
|
|
232
271
|
return Object.assign(new LetopisError(name, text, detail, msg), { cause: e });
|
|
233
272
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { type AuthApi } from './auth.js';
|
|
|
14
14
|
import { type AclApi } from './acl.js';
|
|
15
15
|
import { type LoadInput, type LoadOptions, type LoadReport } from './load.js';
|
|
16
16
|
import { type Batch, type Chain, type ChainRef, type StepFn } from './chain.js';
|
|
17
|
-
import type { QueryEvent, Row } from './types.js';
|
|
17
|
+
import type { LogOptions, QueryEvent, Row } from './types.js';
|
|
18
18
|
import { type CacheOptions, type CacheStats } from './cache.js';
|
|
19
19
|
import { Subscription, type WatchOptions } from './watch.js';
|
|
20
20
|
import { type MaintainReport } from './admin.js';
|
|
@@ -29,12 +29,11 @@ export { ne, gt, gte, lt, lte, between, inList, like, ilike, starts, ends, has,
|
|
|
29
29
|
/**
|
|
30
30
|
* Хотя бы один из фильтров: `db.Product(or({ sku: 'K-1' }, { price: lt(500) }))` — товар K-1 или дешевле 500. Ставится
|
|
31
31
|
* вместо всего фильтра шага, а не значением поля; условия внутри одного фильтра соединяются через «и». В шаге подключения
|
|
32
|
-
* с моделями (`TypedDb`) фильтры внутри — поля и роли класса шага: тип им даёт
|
|
33
|
-
* фабрика, что в реестре операторов (ops.ts), — другой у неё только тип.
|
|
32
|
+
* с моделями (`TypedDb`) фильтры внутри — поля и роли класса шага: тип им даёт шаг.
|
|
34
33
|
*/
|
|
35
34
|
export declare const or: <F extends object>(...filters: NoInfer<F>[]) => OrOp<F>;
|
|
36
35
|
export { cursorOf } from './sql.js';
|
|
37
|
-
export { inc, isInc, type IncMarker, type WriteRow } from './write.js';
|
|
36
|
+
export { inc, isInc, type DeleteOptions, type Expected, type IncMarker, type RestoreOptions, type WriteRow } from './write.js';
|
|
38
37
|
export { totpCode, hashPassword, verifyHash, type AuthApi, type LoginResult, type Credential, type SessionInfo } from './auth.js';
|
|
39
38
|
export type { AclApi, AclDecision, AclDataDecision, AclOp, AclRule, AclRuleSpec, AclTarget } from './acl.js';
|
|
40
39
|
export type { LoadOptions, LoadReport, LoadIssue, LoadInput } from './load.js';
|
|
@@ -47,13 +46,15 @@ export type { WatchOptions, WatchEvent, Subscription } from './watch.js';
|
|
|
47
46
|
export type { MaintainReport } from './admin.js';
|
|
48
47
|
export { up, createTenant, type UpOptions, type UpResult, type TenantOptions, type TenantResult, type LoginOptions } from './up.js';
|
|
49
48
|
export { RESERVED_CLASS_NAMES, CHAIN_METHODS, DB_METHODS } from './types.js';
|
|
50
|
-
export
|
|
49
|
+
export type { LogEvent, LogFn, LogLevel, LogObject, LogOptions, Logger } from './types.js';
|
|
50
|
+
export { LOG_EVENTS, type LogEventName } from './messages/index.js';
|
|
51
|
+
export interface ConnectOptions extends LogOptions {
|
|
51
52
|
/** Строка подключения; либо готовый пул postgres.js в `sql`. */
|
|
52
53
|
dsn?: string;
|
|
53
54
|
sql?: postgres.Sql;
|
|
54
|
-
/** Схема установки, например `v2.salon`. */
|
|
55
|
+
/** Схема установки, например `v2.salon`; имя без точки (`salon`) — как у `up()`: `v2.salon`. */
|
|
55
56
|
schema: string;
|
|
56
|
-
/** Токен сессии; либо ключ
|
|
57
|
+
/** Токен сессии; либо ключ API в `apiKey` (он же `service` — ключ сервиса) — его обменяют на сессию. */
|
|
57
58
|
token?: string;
|
|
58
59
|
apiKey?: string;
|
|
59
60
|
service?: string;
|
|
@@ -119,7 +120,15 @@ export interface DbCore {
|
|
|
119
120
|
* класса. Сами права база применяет при каждом запросе.
|
|
120
121
|
*/
|
|
121
122
|
readonly acl: AclApi;
|
|
122
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* Сырой SQL под сессией и RLS, одной транзакцией (в `db.begin()` — в ней); права те же, что у цепочек. Путь поиска
|
|
125
|
+
* схем у запроса — по умолчанию PostgreSQL, поэтому таблицы `entity`, `log`, `secret` пишутся со схемой установки в
|
|
126
|
+
* двойных кавычках, значения — параметрами шаблона:
|
|
127
|
+
* ```ts
|
|
128
|
+
* const rows = await db.sql`select id, data->>'name' as name from "v2.shop".entity where class = ${'Shop'}`;
|
|
129
|
+
* ```
|
|
130
|
+
* Без схемы (`from entity`) — ошибка PostgreSQL 42P01 «relation does not exist».
|
|
131
|
+
*/
|
|
123
132
|
sql(strings: TemplateStringsArray, ...values: unknown[]): Promise<postgres.Row[]>;
|
|
124
133
|
/** Транзакция под сессией: `fn` получает соединение postgres.js (в `db.begin()` — ту же транзакцию). */
|
|
125
134
|
transaction<T>(fn: (tx: Tx) => Promise<T>): Promise<T>;
|
|
@@ -160,14 +169,15 @@ export interface DbCore {
|
|
|
160
169
|
}): Promise<DbTx>;
|
|
161
170
|
/**
|
|
162
171
|
* Массовая загрузка строк любых классов одной транзакцией (или порциями `commitEvery`) — быстрее записи цепочками.
|
|
163
|
-
* Строки проверяются по тому же описанию класса, что и при обычной записи (данные,
|
|
164
|
-
* командой `COPY`, а
|
|
172
|
+
* Строки проверяются по тому же описанию класса, что и при обычной записи (данные, концы, ключ), уходят в базу
|
|
173
|
+
* командой `COPY`, а концы к объектам вне загрузки сверяет один запрос в конце; права проверяются только при загрузке
|
|
165
174
|
* от имени пользователя (опция `as`). Работает только на администраторском подключении, иначе `admin_required`.
|
|
166
175
|
*/
|
|
167
176
|
load(input: LoadInput, opts?: LoadOptions): Promise<LoadReport>;
|
|
168
177
|
/**
|
|
169
|
-
* Именованная очередь планов
|
|
170
|
-
* у каждого `db.as()` и у каждой `db.begin()` очереди свои: план исполняется от имени
|
|
178
|
+
* Именованная очередь планов подключения (db, db.as(), db.begin()): `batch.Класс(…).create(…)`; `batch.run()` — одной
|
|
179
|
+
* транзакцией. У корня, у каждого `db.as()` и у каждой `db.begin()` очереди свои: план исполняется от имени того
|
|
180
|
+
* подключения, которое его поставило.
|
|
171
181
|
*/
|
|
172
182
|
batch(name: string, opts?: {
|
|
173
183
|
reason?: string;
|
|
@@ -178,6 +188,13 @@ export interface DbCore {
|
|
|
178
188
|
commit(tr?: DbTx): Promise<void>;
|
|
179
189
|
/** Откатить транзакцию `tr` (то же, что `tr.rollback()`). */
|
|
180
190
|
rollback(tr?: DbTx): Promise<void>;
|
|
191
|
+
/**
|
|
192
|
+
* Закрыть подключение: у корня — пул, подписки и обслуживание по расписанию (идущий запуск `maintain()` close() ждёт),
|
|
193
|
+
* в `db.begin()` — откат транзакции; у `db.as()` ничего не делает (пул общий с корнем). После `close()` корня любой
|
|
194
|
+
* вызов этого подключения и производных от него (`db.as()`, `db.begin()`, цепочки, созданные раньше) — `invalid_query`
|
|
195
|
+
* «подключение закрыто (db.close()) — откройте новое connect()»; пул заново не открывается, процесс может завершиться.
|
|
196
|
+
* Повторный `close()` — без ошибки.
|
|
197
|
+
*/
|
|
181
198
|
close(): Promise<void>;
|
|
182
199
|
}
|
|
183
200
|
/** Подключение: служебные методы и классы как стартовые шаги цепочек — `db.Класс(фильтр)`. */
|
|
@@ -188,15 +205,15 @@ export type Db = DbCore & {
|
|
|
188
205
|
export type DbTx = Db;
|
|
189
206
|
/**
|
|
190
207
|
* Подключение со строгими типами по моделям: `connect({ models })` или `connect<Models>()` с объявлениями
|
|
191
|
-
* `letopis types`. Шаг — только известный класс или алиас (модели и системные классы), переходы — по концам,
|
|
192
|
-
* класса; имена, известные только во время выполнения, — `loose(db)
|
|
208
|
+
* `letopis types`. Шаг — только известный класс или алиас (модели и системные классы), переходы — по концам, шаги ролей
|
|
209
|
+
* после глагола — роли класса; имена, известные только во время выполнения, — `loose(db)`. Модели без литеральных
|
|
193
210
|
* имён (`AnyModel[]` вместо списка `as const`) строгим типам не помогут — тогда это `Db`.
|
|
194
211
|
*/
|
|
195
212
|
export type TypedDb<M extends readonly AnyModelLike[]> = string extends M[number]['name'] ? Db : DbCore & StepsOf<M>;
|
|
196
213
|
export type { Batch };
|
|
197
214
|
/**
|
|
198
215
|
* То же подключение без строгих типов: шагом может быть любое имя, в том числе известное только во время выполнения, —
|
|
199
|
-
* `loose(db)[имя]()`. Объект тот же: проверки во время выполнения (неизвестное имя или поле, переход,
|
|
216
|
+
* `loose(db)[имя]()`. Объект тот же: проверки во время выполнения (неизвестное имя или поле, переход, шаг роли) не меняются.
|
|
200
217
|
*/
|
|
201
218
|
export declare function loose(db: DbCore): Db;
|
|
202
219
|
export declare function connect<const M extends readonly AnyModelLike[]>(opts: ConnectOptions & {
|