letopis 1.4.1 → 2.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 +329 -278
- package/CHANGELOG.md +558 -1
- package/README.md +606 -1037
- package/dist/acl.d.ts +78 -1
- package/dist/acl.js +264 -13
- package/dist/admin.d.ts +10 -141
- package/dist/admin.js +24 -175
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +5 -15
- package/dist/cache.d.ts +8 -1
- package/dist/cache.js +33 -10
- package/dist/chain.d.ts +19 -41
- package/dist/chain.js +169 -201
- package/dist/cli.js +51 -88
- package/dist/demo/booking.d.ts +352 -36
- package/dist/describe.d.ts +47 -0
- package/dist/describe.js +195 -0
- package/dist/errors.d.ts +59 -13
- package/dist/errors.js +190 -29
- package/dist/index.d.ts +52 -71
- package/dist/index.js +245 -130
- package/dist/internal.d.ts +12 -0
- package/dist/internal.js +5 -0
- package/dist/jsonschema.js +18 -17
- package/dist/load.d.ts +20 -12
- package/dist/load.js +225 -186
- package/dist/mcp.d.ts +5 -7
- package/dist/mcp.js +171 -215
- package/dist/messages/access.d.ts +90 -0
- package/dist/messages/access.js +113 -0
- package/dist/messages/api.d.ts +171 -0
- package/dist/messages/api.js +215 -0
- package/dist/messages/boot.d.ts +201 -0
- package/dist/messages/boot.js +256 -0
- package/dist/messages/codes.d.ts +172 -0
- package/dist/messages/codes.js +173 -0
- package/dist/messages/index.d.ts +95 -0
- package/dist/messages/index.js +118 -0
- package/dist/messages/mcp.d.ts +306 -0
- package/dist/messages/mcp.js +387 -0
- package/dist/messages/model.d.ts +233 -0
- package/dist/messages/model.js +283 -0
- package/dist/messages/query.d.ts +131 -0
- package/dist/messages/query.js +158 -0
- package/dist/messages/sql-access.d.ts +70 -0
- package/dist/messages/sql-access.js +87 -0
- package/dist/messages/sql-core.d.ts +150 -0
- package/dist/messages/sql-core.js +181 -0
- package/dist/messages/sql-time.d.ts +71 -0
- package/dist/messages/sql-time.js +88 -0
- package/dist/messages/sql-write.d.ts +246 -0
- package/dist/messages/sql-write.js +270 -0
- package/dist/messages/time.d.ts +23 -0
- package/dist/messages/time.js +29 -0
- package/dist/messages/types.d.ts +27 -0
- package/dist/messages/types.js +5 -0
- package/dist/messages/write.d.ts +405 -0
- package/dist/messages/write.js +509 -0
- package/dist/model.d.ts +73 -60
- package/dist/model.js +26 -29
- package/dist/ops.d.ts +35 -0
- package/dist/ops.js +101 -0
- package/dist/pglite.d.ts +0 -9
- package/dist/pglite.js +3 -1
- package/dist/registry.d.ts +3 -19
- package/dist/registry.js +3 -16
- package/dist/signals.d.ts +5 -9
- package/dist/signals.js +5 -9
- package/dist/sql.d.ts +13 -6
- package/dist/sql.js +176 -83
- package/dist/sync.d.ts +9 -3
- package/dist/sync.js +51 -21
- package/dist/tx.d.ts +26 -1
- package/dist/tx.js +44 -14
- package/dist/typed.d.ts +240 -45
- package/dist/types.d.ts +23 -91
- package/dist/types.js +12 -14
- package/dist/up.d.ts +53 -32
- package/dist/up.js +184 -73
- package/dist/uuid.js +5 -5
- package/dist/validate.d.ts +13 -1
- package/dist/validate.js +61 -29
- package/dist/watch.d.ts +11 -10
- package/dist/watch.js +33 -70
- package/dist/write.d.ts +9 -16
- package/dist/write.js +118 -187
- package/llms.txt +22 -25
- package/package.json +8 -11
- package/sql/10-core.sql +7 -50
- package/sql/15-errors.sql +64 -10
- package/sql/20-context.sql +7 -6
- package/sql/30-validate.sql +35 -27
- package/sql/40-class.sql +20 -114
- package/sql/50-acl.sql +8 -13
- package/sql/60-write.sql +220 -667
- package/sql/70-read.sql +20 -16
- package/sql/80-auth.sql +24 -27
- package/sql/90-time.sql +70 -859
- package/sql/95-seed.system.sql +7 -14
- package/sql/99-revision.sql +2 -1
- package/sql/README.md +32 -24
- package/sql/migrate/README.md +14 -0
- package/AGENT-CHEATSHEET.en.md +0 -485
- package/MIGRATION.md +0 -190
- package/README.en.md +0 -2859
- package/dist/import.d.ts +0 -185
- package/dist/import.js +0 -820
- package/dist/project.d.ts +0 -350
- package/dist/project.js +0 -1224
- package/sql/75-history.sql +0 -358
- package/sql/88-derived.sql +0 -1235
- package/sql/migrate/0006-changed-paths.sql +0 -3
- package/sql/migrate/0007-secret-account-fk.sql +0 -12
package/dist/describe.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { accepts } from './sql.js';
|
|
2
|
+
const SYSTEM_CLASSES = new Set(['Class', 'Account', 'member', 'Resource', 'rule']);
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Примеры цепочек (план 2.0, этап 3, п. 7): из реестра — ключ, обязательные поля, концы
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
/** Ключ объекта без кавычек: буквы, цифры, _ и $, не с цифры (имена классов и ролей такие всегда — метасхема Class). */
|
|
7
|
+
const BARE_KEY = /^[A-Za-zА-Яа-яЁё_$][A-Za-zА-Яа-яЁё0-9_$]*$/;
|
|
8
|
+
const ESCAPES = { '\\': '\\\\', "'": "\\'", '\n': '\\n', '\r': '\\r', '\t': '\\t' };
|
|
9
|
+
/** Значение JSON литералом TypeScript, как в README: строки в одинарных кавычках с экранированием, ключи без кавычек, где можно. */
|
|
10
|
+
function lit(v) {
|
|
11
|
+
if (typeof v === 'string') {
|
|
12
|
+
return `'${v.replace(/[\\'\u0000-\u001f\u2028\u2029]/g, (ch) => ESCAPES[ch] ?? `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`)}'`;
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(v))
|
|
15
|
+
return `[${v.map(lit).join(', ')}]`;
|
|
16
|
+
if (v !== null && typeof v === 'object') {
|
|
17
|
+
const entries = Object.entries(v);
|
|
18
|
+
return entries.length ? `{ ${entries.map(([k, x]) => `${BARE_KEY.test(k) ? k : lit(k)}: ${lit(x)}`).join(', ')} }` : '{}';
|
|
19
|
+
}
|
|
20
|
+
return typeof v === 'number' && !Number.isFinite(v) ? '0' : String(v);
|
|
21
|
+
}
|
|
22
|
+
/** Место для id объекта класса конца: '<id Org>', у полиморфного конца — '<id Service|Product>'. */
|
|
23
|
+
const idPlace = (cls) => `<id ${(Array.isArray(cls) ? cls : [cls]).join('|')}>`;
|
|
24
|
+
const isSchema = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
25
|
+
const hasDefault = (v) => isSchema(v) && 'default' in v;
|
|
26
|
+
const SAMPLE_DATE = '2026-01-01';
|
|
27
|
+
/** Образцы строк с форматом — первый, что проходит pattern поля (у z.iso.datetime он задаёт знаки после запятой и пояс). */
|
|
28
|
+
const FORMAT_SAMPLES = {
|
|
29
|
+
'date-time': ['.000Z', 'Z', '.000000Z', '.0Z', '.00Z', '+00:00', '.000+00:00'].map((tail) => `${SAMPLE_DATE}T00:00:00${tail}`),
|
|
30
|
+
date: [SAMPLE_DATE],
|
|
31
|
+
email: ['user@example.com'],
|
|
32
|
+
uuid: ['00000000-0000-4000-8000-000000000000'],
|
|
33
|
+
};
|
|
34
|
+
/** Строка: образец формата или имя поля — с учётом minLength, maxLength и pattern. */
|
|
35
|
+
function stringSample(s, name) {
|
|
36
|
+
// описание класса задаёт арендатор, а описание модели получает агент через MCP: длина образца ограничена (minLength в
|
|
37
|
+
// миллиард не выделит гигабайтную строку), а свой pattern здесь не исполняется вовсе — шаблон с катастрофическим
|
|
38
|
+
// перебором повесил бы процесс и на коротком образце (ревью безопасности коммитов 2e5e6bc, 1e80cf1). Поле с pattern
|
|
39
|
+
// без examples может получить образец, который проверку не пройдёт, — это оговорено в README
|
|
40
|
+
const SAMPLE = 16;
|
|
41
|
+
const min = typeof s.minLength === 'number' ? Math.min(Math.max(0, s.minLength), SAMPLE) : 0;
|
|
42
|
+
const max = typeof s.maxLength === 'number' ? Math.max(0, s.maxLength) : Infinity;
|
|
43
|
+
const fit = (x) => (x.length < min ? x.padEnd(min, 'x') : x).slice(0, Math.min(max, SAMPLE * 4));
|
|
44
|
+
return fit((FORMAT_SAMPLES[s.format] ?? [name || 'x'])[0]);
|
|
45
|
+
}
|
|
46
|
+
/** Число: 1 в пределах minimum, maximum (и исключающих) и кратное multipleOf; integer — целое. */
|
|
47
|
+
function numberSample(s, integer) {
|
|
48
|
+
const num = (k) => (typeof s[k] === 'number' ? s[k] : undefined);
|
|
49
|
+
const step = num('multipleOf') ?? (integer ? 1 : undefined);
|
|
50
|
+
const [min, xmin, max, xmax] = [num('minimum'), num('exclusiveMinimum'), num('maximum'), num('exclusiveMaximum')];
|
|
51
|
+
let v = 1;
|
|
52
|
+
if (min !== undefined && v < min)
|
|
53
|
+
v = min;
|
|
54
|
+
if (xmin !== undefined && v <= xmin)
|
|
55
|
+
v = xmin + (step ?? 1);
|
|
56
|
+
if (max !== undefined && v > max)
|
|
57
|
+
v = max;
|
|
58
|
+
if (xmax !== undefined && v >= xmax)
|
|
59
|
+
v = xmax - (step ?? 1);
|
|
60
|
+
return step ? Math.ceil(v / step) * step : v;
|
|
61
|
+
}
|
|
62
|
+
/** Поля объекта, которые надо передать: обязательные без умолчания (умолчание подставит база), в порядке описания. */
|
|
63
|
+
function neededFields(s, extra = []) {
|
|
64
|
+
const props = isSchema(s.properties) ? s.properties : {};
|
|
65
|
+
const need = new Set([...(Array.isArray(s.required) ? s.required : []), ...extra]);
|
|
66
|
+
return [...new Set([...Object.keys(props), ...need])].filter((k) => need.has(k) && !hasDefault(props[k]));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Образец значения по JSON Schema: examples[0], const, default, первое из enum, первая ветка anyOf или oneOf, иначе по
|
|
70
|
+
* типу — строка, число, true, массив из minItems образцов, объект из обязательных полей.
|
|
71
|
+
*/
|
|
72
|
+
function sample(s, name, depth = 0) {
|
|
73
|
+
if (!isSchema(s))
|
|
74
|
+
return name;
|
|
75
|
+
if (Array.isArray(s.examples) && s.examples.length)
|
|
76
|
+
return s.examples[0];
|
|
77
|
+
if ('const' in s)
|
|
78
|
+
return s.const;
|
|
79
|
+
if ('default' in s)
|
|
80
|
+
return s.default;
|
|
81
|
+
if (Array.isArray(s.enum) && s.enum.length)
|
|
82
|
+
return s.enum[0];
|
|
83
|
+
const branch = Array.isArray(s.anyOf) ? s.anyOf[0] : Array.isArray(s.oneOf) ? s.oneOf[0] : undefined;
|
|
84
|
+
if (isSchema(branch)) {
|
|
85
|
+
const { anyOf: _a, oneOf: _o, ...rest } = s;
|
|
86
|
+
return sample({ ...rest, ...branch }, name, depth);
|
|
87
|
+
}
|
|
88
|
+
const types = (Array.isArray(s.type) ? s.type : typeof s.type === 'string' ? [s.type] : []);
|
|
89
|
+
const type = types.find((x) => x !== 'null') ?? (types.length ? 'null' : isSchema(s.properties) ? 'object' : s.items ? 'array' : 'string');
|
|
90
|
+
if (depth > 8 && (type === 'object' || type === 'array'))
|
|
91
|
+
return type === 'object' ? {} : [];
|
|
92
|
+
switch (type) {
|
|
93
|
+
case 'null': return null;
|
|
94
|
+
case 'boolean': return true;
|
|
95
|
+
case 'integer':
|
|
96
|
+
case 'number': return numberSample(s, type === 'integer');
|
|
97
|
+
case 'array': return Array.from({ length: Math.min(typeof s.minItems === 'number' ? s.minItems : 0, 3) }, () => sample(s.items, name, depth + 1));
|
|
98
|
+
case 'object': {
|
|
99
|
+
const props = isSchema(s.properties) ? s.properties : {};
|
|
100
|
+
return Object.fromEntries(neededFields(s).map((k) => [k, sample(props[k] ?? s.additionalProperties, k, depth + 1)]));
|
|
101
|
+
}
|
|
102
|
+
default: return stringSample(s, name);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Класс, строки которого цепочкой не создают: абстрактный, системный или его потомок. */
|
|
106
|
+
const notCreatable = (c) => c.abstract || c.ancestors.some((n) => SYSTEM_CLASSES.has(n));
|
|
107
|
+
/** Данные create: поля ключа, обязательные поля без умолчания и поля первой ветки условий anyOf и oneOf. */
|
|
108
|
+
function createData(c) {
|
|
109
|
+
const s = c.schema;
|
|
110
|
+
const props = isSchema(s.properties) ? s.properties : {};
|
|
111
|
+
const conds = [s.anyOf, s.oneOf].map((x) => (Array.isArray(x) ? x[0] : undefined));
|
|
112
|
+
const extra = conds.flatMap((x) => (isSchema(x) && Array.isArray(x.required) ? x.required : []));
|
|
113
|
+
const keyFields = c.key.filter((k) => !c.ends[k]);
|
|
114
|
+
const fields = [...new Set([...neededFields(s, extra), ...keyFields])];
|
|
115
|
+
return Object.fromEntries(fields.map((k) => [k, sample(props[k], k)]));
|
|
116
|
+
}
|
|
117
|
+
/** «Создать»: данные и слоты обязательных концов и концов ключа; у множественного конца с min — столько разных id. */
|
|
118
|
+
function createExample(c) {
|
|
119
|
+
if (notCreatable(c))
|
|
120
|
+
return undefined;
|
|
121
|
+
let chain = `db.${c.name}().create(${lit(createData(c))})`;
|
|
122
|
+
for (const [role, e] of Object.entries(c.ends)) {
|
|
123
|
+
const min = e.min ?? 0;
|
|
124
|
+
if (!e.required && min === 0 && !c.key.includes(role))
|
|
125
|
+
continue;
|
|
126
|
+
const place = idPlace(e.class);
|
|
127
|
+
if (!e.many)
|
|
128
|
+
chain += `.${role}.set(${lit(place)})`;
|
|
129
|
+
else
|
|
130
|
+
chain += `.${role}.add(${lit(min > 1 ? Array.from({ length: min }, (_, i) => `${place.slice(0, -1)} #${i + 1}>`) : place)})`;
|
|
131
|
+
}
|
|
132
|
+
return `${chain}.rows()`;
|
|
133
|
+
}
|
|
134
|
+
/** «Найти»: по ключу (поля — те же образцы, что у create; концы — id) или по первому обязательному простому полю. */
|
|
135
|
+
function findExample(c) {
|
|
136
|
+
const props = isSchema(c.schema.properties) ? c.schema.properties : {};
|
|
137
|
+
if (c.key.length) {
|
|
138
|
+
const filter = Object.fromEntries(c.key.map((k) => [k, c.ends[k] ? idPlace(c.ends[k].class) : sample(props[k], k)]));
|
|
139
|
+
return `db.${c.name}(${lit(filter)}).first()`;
|
|
140
|
+
}
|
|
141
|
+
const required = Array.isArray(c.schema.required) ? c.schema.required : [];
|
|
142
|
+
const simple = (v) => ['string', 'number', 'boolean'].includes(typeof v);
|
|
143
|
+
const field = Object.keys(props).find((k) => required.includes(k) && simple(sample(props[k], k)));
|
|
144
|
+
if (field)
|
|
145
|
+
return `db.${c.name}(${lit({ [field]: sample(props[field], field) })}).rows()`;
|
|
146
|
+
const end = Object.entries(c.ends).find(([, e]) => e.required && !e.many);
|
|
147
|
+
return end ? `db.${c.name}(${lit({ [end[0]]: idPlace(end[1].class) })}).rows()` : `db.${c.name}().rows()`;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* «Перейти к соседям»: вперёд по каждому концу класса (шаг-роль; если так зовут класс или алиас, которого конец не
|
|
151
|
+
* принимает, — шаг по классу цели) и назад к классам, чьи концы принимают этот класс (у наследника, получившего конец
|
|
152
|
+
* от предка, — только предок: шаг к нему находит и наследников).
|
|
153
|
+
*/
|
|
154
|
+
function neighborExamples(reg, c, all) {
|
|
155
|
+
const list = (v) => (Array.isArray(v) ? v : [v]);
|
|
156
|
+
const steps = Object.entries(c.ends).map(([role, e]) => {
|
|
157
|
+
const named = reg.find(role);
|
|
158
|
+
return !named || accepts(reg, list(e.class), named.name) ? role : list(e.class)[0];
|
|
159
|
+
});
|
|
160
|
+
const back = all.filter((o) => Object.entries(o.ends).some(([role, e]) => accepts(reg, list(e.class), c.name) && !reg.find(o.ancestors[1] ?? '')?.ends[role]));
|
|
161
|
+
steps.push(...back.map((o) => o.name).sort((x, y) => x.localeCompare(y)));
|
|
162
|
+
return [...new Set(steps)].map((step) => `db.${c.name}(${lit(idPlace(c.name))}).${step}().rows()`);
|
|
163
|
+
}
|
|
164
|
+
function examplesOf(reg, c, all) {
|
|
165
|
+
const create = createExample(c);
|
|
166
|
+
return { ...(create ? { create } : {}), find: findExample(c), neighbors: neighborExamples(reg, c, all) };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Описание модели арендатора вместе с метаданными классов (этап 6, п. 12) и примерами цепочек (план 2.0, этап 3, п. 7):
|
|
170
|
+
* reg — реестр подключения (классы арендатора сессии и System), schema и tenant — схема установки и арендатор сессии.
|
|
171
|
+
*/
|
|
172
|
+
export function describeModel(reg, schema, tenant) {
|
|
173
|
+
const all = reg.all();
|
|
174
|
+
const classes = all.map((c) => ({
|
|
175
|
+
name: c.name,
|
|
176
|
+
kind: c.kind,
|
|
177
|
+
...(c.alias ? { alias: c.alias } : {}),
|
|
178
|
+
abstract: c.abstract,
|
|
179
|
+
...(c.ancestors[1] ? { parent: c.ancestors[1] } : {}),
|
|
180
|
+
ancestors: c.ancestors,
|
|
181
|
+
system: SYSTEM_CLASSES.has(c.name),
|
|
182
|
+
fields: (c.schema.properties ?? {}),
|
|
183
|
+
required: (c.schema.required ?? []),
|
|
184
|
+
...(Array.isArray(c.schema.anyOf) ? { anyOf: c.schema.anyOf } : {}),
|
|
185
|
+
...(Array.isArray(c.schema.oneOf) ? { oneOf: c.schema.oneOf } : {}),
|
|
186
|
+
ends: c.ends,
|
|
187
|
+
key: c.key,
|
|
188
|
+
history: c.history,
|
|
189
|
+
ownerDefault: c.ownerDefault,
|
|
190
|
+
...(c.cache ? { cache: c.cache } : {}),
|
|
191
|
+
meta: c.meta,
|
|
192
|
+
examples: examplesOf(reg, c, all),
|
|
193
|
+
})).sort((x, y) => Number(x.system) - Number(y.system) || x.name.localeCompare(y.name));
|
|
194
|
+
return { schema, tenant, classes };
|
|
195
|
+
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,29 +1,75 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* базы; код — имя из каталога (`error_catalog()` в lib/sql/15-errors.sql) или код самой библиотеки.
|
|
4
|
-
*/
|
|
1
|
+
import { type MsgKey, type Params } from './messages/index.js';
|
|
2
|
+
export type { MsgKey, Params } from './messages/index.js';
|
|
5
3
|
/** Нарушение правила данных: путь, ключевое слово JSON Schema, сообщение. Один формат для Zod, валидатора и базы. */
|
|
6
4
|
export interface Issue {
|
|
7
5
|
path: (string | number)[];
|
|
8
6
|
keyword: string;
|
|
9
7
|
message: string;
|
|
10
8
|
}
|
|
9
|
+
/** Текст записи каталога с параметрами (params.similar — «— похожие: …»): сообщения нарушений, описания, отчёты CLI. */
|
|
10
|
+
export declare function lmsg(key: MsgKey, params?: Params): string;
|
|
11
|
+
/**
|
|
12
|
+
* Предупреждение по записи каталога — «letopis: <текст>» в console.warn; once — ключ «один раз на процесс». Неизвестное
|
|
13
|
+
* поле фильтра, sort() и агрегата с 2.0 — уже не предупреждение, а отказ query.unknown_field (sql.ts, checkField).
|
|
14
|
+
*/
|
|
15
|
+
export declare function lwarn(key: MsgKey, params?: Params, opts?: {
|
|
16
|
+
once?: string;
|
|
17
|
+
}): void;
|
|
11
18
|
export declare class LetopisError extends Error {
|
|
12
19
|
readonly code: string;
|
|
13
20
|
readonly detail: unknown;
|
|
14
|
-
|
|
21
|
+
/** Ключ каталога сообщений: у ошибок lerr и у ошибок базы с ключом в DETAIL; у ошибок без ключа — нет. */
|
|
22
|
+
readonly key?: string;
|
|
23
|
+
/** Параметры шаблона ключа (similar — похожие имена). */
|
|
24
|
+
readonly params?: Params;
|
|
25
|
+
/** Что делать: своя подсказка записи каталога или подсказка кода. */
|
|
26
|
+
readonly hint?: string;
|
|
27
|
+
/** Строка кода в разделе «Ошибки» README на GitHub по тегу версии пакета. */
|
|
28
|
+
readonly docs?: string;
|
|
29
|
+
constructor(code: string, message: string, detail?: unknown, msg?: {
|
|
30
|
+
key: string;
|
|
31
|
+
params?: Params;
|
|
32
|
+
});
|
|
15
33
|
}
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Ошибка по ключу каталога: код — из записи, текст — шаблон с параметрами (params.similar — похожие имена дописываются
|
|
36
|
+
* сами); extra.detail — подробности (`err.detail`), extra.cause — исходная ошибка.
|
|
37
|
+
*/
|
|
38
|
+
export declare function lerr(key: MsgKey, params?: Params, extra?: {
|
|
39
|
+
detail?: unknown;
|
|
40
|
+
cause?: unknown;
|
|
41
|
+
}): LetopisError;
|
|
42
|
+
/** Похожие имена: до пяти ближайших по Левенштейну (без учёта регистра) или содержащих искомое. */
|
|
43
|
+
export declare function similar(name: string, candidates: Iterable<string>, n?: number): string[];
|
|
44
|
+
/**
|
|
45
|
+
* Неизвестный ключ опций — invalid_data с похожими именами (план 2.0, этап 4, п. 2; 7.10 №4; в 1.5 было предупреждение,
|
|
46
|
+
* 7.9 №6). known — все ключи опций: объект `satisfies Record<keyof Опции, true>`, пропуск ключа — ошибка типов.
|
|
47
|
+
*/
|
|
48
|
+
export declare function checkOptions(where: string, opts: unknown, known: Readonly<Record<string, true>>): void;
|
|
49
|
+
/** Данные не проходят описание класса (`invalid_data`); текст — по ключу (по умолчанию api.validation_failed). */
|
|
21
50
|
export declare class ValidationError extends LetopisError {
|
|
22
51
|
readonly issues: Issue[];
|
|
23
|
-
constructor(cls: string, issues: Issue[]
|
|
52
|
+
constructor(cls: string, issues: Issue[], msg?: {
|
|
53
|
+
key: string;
|
|
54
|
+
params?: Params;
|
|
55
|
+
});
|
|
24
56
|
}
|
|
25
57
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
58
|
+
* Нарушения из базы (validate_data, inc): у нарушения базы ещё key и params, наружу они не идут — форма та же,
|
|
59
|
+
* { path, keyword, message }, message — текст, который собрала база. Нарушение без ключа (схема прежней сборки) — как
|
|
60
|
+
* есть.
|
|
61
|
+
*/
|
|
62
|
+
export declare function issuesFromDb(issues: unknown): Issue[];
|
|
63
|
+
/**
|
|
64
|
+
* Строки отказа ужесточения из базы (detail.rows от family_conflicts): у строки ещё key и params, наружу они не идут
|
|
65
|
+
* (problem — текст базы), у нарушений в поле issues — тоже. Строка без ключа — как есть.
|
|
66
|
+
*/
|
|
67
|
+
export declare function findingsFromDb<T>(rows: readonly T[]): T[];
|
|
68
|
+
/**
|
|
69
|
+
* Ошибка базы → LetopisError (ValidationError для invalid_data с нарушениями). Детали из DETAIL (JSON) переносятся в
|
|
70
|
+
* detail; ключ каталога и параметры из DETAIL (lt_fail(ключ, …)) — в key и params, текст — тот, что база собрала по ним
|
|
71
|
+
* из русского шаблона; без ключа — текст базы. У нарушений (issues) и строк отказа (detail.rows) key и params наружу не
|
|
72
|
+
* идут. Внутренняя ошибка базы (XX000) с ключом в DETAIL — код internal. Повторяемый конфликт (40001, 40P01) и прочие
|
|
73
|
+
* ошибки возвращаются как есть: на SQLSTATE конфликта держатся повторы (tx.ts).
|
|
28
74
|
*/
|
|
29
75
|
export declare function fromDbError(e: unknown): unknown;
|
package/dist/errors.js
CHANGED
|
@@ -1,30 +1,131 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ошибки letopis 1.0 (план, §7.1, этап 2, п. 10). Текст — «letopis: <код>: <сообщение>», как у ошибок
|
|
3
3
|
* базы; код — имя из каталога (`error_catalog()` в lib/sql/15-errors.sql) или код самой библиотеки.
|
|
4
|
+
*
|
|
5
|
+
* Каталог сообщений (план 2.0, этап 3, п. 3; lib/src/messages): ошибка по ключу — `lerr(ключ, параметры)`, текст —
|
|
6
|
+
* шаблон записи с параметрами. У каждой ошибки — подсказка «что делать» (`hint`) и ссылка на строку её кода в разделе
|
|
7
|
+
* «Ошибки» README на GitHub по тегу версии (`docs`). База собирает текст по ключу и параметрам из того же шаблона и
|
|
8
|
+
* кладёт их в DETAIL (`lt_fail(ключ, …)`), а fromDbError переносит их в поля ошибки (`key`, `params`); без ключа —
|
|
9
|
+
* текст базы как есть. У нарушений (validate_data) и строк отказа ужесточения key и params наружу не идут: issuesFromDb,
|
|
10
|
+
* findingsFromDb.
|
|
4
11
|
*/
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import { SIMILAR, codeOf, msgOf, render } from './messages/index.js';
|
|
14
|
+
const VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
15
|
+
/** Раздел «Ошибки» README по тегу версии пакета; якорь — сам код (`<a id="код"></a>` в строке таблицы). */
|
|
16
|
+
const DOCS = `https://github.com/alepri51/letopis/blob/v${VERSION}/lib/README.md`;
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Тексты и ошибки по каталогу
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
function textOf(key, params) {
|
|
21
|
+
const m = msgOf(key);
|
|
22
|
+
if (!m)
|
|
23
|
+
return key;
|
|
24
|
+
const s = render(m.ru, params);
|
|
25
|
+
const sim = params?.similar;
|
|
26
|
+
return Array.isArray(sim) && sim.length ? s + render(SIMILAR, { similar: sim.join(', ') }) : s;
|
|
27
|
+
}
|
|
28
|
+
/** Текст записи каталога с параметрами (params.similar — «— похожие: …»): сообщения нарушений, описания, отчёты CLI. */
|
|
29
|
+
export function lmsg(key, params) {
|
|
30
|
+
return textOf(key, params);
|
|
31
|
+
}
|
|
32
|
+
const warned = new Set();
|
|
33
|
+
/**
|
|
34
|
+
* Предупреждение по записи каталога — «letopis: <текст>» в console.warn; once — ключ «один раз на процесс». Неизвестное
|
|
35
|
+
* поле фильтра, sort() и агрегата с 2.0 — уже не предупреждение, а отказ query.unknown_field (sql.ts, checkField).
|
|
36
|
+
*/
|
|
37
|
+
export function lwarn(key, params, opts = {}) {
|
|
38
|
+
if (opts.once !== undefined) {
|
|
39
|
+
if (warned.has(opts.once))
|
|
40
|
+
return;
|
|
41
|
+
if (warned.size >= 1000)
|
|
42
|
+
warned.clear();
|
|
43
|
+
warned.add(opts.once);
|
|
44
|
+
}
|
|
45
|
+
console.warn(`letopis: ${textOf(key, params)}`);
|
|
46
|
+
}
|
|
5
47
|
export class LetopisError extends Error {
|
|
6
48
|
code;
|
|
7
49
|
detail;
|
|
8
|
-
constructor(code, message, detail) {
|
|
50
|
+
constructor(code, message, detail, msg) {
|
|
9
51
|
super(`letopis: ${code}: ${message}`);
|
|
10
52
|
this.name = 'LetopisError';
|
|
11
53
|
this.code = code;
|
|
12
54
|
this.detail = detail;
|
|
55
|
+
const self = this;
|
|
56
|
+
if (msg) {
|
|
57
|
+
self.key = msg.key;
|
|
58
|
+
self.params = msg.params ?? {};
|
|
59
|
+
}
|
|
60
|
+
const sign = codeOf(code);
|
|
61
|
+
const hint = (msg ? msgOf(msg.key)?.hint : undefined) ?? sign?.hint;
|
|
62
|
+
if (hint)
|
|
63
|
+
self.hint = hint;
|
|
64
|
+
if (sign)
|
|
65
|
+
self.docs = `${DOCS}#${code}`;
|
|
13
66
|
}
|
|
14
67
|
}
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
return
|
|
68
|
+
/**
|
|
69
|
+
* Ошибка по ключу каталога: код — из записи, текст — шаблон с параметрами (params.similar — похожие имена дописываются
|
|
70
|
+
* сами); extra.detail — подробности (`err.detail`), extra.cause — исходная ошибка.
|
|
71
|
+
*/
|
|
72
|
+
export function lerr(key, params, extra) {
|
|
73
|
+
const e = new LetopisError(msgOf(key)?.code ?? 'invalid_query', lmsg(key, params), extra?.detail, { key, ...(params ? { params } : {}) });
|
|
74
|
+
return extra && 'cause' in extra ? Object.assign(e, { cause: extra.cause }) : e;
|
|
22
75
|
}
|
|
23
|
-
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Похожие имена (общие: цепочки, фильтры, опции, MCP)
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
/** Расстояние Левенштейна (вставка, удаление, замена). */
|
|
80
|
+
function distance(a, b) {
|
|
81
|
+
if (!a.length)
|
|
82
|
+
return b.length;
|
|
83
|
+
if (!b.length)
|
|
84
|
+
return a.length;
|
|
85
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
86
|
+
for (let i = 1; i <= a.length; i++) {
|
|
87
|
+
const cur = [i];
|
|
88
|
+
for (let j = 1; j <= b.length; j++)
|
|
89
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
90
|
+
prev = cur;
|
|
91
|
+
}
|
|
92
|
+
return prev[b.length];
|
|
93
|
+
}
|
|
94
|
+
/** Похожие имена: до пяти ближайших по Левенштейну (без учёта регистра) или содержащих искомое. */
|
|
95
|
+
export function similar(name, candidates, n = 5) {
|
|
96
|
+
if (name.length > 64)
|
|
97
|
+
return [];
|
|
98
|
+
const want = name.toLowerCase();
|
|
99
|
+
const max = Math.max(2, Math.ceil(want.length / 3));
|
|
100
|
+
const scored = [];
|
|
101
|
+
for (const x of new Set(candidates)) {
|
|
102
|
+
if (x === name)
|
|
103
|
+
continue;
|
|
104
|
+
const low = x.toLowerCase();
|
|
105
|
+
const d = distance(want, low);
|
|
106
|
+
if (d <= max || (want.length >= 3 && low.includes(want)))
|
|
107
|
+
scored.push({ x, d });
|
|
108
|
+
}
|
|
109
|
+
return scored.sort((p, q) => p.d - q.d || (p.x < q.x ? -1 : p.x > q.x ? 1 : 0)).slice(0, n).map((s) => s.x);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Неизвестный ключ опций — invalid_data с похожими именами (план 2.0, этап 4, п. 2; 7.10 №4; в 1.5 было предупреждение,
|
|
113
|
+
* 7.9 №6). known — все ключи опций: объект `satisfies Record<keyof Опции, true>`, пропуск ключа — ошибка типов.
|
|
114
|
+
*/
|
|
115
|
+
export function checkOptions(where, opts, known) {
|
|
116
|
+
if (!opts || typeof opts !== 'object')
|
|
117
|
+
return;
|
|
118
|
+
for (const k of Object.keys(opts)) {
|
|
119
|
+
if (!Object.hasOwn(known, k))
|
|
120
|
+
throw lerr('api.unknown_option', { where, name: k, similar: similar(k, Object.keys(known)) });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Данные не проходят описание класса (`invalid_data`); текст — по ключу (по умолчанию api.validation_failed). */
|
|
24
124
|
export class ValidationError extends LetopisError {
|
|
25
125
|
issues;
|
|
26
|
-
constructor(cls, issues) {
|
|
27
|
-
|
|
126
|
+
constructor(cls, issues, msg) {
|
|
127
|
+
const m = msg ?? { key: 'api.validation_failed', params: { cls } };
|
|
128
|
+
super('invalid_data', textOf(m.key, m.params), { issues }, m);
|
|
28
129
|
this.name = 'ValidationError';
|
|
29
130
|
this.issues = issues;
|
|
30
131
|
}
|
|
@@ -33,14 +134,55 @@ export class ValidationError extends LetopisError {
|
|
|
33
134
|
const LT_NAMES = {
|
|
34
135
|
LT001: 'no_session', LT002: 'target_not_found', LT003: 'invalid_data', LT004: 'undeclared_end', LT005: 'tenant_denied',
|
|
35
136
|
LT006: 'acl_denied', LT007: 'id_mismatch', LT008: 'id_from_db', LT009: 'immutable_key', LT010: 'invalid_class',
|
|
36
|
-
|
|
37
|
-
LT015: 'recreated_in_statement',
|
|
38
|
-
LT019: 'reset_disabled', LT020: 'confirm_mismatch', LT021: 'derived_class', LT022: 'aggregate_field',
|
|
39
|
-
LT023: 'projector_pending',
|
|
137
|
+
LT013: 'tightening_conflict', LT014: 'isolation_level',
|
|
138
|
+
LT015: 'recreated_in_statement', LT017: 'delete_restricted', LT018: 'not_deleted',
|
|
40
139
|
};
|
|
140
|
+
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
141
|
+
/**
|
|
142
|
+
* Нарушения из базы (validate_data, inc): у нарушения базы ещё key и params, наружу они не идут — форма та же,
|
|
143
|
+
* { path, keyword, message }, message — текст, который собрала база. Нарушение без ключа (схема прежней сборки) — как
|
|
144
|
+
* есть.
|
|
145
|
+
*/
|
|
146
|
+
export function issuesFromDb(issues) {
|
|
147
|
+
if (!Array.isArray(issues))
|
|
148
|
+
return issues;
|
|
149
|
+
return issues.map((i) => {
|
|
150
|
+
if (!isObj(i) || typeof i.key !== 'string')
|
|
151
|
+
return i;
|
|
152
|
+
const { key: _key, params: _params, ...rest } = i;
|
|
153
|
+
rest.message = String(rest.message ?? '');
|
|
154
|
+
return rest;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
41
157
|
/**
|
|
42
|
-
*
|
|
43
|
-
* (
|
|
158
|
+
* Строки отказа ужесточения из базы (detail.rows от family_conflicts): у строки ещё key и params, наружу они не идут
|
|
159
|
+
* (problem — текст базы), у нарушений в поле issues — тоже. Строка без ключа — как есть.
|
|
160
|
+
*/
|
|
161
|
+
export function findingsFromDb(rows) {
|
|
162
|
+
return rows.map((r) => {
|
|
163
|
+
if (!isObj(r) || typeof r.key !== 'string')
|
|
164
|
+
return r;
|
|
165
|
+
const { key: _key, params: _params, ...rest } = r;
|
|
166
|
+
if (Array.isArray(rest.issues))
|
|
167
|
+
rest.issues = issuesFromDb(rest.issues);
|
|
168
|
+
return rest;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/** DETAIL ошибки базы: JSON — разобранный, иначе строка как есть. */
|
|
172
|
+
function parseDetail(detail) {
|
|
173
|
+
try {
|
|
174
|
+
return detail ? JSON.parse(detail) : undefined;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return detail;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Ошибка базы → LetopisError (ValidationError для invalid_data с нарушениями). Детали из DETAIL (JSON) переносятся в
|
|
182
|
+
* detail; ключ каталога и параметры из DETAIL (lt_fail(ключ, …)) — в key и params, текст — тот, что база собрала по ним
|
|
183
|
+
* из русского шаблона; без ключа — текст базы. У нарушений (issues) и строк отказа (detail.rows) key и params наружу не
|
|
184
|
+
* идут. Внутренняя ошибка базы (XX000) с ключом в DETAIL — код internal. Повторяемый конфликт (40001, 40P01) и прочие
|
|
185
|
+
* ошибки возвращаются как есть: на SQLSTATE конфликта держатся повторы (tx.ts).
|
|
44
186
|
*/
|
|
45
187
|
export function fromDbError(e) {
|
|
46
188
|
const err = e;
|
|
@@ -48,25 +190,44 @@ export function fromDbError(e) {
|
|
|
48
190
|
// которому существующую строку менять нельзя («USING expression»), остаётся 42501 (§2.5). Признак —
|
|
49
191
|
// функция сервера, а не текст: текст зависит от языка сервера
|
|
50
192
|
if (err.code === '42501' && err.routine === 'ExecWithCheckOptions' && !/USING/.test(err.message ?? '')) {
|
|
51
|
-
return
|
|
193
|
+
return lerr('api.row_check_denied', undefined, { cause: e });
|
|
52
194
|
}
|
|
53
|
-
|
|
54
|
-
if (!name)
|
|
55
|
-
return e;
|
|
195
|
+
let name;
|
|
56
196
|
let detail;
|
|
57
|
-
|
|
58
|
-
|
|
197
|
+
if (err.code === 'XX000') {
|
|
198
|
+
// своя внутренняя ошибка — lt_raise('XX000', ключ, …): ключ каталога в DETAIL; голые XX000 самого PostgreSQL («tuple
|
|
199
|
+
// concurrently updated» и т. п.) — как есть
|
|
200
|
+
detail = parseDetail(err.detail);
|
|
201
|
+
if (isObj(detail) && typeof detail.key === 'string')
|
|
202
|
+
name = 'internal';
|
|
59
203
|
}
|
|
60
|
-
|
|
61
|
-
|
|
204
|
+
else if (err.code) {
|
|
205
|
+
name = LT_NAMES[err.code];
|
|
206
|
+
if (name)
|
|
207
|
+
detail = parseDetail(err.detail);
|
|
208
|
+
}
|
|
209
|
+
if (!name)
|
|
210
|
+
return e;
|
|
211
|
+
// ключ каталога (план 2.0, этап 3, п. 3): в detail остаются прежние поля, а ключ и параметры — в key и params
|
|
212
|
+
let msg;
|
|
213
|
+
if (isObj(detail) && typeof detail.key === 'string') {
|
|
214
|
+
const { key, params, nodetail, ...rest } = detail;
|
|
215
|
+
msg = { key: key, params: isObj(params) ? params : {} };
|
|
216
|
+
// detail — как до каталога: прежние поля, {} в том числе (lt_fail без detail давала {}); признак nodetail — DETAIL у
|
|
217
|
+
// места не было (raise без detail), и detail нет. Сам признак наружу не идёт
|
|
218
|
+
detail = nodetail === true ? undefined : rest;
|
|
62
219
|
}
|
|
220
|
+
// текст база собрала по ключу и параметрам из того же русского шаблона — он остаётся как есть
|
|
63
221
|
const text = (err.message ?? '').replace(/^letopis: [a-z_]+: /, '');
|
|
222
|
+
// строки отказа с находками (ужесточение: problem по ключу) — без key и params
|
|
223
|
+
if (isObj(detail) && Array.isArray(detail.rows))
|
|
224
|
+
detail = { ...detail, rows: findingsFromDb(detail.rows) };
|
|
64
225
|
const issues = detail?.issues;
|
|
65
226
|
if (name === 'invalid_data' && Array.isArray(issues)) {
|
|
66
|
-
const cls = /класса (\S+)/.exec(text)?.[1] ?? '?';
|
|
67
|
-
const v = new ValidationError(cls, issues);
|
|
68
|
-
// текст
|
|
227
|
+
const cls = msg ? String(msg.params.cls ?? '?') : /класса (\S+)/.exec(text)?.[1] ?? '?';
|
|
228
|
+
const v = new ValidationError(cls, issuesFromDb(issues), msg);
|
|
229
|
+
// текст целиком — тот, что собрала база по шаблону записи
|
|
69
230
|
return Object.assign(v, { cause: e, ...(text ? { message: `letopis: invalid_data: ${text}` } : {}) });
|
|
70
231
|
}
|
|
71
|
-
return Object.assign(new LetopisError(name, text, detail), { cause: e });
|
|
232
|
+
return Object.assign(new LetopisError(name, text, detail, msg), { cause: e });
|
|
72
233
|
}
|