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/index.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export { syncModels, generateTypes, schemaToTs, type SyncResult } from './sync.j
|
|
|
29
29
|
export { ne, gt, gte, lt, lte, between, inList, like, ilike, starts, ends, has, hasAny, hasAll, exists, isNull, not, or, isOp } from './ops.js';
|
|
30
30
|
export { cursorOf } from './sql.js';
|
|
31
31
|
export { inc, isInc, type IncMarker, type WriteRow } from './write.js';
|
|
32
|
-
export { totpCode, hashPassword, verifyHash, type AuthApi, type LoginResult, type Credential } from './auth.js';
|
|
32
|
+
export { totpCode, hashPassword, verifyHash, type AuthApi, type LoginResult, type Credential, type SessionInfo } from './auth.js';
|
|
33
33
|
export type { AclApi, AclDecision, AclDataDecision, AclOp } from './acl.js';
|
|
34
34
|
export type { LoadOptions, LoadReport, LoadIssue, LoadInput } from './load.js';
|
|
35
35
|
export type { Chain, ChainCore, StepFn, BatchEntry } from './chain.js';
|
|
@@ -54,36 +54,41 @@ export interface ConnectOptions {
|
|
|
54
54
|
sql?: postgres.Sql;
|
|
55
55
|
/** Схема установки, например `v2.salon`. */
|
|
56
56
|
schema: string;
|
|
57
|
-
/** Токен сессии; либо ключ APIKEY в `apiKey` (он же `service` — ключ сервиса) — его обменяют на сессию. */
|
|
57
|
+
/** Токен сессии; либо ключ `APIKEY` в `apiKey` (он же `service` — ключ сервиса) — его обменяют на сессию. */
|
|
58
58
|
token?: string;
|
|
59
59
|
apiKey?: string;
|
|
60
60
|
service?: string;
|
|
61
61
|
/** Продлевать сессию раз в минуту работы (по умолчанию да, §2.9). */
|
|
62
62
|
refreshSession?: boolean;
|
|
63
|
-
/** Разрешить роль, которая обходит RLS (суперпользователь, BYPASSRLS
|
|
63
|
+
/** Разрешить роль, которая обходит RLS (суперпользователь, `BYPASSRLS`, член роли-владельца), — администраторское подключение (§2.9). */
|
|
64
64
|
allowBypassRls?: boolean;
|
|
65
|
-
/** Имя роли-владельца (по умолчанию letopis_owner). */
|
|
65
|
+
/** Имя роли-владельца (по умолчанию `letopis_owner`). */
|
|
66
66
|
owner?: string;
|
|
67
67
|
/** Интервал отправки отложенного сигнала, мс (по умолчанию 100; решение 9 точки А). */
|
|
68
68
|
notifyIntervalMs?: number;
|
|
69
|
-
/** Размер пула, если подключение создаёт сам connect()
|
|
69
|
+
/** Размер пула, если подключение создаёт сам `connect()`. */
|
|
70
70
|
max?: number;
|
|
71
71
|
/** Хук на каждый запрос цепочки (метрики, лог). */
|
|
72
72
|
onQuery?: (e: QueryEvent) => void;
|
|
73
|
-
/** Порог медленного запроса, мс: событие получает slow
|
|
73
|
+
/** Порог медленного запроса, мс: событие получает `slow`; без `onQuery` — `console.warn`. */
|
|
74
74
|
slowMs?: number;
|
|
75
|
-
/** Модели letopis/model — только для типов цепочек (TypedDb); описания классов берутся из базы. */
|
|
75
|
+
/** Модели `letopis/model` — только для типов цепочек (`TypedDb`); описания классов берутся из базы. */
|
|
76
76
|
models?: readonly AnyModelLike[];
|
|
77
77
|
/**
|
|
78
|
-
* Слушать сигналы базы (LISTEN
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
78
|
+
* Слушать сигналы базы (`LISTEN`). `false` — для пула в режиме транзакций (PgBouncer): тогда реестр классов сам не
|
|
79
|
+
* обновляется (новые и изменённые классы подключение увидит после `db.refresh()`), подписка `watch()` только опрашивает
|
|
80
|
+
* журнал, а кэш результатов работает лишь с `cache: { ttlOnly: true }` — по сроку. В установке с
|
|
81
|
+
* `up({ signals: 'none' })` так всегда, только изменения классов подключение раз в 5 секунд проверяет само.
|
|
82
82
|
*/
|
|
83
83
|
listen?: boolean;
|
|
84
|
-
/** Кэш результатов (§2.12): срок по умолчанию, размер, ttlOnly
|
|
84
|
+
/** Кэш результатов (§2.12): срок по умолчанию, размер, `ttlOnly`; `false` — выключен. */
|
|
85
85
|
cache?: CacheOptions | false;
|
|
86
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Запускать `maintain()` по расписанию: интервал — число миллисекунд или строка вроде `'15m'`, `'1h'`, от 1 мс до
|
|
88
|
+
* 2³¹−1 мс (около 24 суток); иначе `connect()` отказывает с `invalid_data`, не открывая соединений. Если обслуживание в
|
|
89
|
+
* этот момент уже ведёт другой процесс или ещё идёт прошлый запуск, запуск пропускается. `close()` ждёт, пока идущий
|
|
90
|
+
* запуск доделает текущую пачку.
|
|
91
|
+
*/
|
|
87
92
|
maintain?: string | number;
|
|
88
93
|
}
|
|
89
94
|
export interface DbCore {
|
|
@@ -92,50 +97,72 @@ export interface DbCore {
|
|
|
92
97
|
/** Арендатор сессии. */
|
|
93
98
|
readonly tenant: string | null;
|
|
94
99
|
readonly registry: Registry;
|
|
95
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Вход и сессии: способы входа (креды) — пароль, ключ API, TOTP, одноразовый код из письма или СМС, привязка внешней
|
|
102
|
+
* учётной записи; вход по ним, выпуск и гашение сессий, выбор арендатора сессии.
|
|
103
|
+
*/
|
|
96
104
|
readonly auth: AuthApi;
|
|
97
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Права того, от чьего имени идёт запрос, — заранее, для интерфейса и проверок в приложении: `check(endpoint)` — можно
|
|
107
|
+
* ли обратиться к ресурсу приложения, `checkData(класс, операция)` — можно ли читать, менять или удалять объекты
|
|
108
|
+
* класса. Сами права база применяет при каждом запросе.
|
|
109
|
+
*/
|
|
98
110
|
readonly acl: AclApi;
|
|
99
|
-
/** Сырой SQL под сессией и RLS, одной транзакцией (в db.begin() — в ней): db.sql`select
|
|
111
|
+
/** Сырой SQL под сессией и RLS, одной транзакцией (в `db.begin()` — в ней): `` db.sql`select …` ``. */
|
|
100
112
|
sql(strings: TemplateStringsArray, ...values: unknown[]): Promise<postgres.Row[]>;
|
|
101
|
-
/** Транзакция под сессией: fn получает соединение postgres.js (в db.begin() — ту же транзакцию). */
|
|
113
|
+
/** Транзакция под сессией: `fn` получает соединение postgres.js (в `db.begin()` — ту же транзакцию). */
|
|
102
114
|
transaction<T>(fn: (tx: Tx) => Promise<T>): Promise<T>;
|
|
103
|
-
/** id объекта
|
|
115
|
+
/** id объекта по классу и значениям ключа — такой же, какой вычислит база в арендаторе сессии. */
|
|
104
116
|
idOf(cls: string, key: Readonly<Record<string, unknown>>): string;
|
|
105
117
|
/** Перечитать описания классов. */
|
|
106
118
|
refresh(): Promise<void>;
|
|
107
119
|
/** Отправить накопленные сигналы сейчас. */
|
|
108
120
|
flushSignals(): Promise<void>;
|
|
109
121
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
122
|
+
* Проверяет целостность журнала всей установки: сходится ли хэш каждой версии с хэшем предыдущей и текущая строка
|
|
123
|
+
* объекта — с его последней версией; с `{ data: true }` проверяет ещё каждую строку. Якорь в ответе — один хэш текущих
|
|
124
|
+
* версий всех объектов: храните его вне базы, тогда заметна и подмена самых свежих версий. Нужно право `letopis.verify`
|
|
125
|
+
* (по умолчанию оно у системного администратора) или администраторское подключение.
|
|
112
126
|
*/
|
|
113
127
|
verify(opts?: {
|
|
114
128
|
data?: boolean;
|
|
115
129
|
}): Promise<VerifyResult>;
|
|
116
130
|
/** Обслуживание: истёкшие сессии и коды, прореживание журнала по политике хранения, очередь сигналов. */
|
|
117
131
|
maintain(opts?: MaintainOptions): Promise<MaintainReport>;
|
|
118
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Сбрасывает тестовую установку: сессии, данные одного арендатора или всё — смотря по `level`. Работает, только если
|
|
134
|
+
* установка поставлена с `up({ allowReset: true })`, у вызывающего есть право `letopis.reset` (или подключение
|
|
135
|
+
* администраторское), а `confirm` совпадает с именем схемы.
|
|
136
|
+
*/
|
|
119
137
|
reset(opts: ResetOptions): Promise<ResetResult>;
|
|
120
|
-
/**
|
|
138
|
+
/**
|
|
139
|
+
* Стирает версии, сделанные раньше момента `before`; текущая версия остаётся всегда. Вместо стёртого начала истории
|
|
140
|
+
* остаётся служебная запись `trim` со временем появления объекта.
|
|
141
|
+
*/
|
|
121
142
|
trimHistory(target: string | string[] | Row | Row[], before: string | Date): Promise<number>;
|
|
122
|
-
/**
|
|
143
|
+
/**
|
|
144
|
+
* Меняет ключ класса. Объекты его семейства (сам класс и потомки), у которых по новому ключу выходит другой id,
|
|
145
|
+
* переезжают на новый id, а ссылки на них переводятся.
|
|
146
|
+
*/
|
|
123
147
|
rekeyClass(cls: string, key: string[]): Promise<number>;
|
|
124
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* Описание модели для программ и языковых моделей: классы арендатора сессии и общие классы System с полями, ключом,
|
|
150
|
+
* ссылками и метаданными `meta`. `describeText(db.describe())` превращает его в текст.
|
|
151
|
+
*/
|
|
125
152
|
describe(): ModelDescription;
|
|
126
|
-
/** Подписка по курсору: for await (const ev of db.watch({ from }))
|
|
153
|
+
/** Подписка по курсору: `for await (const ev of db.watch({ from })) …`; `sub.cursor` — откуда продолжить. */
|
|
127
154
|
watch(opts?: WatchOptions): Subscription;
|
|
128
155
|
/** Кэш результатов подключения. */
|
|
129
156
|
readonly cache: {
|
|
130
157
|
clear(): void;
|
|
131
158
|
stats(): CacheStats;
|
|
132
159
|
};
|
|
133
|
-
/** Начать путь с готового узла: db.entity(row | цепочка)
|
|
160
|
+
/** Начать путь с готового узла: `db.entity(row | цепочка)`. */
|
|
134
161
|
entity(x: Chain | Row): Chain;
|
|
135
162
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
163
|
+
* Подключение на том же пуле от имени другого аккаунта: запросы идут с его правами и в его арендаторе. Журнал
|
|
164
|
+
* записывает автором версий этот аккаунт, а исполнителем (`agent`) — аккаунт сессии, например сервис. Нужно право
|
|
165
|
+
* `auth.impersonate`.
|
|
139
166
|
*/
|
|
140
167
|
as(account: string | {
|
|
141
168
|
id: string;
|
|
@@ -143,50 +170,51 @@ export interface DbCore {
|
|
|
143
170
|
owner?: string;
|
|
144
171
|
tenant?: string;
|
|
145
172
|
}): Promise<Db>;
|
|
146
|
-
/** Явная транзакция read committed
|
|
147
|
-
/** Явная транзакция; reason — причина изменения для всех операций блока (план 1.1). */
|
|
173
|
+
/** Явная транзакция `read committed`: те же цепочки, `commit()`, `rollback()`, `lock()`, `forUpdate()`; `reason` — причина изменения для всех операций блока (план 1.1). */
|
|
148
174
|
begin(opts?: {
|
|
149
175
|
reason?: string;
|
|
150
176
|
}): Promise<DbTx>;
|
|
151
177
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
178
|
+
* Массовая загрузка строк любых классов одной транзакцией (или порциями `commitEvery`) — быстрее записи цепочками.
|
|
179
|
+
* Строки проверяются по тому же описанию класса, что и при обычной записи (данные, ссылки, ключ), уходят в базу
|
|
180
|
+
* командой `COPY`, а ссылки на объекты вне загрузки сверяет один запрос в конце; права проверяются только при загрузке
|
|
181
|
+
* от имени пользователя (опция `as`). Работает только на администраторском подключении, иначе `admin_required`.
|
|
154
182
|
*/
|
|
155
183
|
load(input: LoadInput, opts?: LoadOptions): Promise<LoadReport>;
|
|
156
184
|
/**
|
|
157
|
-
* Проектор из журнала (план 1.1, §2.4): строит строки производных классов (derivedBy:
|
|
158
|
-
* классов from
|
|
159
|
-
* Только на администраторском подключении (иначе admin_required).
|
|
185
|
+
* Проектор из журнала (план 1.1, §2.4): строит строки производных классов (`derivedBy: имя`) из изменений
|
|
186
|
+
* классов `from`; `run()` — догнать журнал и следовать за ним, `rebuild()` — перестроить с текущего состояния.
|
|
187
|
+
* Только на администраторском подключении (иначе `admin_required`).
|
|
160
188
|
*/
|
|
161
189
|
projector(name: string, opts: ProjectorOptions): Projector;
|
|
162
190
|
/**
|
|
163
191
|
* Ремонт агрегатов в базе (план 1.1, §2.4): поля класса (с потомками) во всех арендаторах — заново из живых строк
|
|
164
192
|
* источников, под исключительными блокировками; версия — только у исправленных. Возвращает их число. Только на
|
|
165
|
-
* администраторском подключении (иначе admin_required).
|
|
193
|
+
* администраторском подключении (иначе `admin_required`).
|
|
166
194
|
*/
|
|
167
195
|
rebuildAggregates(cls: string): Promise<number>;
|
|
168
196
|
/**
|
|
169
|
-
* Именованная очередь планов фасада: batch.Класс(…).create(…)
|
|
170
|
-
* у каждого db.as() и у каждой db.begin() очереди свои: план исполняется от имени фасада, который его поставил.
|
|
197
|
+
* Именованная очередь планов фасада: `batch.Класс(…).create(…)`; `batch.run()` — одной транзакцией. У корня,
|
|
198
|
+
* у каждого `db.as()` и у каждой `db.begin()` очереди свои: план исполняется от имени фасада, который его поставил.
|
|
171
199
|
*/
|
|
172
200
|
batch(name: string, opts?: {
|
|
173
201
|
reason?: string;
|
|
174
202
|
}): Batch;
|
|
175
|
-
/** Advisory-блокировка до конца транзакции — только в db.begin() (иначе tx_required). */
|
|
203
|
+
/** Advisory-блокировка до конца транзакции — только в `db.begin()` (иначе `tx_required`). */
|
|
176
204
|
lock(...keys: (string | number)[]): Promise<void>;
|
|
177
|
-
/** Зафиксировать транзакцию tr (то же, что tr.commit()). */
|
|
205
|
+
/** Зафиксировать транзакцию `tr` (то же, что `tr.commit()`). */
|
|
178
206
|
commit(tr?: DbTx): Promise<void>;
|
|
179
|
-
/** Откатить транзакцию tr (то же, что tr.rollback()). */
|
|
207
|
+
/** Откатить транзакцию `tr` (то же, что `tr.rollback()`). */
|
|
180
208
|
rollback(tr?: DbTx): Promise<void>;
|
|
181
209
|
close(): Promise<void>;
|
|
182
210
|
}
|
|
183
|
-
/** Подключение: служебные методы и классы как стартовые шаги цепочек — db.Класс(фильтр)
|
|
211
|
+
/** Подключение: служебные методы и классы как стартовые шаги цепочек — `db.Класс(фильтр)`. */
|
|
184
212
|
export type Db = DbCore & {
|
|
185
213
|
[className: string]: StepFn;
|
|
186
214
|
};
|
|
187
|
-
/** Явная транзакция db.begin()
|
|
215
|
+
/** Явная транзакция `db.begin()`: тот же API; ошибка любого запроса её ломает (§2.5). */
|
|
188
216
|
export type DbTx = Db;
|
|
189
|
-
/** Подключение с типами шагов, строк и фильтров по моделям: connect({ models })
|
|
217
|
+
/** Подключение с типами шагов, строк и фильтров по моделям: `connect({ models })`. */
|
|
190
218
|
export type TypedDb<M extends readonly AnyModelLike[]> = DbCore & StepsOf<M> & {
|
|
191
219
|
[className: string]: StepFn;
|
|
192
220
|
};
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { LetopisError, fromDbError, removed } from './errors.js';
|
|
|
12
12
|
import { Registry } from './registry.js';
|
|
13
13
|
import { AclPasses, ExplicitTx, Notifier, poolExecutor, runTx } from './tx.js';
|
|
14
14
|
import { checkReason } from './write.js';
|
|
15
|
-
import { registerDb } from './internal.js';
|
|
15
|
+
import { limits, registerDb } from './internal.js';
|
|
16
16
|
import { makeAuth } from './auth.js';
|
|
17
17
|
import { makeAcl } from './acl.js';
|
|
18
18
|
import { idOf as idOfClass } from './uuid.js';
|
|
@@ -23,7 +23,7 @@ import { ACL_CLASSES, ResultCache } from './cache.js';
|
|
|
23
23
|
import { Subscription } from './watch.js';
|
|
24
24
|
import { Projector } from './project.js';
|
|
25
25
|
import { SIGNAL_MODES, Signals, bucket, throttle } from './signals.js';
|
|
26
|
-
import { describe as describeModel, maintain as runMaintain, rebuildAggregates as runRebuildAggregates, rekeyClass as runRekeyClass, reset as runReset, scheduleMaintain, trimHistory as runTrim, verify as runVerify, } from './admin.js';
|
|
26
|
+
import { describe as describeModel, maintain as runMaintain, rebuildAggregates as runRebuildAggregates, rekeyClass as runRekeyClass, reset as runReset, MAINTAIN_STOP, parseInterval, scheduleMaintain, trimHistory as runTrim, verify as runVerify, } from './admin.js';
|
|
27
27
|
import { stampDrift } from './up.js';
|
|
28
28
|
export { DDL_REVISION } from './types.js';
|
|
29
29
|
export { uuidv5, v5Name, v5Id, idOf } from './uuid.js';
|
|
@@ -120,13 +120,23 @@ function makeDb(sh, scope, exec, owner, tx) {
|
|
|
120
120
|
throw removed('watch(cb) и watch(класс, cb)', 'watch({ from }) — подписка по курсору');
|
|
121
121
|
const reg = scope.registry();
|
|
122
122
|
const classes = o.classes ? [...new Set(o.classes.flatMap((c) => reg.family(c).map((x) => x.name)))] : null;
|
|
123
|
-
return new Subscription({
|
|
124
|
-
S: sh.S, exec: poolExecutor(txc), classes,
|
|
123
|
+
return sh.track(new Subscription({
|
|
124
|
+
S: sh.S, exec: poolExecutor(txc), classes, onClose: (s) => void sh.subs.delete(s),
|
|
125
|
+
// не бросает: класс удалили — список, снятый при watch(); иначе ошибка в общем разборе сигналов остановила бы
|
|
126
|
+
// доставку другим подписчикам
|
|
127
|
+
...(o.classes ? { classesOf: () => {
|
|
128
|
+
try {
|
|
129
|
+
return [...new Set(o.classes.flatMap((c) => scope.registry().family(c).map((x) => x.name)))];
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return classes;
|
|
133
|
+
}
|
|
134
|
+
} } : {}),
|
|
125
135
|
...(sh.listen ? {
|
|
126
136
|
onSignal: (fn) => { sh.signals.add(fn); return () => void sh.signals.delete(fn); },
|
|
127
137
|
tenant: () => (sh.allTenants ? null : sh.sig.tenantToken(scope.tenant())),
|
|
128
138
|
} : {}),
|
|
129
|
-
}, o);
|
|
139
|
+
}, o));
|
|
130
140
|
},
|
|
131
141
|
cache: {
|
|
132
142
|
clear: () => sh.cache?.clear(),
|
|
@@ -152,7 +162,7 @@ function makeDb(sh, scope, exec, owner, tx) {
|
|
|
152
162
|
const tenant = r.tenant;
|
|
153
163
|
const registry = await sh.registryFor(tenant, scoped);
|
|
154
164
|
if (registry)
|
|
155
|
-
return makeDb(sh, { txc: scoped, tenant: () => tenant, registry: () => registry, root: false }, poolExecutor(scoped), nextOwner);
|
|
165
|
+
return makeDb(sh, { txc: scoped, tenant: () => tenant, registry: () => sh.registryOf(tenant, registry, scoped), root: false }, poolExecutor(scoped), nextOwner);
|
|
156
166
|
if (attempt === 3)
|
|
157
167
|
throw new LetopisError('no_session', 'db.as(): контекст сессии меняется — реестр арендатора не загружен');
|
|
158
168
|
[r] = await runTx(scoped, (q) => q.unsafe(`select account, tenant from ${sh.S}.ctx()`), { read: true });
|
|
@@ -233,6 +243,8 @@ export async function connect(opts) {
|
|
|
233
243
|
for (const k of Object.keys(opts))
|
|
234
244
|
if (k in REMOVED_OPTIONS)
|
|
235
245
|
throw removed(`опция connect({ ${k} })`, REMOVED_OPTIONS[k]);
|
|
246
|
+
// интервал обслуживания — до пула и таймеров: неверный не оставляет за собой открытых соединений
|
|
247
|
+
const maintainEvery = opts.maintain === undefined ? undefined : parseInterval(opts.maintain);
|
|
236
248
|
const own = !opts.sql;
|
|
237
249
|
// timestamptz — строкой: Date в JS режет микросекунды, а на них держатся asOf и курсоры по времени
|
|
238
250
|
const sql = (opts.sql ?? postgres(opts.dsn, {
|
|
@@ -244,6 +256,10 @@ export async function connect(opts) {
|
|
|
244
256
|
const owner = opts.owner ?? 'letopis_owner';
|
|
245
257
|
try {
|
|
246
258
|
// роль, которая обходит RLS, видела бы данные всех арендаторов (§2.9)
|
|
259
|
+
// роли владельца нет (опечатка в owner) — понятная ошибка, а не сырая 42704 PostgreSQL (план 2.0, 7.5 №3)
|
|
260
|
+
const [known] = await sql `select exists (select 1 from pg_roles where rolname = ${owner}) as ok`;
|
|
261
|
+
if (!known.ok)
|
|
262
|
+
throw new LetopisError('invalid_data', `connect: роли владельца ${owner} в базе нет — проверьте опцию owner (по умолчанию letopis_owner)`);
|
|
247
263
|
const [role] = await sql `select r.rolsuper, r.rolbypassrls,
|
|
248
264
|
pg_has_role(current_user, ${owner}, 'USAGE') as inherits, pg_has_role(current_user, ${owner}, 'SET') as can_set
|
|
249
265
|
from pg_roles r where r.rolname = current_user`;
|
|
@@ -268,7 +284,11 @@ export async function connect(opts) {
|
|
|
268
284
|
throw new LetopisError('invalid_data', `connect: схемы ${opts.schema} нет — установите её: up({ schema })`);
|
|
269
285
|
if (rev < DDL_REVISION)
|
|
270
286
|
console.warn(`letopis: движок схемы ${opts.schema} ревизии ${rev}, библиотека ждёт ${DDL_REVISION} — обновите: up({ schema: '${opts.schema}', upgrade: true })`);
|
|
271
|
-
|
|
287
|
+
// схему обновил более новый пакет (во время раскатки старые экземпляры ещё работают): подключаемся, но предупреждаем —
|
|
288
|
+
// этот пакет может не знать функций и колонок новой ревизии; up() такую схему не трогает и отказывает
|
|
289
|
+
else if (rev > DDL_REVISION)
|
|
290
|
+
console.warn(`letopis: движок схемы ${opts.schema} ревизии ${rev} новее библиотеки (она знает ${DDL_REVISION}) — обновите пакет letopis`);
|
|
291
|
+
else {
|
|
272
292
|
// ревизия та же, а функции схемы ставила другая версия движка — штамп в комментарии схемы (99-revision.sql)
|
|
273
293
|
const drift = await stampDrift(opts.schema, String(rv.c ?? ''));
|
|
274
294
|
if (drift)
|
|
@@ -310,8 +330,18 @@ export async function connect(opts) {
|
|
|
310
330
|
return null;
|
|
311
331
|
return q.unsafe(`select tenant, rev, data from ${S}.entity where class = 'Class' and tenant in (${S}.ctx_tenant(), '${SYSTEM_ID}')`);
|
|
312
332
|
}, { read: true });
|
|
313
|
-
// реестры db.as() — общие на арендатора; txc — контекст последнего успешного as() этого
|
|
333
|
+
// реестры db.as() — общие на арендатора; txc — контекст последнего успешного as() этого арендатора. Порядок карты —
|
|
334
|
+
// давность обращения: сверх limits.asRegistries вытесняется самый давний (план 2.0, 7.4 №8 — иначе процесс,
|
|
335
|
+
// который ходит от имени многих арендаторов, копил бы реестры и перечитывал их все на каждый сигнал классов)
|
|
314
336
|
const others = new Map();
|
|
337
|
+
let latest = null;
|
|
338
|
+
const keep = (tenant, entry) => {
|
|
339
|
+
latest = tenant;
|
|
340
|
+
others.delete(tenant);
|
|
341
|
+
others.set(tenant, entry);
|
|
342
|
+
while (others.size > Math.max(1, limits.asRegistries))
|
|
343
|
+
others.delete(others.keys().next().value);
|
|
344
|
+
};
|
|
315
345
|
const loading = new Map();
|
|
316
346
|
// токены классов (режим opaque) — по именам всех реестров подключения
|
|
317
347
|
const tokens = () => sig.setClasses([root.registry, ...[...others.values()].map((o) => o.registry)].flatMap((r) => r.all().map((c) => c.name)));
|
|
@@ -337,7 +367,7 @@ export async function connect(opts) {
|
|
|
337
367
|
const registryFor = async (tenant, txc) => {
|
|
338
368
|
const known = others.get(tenant);
|
|
339
369
|
if (known) {
|
|
340
|
-
known.txc
|
|
370
|
+
keep(tenant, { registry: known.registry, txc });
|
|
341
371
|
return known.registry;
|
|
342
372
|
}
|
|
343
373
|
// одновременные as() нового арендатора получают один реестр
|
|
@@ -349,7 +379,7 @@ export async function connect(opts) {
|
|
|
349
379
|
return null;
|
|
350
380
|
const registry = new Registry();
|
|
351
381
|
registry.load(rows);
|
|
352
|
-
|
|
382
|
+
keep(tenant, { registry, txc });
|
|
353
383
|
tokens();
|
|
354
384
|
return registry;
|
|
355
385
|
})().finally(() => loading.delete(tenant));
|
|
@@ -357,6 +387,20 @@ export async function connect(opts) {
|
|
|
357
387
|
}
|
|
358
388
|
return pending;
|
|
359
389
|
};
|
|
390
|
+
// фасад db.as() берёт реестр через карту: свежий реестр арендатора, если его перечитал другой as(); вытесненный —
|
|
391
|
+
// обратно в карту (его снова обновляют сигналы) и перечитать фоном: пока он был вне карты, сигналы его не трогали
|
|
392
|
+
const registryOf = (tenant, registry, txc) => {
|
|
393
|
+
const cur = others.get(tenant);
|
|
394
|
+
if (cur) {
|
|
395
|
+
if (latest !== tenant)
|
|
396
|
+
keep(tenant, cur);
|
|
397
|
+
return cur.registry;
|
|
398
|
+
}
|
|
399
|
+
keep(tenant, { registry, txc });
|
|
400
|
+
void loadClasses(txc, tenant).then((rows) => { if (rows)
|
|
401
|
+
registry.load(rows); }).catch(() => { }).finally(tokens);
|
|
402
|
+
return registry;
|
|
403
|
+
};
|
|
360
404
|
// ключ сигналов (режимы tenant и opaque) — сессии с правом letopis.signals; без права процесс
|
|
361
405
|
// работает без ключа: арендатора (а в opaque — и класса) в сигнале не различает
|
|
362
406
|
const fetchKey = async () => {
|
|
@@ -462,8 +506,9 @@ export async function connect(opts) {
|
|
|
462
506
|
stamper.unref?.();
|
|
463
507
|
}
|
|
464
508
|
let stopMaintain = null;
|
|
509
|
+
const subs = new Set();
|
|
465
510
|
const shared = {
|
|
466
|
-
sql, schema: opts.schema, S, notifier, passes, root, registryFor, refresh, onQuery: opts.onQuery, slowMs: opts.slowMs,
|
|
511
|
+
sql, schema: opts.schema, S, notifier, passes, root, registryFor, registryOf, refresh, onQuery: opts.onQuery, slowMs: opts.slowMs,
|
|
467
512
|
admin: Boolean(bypass && (role.rolsuper || role.can_set)),
|
|
468
513
|
ownerRole: owner,
|
|
469
514
|
cache,
|
|
@@ -471,8 +516,18 @@ export async function connect(opts) {
|
|
|
471
516
|
allTenants: Boolean(bypass),
|
|
472
517
|
signals,
|
|
473
518
|
listen,
|
|
519
|
+
subs,
|
|
520
|
+
track: (s) => {
|
|
521
|
+
subs.add(s);
|
|
522
|
+
return s;
|
|
523
|
+
},
|
|
474
524
|
async close() {
|
|
475
|
-
|
|
525
|
+
// живые подписки закрываются: иначе с чужим пулом (connect({ sql })) они опрашивали бы журнал и после close()
|
|
526
|
+
// (план 2.0, 7.4 №8)
|
|
527
|
+
for (const s of [...subs])
|
|
528
|
+
s.close();
|
|
529
|
+
// идущий maintain() по расписанию доделывает пачку: пул закрывается после неё, а не посреди транзакции
|
|
530
|
+
await stopMaintain?.();
|
|
476
531
|
resync.stop();
|
|
477
532
|
startReload.stop();
|
|
478
533
|
if (stamper)
|
|
@@ -484,8 +539,8 @@ export async function connect(opts) {
|
|
|
484
539
|
},
|
|
485
540
|
};
|
|
486
541
|
const db = makeDb(shared, { txc: ctx, tenant: () => root.tenant, registry: () => root.registry, root: true }, poolExecutor(ctx));
|
|
487
|
-
if (
|
|
488
|
-
stopMaintain = scheduleMaintain(() => db.maintain(),
|
|
542
|
+
if (maintainEvery !== undefined)
|
|
543
|
+
stopMaintain = scheduleMaintain((stopped) => db.maintain({ [MAINTAIN_STOP]: stopped }), maintainEvery);
|
|
489
544
|
return db;
|
|
490
545
|
}
|
|
491
546
|
catch (e) {
|
package/dist/internal.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export interface DbInternals {
|
|
|
10
10
|
/** Фасад явной транзакции db.begin(). */
|
|
11
11
|
readonly explicit: boolean;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Пределы подключения, которых нет в опциях: реестров классов db.as() — не больше asRegistries арендаторов (план 2.0,
|
|
15
|
+
* 7.4 №8). Тест меняет предел, чтобы проверить вытеснение без тысячи арендаторов.
|
|
16
|
+
*/
|
|
17
|
+
export declare const limits: {
|
|
18
|
+
asRegistries: number;
|
|
19
|
+
};
|
|
13
20
|
/** Запомнить фасад (зовёт makeDb для каждого фасада, который отдаёт наружу). */
|
|
14
21
|
export declare function registerDb(db: object, info: DbInternals): void;
|
|
15
22
|
/** Свойства фасада; undefined — объект не фасад connect() этого пакета. */
|
package/dist/internal.js
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* фасад db обходит RLS, не полагаясь на то, что сообщает о себе сам вызывающий.
|
|
5
5
|
*/
|
|
6
6
|
const facades = new WeakMap();
|
|
7
|
+
/**
|
|
8
|
+
* Пределы подключения, которых нет в опциях: реестров классов db.as() — не больше asRegistries арендаторов (план 2.0,
|
|
9
|
+
* 7.4 №8). Тест меняет предел, чтобы проверить вытеснение без тысячи арендаторов.
|
|
10
|
+
*/
|
|
11
|
+
export const limits = { asRegistries: 1000 };
|
|
7
12
|
/** Запомнить фасад (зовёт makeDb для каждого фасада, который отдаёт наружу). */
|
|
8
13
|
export function registerDb(db, info) {
|
|
9
14
|
facades.set(db, info);
|
package/dist/load.d.ts
CHANGED
|
@@ -12,7 +12,10 @@ export interface LoadOptions {
|
|
|
12
12
|
batch?: number;
|
|
13
13
|
/** Фиксировать порциями примерно по N строк (порция — классы в порядке зависимостей, цикл — целиком). */
|
|
14
14
|
commitEvery?: number;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Режим истории (импорт): каждая строка — версия объекта с номером `$rev`, временем `$at` и видом операции `$op`; так
|
|
17
|
+
* переносят историю объектов из другой системы.
|
|
18
|
+
*/
|
|
16
19
|
withHistory?: boolean;
|
|
17
20
|
/** В конце перепроверить загруженные строки проверкой базы. */
|
|
18
21
|
verify?: boolean;
|
|
@@ -20,7 +23,10 @@ export interface LoadOptions {
|
|
|
20
23
|
class?: string;
|
|
21
24
|
/** Арендатор строк (по умолчанию — арендатор сессии). */
|
|
22
25
|
tenant?: string;
|
|
23
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Исполнитель записи — строка, которую журнал сохранит в поле `agent` загруженных версий рядом с автором (по умолчанию
|
|
28
|
+
* «letopis load»; импорт — «letopis import»).
|
|
29
|
+
*/
|
|
24
30
|
agent?: string;
|
|
25
31
|
/**
|
|
26
32
|
* Загрузка от имени пользователя (§2.6, п. 10): автор версий — он; арендатор (по умолчанию —
|
|
@@ -128,7 +134,8 @@ export declare function load(env: LoadEnv, input: LoadInput, opts?: LoadOptions)
|
|
|
128
134
|
* Первая заливка (план 1.1, §2.3): загрузки fn — одной транзакцией в пустую схему. Блокировка таблиц до
|
|
129
135
|
* проверки; в схеме только строки системных классов и записи reset (иначе not_empty); они (с seq и tx
|
|
130
136
|
* журнала) и определения ограничений, индексов и триггеров из каталога (с режимом включения tgenabled:
|
|
131
|
-
* pg_get_triggerdef его не передаёт) сохраняются в памяти;
|
|
137
|
+
* pg_get_triggerdef его не передаёт) сохраняются в памяти; внешний ключ secret.account снимается до TRUNCATE и
|
|
138
|
+
* возвращается после первичного ключа entity; TRUNCATE; триггеры, ограничения и индексы
|
|
132
139
|
* снимаются — BEFORE ROW делает COPY построчным одним своим наличием, триггеры после оператора копят
|
|
133
140
|
* переходные таблицы; системные строки возвращаются (seq журнала — generated always: overriding system
|
|
134
141
|
* value); загрузки пишут COPY FREEZE. В конце — ограничения (дубликат найдёт сортировка), индексы,
|