umbot-ydb-adapter 1.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/LICENSE +21 -0
- package/LICENSE.md +21 -0
- package/README.md +293 -0
- package/dist/YdbAdapter.d.ts +199 -0
- package/dist/YdbAdapter.js +457 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +8 -0
- package/dist/yql.d.ts +196 -0
- package/dist/yql.js +584 -0
- package/package.json +95 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.YdbAdapter = void 0;
|
|
4
|
+
const plugins_1 = require("umbot/plugins");
|
|
5
|
+
const umbot_1 = require("umbot");
|
|
6
|
+
const core_1 = require("@ydbjs/core");
|
|
7
|
+
const query_1 = require("@ydbjs/query");
|
|
8
|
+
const anonymous_1 = require("@ydbjs/auth/anonymous");
|
|
9
|
+
const environ_1 = require("@ydbjs/auth/environ");
|
|
10
|
+
const metadata_1 = require("@ydbjs/auth/metadata");
|
|
11
|
+
const static_1 = require("@ydbjs/auth/static");
|
|
12
|
+
const yql_1 = require("./yql");
|
|
13
|
+
const DEFAULT_CONNECT_TIMEOUT = 10000;
|
|
14
|
+
const DEFAULT_QUERY_TIMEOUT = 5000;
|
|
15
|
+
// Создание таблицы или индекса в serverless YDB может идти дольше обычного запроса
|
|
16
|
+
const MIN_DDL_TIMEOUT = 30000;
|
|
17
|
+
const CONNECTION_STRING_ENV = 'YDB_CONNECTION_STRING';
|
|
18
|
+
/**
|
|
19
|
+
* Адаптер umbot для YDB (Yandex Database).
|
|
20
|
+
*
|
|
21
|
+
* Хранит `UsersData`, `ImageTokens`, `SoundTokens` и данные своих моделей. Таблицы
|
|
22
|
+
* встроенных моделей создаёт сам (`ensureSchema`). Все значения уходят в запрос
|
|
23
|
+
* параметрами (`$p0`, `$p1`, ...): конкатенации пользовательских данных в YQL нет.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { Bot } from 'umbot';
|
|
28
|
+
* import { YdbAdapter } from 'umbot-ydb-adapter';
|
|
29
|
+
*
|
|
30
|
+
* // Строка подключения — из YDB_CONNECTION_STRING, авторизация — из окружения
|
|
31
|
+
* const bot = new Bot().use(new YdbAdapter());
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
class YdbAdapter extends plugins_1.BaseDbAdapter {
|
|
35
|
+
dbFormat = 'ydb';
|
|
36
|
+
#driver;
|
|
37
|
+
#sql;
|
|
38
|
+
#tables = new Map();
|
|
39
|
+
/**
|
|
40
|
+
* @param config Параметры подключения (см. {@link IYdbAdapterConfig}). Без них строка подключения
|
|
41
|
+
* берётся из `appConfig.db` или из `YDB_CONNECTION_STRING`.
|
|
42
|
+
*/
|
|
43
|
+
constructor(config) {
|
|
44
|
+
// Пустые host/database не мешают: при выборе строки подключения пустое значение пропускается
|
|
45
|
+
super(config
|
|
46
|
+
? { host: '', database: '', ...config, options: config.options }
|
|
47
|
+
: undefined);
|
|
48
|
+
// Схема встроенных таблиц нужна и без ensureSchema: по ней выбираются типы и индексы
|
|
49
|
+
for (const schema of umbot_1.DB_TABLES_SCHEMA) {
|
|
50
|
+
this.#tables.set(schema.tableName, (0, yql_1.tableInfoFromSchema)(schema));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Опции адаптера из параметров конструктора или из `appConfig.db.options`.
|
|
55
|
+
*/
|
|
56
|
+
#getOptions() {
|
|
57
|
+
const options = this._dbOptions?.options ?? this._appContext.appConfig.db?.options;
|
|
58
|
+
return (options ?? {});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Строка подключения: `options.connectionString`, затем `host` + `database`,
|
|
62
|
+
* затем переменная окружения `YDB_CONNECTION_STRING`.
|
|
63
|
+
*/
|
|
64
|
+
#getConnectionString() {
|
|
65
|
+
const options = this.#getOptions();
|
|
66
|
+
if (options.connectionString) {
|
|
67
|
+
return options.connectionString;
|
|
68
|
+
}
|
|
69
|
+
const host = this._dbOptions?.host || this._appContext.appConfig.db?.host;
|
|
70
|
+
const database = this._dbOptions?.database || this._appContext.appConfig.db?.database;
|
|
71
|
+
if (host && database) {
|
|
72
|
+
const base = host.replace(/\/+$/, '');
|
|
73
|
+
return `${base}/?database=${encodeURIComponent(database)}`;
|
|
74
|
+
}
|
|
75
|
+
return process.env[CONNECTION_STRING_ENV] || undefined;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Провайдер авторизации и TLS-опции для драйвера.
|
|
79
|
+
*/
|
|
80
|
+
#getCredentials(connectionString) {
|
|
81
|
+
const credentials = this.#getOptions().credentials;
|
|
82
|
+
if (credentials && typeof credentials === 'object') {
|
|
83
|
+
return { provider: credentials };
|
|
84
|
+
}
|
|
85
|
+
switch (credentials) {
|
|
86
|
+
case 'metadata':
|
|
87
|
+
return { provider: new metadata_1.MetadataCredentialsProvider() };
|
|
88
|
+
case 'anonymous':
|
|
89
|
+
return { provider: new anonymous_1.AnonymousCredentialsProvider() };
|
|
90
|
+
}
|
|
91
|
+
const user = this._dbOptions?.user || this._appContext.appConfig.db?.user;
|
|
92
|
+
const pass = this._dbOptions?.pass || this._appContext.appConfig.db?.pass;
|
|
93
|
+
if (credentials !== 'environ' && user && pass) {
|
|
94
|
+
// Без TLS-контекста провайдер открыл бы незащищённый канал и для grpcs
|
|
95
|
+
const secureOptions = connectionString.startsWith('grpcs:') ? {} : undefined;
|
|
96
|
+
return {
|
|
97
|
+
provider: new static_1.StaticCredentialsProvider({ username: user, password: pass }, connectionString, secureOptions),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const environ = new environ_1.EnvironCredentialsProvider(connectionString);
|
|
101
|
+
return { provider: environ, secureOptions: environ.secureOptions };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Подключается к YDB: создаёт драйвер и ждёт discovery не дольше `connectTimeout`.
|
|
105
|
+
* Повторный вызов закрывает предыдущее подключение.
|
|
106
|
+
* @returns true при успешном подключении
|
|
107
|
+
*/
|
|
108
|
+
async connect() {
|
|
109
|
+
await this.#closeDriver();
|
|
110
|
+
const connectionString = this.#getConnectionString();
|
|
111
|
+
if (!connectionString) {
|
|
112
|
+
this._saveLog(`Не задана строка подключения: укажите host и database, options.connectionString или переменную ${CONNECTION_STRING_ENV}.`);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
const options = this.#getOptions();
|
|
116
|
+
const connectTimeout = options.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT;
|
|
117
|
+
let driver;
|
|
118
|
+
try {
|
|
119
|
+
for (const info of this.#customTables()) {
|
|
120
|
+
this.#tables.set(info.tableName, info);
|
|
121
|
+
}
|
|
122
|
+
const { provider, secureOptions } = this.#getCredentials(connectionString);
|
|
123
|
+
driver = new core_1.Driver(connectionString, {
|
|
124
|
+
credentialsProvider: provider,
|
|
125
|
+
secureOptions,
|
|
126
|
+
'ydb.sdk.ready_timeout_ms': connectTimeout,
|
|
127
|
+
});
|
|
128
|
+
await driver.ready(AbortSignal.timeout(connectTimeout));
|
|
129
|
+
const sql = options.poolSize === undefined
|
|
130
|
+
? (0, query_1.query)(driver)
|
|
131
|
+
: (0, query_1.query)(driver, { poolOptions: { maxSize: options.poolSize } });
|
|
132
|
+
this.#driver = driver;
|
|
133
|
+
this.#sql = sql;
|
|
134
|
+
const databaseInfo = (this._appContext.database.databaseInfo ??= {});
|
|
135
|
+
databaseInfo.driver = driver;
|
|
136
|
+
databaseInfo.sql = sql;
|
|
137
|
+
this._appContext.log('YDB: подключение установлено');
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
driver?.close();
|
|
142
|
+
// Строку подключения не логируем целиком: в query-параметрах бывают служебные данные
|
|
143
|
+
this._saveLog('Не удалось подключиться к базе данных', error);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Описания своих таблиц из опции `tables`.
|
|
149
|
+
* @returns Описания таблиц; бросает YqlBuildError, если описание некорректно
|
|
150
|
+
*/
|
|
151
|
+
#customTables() {
|
|
152
|
+
return (this.#getOptions().tables ?? []).map(yql_1.tableInfoFromDefinition);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Создаёт недостающие таблицы, колонки и индексы встроенных моделей и таблиц из опции `tables`.
|
|
156
|
+
*
|
|
157
|
+
* Сначала выполняется один запрос-проверка (`SELECT ... LIMIT 0` по всем таблицам и индексам).
|
|
158
|
+
* Если схема уже готова — на этом всё: в Cloud Functions метод вызывается на каждом холодном
|
|
159
|
+
* старте, и DDL на каждый старт стоил бы заметного времени.
|
|
160
|
+
* @param tables Описание таблиц (`DB_TABLES_SCHEMA`)
|
|
161
|
+
* @returns true, если схема готова
|
|
162
|
+
*/
|
|
163
|
+
async ensureSchema(tables) {
|
|
164
|
+
let infos;
|
|
165
|
+
try {
|
|
166
|
+
infos = [...tables.map(yql_1.tableInfoFromSchema), ...this.#customTables()];
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
this._saveLog('ensureSchema: некорректное описание таблицы', error);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
for (const info of infos) {
|
|
173
|
+
this.#tables.set(info.tableName, info);
|
|
174
|
+
}
|
|
175
|
+
if (this.#getOptions().ensureSchema === false || !infos.length) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
if (!this.#sql) {
|
|
179
|
+
this._saveLog('ensureSchema: нет подключения к базе данных');
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
const check = infos.map(yql_1.buildSchemaCheck).join('\n');
|
|
183
|
+
if (await this.#tryExecute(check)) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
for (const info of infos) {
|
|
188
|
+
await this.#prepareTable(info);
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
this._saveLog('ensureSchema: не удалось подготовить таблицы', error);
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Создаёт таблицу, если её нет, и добавляет недостающие колонки и индексы.
|
|
199
|
+
*/
|
|
200
|
+
async #prepareTable(info) {
|
|
201
|
+
await this.#executeDdl((0, yql_1.buildCreateTable)(info));
|
|
202
|
+
for (const [column, type] of info.columns) {
|
|
203
|
+
if (!(await this.#tryExecute((0, yql_1.buildColumnCheck)(info.tableName, column)))) {
|
|
204
|
+
await this.#executeDdl((0, yql_1.buildAddColumn)(info.tableName, column, type));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const index of info.indexes) {
|
|
208
|
+
if (!(await this.#tryExecute((0, yql_1.buildIndexCheck)(info.tableName, index)))) {
|
|
209
|
+
await this.#executeDdl((0, yql_1.buildAddIndex)(info.tableName, index));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Выполняет DDL-запрос с таймаутом не меньше 30 с и без повторов SDK.
|
|
215
|
+
*/
|
|
216
|
+
async #executeDdl(text) {
|
|
217
|
+
const timeout = Math.max(this.#getQueryTimeout(), MIN_DDL_TIMEOUT);
|
|
218
|
+
await this.#execute({ text, params: {} }, { idempotent: false, timeout });
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Таймаут обычного запроса из опций.
|
|
222
|
+
*/
|
|
223
|
+
#getQueryTimeout() {
|
|
224
|
+
return this.#getOptions().queryTimeout ?? DEFAULT_QUERY_TIMEOUT;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Выполняет запрос-проверку схемы.
|
|
228
|
+
* @returns true, если запрос прошёл без ошибок
|
|
229
|
+
*/
|
|
230
|
+
async #tryExecute(text) {
|
|
231
|
+
try {
|
|
232
|
+
await this.#execute({ text, params: {} });
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Выполняет запрос с ограничением по времени (вместе с повторами SDK).
|
|
241
|
+
* @param statement Текст и параметры
|
|
242
|
+
* @param options `idempotent` — можно ли SDK повторить запрос (по умолчанию да);
|
|
243
|
+
* `timeout` — мс, по умолчанию `queryTimeout`; `write` — изменение данных
|
|
244
|
+
* @returns Наборы строк результата
|
|
245
|
+
*/
|
|
246
|
+
async #execute(statement, options = {}) {
|
|
247
|
+
const sql = this.#sql;
|
|
248
|
+
if (!sql) {
|
|
249
|
+
throw new Error('Нет подключения к базе данных');
|
|
250
|
+
}
|
|
251
|
+
let q = sql(statement.text);
|
|
252
|
+
for (const name of Object.keys(statement.params)) {
|
|
253
|
+
q = q.parameter(name, statement.params[name]);
|
|
254
|
+
}
|
|
255
|
+
if (options.write) {
|
|
256
|
+
// YDB меняет таблицы с вторичными индексами только в Serializable-транзакции
|
|
257
|
+
q = q.isolation('serializableReadWrite');
|
|
258
|
+
}
|
|
259
|
+
const timeout = options.timeout ?? this.#getQueryTimeout();
|
|
260
|
+
return (await q.idempotent(options.idempotent ?? true).timeout(timeout));
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Контекст сборки запроса: схема таблицы и правила модели.
|
|
264
|
+
*/
|
|
265
|
+
#context(data) {
|
|
266
|
+
return { table: this.#tables.get(data.tableName), rules: data.rules };
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Выполняет SELECT.
|
|
270
|
+
*
|
|
271
|
+
* Контракт umbot: при `isOne` в `data` — сама запись (не массив); пустая выборка —
|
|
272
|
+
* `{ status: false }` без `error`; сбой — `{ status: false, error }`.
|
|
273
|
+
* @param selectData Информация о таблице
|
|
274
|
+
* @param where Условия выборки
|
|
275
|
+
* @param isOne Вернуть только одну запись
|
|
276
|
+
* @returns Результат выборки
|
|
277
|
+
*/
|
|
278
|
+
async _select(selectData, where, isOne) {
|
|
279
|
+
try {
|
|
280
|
+
const statement = (0, yql_1.buildSelect)(selectData.tableName, where, isOne, this.#context(selectData));
|
|
281
|
+
const [rows = []] = await this.#execute(statement);
|
|
282
|
+
if (!rows.length) {
|
|
283
|
+
return { status: false };
|
|
284
|
+
}
|
|
285
|
+
const data = rows.map(yql_1.normalizeRow);
|
|
286
|
+
return { status: true, data: isOne ? data[0] : data };
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
this._saveLog(`Ошибка SELECT из таблицы "${selectData.tableName}"`, error);
|
|
290
|
+
return { status: false, error: getErrorMessage(error) };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Добавляет запись (UPSERT): повторная запись с тем же ключом перезаписывает строку,
|
|
295
|
+
* поэтому SDK может безопасно повторить запрос после временной ошибки сети.
|
|
296
|
+
* @param insertData Запрос с данными в `data`
|
|
297
|
+
* @returns true при успехе
|
|
298
|
+
*/
|
|
299
|
+
async _insert(insertData) {
|
|
300
|
+
try {
|
|
301
|
+
const statement = (0, yql_1.buildUpsert)(insertData.tableName, insertData.data ?? {}, this.#context(insertData));
|
|
302
|
+
await this.#execute(statement, { write: true });
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
this._saveLog(`Ошибка записи в таблицу "${insertData.tableName}"`, error);
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Обновляет записи по условию `query`. Без условий запрос не выполняется.
|
|
312
|
+
* @param updateData Запрос: условия в `query`, новые значения в `data`
|
|
313
|
+
* @returns true при успехе (в том числе если под условие не попало ни одной записи)
|
|
314
|
+
*/
|
|
315
|
+
async _update(updateData) {
|
|
316
|
+
try {
|
|
317
|
+
const statement = (0, yql_1.buildUpdate)(updateData.tableName, updateData.data ?? {}, updateData.query, this.#context(updateData));
|
|
318
|
+
if (statement) {
|
|
319
|
+
await this.#execute(statement, { write: true });
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
this._saveLog(`Ошибка UPDATE таблицы "${updateData.tableName}"`, error);
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Удаляет записи по условию `query`. Без условий запрос не выполняется.
|
|
330
|
+
* @param removeData Запрос: условия в `query`
|
|
331
|
+
* @returns true при успехе
|
|
332
|
+
*/
|
|
333
|
+
async _remove(removeData) {
|
|
334
|
+
try {
|
|
335
|
+
const statement = (0, yql_1.buildDelete)(removeData.tableName, removeData.query, this.#context(removeData));
|
|
336
|
+
await this.#execute(statement, { write: true });
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
this._saveLog(`Ошибка DELETE из таблицы "${removeData.tableName}"`, error);
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Выполняет произвольный запрос: callback получает клиент запросов и драйвер.
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```ts
|
|
349
|
+
* const count = await usersData.query(async (sql) => {
|
|
350
|
+
* const [[row]] = await sql`SELECT COUNT(*) AS cnt FROM UsersData`;
|
|
351
|
+
* return { status: true, data: row };
|
|
352
|
+
* });
|
|
353
|
+
* ```
|
|
354
|
+
* @param callback Функция с запросом
|
|
355
|
+
* @returns `data` из результата callback или null при ошибке
|
|
356
|
+
*/
|
|
357
|
+
async _query(callback) {
|
|
358
|
+
if (!this.#sql || !this.#driver) {
|
|
359
|
+
this._saveLog('Нет подключения к базе данных');
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const res = await callback(this.#sql, this.#driver);
|
|
364
|
+
if (res.status) {
|
|
365
|
+
return res.data;
|
|
366
|
+
}
|
|
367
|
+
this._saveLog(String(res.error));
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
this._saveLog('Ошибка в query()', error);
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Выполняет произвольный запрос (см. `_query`).
|
|
377
|
+
* @param callback Функция с запросом
|
|
378
|
+
* @returns `data` из результата callback или null при ошибке
|
|
379
|
+
*/
|
|
380
|
+
query(callback) {
|
|
381
|
+
return this._query(callback);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Экранирует строку для своего запроса: результат можно поставить внутрь `'...'` в YQL.
|
|
385
|
+
* Адаптер этим методом не пользуется — значения уходят параметрами; для своих запросов
|
|
386
|
+
* тоже лучше параметры (`` sql`... ${value}` `` в `query()`).
|
|
387
|
+
* @param str Значение
|
|
388
|
+
* @returns Экранированная строка
|
|
389
|
+
*/
|
|
390
|
+
escapeString(str) {
|
|
391
|
+
return (0, yql_1.escapeYqlString)(str);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Проверяет соединение запросом `SELECT 1`.
|
|
395
|
+
* @returns true, если база отвечает
|
|
396
|
+
*/
|
|
397
|
+
async isConnected() {
|
|
398
|
+
if (!this.#sql) {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
await this.#execute({ text: 'SELECT 1', params: {} });
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Закрывает драйвер и очищает `databaseInfo`. Безопасен к повторному вызову.
|
|
411
|
+
*/
|
|
412
|
+
async destroy() {
|
|
413
|
+
await super.destroy();
|
|
414
|
+
await this.#closeDriver();
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Закрывает пул сессий и драйвер, убирает ссылки на них из контекста приложения.
|
|
418
|
+
*/
|
|
419
|
+
async #closeDriver() {
|
|
420
|
+
const driver = this.#driver;
|
|
421
|
+
const sql = this.#sql;
|
|
422
|
+
this.#driver = undefined;
|
|
423
|
+
this.#sql = undefined;
|
|
424
|
+
const databaseInfo = this._appContext?.database?.databaseInfo;
|
|
425
|
+
if (databaseInfo) {
|
|
426
|
+
delete databaseInfo.driver;
|
|
427
|
+
delete databaseInfo.sql;
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
// Пул сессий закрывается до драйвера: иначе сессии на сервере живут до своего таймаута
|
|
431
|
+
await sql?.[Symbol.asyncDispose]();
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
this._saveLog('Ошибка при закрытии пула сессий', error);
|
|
435
|
+
}
|
|
436
|
+
driver?.close();
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Пишет ошибку в лог umbot (секреты маскирует логгер фреймворка).
|
|
440
|
+
* @param message Текст ошибки
|
|
441
|
+
* @param error Исключение
|
|
442
|
+
*/
|
|
443
|
+
_saveLog(message, error) {
|
|
444
|
+
const text = error === undefined ? message : `${message}: ${getErrorMessage(error)}`;
|
|
445
|
+
this._appContext?.logError(`YDB: ${text}`, error === undefined ? {} : { error });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
exports.YdbAdapter = YdbAdapter;
|
|
449
|
+
/**
|
|
450
|
+
* Текст ошибки для лога и поля `error` результата.
|
|
451
|
+
*/
|
|
452
|
+
function getErrorMessage(error) {
|
|
453
|
+
if (error instanceof Error) {
|
|
454
|
+
return error.message;
|
|
455
|
+
}
|
|
456
|
+
return String(error);
|
|
457
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { YdbAdapter } from './YdbAdapter';
|
|
2
|
+
export type { IYdbAdapterConfig, IYdbDbInfo, IYdbOptions, TYdbCredentials } from './YdbAdapter';
|
|
3
|
+
export { SUPPORTED_OPERATORS, YDB_COLUMN_TYPES } from './yql';
|
|
4
|
+
export type { IYdbTableDefinition, TYdbColumnType } from './yql';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.YDB_COLUMN_TYPES = exports.SUPPORTED_OPERATORS = exports.YdbAdapter = void 0;
|
|
4
|
+
var YdbAdapter_1 = require("./YdbAdapter");
|
|
5
|
+
Object.defineProperty(exports, "YdbAdapter", { enumerable: true, get: function () { return YdbAdapter_1.YdbAdapter; } });
|
|
6
|
+
var yql_1 = require("./yql");
|
|
7
|
+
Object.defineProperty(exports, "SUPPORTED_OPERATORS", { enumerable: true, get: function () { return yql_1.SUPPORTED_OPERATORS; } });
|
|
8
|
+
Object.defineProperty(exports, "YDB_COLUMN_TYPES", { enumerable: true, get: function () { return yql_1.YDB_COLUMN_TYPES; } });
|
package/dist/yql.d.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { IDbTableSchema, IModelRules, IQueryData } from 'umbot';
|
|
2
|
+
import type { Value } from '@ydbjs/value';
|
|
3
|
+
/**
|
|
4
|
+
* Типы колонок YDB, которые понимает адаптер.
|
|
5
|
+
*/
|
|
6
|
+
export declare const YDB_COLUMN_TYPES: readonly ["Utf8", "Int64", "Double", "Bool"];
|
|
7
|
+
/**
|
|
8
|
+
* Тип колонки YDB: в него адаптер приводит значение параметра.
|
|
9
|
+
*/
|
|
10
|
+
export type TYdbColumnType = (typeof YDB_COLUMN_TYPES)[number];
|
|
11
|
+
/**
|
|
12
|
+
* Вторичный индекс таблицы.
|
|
13
|
+
*/
|
|
14
|
+
export interface IYdbIndexInfo {
|
|
15
|
+
/** Имя индекса (`idx_<поля>`) */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Поля индекса в порядке объявления */
|
|
18
|
+
fields: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Что адаптер знает о таблице: ключ, типы колонок и индексы.
|
|
22
|
+
*/
|
|
23
|
+
export interface IYdbTableInfo {
|
|
24
|
+
/** Имя таблицы */
|
|
25
|
+
tableName: string;
|
|
26
|
+
/** Колонки первичного ключа: `primaryKeyName` и `uniqueKeys` из схемы umbot */
|
|
27
|
+
primaryKey: readonly string[];
|
|
28
|
+
/** Типы колонок */
|
|
29
|
+
columns: ReadonlyMap<string, TYdbColumnType>;
|
|
30
|
+
/** Вторичные индексы (кроме тех, что совпадают с началом первичного ключа) */
|
|
31
|
+
indexes: readonly IYdbIndexInfo[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Готовый к выполнению запрос: текст YQL и параметры `$pN`.
|
|
35
|
+
*/
|
|
36
|
+
export interface IYqlStatement {
|
|
37
|
+
/** Текст запроса без `DECLARE`: SDK добавляет их сам по типам параметров */
|
|
38
|
+
text: string;
|
|
39
|
+
/** Параметры запроса */
|
|
40
|
+
params: Record<string, Value>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Ошибка сборки запроса: некорректное имя, условие или значение.
|
|
44
|
+
* Адаптер не отправляет такой запрос в базу, а возвращает ошибку вызывающему коду.
|
|
45
|
+
*/
|
|
46
|
+
export declare class YqlBuildError extends Error {
|
|
47
|
+
constructor(message: string);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Контекст сборки запроса: схема таблицы (если известна) и правила модели.
|
|
51
|
+
*/
|
|
52
|
+
export interface IYqlContext {
|
|
53
|
+
/** Схема таблицы, если таблица встроенная или передана в `ensureSchema` */
|
|
54
|
+
table?: IYdbTableInfo | undefined;
|
|
55
|
+
/** Правила валидации модели (`IQuery.rules`) */
|
|
56
|
+
rules?: readonly IModelRules[] | undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Операторы условий, которые понимает адаптер.
|
|
60
|
+
*/
|
|
61
|
+
export declare const SUPPORTED_OPERATORS: readonly ["$gt", "$gte", "$lt", "$lte", "$ne", "$in", "$nin", "$like", "$null"];
|
|
62
|
+
/**
|
|
63
|
+
* Описание своей таблицы для опции `tables`: адаптер создаст её вместе со встроенными
|
|
64
|
+
* и будет искать по её индексам через `VIEW`.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* const orders: IYdbTableDefinition = {
|
|
69
|
+
* tableName: 'Orders',
|
|
70
|
+
* primaryKey: ['orderId'],
|
|
71
|
+
* columns: { orderId: 'Utf8', userId: 'Utf8', total: 'Double', paid: 'Bool' },
|
|
72
|
+
* indexes: [['userId']],
|
|
73
|
+
* };
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export interface IYdbTableDefinition {
|
|
77
|
+
/** Имя таблицы (можно с директорией: `bots/Orders`) */
|
|
78
|
+
tableName: string;
|
|
79
|
+
/** Колонки первичного ключа; каждая должна быть в `columns` */
|
|
80
|
+
primaryKey: readonly string[];
|
|
81
|
+
/** Колонки и их типы */
|
|
82
|
+
columns: Readonly<Record<string, TYdbColumnType>>;
|
|
83
|
+
/** Наборы колонок, по которым нужен вторичный индекс */
|
|
84
|
+
indexes?: readonly (readonly string[])[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Собирает описание таблицы из схемы umbot (`DB_TABLES_SCHEMA`).
|
|
88
|
+
* Все поля встроенных таблиц строковые, поэтому колонки получают тип `Utf8`.
|
|
89
|
+
* @param schema Описание таблицы
|
|
90
|
+
* @returns Описание таблицы для адаптера
|
|
91
|
+
*/
|
|
92
|
+
export declare function tableInfoFromSchema(schema: IDbTableSchema): IYdbTableInfo;
|
|
93
|
+
/**
|
|
94
|
+
* Собирает описание своей таблицы из опции `tables` и проверяет его.
|
|
95
|
+
* @param definition Описание таблицы
|
|
96
|
+
* @returns Описание таблицы для адаптера
|
|
97
|
+
*/
|
|
98
|
+
export declare function tableInfoFromDefinition(definition: IYdbTableDefinition): IYdbTableInfo;
|
|
99
|
+
/**
|
|
100
|
+
* Проверяет имя таблицы и возвращает его в обратных кавычках.
|
|
101
|
+
* @param name Имя таблицы
|
|
102
|
+
* @returns Имя для подстановки в YQL
|
|
103
|
+
*/
|
|
104
|
+
export declare function quoteTable(name: string): string;
|
|
105
|
+
/**
|
|
106
|
+
* Проверяет имя колонки (или индекса) и возвращает его в обратных кавычках.
|
|
107
|
+
* @param name Имя колонки
|
|
108
|
+
* @returns Имя для подстановки в YQL
|
|
109
|
+
*/
|
|
110
|
+
export declare function quoteColumn(name: string): string;
|
|
111
|
+
/**
|
|
112
|
+
* Выбирает вторичный индекс для SELECT. YDB не использует индекс без явного `VIEW`,
|
|
113
|
+
* поэтому поиск по (platform, path) в таблицах токенов без него шёл бы полным перебором.
|
|
114
|
+
* @returns Имя индекса или undefined, если поиск идёт по первичному ключу или индекса нет
|
|
115
|
+
*/
|
|
116
|
+
export declare function chooseIndex(table: IYdbTableInfo | undefined, where: IQueryData | null): string | undefined;
|
|
117
|
+
/**
|
|
118
|
+
* Собирает SELECT.
|
|
119
|
+
* @param tableName Имя таблицы
|
|
120
|
+
* @param where Условия выборки
|
|
121
|
+
* @param isOne Нужна только одна запись
|
|
122
|
+
* @param ctx Контекст запроса
|
|
123
|
+
* @returns Запрос
|
|
124
|
+
*/
|
|
125
|
+
export declare function buildSelect(tableName: string, where: IQueryData | null, isOne: boolean, ctx: IYqlContext): IYqlStatement;
|
|
126
|
+
/**
|
|
127
|
+
* Собирает UPSERT одной записи. Поля со значением undefined пропускаются,
|
|
128
|
+
* null записывается как NULL.
|
|
129
|
+
* @param tableName Имя таблицы
|
|
130
|
+
* @param data Данные записи
|
|
131
|
+
* @param ctx Контекст запроса
|
|
132
|
+
* @returns Запрос
|
|
133
|
+
*/
|
|
134
|
+
export declare function buildUpsert(tableName: string, data: IQueryData, ctx: IYqlContext): IYqlStatement;
|
|
135
|
+
/**
|
|
136
|
+
* Собирает UPDATE. Колонки первичного ключа из SET убираются: YDB не меняет ключ
|
|
137
|
+
* записи, а модель umbot передаёт `uniqueKeys` и в данных, и в условии.
|
|
138
|
+
* @param tableName Имя таблицы
|
|
139
|
+
* @param data Новые значения
|
|
140
|
+
* @param where Условия (обязательны)
|
|
141
|
+
* @param ctx Контекст запроса
|
|
142
|
+
* @returns Запрос или null, если менять нечего
|
|
143
|
+
*/
|
|
144
|
+
export declare function buildUpdate(tableName: string, data: IQueryData, where: IQueryData | null, ctx: IYqlContext): IYqlStatement | null;
|
|
145
|
+
/**
|
|
146
|
+
* Собирает DELETE.
|
|
147
|
+
* @param tableName Имя таблицы
|
|
148
|
+
* @param where Условия (обязательны: удаление всей таблицы этим методом запрещено)
|
|
149
|
+
* @param ctx Контекст запроса
|
|
150
|
+
* @returns Запрос
|
|
151
|
+
*/
|
|
152
|
+
export declare function buildDelete(tableName: string, where: IQueryData | null, ctx: IYqlContext): IYqlStatement;
|
|
153
|
+
/**
|
|
154
|
+
* Собирает CREATE TABLE IF NOT EXISTS. Колонки ключа объявляются NOT NULL.
|
|
155
|
+
* @param table Описание таблицы
|
|
156
|
+
* @returns Текст запроса
|
|
157
|
+
*/
|
|
158
|
+
export declare function buildCreateTable(table: IYdbTableInfo): string;
|
|
159
|
+
/**
|
|
160
|
+
* Собирает запрос-проверку: выполняется без ошибок, только если таблица, все её колонки
|
|
161
|
+
* и индексы уже есть. Данные не читаются (`LIMIT 0`).
|
|
162
|
+
* @param table Описание таблицы
|
|
163
|
+
* @returns Текст запроса (несколько SELECT через `;`)
|
|
164
|
+
*/
|
|
165
|
+
export declare function buildSchemaCheck(table: IYdbTableInfo): string;
|
|
166
|
+
/**
|
|
167
|
+
* Собирает проверку наличия одной колонки.
|
|
168
|
+
*/
|
|
169
|
+
export declare function buildColumnCheck(tableName: string, column: string): string;
|
|
170
|
+
/**
|
|
171
|
+
* Собирает ALTER TABLE ... ADD COLUMN.
|
|
172
|
+
*/
|
|
173
|
+
export declare function buildAddColumn(tableName: string, column: string, type: TYdbColumnType): string;
|
|
174
|
+
/**
|
|
175
|
+
* Собирает проверку наличия индекса.
|
|
176
|
+
*/
|
|
177
|
+
export declare function buildIndexCheck(tableName: string, index: IYdbIndexInfo): string;
|
|
178
|
+
/**
|
|
179
|
+
* Собирает ALTER TABLE ... ADD INDEX.
|
|
180
|
+
*/
|
|
181
|
+
export declare function buildAddIndex(tableName: string, index: IYdbIndexInfo): string;
|
|
182
|
+
/**
|
|
183
|
+
* Экранирует строку для подстановки внутрь строкового литерала YQL (`'...'` или `"..."`).
|
|
184
|
+
* Адаптер сам так не делает — все значения уходят параметрами; функция нужна тем,
|
|
185
|
+
* кто собирает свой запрос через `model.escapeString()`.
|
|
186
|
+
* @param value Значение
|
|
187
|
+
* @returns Строка с экранированными обратным слешем, кавычками и управляющими символами
|
|
188
|
+
*/
|
|
189
|
+
export declare function escapeYqlString(value: string | number): string;
|
|
190
|
+
/**
|
|
191
|
+
* Приводит значение из ответа YDB к виду, который понимают модели umbot:
|
|
192
|
+
* Int64 приходит как bigint, а JSON.stringify и сравнения с числами на нём ломаются.
|
|
193
|
+
* @param row Строка результата
|
|
194
|
+
* @returns Строка с bigint, переведёнными в number (или в строку, если не помещаются)
|
|
195
|
+
*/
|
|
196
|
+
export declare function normalizeRow(row: Record<string, unknown>): Record<string, unknown>;
|