sqmicro-connection
Version:
Connection abstraction for SQ Micro.
205 lines (180 loc) • 7.66 kB
JavaScript
/**
* @module sqmicro-connection
* Соединение. Может восстанавливаться после сбоев. Требуется драйвер соединения,
* например один из модулей `sqmicro-driver-*`. Класс драйвера должен реализовать
* интерфейсы IConnectionDriver, ICopier и, возможно, IQuerist.
*
* Драйвер соединения
* @interface IConnectionDriver
* @property {string} driverName имя драйвера, только чтение.
* @property {boolean} isConnected установлено ли соединение (только чтение)?
*
* @async @method connect установить соединение
* @returns {Promise<this, Error>}
*
* @async @method disconnect разорвать соединение.
* @returns {Promise<this, Error>}
*
* @event connected соединение установлено.
* @event error ошибка соединения.
* @event end соединение закрыто пользователем.
* @event disconnected соединение разорвано вследствие ошибки.
*
* Копировщик.
* @interface ICopier
* @async @method getCopyStream Получить поток для копирования данных.
* @param {string} dest место назначения данных. Интерпретируется драйвером.
* @param {object} options
* @param {Array<string>} options.fieldNames список всех имен полей. Колонки в
* строках сформированы в соответствии с этим массивом.
* @param {string} options.delimiter разделитель колонок.
* @returns {Promise}
*
* SQL запросы.
* @interface IQuerist
* @async @method query Выполнить SQL запрос
* @param {string} sql SQL запрос.
* @param {Array<*>} values значения переменных в SQL запросе.
* @returns {Promise<ResultSet, Error>}
*/
const url = require('url');
const { bindEvents, promisifyEventEmitter, Retrier } = require('sqmicro-commons');
const formatterFactory = require('./formatter-factory');
const Private = Symbol('Private');
const EVENTS = {
disconnected: function(connection) {
connection.connect();
}
};
/**
* Создать объект "соединение" используя данный драйвер в качестве базового класса.
* @param {function} Driver класс драйвера.
* @returns {function} класс соединения.
*/
const connectionFactory = (Driver) => class Connection extends Driver {
/**
* URL соединения без кредов, например для логирования.
* @type {string}
*/
get safeUrl() {
return url.format(this[Private].url, { auth: false });
}
/**
* Создать экземпляр соединения.
* @param {object} config
* @param {string} config.connectionString URL подключения.
* @param {object?|boolean} config.retry Настройки восстановления соединения.
* Подробности и настройки по умолчанию - см. Retrier. Если true —
* использовать настройки по умолчанию, если undefined — не повторять попыток
* соединения.
*/
constructor(config) {
super(config);
const pvt = this[Private] = { };
pvt.url = new url.URL(config.connectionString);
pvt.retrier = createRetrier(config.retry);
bindEvents(this, EVENTS, null, [this]);
}
/**
* Установить соединение. При ошибке - повторять согласно настройкам.
* @returns {Promise<Connection, Error>}
*/
async connect() {
const pvt = this[Private];
await pvt.retrier.retry(
() => super.connect(),
`connecting to ${this.safeUrl}`
);
pvt.isConnected = true;
return this;
}
/**
* Копировать данные в место назначения.
* @param {string} dest имя места назначения. Интерпретируется драйвером.
* @param {iterable<object>} rows строки данных.
* @param {object} options опции
* @param {Array<string>} options.fieldNames список имен полей.
* @param {string} options.delimiter символ-разделитель колонок в строках.
* @returns {Promise}
*/
async copy(dest, rows, options) {
ensureConnected(this);
const formatter = formatterFactory(options);
formatter.appendStream(
await super.getCopyStream(dest, options)
);
for (const row of rows) {
formatter.write(row);
}
return await formatter.end();
}
/**
* Выполнить SQL запрос. Может не поддерживаентся драйвером.
* @todo @typedef {object} ResultSet
* @returns {ResultSet}
*/
async query(...args) {
ensureConnected(this);
return await super.query(...args);
}
};
/**
* Создать класс соединения, используя данный драйвер.
* @todo check Driver implements `IConnectionDriver`
* @param {function|string} driver либо класс, либо имя драйвера. В
* последнем случае клиентскому проекту должен быть доступен соответствующий
* модуль, например если driver = 'pg', то нужен модуль 'sqmicro-driver-pg';
* @returns {function} класс соединения.
* @throws {TypeError} если аргумент не строка и не класс.
* @throws {Error} если модуль драйвера недоступен.
*/
module.exports = function(driver) {
let type = typeof driver;
if (type === 'string') {
driver = getDriver(driver);
type = typeof driver;
}
if (type !== 'function') {
throw TypeError(`driver should be either class or a string`);
}
return connectionFactory(driver);
};
/**
* Создать экземпляр класса Retrier.
* @inner
* @param {object?} options.
* @returns {Retrier}
*/
function createRetrier(options) {
return !options ?
new Retrier(undefined, 1) :
typeof(options) === 'object' ?
new Retrier(options.delay, options.maxAttempts) :
new Retrier();
}
/**
* Загрузить модуль драйвера. Модуль должен экспортировать класс.
* @inner
* @param {string} driverName
* @returns {*} модуль.
*/
function getDriver(driverName) {
const moduleName = `sqmicro-connection-${driverName}`.toLocaleLowerCase();
try {
return require(moduleName);
}
catch(e) {
throw Error(`Please install ${moduleName} to use driver ${driverName}`);
}
}
/**
* Убедиться, что соединение устанвлено.
* @inner
* @param {Connection} connection
* @throws {Error} если соединеине не установлено или разорвано.
*/
function ensureConnected(connection) {
if (!connection.isConnected) {
throw Error(`Can't operate on a disconnected ${connection.safeUrl}`);
}
}