letopis 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT-CHEATSHEET.md +484 -414
- package/CHANGELOG.md +321 -0
- package/LICENSE +75 -21
- package/LICENSE-COMMERCIAL.md +49 -0
- package/README.md +786 -183
- package/api-contract.json +381 -0
- package/dist/acl.d.ts +3 -0
- package/dist/acl.js +19 -2
- package/dist/admin.d.ts +2 -1
- package/dist/admin.js +6 -4
- package/dist/auth.d.ts +18 -3
- package/dist/auth.js +75 -6
- package/dist/chain.d.ts +38 -17
- package/dist/chain.js +434 -63
- package/dist/cli.js +28 -6
- package/dist/errors.d.ts +19 -1
- package/dist/errors.js +46 -6
- package/dist/index.d.ts +10 -4
- package/dist/index.js +136 -46
- package/dist/internal.d.ts +2 -0
- package/dist/load.d.ts +9 -5
- package/dist/load.js +30 -11
- package/dist/log.d.ts +121 -0
- package/dist/log.js +633 -0
- package/dist/mcp.js +26 -11
- package/dist/messages/api.d.ts +66 -0
- package/dist/messages/api.js +90 -1
- package/dist/messages/boot.js +4 -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 +229 -0
- package/dist/messages/log.js +102 -0
- package/dist/messages/model.d.ts +24 -1
- package/dist/messages/model.js +34 -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 +47 -0
- package/dist/messages/write.js +63 -0
- package/dist/model.d.ts +4 -0
- package/dist/model.js +23 -5
- package/dist/sql.d.ts +2 -0
- package/dist/sql.js +77 -25
- package/dist/sync.d.ts +1 -1
- package/dist/sync.js +50 -0
- package/dist/tx.d.ts +10 -1
- package/dist/tx.js +44 -2
- package/dist/typed.d.ts +14 -10
- package/dist/types.d.ts +82 -4
- package/dist/types.js +6 -1
- package/dist/up.d.ts +3 -3
- package/dist/up.js +57 -17
- 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 +78 -1
- package/dist/write.js +357 -39
- package/llms.txt +37 -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/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,15 @@ 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';
|
|
28
|
+
import { stderrLog } from './log.js';
|
|
26
29
|
const USAGE = `letopis sync <модуль> --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--apply | --check]
|
|
27
30
|
letopis types --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--out <файл>] [--system]
|
|
28
31
|
letopis load <файлы…> --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--class <Класс>]
|
|
29
32
|
[--duplicates error|first|last] [--existing error|skip|update] [--commit-every <N>] [--initial]
|
|
30
33
|
letopis maintain --dsn <строка> --schema <v2.имя> [--token <токен> | --api-key <ключ>]
|
|
31
34
|
letopis mcp --dsn <строка> --schema <v2.имя> --token <токен пользователя> [--write]
|
|
32
|
-
(или LETOPIS_DSN, LETOPIS_SCHEMA, LETOPIS_TOKEN; ключ сервиса --api-key не принимается)
|
|
35
|
+
(или LETOPIS_DSN, LETOPIS_SCHEMA, LETOPIS_TOKEN; ключ сервиса --api-key не принимается)
|
|
36
|
+
у любой команды: --log[=debug|info|warn|error|off] — события JSON-строками в stderr (без значения — info)`;
|
|
33
37
|
/** Команды CLI: остальное — USAGE с кодом 2 ещё до подключения. */
|
|
34
38
|
const COMMANDS = new Set(['sync', 'types', 'load', 'maintain', 'mcp']);
|
|
35
39
|
const isModel = (v) => typeof v === 'object' && v !== null && typeof v.describe === 'function' && typeof v.name === 'string';
|
|
@@ -38,9 +42,27 @@ async function loadModels(path) {
|
|
|
38
42
|
const mod = await import(pathToFileURL(resolve(path)).href);
|
|
39
43
|
return [...new Set(Object.values(mod).flatMap((v) => (Array.isArray(v) ? v : [v])).filter(isModel))];
|
|
40
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Флаг --log[=уровень] (логирование 3.0): вынимается из аргументов до parseArgs — у строкового флага parseArgs требует
|
|
47
|
+
* значение, а --log без него значит info.
|
|
48
|
+
*/
|
|
49
|
+
function logFlag(argv) {
|
|
50
|
+
let level;
|
|
51
|
+
const args = [];
|
|
52
|
+
for (const a of argv) {
|
|
53
|
+
if (a === '--log')
|
|
54
|
+
level = 'info';
|
|
55
|
+
else if (a.startsWith('--log='))
|
|
56
|
+
level = (a.slice('--log='.length) || 'info');
|
|
57
|
+
else
|
|
58
|
+
args.push(a);
|
|
59
|
+
}
|
|
60
|
+
return { args, log: level === undefined ? {} : { log: stderrLog, logLevel: level } };
|
|
61
|
+
}
|
|
41
62
|
export async function main(argv) {
|
|
63
|
+
const { args, log } = logFlag(argv);
|
|
42
64
|
const { values, positionals } = parseArgs({
|
|
43
|
-
args
|
|
65
|
+
args,
|
|
44
66
|
allowPositionals: true,
|
|
45
67
|
options: {
|
|
46
68
|
dsn: { type: 'string' }, schema: { type: 'string' }, token: { type: 'string' }, 'api-key': { type: 'string' },
|
|
@@ -67,9 +89,9 @@ export async function main(argv) {
|
|
|
67
89
|
return cmd ? 2 : 0;
|
|
68
90
|
}
|
|
69
91
|
if (cmd === 'mcp')
|
|
70
|
-
return mcp(values);
|
|
92
|
+
return mcp(values, log);
|
|
71
93
|
// загрузчику нужно администраторское подключение (§2.6, п. 1)
|
|
72
|
-
const db = await connect({ dsn: values.dsn, schema: values.schema, token: values.token, apiKey: values['api-key'], max: 2,
|
|
94
|
+
const db = await connect({ dsn: values.dsn, schema: values.schema, token: values.token, apiKey: values['api-key'], max: 2, ...log,
|
|
73
95
|
// maintain работает и на администраторском подключении без сессии; классы со строками синхронизирует
|
|
74
96
|
// администраторское подключение с сессией арендатора
|
|
75
97
|
...(cmd === 'load' || cmd === 'maintain' || cmd === 'sync' ? { allowBypassRls: true } : {}) });
|
|
@@ -145,7 +167,7 @@ export async function main(argv) {
|
|
|
145
167
|
* грузятся только здесь: letopis --help и прочие команды работают без SDK. В stdout — только протокол (предупреждения
|
|
146
168
|
* connect() идут в stderr); процесс живёт, пока клиент не закроет stdin.
|
|
147
169
|
*/
|
|
148
|
-
async function mcp(v) {
|
|
170
|
+
async function mcp(v, log = {}) {
|
|
149
171
|
if (v['api-key'] !== undefined) {
|
|
150
172
|
console.error(lerr('model.mcp_api_key_refused').message);
|
|
151
173
|
return 2;
|
|
@@ -155,7 +177,7 @@ async function mcp(v) {
|
|
|
155
177
|
return 2;
|
|
156
178
|
}
|
|
157
179
|
const { createMcpServer } = await import('./mcp.js');
|
|
158
|
-
const db = await connect({ dsn: v.dsn, schema: v.schema, token: v.token, max: 4 });
|
|
180
|
+
const db = await connect({ dsn: v.dsn, schema: v.schema, token: v.token, max: 4, ...log });
|
|
159
181
|
try {
|
|
160
182
|
const opts = { write: v.write === true };
|
|
161
183
|
// учётные данные — до начала протокола: отказ виден кодом выхода и текстом в 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?: {
|
package/dist/errors.js
CHANGED
|
@@ -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
|
}
|
|
@@ -192,6 +227,10 @@ export function fromDbError(e) {
|
|
|
192
227
|
if (err.code === '42501' && err.routine === 'ExecWithCheckOptions' && !/USING/.test(err.message ?? '')) {
|
|
193
228
|
return lerr('api.row_check_denied', undefined, { cause: e });
|
|
194
229
|
}
|
|
230
|
+
// значение не того типа (22P02: «invalid input syntax for type uuid» и т. п.), которое не поймала проверка до запроса,
|
|
231
|
+
// — ошибка letopis, а не сырая ошибка PostgreSQL (план 2.1, A7); 40001 и 40P01 остаются как есть — на них держатся повторы
|
|
232
|
+
if (err.code === '22P02')
|
|
233
|
+
return lerr('api.db_bad_value', { text: err.message ?? '' }, { cause: e });
|
|
195
234
|
let name;
|
|
196
235
|
let detail;
|
|
197
236
|
if (err.code === 'XX000') {
|
|
@@ -225,9 +264,10 @@ export function fromDbError(e) {
|
|
|
225
264
|
const issues = detail?.issues;
|
|
226
265
|
if (name === 'invalid_data' && Array.isArray(issues)) {
|
|
227
266
|
const cls = msg ? String(msg.params.cls ?? '?') : /класса (\S+)/.exec(text)?.[1] ?? '?';
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
267
|
+
const list = issuesFromDb(issues);
|
|
268
|
+
const v = new ValidationError(cls, list, msg);
|
|
269
|
+
// текст — тот, что собрала база по шаблону записи, и первое нарушение (план 2.1, A8)
|
|
270
|
+
return Object.assign(v, { cause: e, ...(text ? { message: `letopis: invalid_data: ${text}${issuesSuffix(list)}` } : {}) });
|
|
231
271
|
}
|
|
232
272
|
return Object.assign(new LetopisError(name, text, detail, msg), { cause: e });
|
|
233
273
|
}
|
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';
|
|
@@ -34,7 +34,7 @@ export { ne, gt, gte, lt, lte, between, inList, like, ilike, starts, ends, has,
|
|
|
34
34
|
*/
|
|
35
35
|
export declare const or: <F extends object>(...filters: NoInfer<F>[]) => OrOp<F>;
|
|
36
36
|
export { cursorOf } from './sql.js';
|
|
37
|
-
export { inc, isInc, type IncMarker, type WriteRow } from './write.js';
|
|
37
|
+
export { inc, isInc, type DeleteOptions, type Expected, type IncMarker, type RestoreOptions, type WriteRow } from './write.js';
|
|
38
38
|
export { totpCode, hashPassword, verifyHash, type AuthApi, type LoginResult, type Credential, type SessionInfo } from './auth.js';
|
|
39
39
|
export type { AclApi, AclDecision, AclDataDecision, AclOp, AclRule, AclRuleSpec, AclTarget } from './acl.js';
|
|
40
40
|
export type { LoadOptions, LoadReport, LoadIssue, LoadInput } from './load.js';
|
|
@@ -47,11 +47,13 @@ export type { WatchOptions, WatchEvent, Subscription } from './watch.js';
|
|
|
47
47
|
export type { MaintainReport } from './admin.js';
|
|
48
48
|
export { up, createTenant, type UpOptions, type UpResult, type TenantOptions, type TenantResult, type LoginOptions } from './up.js';
|
|
49
49
|
export { RESERVED_CLASS_NAMES, CHAIN_METHODS, DB_METHODS } from './types.js';
|
|
50
|
-
export
|
|
50
|
+
export type { LogEvent, LogFn, LogLevel, LogObject, LogOptions, Logger } from './types.js';
|
|
51
|
+
export { LOG_EVENTS, type LogEventName } from './messages/index.js';
|
|
52
|
+
export interface ConnectOptions extends LogOptions {
|
|
51
53
|
/** Строка подключения; либо готовый пул postgres.js в `sql`. */
|
|
52
54
|
dsn?: string;
|
|
53
55
|
sql?: postgres.Sql;
|
|
54
|
-
/** Схема установки, например `v2.salon`. */
|
|
56
|
+
/** Схема установки, например `v2.salon`; имя без точки (`salon`) — как у `up()`: `v2.salon`. */
|
|
55
57
|
schema: string;
|
|
56
58
|
/** Токен сессии; либо ключ `APIKEY` в `apiKey` (он же `service` — ключ сервиса) — его обменяют на сессию. */
|
|
57
59
|
token?: string;
|
|
@@ -178,6 +180,10 @@ export interface DbCore {
|
|
|
178
180
|
commit(tr?: DbTx): Promise<void>;
|
|
179
181
|
/** Откатить транзакцию `tr` (то же, что `tr.rollback()`). */
|
|
180
182
|
rollback(tr?: DbTx): Promise<void>;
|
|
183
|
+
/**
|
|
184
|
+
* Закрыть подключение: у корня — пул и подписки, в `db.begin()` — откат транзакции; у фасада `db.as()` ничего не делает
|
|
185
|
+
* (пул общий с корнем).
|
|
186
|
+
*/
|
|
181
187
|
close(): Promise<void>;
|
|
182
188
|
}
|
|
183
189
|
/** Подключение: служебные методы и классы как стартовые шаги цепочек — `db.Класс(фильтр)`. */
|