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/auth.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { createHmac, randomBytes, scrypt, timingSafeEqual } from 'node:crypto';
|
|
13
13
|
import { promisify } from 'node:util';
|
|
14
14
|
import { lerr } from './errors.js';
|
|
15
|
+
import { shortIntervalMs } from './admin.js';
|
|
15
16
|
const scryptAsync = promisify(scrypt);
|
|
16
17
|
const SCRYPT = { N: 32768, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
|
|
17
18
|
const KEYLEN = 32;
|
|
@@ -83,7 +84,46 @@ export function totpCode(secretBase32, atMs = Date.now()) {
|
|
|
83
84
|
return String(code).padStart(TOTP_DIGITS, '0');
|
|
84
85
|
}
|
|
85
86
|
const accId = (a) => (typeof a === 'object' ? a.id : a);
|
|
86
|
-
|
|
87
|
+
/** Логин без крайних пробелов (план 2.1, A12): « anna » и «anna» — один логин; не строка — как есть (отказ даст база). */
|
|
88
|
+
const trimLogin = (v) => (typeof v === 'string' ? v.trim() : v);
|
|
89
|
+
/**
|
|
90
|
+
* Срок ttl (3.0.1) — интервал для базы: число секунд, короткая форма как у connect({ maintain }) ('30s', '15m', '2h',
|
|
91
|
+
* '1d') или строка-интервал PostgreSQL ('15 minutes') — её разбирает база, ошибку разбора переводит ttlGuard. Не задан —
|
|
92
|
+
* null (срок по умолчанию). Не число больше нуля, пустая строка, другой тип — invalid_data (access.bad_ttl).
|
|
93
|
+
*/
|
|
94
|
+
export function ttlOf(v, where) {
|
|
95
|
+
if (v === undefined || v === null)
|
|
96
|
+
return null;
|
|
97
|
+
if (typeof v === 'number') {
|
|
98
|
+
if (Number.isFinite(v) && v > 0)
|
|
99
|
+
return `${Math.max(1, Math.round(v))} seconds`;
|
|
100
|
+
}
|
|
101
|
+
else if (typeof v === 'string' && v.trim() !== '') {
|
|
102
|
+
const ms = shortIntervalMs(v);
|
|
103
|
+
if (ms === null)
|
|
104
|
+
return v.trim();
|
|
105
|
+
if (ms > 0)
|
|
106
|
+
return `${ms / 1000} seconds`;
|
|
107
|
+
}
|
|
108
|
+
throw lerr('access.bad_ttl', { where, value: shownTtl(v) });
|
|
109
|
+
}
|
|
110
|
+
const shownTtl = (v) => (typeof v === 'string' ? `'${v}'` : typeof v === 'number' || typeof v === 'boolean' || v == null ? String(v) : typeof v);
|
|
111
|
+
/**
|
|
112
|
+
* Строка ttl, которую не принял разбор интервала PostgreSQL (22007 — не интервал, 22008 и 22015 — вне диапазона), —
|
|
113
|
+
* invalid_data access.bad_ttl вместо сырой ошибки базы; прочие ошибки — как есть.
|
|
114
|
+
*/
|
|
115
|
+
export async function ttlGuard(run, ttl, where) {
|
|
116
|
+
try {
|
|
117
|
+
return await run;
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
const code = e?.code;
|
|
121
|
+
if (typeof ttl === 'string' && (code === '22007' || code === '22008' || code === '22015')) {
|
|
122
|
+
throw lerr('access.bad_ttl', { where, value: shownTtl(ttl) }, { cause: e });
|
|
123
|
+
}
|
|
124
|
+
throw e;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
87
127
|
/** Фасад db.auth. onSwitch — подключение перечитывает арендатора, реестр и пропуска прав. */
|
|
88
128
|
export function makeAuth(S, exec, onSwitch, canSwitch) {
|
|
89
129
|
const one = async (text, params = []) => {
|
|
@@ -93,7 +133,8 @@ export function makeAuth(S, exec, onSwitch, canSwitch) {
|
|
|
93
133
|
// Вход после первой проверки. Код TOTP проверяет база той же функцией, что verifyTotp (право auth.totp,
|
|
94
134
|
// окно ±1 шаг, повтор шага, счётчик неудач и блокировка), секрет из базы не выходит; способ TOTP
|
|
95
135
|
// сессия получает только после удачи. Без кода — сессия без отметки TOTP, как раньше.
|
|
96
|
-
const login = async (account, ttl, methods, totp) => {
|
|
136
|
+
const login = async (account, ttl, methods, totp, where = 'auth') => {
|
|
137
|
+
const interval = ttlOf(ttl, where);
|
|
97
138
|
if (!account)
|
|
98
139
|
return null;
|
|
99
140
|
if (totp != null) {
|
|
@@ -102,28 +143,31 @@ export function makeAuth(S, exec, onSwitch, canSwitch) {
|
|
|
102
143
|
return null;
|
|
103
144
|
methods = [...methods, 'TOTP'];
|
|
104
145
|
}
|
|
105
|
-
const r = await one(`select ${S}.issue_session($1::uuid, coalesce($2::interval, interval '7 days'), $3::text[]) as t`, [account,
|
|
146
|
+
const r = await ttlGuard(one(`select ${S}.issue_session($1::uuid, coalesce($2::interval, interval '7 days'), $3::text[]) as t`, [account, interval, methods]), ttl, where);
|
|
106
147
|
return { account, token: r.t };
|
|
107
148
|
};
|
|
108
149
|
return {
|
|
109
150
|
async setPassword(a) {
|
|
110
151
|
const hash = await hashPassword(a.password);
|
|
111
|
-
const r = await one(`select ${S}.auth_set_password($1::uuid, $2, $3, $4, $5, $6) as id`, [accId(a.account), a.identifier, hash, a.kind ?? 'PASSWORD', a.confirmed ?? null, a.keepCurrent ?? true]);
|
|
152
|
+
const r = await one(`select ${S}.auth_set_password($1::uuid, $2, $3, $4, $5, $6) as id`, [accId(a.account), trimLogin(a.identifier), hash, a.kind ?? 'PASSWORD', a.confirmed ?? null, a.keepCurrent ?? true]);
|
|
112
153
|
return r.id;
|
|
113
154
|
},
|
|
114
155
|
async verifyPassword(a) {
|
|
115
|
-
|
|
116
|
-
const
|
|
156
|
+
ttlOf(a.ttl, 'verifyPassword');
|
|
157
|
+
const get = async (ident) => (await exec.run((q) => q.unsafe(`select account, hash, confirmed from ${S}.auth_password_get($1, $2)`, [ident, a.kind ?? 'PASSWORD'])))[0];
|
|
158
|
+
// логин без крайних пробелов (план 2.1, A12); кред, записанный до 2.1 с пробелами, находится по логину как есть
|
|
159
|
+
const ident = trimLogin(a.identifier);
|
|
160
|
+
const cred = (await get(ident)) ?? (ident !== a.identifier ? await get(a.identifier) : undefined);
|
|
117
161
|
if (!cred)
|
|
118
162
|
return dummyVerify(a.password).then(() => null);
|
|
119
163
|
if (!(await verifyHash(a.password, cred.hash)))
|
|
120
164
|
return null;
|
|
121
165
|
if (!cred.confirmed && a.requireConfirmed !== false)
|
|
122
166
|
return null;
|
|
123
|
-
return login(cred.account, a.ttl, [a.kind ?? 'PASSWORD'], a.totp);
|
|
167
|
+
return login(cred.account, a.ttl, [a.kind ?? 'PASSWORD'], a.totp, 'verifyPassword');
|
|
124
168
|
},
|
|
125
169
|
async issueApiKey(a) {
|
|
126
|
-
return one(`select key, id from ${S}.auth_issue_apikey($1::uuid, $2, $3::interval)`, [accId(a.account), a.name ?? null, ttlOf(a.ttl)]);
|
|
170
|
+
return ttlGuard(one(`select key, id from ${S}.auth_issue_apikey($1::uuid, $2, $3::interval)`, [accId(a.account), a.name ?? null, ttlOf(a.ttl, 'issueApiKey')]), a.ttl, 'issueApiKey');
|
|
127
171
|
},
|
|
128
172
|
async verifyApiKey(key) {
|
|
129
173
|
return (await one(`select ${S}.auth_login_apikey($1) as r`, [key])).r;
|
|
@@ -148,7 +192,11 @@ export function makeAuth(S, exec, onSwitch, canSwitch) {
|
|
|
148
192
|
return (await one(`select ${S}.auth_totp_enabled($1::uuid) as ok`, [accId(account)])).ok;
|
|
149
193
|
},
|
|
150
194
|
async issueOtp(a) {
|
|
151
|
-
|
|
195
|
+
// 3.0.1: число попыток вне 1–10 — отказ, а не молчаливое приведение к пределу в базе
|
|
196
|
+
const max = a.maxAttempts;
|
|
197
|
+
if (max !== undefined && !(Number.isInteger(max) && max >= 1 && max <= 10))
|
|
198
|
+
throw lerr('access.bad_max_attempts', { value: shownTtl(max) });
|
|
199
|
+
const r = await ttlGuard(one(`select ${S}.auth_issue_otp($1::uuid, $2, $3, coalesce($4::interval, interval '10 minutes'), $5, $6) as c`, [accId(a.account), a.identifier, a.kind ?? 'OTP', ttlOf(a.ttl, 'issueOtp'), a.digits ?? 6, max ?? 5]), a.ttl, 'issueOtp');
|
|
152
200
|
return { code: r.c };
|
|
153
201
|
},
|
|
154
202
|
async verifyOtp(a) {
|
|
@@ -158,8 +206,9 @@ export function makeAuth(S, exec, onSwitch, canSwitch) {
|
|
|
158
206
|
return (await one(`select ${S}.auth_link($1::uuid, $2, $3, $4::jsonb, $5) as id`, [accId(a.account), a.kind, a.identifier, (a.meta ?? {}), a.confirmed ?? false])).id;
|
|
159
207
|
},
|
|
160
208
|
async lookup(a) {
|
|
209
|
+
ttlOf(a.ttl, 'lookup');
|
|
161
210
|
const r = await one(`select ${S}.auth_lookup($1, $2, $3) as a`, [a.kind, a.identifier, a.requireConfirmed ?? true]);
|
|
162
|
-
return login(r.a, a.ttl, [a.kind], a.totp);
|
|
211
|
+
return login(r.a, a.ttl, [a.kind], a.totp, 'lookup');
|
|
163
212
|
},
|
|
164
213
|
async credentials(account) {
|
|
165
214
|
return exec.run((q) => q.unsafe(`select * from ${S}.auth_credentials($1::uuid)`, [accId(account)]));
|
|
@@ -171,7 +220,7 @@ export function makeAuth(S, exec, onSwitch, canSwitch) {
|
|
|
171
220
|
return (await one(`select ${S}.auth_set_flags($1::uuid, $2, $3) as ok`, [id, flags.confirmed ?? null, flags.enabled ?? null])).ok;
|
|
172
221
|
},
|
|
173
222
|
async sessionFor(account, opts) {
|
|
174
|
-
return (await login(accId(account), opts?.ttl, ['SERVICE'])).token;
|
|
223
|
+
return (await login(accId(account), opts?.ttl, ['SERVICE'], undefined, 'sessionFor')).token;
|
|
175
224
|
},
|
|
176
225
|
async session(token) {
|
|
177
226
|
const r = (await one(`select ${S}.auth_session_info($1) as r`, [token])).r;
|
|
@@ -179,9 +228,12 @@ export function makeAuth(S, exec, onSwitch, canSwitch) {
|
|
|
179
228
|
return r && { account: r.account, tenant: r.tenant, member: r.member, expiresAt: r.expiresAt, loginAt: r.loginAt, methods: r.methods };
|
|
180
229
|
},
|
|
181
230
|
async refresh(opts) {
|
|
182
|
-
return (await one(`select ${S}.auth_refresh(coalesce($1::interval, interval '7 days')) as e`, [ttlOf(opts?.ttl)])).e;
|
|
231
|
+
return (await ttlGuard(one(`select ${S}.auth_refresh(coalesce($1::interval, interval '7 days')) as e`, [ttlOf(opts?.ttl, 'refresh')]), opts?.ttl, 'refresh')).e;
|
|
183
232
|
},
|
|
184
|
-
async revoke() {
|
|
233
|
+
async revoke(token) {
|
|
234
|
+
// с токеном — выход пользователя по токену (план 2.1, A3): право auth.session, как у session(token)
|
|
235
|
+
if (token !== undefined)
|
|
236
|
+
return (await one(`select ${S}.auth_logout_token($1) as ok`, [token])).ok;
|
|
185
237
|
return (await one(`select ${S}.auth_logout() as ok`)).ok;
|
|
186
238
|
},
|
|
187
239
|
async revokeAll(account) {
|
|
@@ -194,8 +246,9 @@ export function makeAuth(S, exec, onSwitch, canSwitch) {
|
|
|
194
246
|
await onSwitch();
|
|
195
247
|
},
|
|
196
248
|
async tenants() {
|
|
197
|
-
|
|
198
|
-
|
|
249
|
+
// name и personal — с ревизии 9 (план 2.1, A5): personal — собственный арендатор аккаунта сессии (tenant = account)
|
|
250
|
+
const rows = await exec.run((q) => q.unsafe(`select tenant, roles, name, personal from ${S}.auth_tenants()`));
|
|
251
|
+
return rows.map((r) => ({ tenant: r.tenant, roles: r.roles, name: r.name ?? null, personal: r.personal === true }));
|
|
199
252
|
},
|
|
200
253
|
async purgeAccount(account) {
|
|
201
254
|
const r = (await one(`select ${S}.account_purge($1::uuid) as r`, [accId(account)])).r;
|
|
@@ -206,3 +259,64 @@ export function makeAuth(S, exec, onSwitch, canSwitch) {
|
|
|
206
259
|
},
|
|
207
260
|
};
|
|
208
261
|
}
|
|
262
|
+
const argOf = (args, i = 0) => (args[i] !== null && typeof args[i] === 'object' ? args[i] : {});
|
|
263
|
+
/** Аккаунт аргумента: id или { id }. */
|
|
264
|
+
const accOf = (a) => (typeof a === 'string' ? a : a !== null && typeof a === 'object' && typeof a.id === 'string' ? a.id : undefined);
|
|
265
|
+
const loginOf = (v) => (typeof v === 'string' ? { login: v.trim() } : {});
|
|
266
|
+
/** Вход: успех — auth.login (account — вошедший), отказ — auth.fail. */
|
|
267
|
+
function loginEvent(r, method, extra = {}) {
|
|
268
|
+
const res = r;
|
|
269
|
+
const detail = { method, ...extra };
|
|
270
|
+
return res && typeof res === 'object' ? { event: 'auth.login', account: res.account, detail } : { event: 'auth.fail', detail };
|
|
271
|
+
}
|
|
272
|
+
const credential = (action, detail) => ({ event: 'auth.credential', detail: { action, ...detail } });
|
|
273
|
+
const call = (method, detail = {}) => ({ event: 'auth.call', detail: { method, ...detail } });
|
|
274
|
+
/** Что пишет в лог каждый метод db.auth. */
|
|
275
|
+
export const AUTH_LOG = {
|
|
276
|
+
setPassword: (args, id) => {
|
|
277
|
+
const a = argOf(args);
|
|
278
|
+
return credential('setPassword', { account: accOf(a.account), kind: a.kind ?? 'PASSWORD', ...loginOf(a.identifier), id });
|
|
279
|
+
},
|
|
280
|
+
verifyPassword: (args, r) => {
|
|
281
|
+
const a = argOf(args);
|
|
282
|
+
return loginEvent(r, String(a.kind ?? 'PASSWORD'), { ...loginOf(a.identifier), ...(a.totp != null ? { totp: true } : {}) });
|
|
283
|
+
},
|
|
284
|
+
issueApiKey: (args, r) => credential('issueApiKey', { account: accOf(argOf(args).account), name: argOf(args).name, id: r?.id }),
|
|
285
|
+
verifyApiKey: (_args, r) => loginEvent(r, 'APIKEY'),
|
|
286
|
+
issueKeySecret: (args, r) => credential('issueKeySecret', { account: accOf(argOf(args).account), name: argOf(args).name, id: r?.id }),
|
|
287
|
+
verifyKeySecret: (_args, r) => loginEvent(r, 'KEYSECRET'),
|
|
288
|
+
enrollTotp: (args) => credential('enrollTotp', { account: accOf(argOf(args).account) }),
|
|
289
|
+
verifyTotp: (args, ok) => (ok === true
|
|
290
|
+
? call('verifyTotp', { account: accOf(argOf(args).account), ok: true })
|
|
291
|
+
: { event: 'auth.fail', detail: { method: 'TOTP', account: accOf(argOf(args).account) } }),
|
|
292
|
+
totpEnabled: (args, r) => call('totpEnabled', { account: accOf(args[0]), enabled: r }),
|
|
293
|
+
issueOtp: (args) => {
|
|
294
|
+
const a = argOf(args);
|
|
295
|
+
return credential('issueOtp', { account: accOf(a.account), kind: a.kind ?? 'OTP', ...loginOf(a.identifier) });
|
|
296
|
+
},
|
|
297
|
+
verifyOtp: (args, r) => {
|
|
298
|
+
const a = argOf(args);
|
|
299
|
+
return loginEvent(r, String(a.kind ?? 'OTP'), loginOf(a.identifier));
|
|
300
|
+
},
|
|
301
|
+
link: (args, id) => {
|
|
302
|
+
const a = argOf(args);
|
|
303
|
+
return credential('link', { account: accOf(a.account), kind: a.kind, ...loginOf(a.identifier), id });
|
|
304
|
+
},
|
|
305
|
+
lookup: (args, r) => {
|
|
306
|
+
const a = argOf(args);
|
|
307
|
+
return loginEvent(r, String(a.kind), { ...loginOf(a.identifier), ...(a.totp != null ? { totp: true } : {}) });
|
|
308
|
+
},
|
|
309
|
+
credentials: (args, r) => ({ ...call('credentials', { account: accOf(args[0]) }), rows: Array.isArray(r) ? r.length : undefined }),
|
|
310
|
+
revokeCredential: (args, ok) => ({ event: 'auth.revoke', detail: { target: 'credential', id: args[0], ok } }),
|
|
311
|
+
setFlags: (args, ok) => credential('setFlags', { id: args[0], ...argOf(args, 1), ok }),
|
|
312
|
+
sessionFor: (args) => ({ event: 'auth.session', detail: { account: accOf(args[0]), method: 'SERVICE' } }),
|
|
313
|
+
session: (_args, r) => call('session', { found: r !== null }),
|
|
314
|
+
refresh: () => call('refresh'),
|
|
315
|
+
revoke: (args, ok) => (args[0] !== undefined
|
|
316
|
+
? { event: 'auth.revoke', detail: { target: 'token', ok } }
|
|
317
|
+
: { event: 'auth.logout', detail: { ok } }),
|
|
318
|
+
revokeAll: (args, n) => ({ event: 'auth.revoke', detail: { target: 'all', account: accOf(args[0]), n } }),
|
|
319
|
+
switch: (args, _r, before) => ({ event: 'auth.switch', tenant: String(args[0]), detail: { from: before } }),
|
|
320
|
+
tenants: (_args, r) => ({ ...call('tenants'), rows: Array.isArray(r) ? r.length : undefined }),
|
|
321
|
+
purgeAccount: (args, r) => ({ event: 'account.purge', detail: { account: accOf(args[0]), ...r } }),
|
|
322
|
+
};
|
package/dist/chain.d.ts
CHANGED
|
@@ -2,7 +2,9 @@ import { type ResultCache } from './cache.js';
|
|
|
2
2
|
import type { ClassInfo, Registry } from './registry.js';
|
|
3
3
|
import { type Step } from './sql.js';
|
|
4
4
|
import type { Executor } from './tx.js';
|
|
5
|
-
import type
|
|
5
|
+
import { type ChainMods, type Cursor, type Filter, type Path, type QueryEvent, type Row } from './types.js';
|
|
6
|
+
import { type Log } from './log.js';
|
|
7
|
+
import { type DeleteOptions, type RestoreOptions } from './write.js';
|
|
6
8
|
export interface ChainCtx {
|
|
7
9
|
readonly reg: Registry;
|
|
8
10
|
readonly schema: string;
|
|
@@ -10,6 +12,10 @@ export interface ChainCtx {
|
|
|
10
12
|
readonly exec: Executor;
|
|
11
13
|
/** Арендатор сессии. */
|
|
12
14
|
readonly tenant: () => string | null;
|
|
15
|
+
/** Аккаунт сессии (с имперсонацией — тот, от чьего имени): арендатор равен ему — личный арендатор. */
|
|
16
|
+
readonly account?: () => string | null;
|
|
17
|
+
/** Фасад входит по токену сессии (connect({ token }), ключ API): без арендатора сессии шаг — sessionless(). */
|
|
18
|
+
readonly hasToken?: () => boolean;
|
|
13
19
|
/** Владелец новых строк по умолчанию (db.as(…, { owner })). */
|
|
14
20
|
readonly owner?: string;
|
|
15
21
|
readonly onQuery?: (e: QueryEvent) => void;
|
|
@@ -28,6 +34,8 @@ export interface ChainCtx {
|
|
|
28
34
|
tenant?: string | null;
|
|
29
35
|
};
|
|
30
36
|
};
|
|
37
|
+
/** Логгер фасада (lib/src/log.ts): события read, write, delete, restore, кэша и error. */
|
|
38
|
+
readonly log?: Log;
|
|
31
39
|
}
|
|
32
40
|
/** План в очереди батча; steps обновляется по мере роста цепочки. */
|
|
33
41
|
export interface BatchEntry {
|
|
@@ -53,7 +61,7 @@ export type Batch = {
|
|
|
53
61
|
};
|
|
54
62
|
export interface ChainCore {
|
|
55
63
|
/**
|
|
56
|
-
* Все пути цепочки. Путь — строки всех шагов, по одной на шаг, связанные
|
|
64
|
+
* Все пути цепочки. Путь — строки всех шагов, по одной на шаг, связанные концами; приходит объектом, где ключ — имя
|
|
57
65
|
* шага (или имя из `alias()`), а значение — строка этого шага: `{ Shop: …, Product: … }`. Если к объекту последнего
|
|
58
66
|
* шага ведут несколько путей, придут все.
|
|
59
67
|
*/
|
|
@@ -65,11 +73,14 @@ export interface ChainCore {
|
|
|
65
73
|
rows(): Promise<Row[]>;
|
|
66
74
|
first(): Promise<Row | null>;
|
|
67
75
|
ids(): Promise<string[]>;
|
|
68
|
-
/** Число
|
|
76
|
+
/** Число объектов последнего шага; { paths: true } — число путей. */
|
|
69
77
|
count(opts?: {
|
|
70
78
|
paths?: boolean;
|
|
71
79
|
}): Promise<number>;
|
|
72
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Все версии объектов последнего шага (надгробия — $deleted), по id и номеру версии. Шаг с явными id находит и
|
|
82
|
+
* удалённые объекты: `db.Класс(id).versions()` отдаёт историю удалённого.
|
|
83
|
+
*/
|
|
73
84
|
versions(): Promise<Row[]>;
|
|
74
85
|
/**
|
|
75
86
|
* Сумма поля `'data.<путь>'` по объектам последнего шага: значения приводятся к числу, складывает база. `limit()` и
|
|
@@ -101,10 +112,14 @@ export interface ChainCore {
|
|
|
101
112
|
/**
|
|
102
113
|
* Не больше n строк ответа: объектов последнего шага, у `paths()` — путей, у `versions()` — версий, перед `countBy()` —
|
|
103
114
|
* групп. До глагола сужает его цели. Сразу после глагола перед `rows`, `first`, `ids`, `count` и `paths` —
|
|
104
|
-
* `invalid_query`: глагол уже прошёл по всем целям, сужать нечего.
|
|
115
|
+
* `invalid_query`: глагол уже прошёл по всем целям, сужать нечего. `n` — целое от 0: дробное — `invalid_query`,
|
|
116
|
+
* отрицательное отклоняет PostgreSQL (ошибка базы с SQLSTATE 2201W).
|
|
105
117
|
*/
|
|
106
118
|
limit(n: number): Chain;
|
|
107
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Пропустить первые n строк ответа — тех же, что считает `limit()`; где действует — как у `limit()`. `n` — целое от 0:
|
|
121
|
+
* дробное — `invalid_query`, отрицательное отклоняет PostgreSQL (ошибка базы с SQLSTATE 2201X).
|
|
122
|
+
*/
|
|
108
123
|
offset(n: number): Chain;
|
|
109
124
|
/** Сортировка по полю последнего шага: 'at' | 'rev' | 'id' | 'data.<путь>'. */
|
|
110
125
|
sort(field: string, dir?: 'asc' | 'desc' | boolean): Chain;
|
|
@@ -118,8 +133,9 @@ export interface ChainCore {
|
|
|
118
133
|
*/
|
|
119
134
|
after(cursor: Cursor): Chain;
|
|
120
135
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
136
|
+
* Шаг, после которого стоит, находит и удалённые объекты — модификатор своего шага, как `exact()`:
|
|
137
|
+
* `db.Member(id).withDeleted().Loan().withDeleted()` — удалённые выдачи удалённого участника. Удалённый объект приходит
|
|
138
|
+
* надгробием — своей последней версией, версией удаления, — с `$deleted: true`.
|
|
123
139
|
*/
|
|
124
140
|
withDeleted(): Chain;
|
|
125
141
|
/** Дети любой глубины на переходе к тому же классу; $depth в строке. */
|
|
@@ -153,7 +169,7 @@ export interface ChainCore {
|
|
|
153
169
|
* следующий шаг пойдёт от него.
|
|
154
170
|
*/
|
|
155
171
|
entity(x: ChainRef | Row): Chain;
|
|
156
|
-
/** Создать объект: концы — из пути и
|
|
172
|
+
/** Создать объект: концы — из пути и шагов роли; занятый ключ — ошибка exists с id. */
|
|
157
173
|
create(data?: Record<string, unknown>, opts?: {
|
|
158
174
|
reason?: string;
|
|
159
175
|
}): Chain;
|
|
@@ -173,22 +189,28 @@ export interface ChainCore {
|
|
|
173
189
|
}): Chain;
|
|
174
190
|
/**
|
|
175
191
|
* Удаляет цели шага; строки, которые на них ссылаются, база удаляет вместе с ними, отвязывает или не даёт удалить цель —
|
|
176
|
-
* как задано в `onDelete` их
|
|
177
|
-
* вернёт все строки, которых оно коснулось бы, у каждой `$action` — что с ней будет.
|
|
178
|
-
*
|
|
179
|
-
* `
|
|
192
|
+
* как задано в `onDelete` их концов. Без `{ confirm: true }` — предпросмотр удаления: ничего не удаляется, терминал
|
|
193
|
+
* вернёт все строки, которых оно коснулось бы, у каждой `$action` — что с ней будет. `{ confirm: true, expect: предпросмотр }`
|
|
194
|
+
* удаляет ровно показанное: в транзакции удаления предпросмотр строится заново и сравнивается с `expect` по `id`, `rev` и
|
|
195
|
+
* `$action`; расхождение — `conflict` (ничего не удалено, новый предпросмотр — `err.detail.preview`); цели сужают фильтр, id,
|
|
196
|
+
* `tags()`, `owner()` или `limit()` хотя бы у одного шага, иначе `invalid_query`. `{ confirm: true }` без `expect` —
|
|
197
|
+
* `invalid_query` (`write.confirm_without_expect`); осознанно удалить текущий состав целей без сверки (и без фильтра —
|
|
198
|
+
* все строки) — `{ confirm: true, all: true }`.
|
|
180
199
|
*/
|
|
181
|
-
delete(opts?:
|
|
182
|
-
confirm?: boolean;
|
|
183
|
-
all?: boolean;
|
|
184
|
-
reason?: string;
|
|
185
|
-
}): Chain;
|
|
200
|
+
delete(opts?: DeleteOptions): Chain;
|
|
186
201
|
/** Чтение с блокировкой строк последнего шага до конца db.begin() (rows, first, ids). */
|
|
187
202
|
forUpdate(): Chain;
|
|
188
|
-
/**
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Восстановить удалённые объекты шага: последний снимок новой версией (id прежний). `{ expect }` — надгробия `{ id, rev }`
|
|
205
|
+
* из ответа `delete()` или `withDeleted()`: восстанавливаются только цели из `expect`, и только если последняя версия
|
|
206
|
+
* каждой — та же; иначе ничего не восстановлено: уже восстановлена — `not_deleted` (`write.restore_already`), id занял
|
|
207
|
+
* новый объект — `not_deleted` (`write.restore_id_taken`), прочее (удалена снова, менялась) — `conflict`
|
|
208
|
+
* (`write.restore_stale`); причины по целям — `err.detail.stale`. `{ expect, partial: true }` — восстановить то, что
|
|
209
|
+
* можно: остальные цели приходят строками `{ …строка, $action: 'skip', $why }`. Без `expect` — `invalid_query`
|
|
210
|
+
* (`write.restore_without_expect`), и для явных id; осознанно вернуть все удалённые цели шага без сверки —
|
|
211
|
+
* `{ all: true }`.
|
|
212
|
+
*/
|
|
213
|
+
restore(opts: RestoreOptions): Chain;
|
|
192
214
|
/** Кэш результатов (§2.12): ответ хранится в памяти процесса ttl секунд (по умолчанию 60). */
|
|
193
215
|
cache(opts?: {
|
|
194
216
|
ttl?: number;
|
|
@@ -196,8 +218,8 @@ export interface ChainCore {
|
|
|
196
218
|
}
|
|
197
219
|
/**
|
|
198
220
|
* Шаг цепочки — имя класса, его алиаса или роли с необязательным фильтром: `db.Shop(id).Product({ price: gt(1000) })`;
|
|
199
|
-
* каждый следующий шаг переходит по
|
|
200
|
-
* роли без вызова задаёт
|
|
221
|
+
* каждый следующий шаг переходит по концам от объектов предыдущего. После `create()`, `update()` или `upsert()` имя
|
|
222
|
+
* роли без вызова задаёт конец записываемой строки: `.Роль.set(…)`, `.Роль.unset()`, у множественного конца ещё
|
|
201
223
|
* `.Роль.add(…)` и `.Роль.remove(…)`.
|
|
202
224
|
*/
|
|
203
225
|
export type StepFn = ((filter?: Filter) => Chain) & {
|
|
@@ -209,7 +231,7 @@ export type StepFn = ((filter?: Filter) => Chain) & {
|
|
|
209
231
|
export type Chain = ChainCore & {
|
|
210
232
|
[className: string]: StepFn;
|
|
211
233
|
};
|
|
212
|
-
/** Любая цепочка — с типами по моделям (`TypedChain`) или без них (`Chain`): аргумент `entity()` и значение
|
|
234
|
+
/** Любая цепочка — с типами по моделям (`TypedChain`) или без них (`Chain`): аргумент `entity()` и значение шага роли (`.Роль.set(цепочка)`). */
|
|
213
235
|
export type ChainRef = Pick<ChainCore, 'ids'>;
|
|
214
236
|
/** Внутренний канал: план чужой ленивой цепочки (entity()). */
|
|
215
237
|
export declare const PLAN: unique symbol;
|
|
@@ -220,6 +242,11 @@ export declare function lookupStep(reg: Registry, name: string): {
|
|
|
220
242
|
cls: ClassInfo;
|
|
221
243
|
role?: string;
|
|
222
244
|
} | undefined;
|
|
245
|
+
/**
|
|
246
|
+
* Цепочка в читаемом виде: `Shop({"name":"A"}).Product.limit(10).rows()`. Фильтры — значениями (поля-секреты — '***'),
|
|
247
|
+
* данные записи — только именами полей.
|
|
248
|
+
*/
|
|
249
|
+
export declare function chainText(log: Log, steps: readonly Step[], mods?: ChainMods, term?: string, args?: readonly unknown[]): string;
|
|
223
250
|
export declare function makeChain(ctx: ChainCtx, steps: Step[], mods: ChainMods, nodes?: NodeMap): Chain;
|
|
224
251
|
/**
|
|
225
252
|
* Батч: очередь планов; каждый вызов класса — своя ссылка на очередь (свой план). run() исполняет очередь
|