Архитектура универсального провайдера баз данных на TypeScript без JDBC
Почему TypeScript не хватает единого стандарта
В Java и .NET взаимодействие с СУБД строится вокруг формализованных интерфейсов уровня платформы. В Java эту роль выполняет JDBC: приложение работает с `Driver`, `Connection`, `Statement` и `ResultSet`, а конкретный производитель поставляет совместимый драйвер. В .NET аналогичную задачу решает ADO.NET с классами `DbConnection`, `DbCommand` и `DbDataReader`.
В средах Node.js, Bun и Deno подобного системного SPI нет. Разработчику приходится подключать независимые пакеты: `pg` для PostgreSQL, `mysql2` для MySQL, `oracledb` для Oracle, `better-sqlite3` или `node:sqlite` для SQLite. Для Redis, MongoDB и Cassandra используются уже принципиально другие модели взаимодействия и форматы данных.
Поэтому универсальный провайдер баз данных TypeScript приходится проектировать самостоятельно. Особенно остро эта задача проявляется при создании единого desktop- или web-клиента, который должен поддерживать реляционные, документные, key-value, OLAP- и встраиваемые хранилища.
Архитектурная проблема
Такое приложение сталкивается сразу с несколькими ограничениями. У разных СУБД отличаются языки запросов, жизненный цикл соединений, способы получения метаданных и правила обработки ошибок. Даже понятие "таблица" универсальным не является: в MongoDB это коллекция, в Redis - набор ключей, а в ClickHouse или Trino присутствуют собственные особенности каталогов и схем.
Дополнительная сложность связана с размером приложения. Если статически импортировать драйверы пятнадцати и более систем, увеличатся итоговый размер сборки, время запуска и потребление памяти. При этом пользователь обычно работает только с одной-двумя базами одновременно.
Отдельная задача - единое представление схемы. Интерфейсу клиента удобно показывать дерево вида "контейнеры → папки → объекты → колонки и индексы", но PostgreSQL получает метаданные из `pg_catalog`, MySQL - из `information_schema`, SQLite - через `PRAGMA`, а Redis вообще требует анализа ключей и соглашений об именовании.
Наконец, современные продукты должны учитывать сценарии с ИИ-агентами. Сгенерированный запрос нельзя считать безопасным только потому, что он выглядит как `SELECT`. Режим "только чтение" должен обеспечиваться на уровне соединения и самой СУБД, а не исключительно проверкой текста запроса.
Единый контракт вместо общего протокола
Практичное решение - слой `DatabaseProvider`, построенный по принципам Adapter и Strategy. Провайдер скрывает особенности конкретного драйвера, но не пытается заново реализовать сетевой протокол. Вместо этого он использует проверенные npm-пакеты и приводит их поведение к общей модели.
Такой подход можно рассматривать как архитектуру работы с базами данных TypeScript: приложение взаимодействует не с `pg`, `mongodb` или `ioredis` напрямую, а с абстрактным контрактом. Провайдер обычно отвечает за подключение, выполнение команд, получение схемы, чтение данных, закрытие ресурсов и преобразование ошибок.
Для каждой технологии создаётся собственный адаптер:
- PostgreSQL работает поверх `pg`;
- MySQL и MariaDB - поверх `mysql2`;
- SQLite - через нативный драйвер;
- MongoDB - через официальный клиент;
- Redis - через `ioredis`;
- ClickHouse, Trino и другие аналитические системы получают специализированные реализации.
В результате подключение к базе данных TypeScript становится единообразным на уровне приложения, хотя внутри адаптера сохраняются нативные возможности конкретной СУБД. Это важный компромисс: общий интерфейс не должен уничтожать полезные различия между движками.
Подробное рассмотрение подобной модели, включая разделение провайдеров и загрузку драйверов, представлено в материале об универсальной архитектуре доступа к базам данных на TypeScript.
Динамическая загрузка драйверов
Чтобы не включать все зависимости в стартовый бандл, провайдеры можно загружать лениво. Приложение сначала определяет тип подключения, а затем импортирует только необходимый модуль. Это снижает RSS и ускоряет запуск интерфейса.
Механизм может выглядеть как реестр фабрик:
```ts
type ProviderFactory = () => Promise
const providers: Record
postgres: async () => new (await import("./postgres-provider")).PostgresProvider(),
sqlite: async () => new (await import("./sqlite-provider")).SqliteProvider(),
};
```
Такой реестр также упрощает расширение продукта: добавление новой СУБД не требует переписывать центральную логику клиента. Достаточно зарегистрировать новый тип и реализовать его адаптер.
Унификация схем и Object Surface API
Пользовательский интерфейс не должен знать, что один драйвер читает `information_schema`, другой вызывает `PRAGMA`, а третий обращается к системным коллекциям. Поэтому провайдер возвращает нормализованное описание объектов.
Условная модель может включать:
```ts
interface SchemaNode {
kind: "database" | "schema" | "table" | "collection" | "keyspace";
name: string;
children?: SchemaNode[];
columns?: ColumnInfo[];
indexes?: IndexInfo[];
}
```
На практике это не означает, что все источники насильно превращаются в таблицы. Для MongoDB сохраняется понятие коллекции, для Redis - пространства ключей, а для OLAP-движков могут отображаться каталоги и представления. Общим становится не внутреннее устройство, а поверхность API, которую использует UI.
Безопасность и режим Read-Only
Для ИИ-агентов особенно опасно полагаться только на фильтрацию SQL по ключевым словам. Запрос может содержать вложенные конструкции, функции, вызовы процедур или команды, специфичные для конкретного движка.
Надёжнее создавать отдельные соединения с ограниченными правами. В PostgreSQL можно использовать пользователя без разрешений на изменение данных и включить режим `default_transaction_read_only`. В других СУБД применяются отдельные аккаунты, роли, read-only-сессии или прокси-уровень.
Таким образом, безопасность строится в несколько слоёв: минимальные права пользователя, режим соединения, ограничение времени выполнения, лимит объёма результата и дополнительная проверка команд на уровне приложения. Такой подход защищает не только от ошибок ИИ, но и от случайных действий оператора.
Блокировки embedded-баз и SSH-туннели
Встраиваемые движки часто используют эксклюзивную блокировку файла. Если два компонента попытаются независимо открыть один SQLite-файл, приложение получит конфликт владельцев или ошибку доступа.
Решением становится повторное использование уже созданного хэндла. Провайдер хранит открытые ресурсы в реестре, где ключом выступает нормализованный путь к файлу. Повторное подключение возвращает существующую сессию, а закрытие выполняется только после ухода последнего потребителя.
Для удалённых СУБД аналогичную роль играет управление SSH-туннелем. Клиент может автоматически поднять локальный порт, направить через него подключение к базе и корректно завершить туннель вместе с последним активным провайдером. Важно отделить жизненный цикл транспорта от жизненного цикла SQL-сессии: один туннель способен обслуживать несколько соединений.
Почему не стоит сразу выбирать тяжёлую ORM
ORM для TypeScript и Node.js удобна в прикладной разработке, где есть единая модель данных, миграции и CRUD-операции. Однако универсальный клиент СУБД решает другую задачу: он должен показывать нативные возможности десятков движков, выполнять диагностические запросы и сохранять доступ к планам выполнения.
Поэтому в такой архитектуре чаще выбирают тонкие драйверы и чистый SQL. ORM может использоваться внутри отдельного бизнес-приложения, но становиться фундаментом универсального инструмента ей не обязательно. В противном случае разработчик столкнётся с потерей специфических возможностей и сложными компромиссами при поддержке NoSQL и аналитических платформ.
Выводы
Универсальная работа с базами данных в TypeScript невозможна за счёт одного универсального драйвера: экосистема JavaScript не предоставляет аналога JDBC. Однако эту проблему можно решить на уровне архитектуры, создав собственный SPI и набор адаптеров поверх существующих пакетов.
Ключевыми принципами становятся ленивый импорт, единый контракт провайдера, нормализация схемы, повторное использование файловых хэндлов, управление SSH-транспортом и защита read-only-соединений. В результате драйверы баз данных для Node.js остаются специализированными, но приложение получает предсказуемый интерфейс.
Именно такой слой позволяет масштабировать клиент с нескольких источников до десятков СУБД без превращения кодовой базы в набор несвязанных интеграций. Абрибас? ചികിത.


