letopis 1.4.0 → 1.4.2
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.en.md +15 -7
- package/AGENT-CHEATSHEET.md +19 -11
- package/CHANGELOG.md +299 -1
- package/README.en.md +180 -62
- package/README.md +165 -55
- package/dist/acl.d.ts +11 -5
- package/dist/admin.d.ts +30 -14
- package/dist/admin.js +53 -18
- package/dist/auth.d.ts +61 -17
- package/dist/auth.js +16 -2
- package/dist/cache.d.ts +5 -1
- package/dist/cache.js +3 -2
- package/dist/chain.d.ts +92 -15
- package/dist/chain.js +56 -7
- package/dist/cli.js +16 -2
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +5 -5
- package/dist/import.js +7 -0
- package/dist/index.d.ts +73 -45
- package/dist/index.js +69 -14
- package/dist/internal.d.ts +7 -0
- package/dist/internal.js +5 -0
- package/dist/load.d.ts +10 -3
- package/dist/load.js +39 -11
- package/dist/mcp.d.ts +20 -11
- package/dist/mcp.js +8 -6
- package/dist/model.d.ts +4 -1
- package/dist/model.js +7 -1
- package/dist/ops.d.ts +58 -18
- package/dist/ops.js +58 -18
- package/dist/project.d.ts +39 -11
- package/dist/project.js +75 -15
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +4 -1
- package/dist/signals.d.ts +4 -4
- package/dist/signals.js +4 -4
- package/dist/sql.d.ts +8 -4
- package/dist/sql.js +110 -25
- package/dist/sync.js +11 -4
- package/dist/tx.d.ts +15 -0
- package/dist/tx.js +22 -4
- package/dist/types.d.ts +21 -7
- package/dist/types.js +13 -6
- package/dist/up.d.ts +48 -25
- package/dist/up.js +94 -41
- package/dist/validate.d.ts +9 -2
- package/dist/validate.js +19 -3
- package/dist/watch.d.ts +29 -5
- package/dist/watch.js +62 -13
- package/dist/write.d.ts +3 -2
- package/dist/write.js +52 -19
- package/package.json +8 -4
- package/sql/10-core.sql +6 -1
- package/sql/60-write.sql +47 -18
- package/sql/70-read.sql +13 -7
- package/sql/75-history.sql +106 -28
- package/sql/80-auth.sql +41 -3
- package/sql/88-derived.sql +3 -1
- package/sql/90-time.sql +118 -30
- package/sql/95-seed.system.sql +6 -3
- package/sql/99-revision.sql +1 -1
- package/sql/migrate/0007-secret-account-fk.sql +12 -0
package/dist/chain.d.ts
CHANGED
|
@@ -32,6 +32,11 @@ export interface ChainCtx {
|
|
|
32
32
|
/** План в очереди батча; steps обновляется по мере роста цепочки. */
|
|
33
33
|
export interface BatchEntry {
|
|
34
34
|
steps: Step[];
|
|
35
|
+
/** Модификатор выборки, поставленный сразу после последнего глагола плана: run() откажет (`afterVerbError`). */
|
|
36
|
+
loose?: {
|
|
37
|
+
name: string;
|
|
38
|
+
verb: string;
|
|
39
|
+
};
|
|
35
40
|
}
|
|
36
41
|
/** Ссылка цепочки на очередь: первый глагол регистрирует план, следующие — обновляют. */
|
|
37
42
|
export interface BatchRef {
|
|
@@ -47,9 +52,16 @@ export type Batch = {
|
|
|
47
52
|
[className: string]: StepFn;
|
|
48
53
|
};
|
|
49
54
|
export interface ChainCore {
|
|
50
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Все пути цепочки. Путь — строки всех шагов, по одной на шаг, связанные ссылками; приходит объектом, где ключ — имя
|
|
57
|
+
* шага (или имя из `alias()`), а значение — строка этого шага: `{ Shop: …, Product: … }`. Если к объекту последнего
|
|
58
|
+
* шага ведут несколько путей, придут все.
|
|
59
|
+
*/
|
|
51
60
|
paths(): Promise<Path[]>;
|
|
52
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Строки объектов последнего шага — каждый объект один раз, сколько бы путей к нему ни вело. Если цепочка кончается
|
|
63
|
+
* глаголом записи — строки этого глагола.
|
|
64
|
+
*/
|
|
53
65
|
rows(): Promise<Row[]>;
|
|
54
66
|
first(): Promise<Row | null>;
|
|
55
67
|
ids(): Promise<string[]>;
|
|
@@ -68,41 +80,93 @@ export interface ChainCore {
|
|
|
68
80
|
diff(opts?: DiffOptions): Promise<DiffResult[]>;
|
|
69
81
|
/** Кто, когда и почему последним изменил каждое поле версии (по умолчанию текущей), план 1.1. */
|
|
70
82
|
blame(opts?: BlameOptions): Promise<BlameResult[]>;
|
|
71
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Сумма поля `'data.<путь>'` по объектам последнего шага: значения приводятся к числу, складывает база. `limit()` и
|
|
85
|
+
* `offset()` на итог не влияют; нет ни одного значения — `null`.
|
|
86
|
+
*/
|
|
72
87
|
sum(field: string): Promise<number | null>;
|
|
88
|
+
/**
|
|
89
|
+
* Среднее значение поля `'data.<путь>'` по объектам последнего шага: значения приводятся к числу, считает база.
|
|
90
|
+
* `limit()` и `offset()` на итог не влияют; нет ни одного значения — `null`.
|
|
91
|
+
*/
|
|
73
92
|
avg(field: string): Promise<number | null>;
|
|
93
|
+
/**
|
|
94
|
+
* Наименьшее значение поля `'data.<путь>'` среди объектов последнего шага; считает база. Числа сравниваются как числа
|
|
95
|
+
* и приходят числом, даты — как даты, поле без описания в классе — как текст. `limit()` и `offset()` на итог не
|
|
96
|
+
* влияют; нет ни одного значения — `null`.
|
|
97
|
+
*/
|
|
74
98
|
min(field: string): Promise<unknown>;
|
|
99
|
+
/**
|
|
100
|
+
* Наибольшее значение поля `'data.<путь>'` среди объектов последнего шага; считает база. Числа сравниваются как числа
|
|
101
|
+
* и приходят числом, даты — как даты, поле без описания в классе — как текст. `limit()` и `offset()` на итог не
|
|
102
|
+
* влияют; нет ни одного значения — `null`.
|
|
103
|
+
*/
|
|
75
104
|
max(field: string): Promise<unknown>;
|
|
105
|
+
/**
|
|
106
|
+
* Сколько объектов последнего шага с каждым значением поля `'data.<путь>'`: объект «значение → число», объекты без
|
|
107
|
+
* значения — под ключом `'null'`. Перед `countBy()` `limit()` и `offset()` отбирают группы — от самых частых значений.
|
|
108
|
+
*/
|
|
76
109
|
countBy(field: string): Promise<Record<string, number>>;
|
|
110
|
+
/**
|
|
111
|
+
* Не больше n строк ответа: объектов последнего шага, у `paths()` — путей, у `versions()` — версий, перед `countBy()` —
|
|
112
|
+
* групп. До глагола сужает его цели. Сразу после глагола перед `rows`, `first`, `ids`, `count` и `paths` —
|
|
113
|
+
* `invalid_query`: глагол уже прошёл по всем целям, сужать нечего.
|
|
114
|
+
*/
|
|
77
115
|
limit(n: number): Chain;
|
|
116
|
+
/** Пропустить первые n строк ответа — тех же, что считает `limit()`; где действует — как у `limit()`. */
|
|
78
117
|
offset(n: number): Chain;
|
|
79
118
|
/** Сортировка по полю последнего шага: 'at' | 'rev' | 'id' | 'data.<путь>'. */
|
|
80
119
|
sort(field: string, dir?: 'asc' | 'desc' | boolean): Chain;
|
|
81
120
|
/** Срез «как было на момент T»: версии журнала. */
|
|
82
121
|
asOf(t: string | Date): Chain;
|
|
83
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Постраничный вывод по курсору: ответ начинается строго после строки, на которой кончилась прошлая страница, в
|
|
124
|
+
* порядке `sort()` (без `sort()` — ошибка `invalid_query`). Курсор этой строки даёт `cursorOf(строка, поле)` с тем же
|
|
125
|
+
* полем, что в `sort()`. Листает `rows()`, `ids()`, `versions()` и `paths()` (курсор пути — `cursorOf(путь, поле)`);
|
|
126
|
+
* `count()`, агрегаты, `diff()` и `blame()` курсор не учитывают.
|
|
127
|
+
*/
|
|
84
128
|
after(cursor: Cursor): Chain;
|
|
85
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* Последний шаг находит и удалённые объекты. Удалённый объект приходит надгробием — своей последней версией, версией
|
|
131
|
+
* удаления, — с `$deleted: true`.
|
|
132
|
+
*/
|
|
86
133
|
withDeleted(): Chain;
|
|
87
134
|
/** Дети любой глубины на переходе к тому же классу; $depth в строке. */
|
|
88
135
|
deep(max?: number): Chain;
|
|
89
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* Последний шаг находит объекты только своего класса — без классов-наследников (`extends` в модели), которых без
|
|
138
|
+
* `exact()` находит тоже.
|
|
139
|
+
*/
|
|
90
140
|
exact(): Chain;
|
|
91
141
|
/** Ключ текущего шага в путях. */
|
|
92
142
|
alias(name: string): Chain;
|
|
93
|
-
/**
|
|
143
|
+
/**
|
|
144
|
+
* Теги строки. В чтении и перед глаголами — отбор: строка или список — строки со всеми этими тегами (пустой список
|
|
145
|
+
* подходит любой строке), `has`, `hasAny`, `hasAll` — оператором. Перед `create()` и `upsert()` — теги новой строки
|
|
146
|
+
* (строка или список). Перед `update()` строка или список и отбирают, и заменяют теги целиком, а оператор только
|
|
147
|
+
* отбирает.
|
|
148
|
+
*/
|
|
94
149
|
tags(v: string | string[] | object): Chain;
|
|
95
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* Владелец строк последнего шага (поле `owner`). В чтении и перед `update()`, `delete()` и другими глаголами — отбор:
|
|
152
|
+
* только строки этого владельца; перед `create()` и `upsert()` — владелец новой строки (существующей строке `upsert()`
|
|
153
|
+
* владельца не меняет).
|
|
154
|
+
*/
|
|
96
155
|
owner(v: string | {
|
|
97
156
|
id: string;
|
|
98
157
|
}): Chain;
|
|
99
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* Добавляет в цепочку шаги из строки ответа (шаг её класса с её id) или из ещё не исполненной цепочки без глаголов
|
|
160
|
+
* записи (все её шаги как есть); `limit()`, `sort()`, `asOf()` и другие модификаторы всей цепочки не переносятся. Та
|
|
161
|
+
* же цепочка (та же переменная) второй раз в одном пути шагов не добавляет, а возвращает к своему последнему шагу:
|
|
162
|
+
* следующий шаг пойдёт от него.
|
|
163
|
+
*/
|
|
100
164
|
entity(x: Chain | Row): Chain;
|
|
101
165
|
/** Создать объект: концы — из пути и слотов; занятый ключ — ошибка exists с id. */
|
|
102
166
|
create(data?: Record<string, unknown>, opts?: {
|
|
103
167
|
reason?: string;
|
|
104
168
|
}): Chain;
|
|
105
|
-
/** Новая версия каждой цели шага: слияние патча в базе; inc(n) — приращение; { rev } —
|
|
169
|
+
/** Новая версия каждой цели шага: слияние патча в базе; inc(n) — приращение; { rev } — с проверкой версии. */
|
|
106
170
|
update(patch?: Record<string, unknown>, opts?: {
|
|
107
171
|
rev?: number;
|
|
108
172
|
reason?: string;
|
|
@@ -111,7 +175,11 @@ export interface ChainCore {
|
|
|
111
175
|
upsert(data: Record<string, unknown>, opts?: {
|
|
112
176
|
reason?: string;
|
|
113
177
|
}): Chain;
|
|
114
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* Удаляет цели шага; строки, которые на них ссылаются, база удаляет вместе с ними, отвязывает или не даёт удалить цель —
|
|
180
|
+
* как задано в `onDelete` их ссылок. Без `{ confirm: true }` — предпросмотр удаления: ничего не удаляется, терминал
|
|
181
|
+
* вернёт все строки, которых оно коснулось бы, у каждой `$action` — что с ней будет.
|
|
182
|
+
*/
|
|
115
183
|
delete(opts?: {
|
|
116
184
|
confirm?: boolean;
|
|
117
185
|
reason?: string;
|
|
@@ -130,12 +198,19 @@ export interface ChainCore {
|
|
|
130
198
|
restore(opts?: {
|
|
131
199
|
reason?: string;
|
|
132
200
|
}): Chain;
|
|
133
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Стирает историю удалённых объектов шага: все их версии в журнале, от каждого остаётся одна запись `purge`. Удалённые
|
|
203
|
+
* объекты шаг находит сам, без `withDeleted()`. Без `{ confirm: true }` — предпросмотр: ничего не стирается, терминал
|
|
204
|
+
* вернёт надгробия с `$versions` — сколько версий сотрётся.
|
|
205
|
+
*/
|
|
134
206
|
purge(opts?: {
|
|
135
207
|
confirm?: boolean;
|
|
136
208
|
reason?: string;
|
|
137
209
|
}): Chain;
|
|
138
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* Новое значение ключа объектов шага. id вычисляется из ключа, поэтому объект получает новый id: ссылки других строк
|
|
212
|
+
* на него переводятся на новый id, а журнал связывает старый id с новым.
|
|
213
|
+
*/
|
|
139
214
|
rekey(patch: Record<string, unknown>, opts?: {
|
|
140
215
|
reason?: string;
|
|
141
216
|
}): Chain;
|
|
@@ -145,8 +220,10 @@ export interface ChainCore {
|
|
|
145
220
|
}): Chain;
|
|
146
221
|
}
|
|
147
222
|
/**
|
|
148
|
-
* Шаг
|
|
149
|
-
*
|
|
223
|
+
* Шаг цепочки — имя класса, его алиаса или роли с необязательным фильтром: `db.Shop(id).Product({ price: gt(1000) })`;
|
|
224
|
+
* каждый следующий шаг переходит по ссылкам от объектов предыдущего. После `create()`, `update()` или `upsert()` имя
|
|
225
|
+
* роли без вызова задаёт ссылку записываемой строки: `.Роль.set(…)`, `.Роль.unset()`, у множественной ссылки ещё
|
|
226
|
+
* `.Роль.add(…)` и `.Роль.remove(…)`.
|
|
150
227
|
*/
|
|
151
228
|
export type StepFn = ((filter?: Filter) => Chain) & {
|
|
152
229
|
set(value: unknown): Chain;
|
package/dist/chain.js
CHANGED
|
@@ -132,6 +132,27 @@ async function read(ctx, steps, mods, mode, agg) {
|
|
|
132
132
|
const ev = mode === 'countPaths' ? 'count' : mode;
|
|
133
133
|
return exec(ctx, q, ev, steps, mods).then((res) => ({ q, res }));
|
|
134
134
|
}
|
|
135
|
+
/** Модификатор выборки, заданный в mods, — именем метода цепочки; forUpdate() отказывает сам (lock_unsupported). */
|
|
136
|
+
function modName(mods) {
|
|
137
|
+
const key = Object.keys(mods).find((k) => mods[k] !== undefined && k !== 'forUpdate');
|
|
138
|
+
return key === 'order' || key === 'desc' ? 'sort' : key;
|
|
139
|
+
}
|
|
140
|
+
function afterVerbError(name, verb) {
|
|
141
|
+
return new LetopisError('invalid_query', `${name}() после ${verb}() ни на что не действует: глагол проходит по всем целям шага, а ответ — его строки. ` +
|
|
142
|
+
`Цели сужают limit(), offset() и sort() до глагола: db.Класс(…).limit(n).${verb}(…)`);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Терминалы rows, first, ids, count и paths цепочки, которая кончается глаголом, отдают строки самой правки.
|
|
146
|
+
* Модификатор выборки между глаголом и терминалом ни на что не подействовал бы: limit резал бы только ответ, хотя
|
|
147
|
+
* правка прошла по всем целям, остальные молча пропускались. Поэтому — отказ до записи. После глагола модификатор
|
|
148
|
+
* относится к чтению за ним (шаг класса, versions(), diff(), blame(), агрегаты) или сужает цели следующего глагола.
|
|
149
|
+
*/
|
|
150
|
+
function checkAfterVerb(steps, mods) {
|
|
151
|
+
const op = steps[steps.length - 1]?.op;
|
|
152
|
+
const name = op ? modName(mods) : undefined;
|
|
153
|
+
if (op && name)
|
|
154
|
+
throw afterVerbError(name, op.kind);
|
|
155
|
+
}
|
|
135
156
|
/**
|
|
136
157
|
* Цепочка с глаголами: весь план одной транзакцией (§2.5). Результат приводится к виду ответа
|
|
137
158
|
* чтения (строки rows/paths, id, n, агрегаты), чтобы терминалы разбирали его одинаково.
|
|
@@ -223,8 +244,11 @@ const rowOf = (r) => (r.done ? r.row : toRow(r.row));
|
|
|
223
244
|
/** Значение слота: id, список id, строка ответа или ленивая цепочка. */
|
|
224
245
|
function slotValue(v) {
|
|
225
246
|
const peek = peekPlan(v);
|
|
226
|
-
if (peek)
|
|
247
|
+
if (peek) {
|
|
248
|
+
// модификатор сразу после глагола вложенной цепочки — тот же отказ, что у терминала (план 2.0, 7.2 №3)
|
|
249
|
+
checkAfterVerb(peek.steps, peek.mods);
|
|
227
250
|
return { plan: peek.steps, mods: peek.mods };
|
|
251
|
+
}
|
|
228
252
|
if (v === null || typeof v === 'string')
|
|
229
253
|
return v;
|
|
230
254
|
if (Array.isArray(v))
|
|
@@ -279,6 +303,13 @@ function classProp(ctx, name, found, ownerSteps, mods, nodes) {
|
|
|
279
303
|
});
|
|
280
304
|
}
|
|
281
305
|
export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
|
|
306
|
+
// батч: терминала нет, план исполнит run() — он и откажет, если цепочка кончилась модификатором после глагола
|
|
307
|
+
// (как checkAfterVerb вне батча); следующий шаг или глагол забирает модификатор себе, и отметка снимается
|
|
308
|
+
if (ctx.batch?.entry) {
|
|
309
|
+
const op = steps[steps.length - 1]?.op;
|
|
310
|
+
const name = op ? modName(mods) : undefined;
|
|
311
|
+
ctx.batch.entry.loose = op && name ? { name, verb: op.kind } : undefined;
|
|
312
|
+
}
|
|
282
313
|
const withLast = (patch) => {
|
|
283
314
|
const lastOp = steps[steps.length - 1]?.op;
|
|
284
315
|
if (lastOp)
|
|
@@ -369,21 +400,35 @@ export function makeChain(ctx, steps, mods, nodes = NO_NODES) {
|
|
|
369
400
|
return entityStep;
|
|
370
401
|
case 'paths':
|
|
371
402
|
return async () => {
|
|
372
|
-
|
|
403
|
+
checkAfterVerb(steps, mods);
|
|
404
|
+
// q === null — пути из результата глагола: строки уже готовы. Ключи — в порядке шагов (jsonb хранит их
|
|
405
|
+
// по длине), узел последнего шага — последним: по нему cursorOf(путь, поле) берёт курсор (план 2.0, 7.1 №3)
|
|
373
406
|
const { q, res } = await read(ctx, steps, mods, 'paths');
|
|
374
|
-
|
|
407
|
+
if (q === null)
|
|
408
|
+
return res.map((r) => r.path);
|
|
409
|
+
return res.map((r) => Object.fromEntries(q.keys.map((k) => [k, toRow(r.path[k])])));
|
|
375
410
|
};
|
|
376
411
|
case 'rows':
|
|
377
|
-
return async () =>
|
|
412
|
+
return async () => {
|
|
413
|
+
checkAfterVerb(steps, mods);
|
|
414
|
+
return (await read(ctx, steps, mods, 'rows')).res.map(rowOf);
|
|
415
|
+
};
|
|
378
416
|
case 'first':
|
|
379
417
|
return async () => {
|
|
418
|
+
checkAfterVerb(steps, mods);
|
|
380
419
|
const { res } = await read(ctx, steps, { ...mods, limit: 1 }, 'rows');
|
|
381
420
|
return res[0] ? rowOf(res[0]) : null;
|
|
382
421
|
};
|
|
383
422
|
case 'ids':
|
|
384
|
-
return async () =>
|
|
423
|
+
return async () => {
|
|
424
|
+
checkAfterVerb(steps, mods);
|
|
425
|
+
return (await read(ctx, steps, mods, 'ids')).res.map((r) => r.id);
|
|
426
|
+
};
|
|
385
427
|
case 'count':
|
|
386
|
-
return async (opts) =>
|
|
428
|
+
return async (opts) => {
|
|
429
|
+
checkAfterVerb(steps, mods);
|
|
430
|
+
return (await read(ctx, steps, mods, opts?.paths ? 'countPaths' : 'count')).res[0].n;
|
|
431
|
+
};
|
|
387
432
|
case 'versions':
|
|
388
433
|
return async (opts) => {
|
|
389
434
|
warnNoHistory(steps, 'versions');
|
|
@@ -503,7 +548,11 @@ export function makeBatch(ctx, queue) {
|
|
|
503
548
|
switch (prop) {
|
|
504
549
|
case 'run':
|
|
505
550
|
return async () => {
|
|
506
|
-
const
|
|
551
|
+
const entries = queue.splice(0, queue.length);
|
|
552
|
+
const loose = entries.find((x) => x.loose)?.loose;
|
|
553
|
+
if (loose)
|
|
554
|
+
throw afterVerbError(loose.name, loose.verb);
|
|
555
|
+
const plans = entries.map((x) => x.steps);
|
|
507
556
|
for (const p of plans)
|
|
508
557
|
precheckPlan(ctx.reg, p);
|
|
509
558
|
if (!plans.length)
|
package/dist/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ import { resolve } from 'node:path';
|
|
|
24
24
|
import { connect, describeText } from './index.js';
|
|
25
25
|
import { syncModels, generateTypes } from './sync.js';
|
|
26
26
|
import { import021 } from './import.js';
|
|
27
|
+
import { LetopisError } from './errors.js';
|
|
27
28
|
const USAGE = `letopis sync <модуль> --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--apply]
|
|
28
29
|
letopis types --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--out <файл>]
|
|
29
30
|
letopis load <файлы…> --dsn <строка> --schema <v2.имя> (--token <токен> | --api-key <ключ>) [--class <Класс>]
|
|
@@ -35,6 +36,8 @@ letopis import --from <строка 0.21> --from-schema <v1.имя> --dsn <ст
|
|
|
35
36
|
[--rename <Старое>=<Новое>]… [--update] [--commit-every <N>] [--partition <имя>] [--initial]
|
|
36
37
|
letopis mcp --dsn <строка> --schema <v2.имя> --token <токен пользователя> [--write]
|
|
37
38
|
(или LETOPIS_DSN, LETOPIS_SCHEMA, LETOPIS_TOKEN; ключ сервиса --api-key не принимается)`;
|
|
39
|
+
/** Команды CLI: остальное — USAGE с кодом 2 ещё до подключения. */
|
|
40
|
+
const COMMANDS = new Set(['sync', 'types', 'load', 'verify', 'maintain', 'describe', 'import', 'mcp']);
|
|
38
41
|
const isModel = (v) => typeof v === 'object' && v !== null && typeof v.describe === 'function' && typeof v.name === 'string';
|
|
39
42
|
export async function main(argv) {
|
|
40
43
|
const { values, positionals } = parseArgs({
|
|
@@ -58,7 +61,12 @@ export async function main(argv) {
|
|
|
58
61
|
values.schema ??= process.env.LETOPIS_SCHEMA || undefined;
|
|
59
62
|
values.token ??= process.env.LETOPIS_TOKEN || undefined;
|
|
60
63
|
}
|
|
61
|
-
|
|
64
|
+
// --help — всегда код 0; неизвестная команда — USAGE до подключения к базе (план 2.0, 7.5 №4)
|
|
65
|
+
if (values.help) {
|
|
66
|
+
console.log(USAGE);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
if (!cmd || !COMMANDS.has(cmd) || !values.dsn || !values.schema) {
|
|
62
70
|
console.log(USAGE);
|
|
63
71
|
return cmd ? 2 : 0;
|
|
64
72
|
}
|
|
@@ -117,7 +125,7 @@ export async function main(argv) {
|
|
|
117
125
|
return 2;
|
|
118
126
|
}
|
|
119
127
|
if (values.trust)
|
|
120
|
-
throw new
|
|
128
|
+
throw new LetopisError('removed', '--trust снят в 1.0.0 — запрос по целям вне загрузки стоит 0,1–1,3 % её времени');
|
|
121
129
|
// несколько файлов — одна загрузка и одна транзакция; в файле — объект на строку
|
|
122
130
|
const rows = [];
|
|
123
131
|
for (const f of files) {
|
|
@@ -146,6 +154,12 @@ export async function main(argv) {
|
|
|
146
154
|
console.log(JSON.stringify(r));
|
|
147
155
|
return 0;
|
|
148
156
|
}
|
|
157
|
+
if (cmd === 'describe' || cmd === 'types') {
|
|
158
|
+
// обе команды читают реестр сессии: без действующей сессии (токен истёк, погашен, неверен или не задан) он пуст,
|
|
159
|
+
// и команда выдала бы пустой результат с кодом 0, а с --out затёрла бы им файл. acl.check ничего не пишет, а без
|
|
160
|
+
// сессии отказывает no_session — тогда выход с кодом 1, файл не тронут
|
|
161
|
+
await db.acl.check('letopis.cli');
|
|
162
|
+
}
|
|
149
163
|
if (cmd === 'describe') {
|
|
150
164
|
const d = db.describe();
|
|
151
165
|
const text = values.format === 'text' ? describeText(d) : JSON.stringify(d, null, 2);
|
package/dist/errors.d.ts
CHANGED
|
@@ -13,10 +13,10 @@ export declare class LetopisError extends Error {
|
|
|
13
13
|
readonly detail: unknown;
|
|
14
14
|
constructor(code: string, message: string, detail?: unknown);
|
|
15
15
|
}
|
|
16
|
+
/** Версия, в которой снято старое API (её берёт и контракт API — scripts/gen-api-contract.mjs). */
|
|
17
|
+
export declare const REMOVED_IN = "1.0.0";
|
|
16
18
|
/** Охранник снесённого API (план, этап 3, п. 11): понятная ошибка с версией и заменой. */
|
|
17
19
|
export declare function removed(name: string, replacement: string): LetopisError;
|
|
18
|
-
/** Метод зарезервирован, но появится на следующем этапе плана. */
|
|
19
|
-
export declare function notYet(name: string, stage: number): LetopisError;
|
|
20
20
|
/** Данные не проходят описание класса (`invalid_data`). */
|
|
21
21
|
export declare class ValidationError extends LetopisError {
|
|
22
22
|
readonly issues: Issue[];
|
package/dist/errors.js
CHANGED
|
@@ -12,13 +12,13 @@ export class LetopisError extends Error {
|
|
|
12
12
|
this.detail = detail;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
/** Версия, в которой снято старое API (её берёт и контракт API — scripts/gen-api-contract.mjs). */
|
|
16
|
+
export const REMOVED_IN = '1.0.0';
|
|
15
17
|
/** Охранник снесённого API (план, этап 3, п. 11): понятная ошибка с версией и заменой. */
|
|
16
18
|
export function removed(name, replacement) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export function notYet(name, stage) {
|
|
21
|
-
return new LetopisError('not_implemented', `${name} появится на этапе ${stage} плана 1.0`);
|
|
19
|
+
// «опция …» — женского рода; методы и свойства — мужского
|
|
20
|
+
const verb = name.startsWith('опция ') ? 'снята' : / и /.test(name) ? 'сняты' : 'снят';
|
|
21
|
+
return new LetopisError('removed', `${name} ${verb} в ${REMOVED_IN} — ${replacement}`);
|
|
22
22
|
}
|
|
23
23
|
/** Данные не проходят описание класса (`invalid_data`). */
|
|
24
24
|
export class ValidationError extends LetopisError {
|
package/dist/import.js
CHANGED
|
@@ -625,7 +625,14 @@ export async function import021(opts) {
|
|
|
625
625
|
}
|
|
626
626
|
if (secretRows.length) {
|
|
627
627
|
await x.begin(async (tx) => {
|
|
628
|
+
// кред аккаунта, которого в установке нет (его нет и в Account 0.21), привязать не к чему: строка secret ссылается
|
|
629
|
+
// на аккаунт внешним ключом (ревизия 7) — такой кред пропускается и считается в skipped
|
|
630
|
+
const known = new Set((await tx.unsafe(`select id::text as id from ${S}.entity where id = any ($1::uuid[])`, [[...new Set(secretRows.map((s) => s.account))]])).map((r) => r.id));
|
|
628
631
|
for (const s of secretRows) {
|
|
632
|
+
if (!known.has(s.account)) {
|
|
633
|
+
report.credentials.skipped++;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
629
636
|
const [dup] = await tx.unsafe(`select account::text as account from ${S}.secret
|
|
630
637
|
where kind = $1 and casefold(ident) collate pg_unicode_fast = casefold($2) collate pg_unicode_fast and deleted_at is null`, [s.kind, s.ident]);
|
|
631
638
|
if (dup) {
|