Skip to content

Commit ed425f7

Browse files
authored
Merge pull request #90 from max36895/v-3.0.0
V 3.0.0
2 parents e07a3af + 90d2d1f commit ed425f7

6 files changed

Lines changed: 708 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
/dist/*
55
/docs/*
66
/node_modules/*
7+
/reps/*
78
/coverage/*
89
/package-lock.json

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "umbot",
3-
"version": "3.0.12",
3+
"version": "3.0.13",
44
"description": "Мультиплатформенный фреймворк для создания голосовых навыков и чат-ботов с единой бизнес-логикой. Встроенная поддержка ВКонтакте, Telegram, Viber, MAX, Яндекс Алисы, Маруси и Сбера SmartApp. Архитектура на адаптерах позволяет подключать любые другие платформы без изменения основного кода.",
55
"keywords": [
66
"vk",

src/docs/adapter/dbAdapter.md

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
# Адаптеры баз данных (DB Adapters)
2+
3+
Фреймворк umbot не знает, используете вы SQL, NoSQL или файловую систему. Он оперирует абстрактными объектами IQuery и IQueryData. Ваша задача как разработчика адаптера — написать "транслятор", который превращает эти абстракции в реальные запросы к вашей СУБД.
4+
5+
## Архитектура: Template Method
6+
7+
Базовый класс `BaseDbAdapter` (из `umbot/plugins`) берет на себя рутину:
8+
9+
- Замер времени выполнения запросов (метрики EMetric.DB_SELECT, DB_INSERT и т.д.).
10+
- Управление жизненным циклом (вызов connect при старте).
11+
- Обертки над вашими методами (публичные `select`, `insert` вызывают ваши `_select`, `_insert`).
12+
13+
### Почему мы переопределяем \_select, а не select?
14+
15+
Публичные методы (`select`, `insert`, `update`, `remove`) в `BaseDbAdapter` уже написаны. Они оборачивают ваши внутренние методы (`_select`, `_insert`), чтобы замерять время выполнения и логировать метрики. Если вы переопределите select(), вы сломаете сбор метрик и логику повторных подключений. Вы всегда реализуете только методы с подчеркиванием.
16+
17+
## Обязательный контракт (что нужно реализовать)
18+
19+
Наследуемся от `BaseDbAdapter` и реализуем:
20+
21+
1. connect(): Promise<boolean> — Устанавливаете соединение с БД.
22+
2. isConnected(): Promise<boolean> — Проверяете, живо ли соединение (например, делаете ping БД).
23+
3. \_select(selectData: IQuery, where: IQueryData | null, isOne: boolean): Promise<IModelRes> — Поиск.
24+
4. \_insert(insertData: IQuery): Promise<boolean> — Добавление.
25+
5. \_update(updateData: IQuery): Promise<boolean> — Обновление.
26+
6. \_remove(removeData: IQuery): Promise<boolean> — Удаление.
27+
7. destroy(): Promise<void> — Закрываете пул соединений при остановке приложения.
28+
29+
## Форматы данных (Шпаргалка):
30+
31+
### Вход (IQuery): То, что фреймворк передает вам.
32+
33+
Это объект, который фреймворк передает в ваши методы \_select, \_insert и т.д.
34+
35+
```ts
36+
{
37+
tableName: 'UsersData', // Имя таблицы/коллекции
38+
primaryKeyName: 'userId', // Первичный ключ
39+
query: { userId: '123' }, // Условия WHERE (может быть null)
40+
data: { name: 'John' }, // Данные для SET (может быть null)
41+
rules: [{ name: ['name'], type: 'string', max: 50 }] // Правила валидации
42+
}
43+
```
44+
45+
### Условия и данные (IQueryData)
46+
47+
Формат query и data внутри IQuery.
48+
Важно: Значения могут быть не только примитивами, но и объектами с операторами. Фреймворк не навязывает конкретный диалект (например, $gt для Mongo или > для SQL). Адаптер сам решает, как интерпретировать эти операторы.
49+
50+
```ts
51+
// Простое условие (равенство)
52+
{ userId: '123', platform: 'alisa' }
53+
54+
// Условие с оператором (адаптер должен сам распарсить это в SQL `age > 18` или Mongo `$gt`)
55+
{ age: { $gt: 18 }, status: 'active' }
56+
```
57+
58+
### Выходные данные (IModelRes)
59+
60+
То, что вы обязаны вернуть из метода `_select`.
61+
62+
```ts
63+
// Успех (даже если ничего не найдено, status должен быть true, а data - пустым массивом или null)
64+
{ status: true, data: { userId: '123', name: 'John' } }
65+
{ status: true, data: [] }
66+
67+
// Ошибка (сбой подключения, синтаксическая ошибка и т.д.)
68+
{ status: false, error: 'Connection timeout' }
69+
```
70+
71+
Для методов `_insert`, `_update`, `_remove` вы возвращаете просто boolean (true при успехе, false при ошибке).
72+
73+
## Критические нюансы (Скрытые контракты)
74+
75+
### 1. Валидация данных
76+
77+
В базовом классе `BaseDbAdapter` нет встроенного метода `validate()`.
78+
Однако в `MongoAdapter` он реализован для валидации данных по правилам модели (`IModelRules`).
79+
80+
Если вы хотите, чтобы ваш адаптер также валидировал данные (обрезал строки по `max`,
81+
приводил типы), реализуйте метод `validate()` в своём классе:
82+
83+
```ts
84+
public validate(query: IQuery, element: IQueryData | null): IQueryData {
85+
if (!element) return {};
86+
87+
const rules = query.rules;
88+
if (rules) {
89+
rules.forEach((rule) => {
90+
rule.name.forEach((fieldName) => {
91+
if (rule.type === 'string' || rule.type === 'text') {
92+
if (rule.max !== undefined) {
93+
element[fieldName] = Text.resize(element[fieldName] as string, rule.max);
94+
}
95+
element[fieldName] = this.escapeString(element[fieldName] as string);
96+
} else if (rule.type === 'integer' || rule.type === 'int') {
97+
element[fieldName] = +(element[fieldName] as number);
98+
}
99+
});
100+
});
101+
}
102+
return element;
103+
}
104+
```
105+
106+
Затем вызывайте его в `_insert()` и `_update()`:
107+
108+
```ts
109+
public async _insert(insertData: IQuery): Promise<boolean> {
110+
const validData = this.validate(insertData, insertData.data);
111+
// ... выполнение запроса с validData
112+
}
113+
```
114+
115+
**Примечание**: Валидация в модели (`Model.validate()`) и в адаптере (`validate()`) — это разные вещи.
116+
Модель валидирует свои данные перед сохранением, а адаптер валидирует данные по правилам `IModelRules`
117+
перед выполнением запроса к БД.
118+
119+
_Зачем тогда в `IQuery` передаются `rules`?_
120+
Они нужны вам для **маппинга типов** специфичных для вашей СУБД. Например, если вы пишете SQL-адаптер, вы можете использовать `rules`, чтобы понять, что поле с `type: 'object'` нужно сериализовать в JSON-строку перед вставкой, а `max: 150` использовать для динамического создания `VARCHAR(150)`.
121+
122+
### 2. Хранение подключения (Connection Pool)
123+
124+
Чтобы не создавать новое подключение к БД на каждый запрос, фреймворк предоставляет синглтон-хранилище.
125+
При успешном `connect()` вы должны сохранить пул соединений в `this._appContext.database.databaseInfo`.
126+
127+
```ts
128+
async connect(): Promise<boolean> {
129+
const pool = await createMyDbPool(this._dbOptions);
130+
// Сохраняем пул, чтобы использовать его в _select/_insert
131+
this._appContext.database.databaseInfo = { myDbPool: pool };
132+
return true;
133+
}
134+
```
135+
136+
### 3. Произвольные запросы (\_query)
137+
138+
Если разработчику приложения нужно выполнить "сырой" SQL-запрос или агрегацию, он использует метод `model.query(callback)`.
139+
В `BaseDbAdapter` публичный `query` просто вызывает `_query`. По умолчанию `_query` возвращает `null`. Если вы хотите поддержать кастомные запросы, переопределите `_query`, передав в callback ваше подключение.
140+
141+
### 4. Обработка ошибок
142+
143+
Не бросайте исключения (`throw new Error`) из методов `_select`, `_insert`, `_update`, `_remove`.
144+
Фреймворк ожидает, что вы сами обработаете ошибки внутри метода и вернете `false` или `{ status: false, error: ... }`.
145+
146+
## Универсальный пример реализации (Псевдокод)
147+
148+
```ts
149+
import { BaseDbAdapter } from 'umbot/plugins';
150+
import { IQuery, IQueryData, IModelRes, TQueryCb } from 'umbot';
151+
152+
export class MyCustomDbAdapter extends BaseDbAdapter {
153+
async connect(): Promise<boolean> {
154+
try {
155+
// 1. Создаем пул соединений
156+
const pool = await myDbDriver.connect(this._dbOptions);
157+
// 2. Сохраняем его в контекст
158+
this._appContext.database.databaseInfo = { pool };
159+
return true;
160+
} catch (err) {
161+
return false;
162+
}
163+
}
164+
165+
async isConnected(): Promise<boolean> {
166+
const pool = this._appContext.database.databaseInfo?.pool;
167+
return pool ? await pool.ping() : false;
168+
}
169+
170+
async _select(
171+
selectData: IQuery,
172+
where: IQueryData | null,
173+
isOne: boolean,
174+
): Promise<IModelRes> {
175+
const pool = this._appContext.database.databaseInfo?.pool;
176+
if (!pool) return { status: false, error: 'No DB connection' };
177+
178+
try {
179+
// 1. Парсим абстрактные условия where в SQL/NoSQL запрос
180+
const sqlQuery = this.buildSelectQuery(selectData.tableName, where, isOne);
181+
182+
// 2. Выполняем запрос
183+
const result = await pool.execute(sqlQuery);
184+
185+
// 3. Возвращаем в формате IModelRes
186+
return { status: true, data: result };
187+
} catch (err) {
188+
// Не бросаем исключение, а возвращаем статус false
189+
return { status: false, error: (err as Error).message };
190+
}
191+
}
192+
193+
async _insert(insertData: IQuery): Promise<boolean> {
194+
const pool = this._appContext.database.databaseInfo?.pool;
195+
if (!pool) return false;
196+
197+
try {
198+
// Валидация (так как в BaseDbAdapter её нет, используем свою)
199+
const validData = this.validate(insertData, insertData.data);
200+
const sqlQuery = this.buildInsertQuery(insertData.tableName, validData);
201+
await pool.execute(sqlQuery);
202+
return true;
203+
} catch (err) {
204+
return false;
205+
}
206+
}
207+
208+
// Переопределяем _query, чтобы поддержать сырые запросы от разработчика
209+
public async _query(callback: TQueryCb): Promise<unknown> {
210+
const pool = this._appContext.database.databaseInfo?.pool;
211+
if (pool) {
212+
// Передаем пул в callback разработчика
213+
return await callback(pool, pool);
214+
}
215+
return null;
216+
}
217+
218+
async destroy(): Promise<void> {
219+
const pool = this._appContext.database.databaseInfo?.pool;
220+
if (pool) await pool.close();
221+
}
222+
223+
// --- Вспомогательные методы ---
224+
225+
// Своя валидация
226+
private validate(query: IQuery, data: IQueryData | null): IQueryData {
227+
if (!data) return {};
228+
// Здесь можно пройтись по query.rules и обрезать строки по max
229+
return data;
230+
}
231+
232+
// Транслятор IQueryData в SQL (упрощенно)
233+
private buildSelectQuery(table: string, where: IQueryData | null, isOne: boolean): string {
234+
let sql = `SELECT * FROM ${table}`;
235+
if (where) {
236+
const conditions = Object.keys(where).map((key) => {
237+
const val = where[key];
238+
// Поддержка операторов
239+
if (typeof val === 'object' && val !== null && val.$gt !== undefined) {
240+
return `${key} > ${val.$gt}`;
241+
}
242+
return `${key} = '${val}'`;
243+
});
244+
sql += ` WHERE ${conditions.join(' AND ')}`;
245+
}
246+
if (isOne) sql += ' LIMIT 1';
247+
return sql;
248+
}
249+
250+
private buildInsertQuery(table: string, data: IQueryData): string {
251+
// ... логика формирования INSERT
252+
return '';
253+
}
254+
}
255+
```

0 commit comments

Comments
 (0)