@adonisjs/lucid
Version:
SQL ORM built on top of Active Record pattern
319 lines (318 loc) • 10.3 kB
JavaScript
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import knex from 'knex';
import { EventEmitter } from 'node:events';
import { patchKnex } from 'knex-dynamic-connection';
// @ts-expect-error
import { resolveClientNameWithAliases } from 'knex/lib/util/helpers.js';
import * as errors from '../errors.js';
import LibSQLClient from '../clients/libsql.cjs';
import { clientsNames } from '../dialects/index.js';
import { Logger as ConnectionLogger } from './logger.js';
/**
* Connection class manages a given database connection. Internally it uses
* knex to build the database connection with appropriate database
* driver.
*/
export class Connection extends EventEmitter {
name;
config;
logger;
/**
* Reference to knex. The instance is created once the `open`
* method is invoked
*/
client;
/**
* Read client when read/write replicas are defined in the config, otherwise
* it is a reference to the `client`.
*/
readClient;
/**
* Connection dialect name.
* @deprecated
* @see clientName
*/
dialectName;
/**
* Connection client name.
*/
clientName;
/**
* A boolean to know if connection operates on read/write
* replicas
*/
hasReadWriteReplicas;
/**
* Config for one or more read replicas. Only exists, when replicas are
* defined
*/
readReplicas = [];
/**
* The round robin counter for reading config
*/
roundRobinCounter = 0;
constructor(name, config, logger) {
super();
this.name = name;
this.config = config;
this.logger = logger;
this.validateConfig();
this.clientName = resolveClientNameWithAliases(this.config.client);
this.dialectName = this.clientName;
this.hasReadWriteReplicas = !!(this.config.replicas &&
this.config.replicas.read &&
this.config.replicas.write);
if (!clientsNames.includes(this.clientName)) {
throw new errors.E_UNSUPPORTED_CLIENT([this.clientName]);
}
}
/**
* Validates the config to ensure that read/write replicas are defined
* properly.
*/
validateConfig() {
if (this.config.replicas) {
if (!this.config.replicas.read || !this.config.replicas.write) {
throw new errors.E_INCOMPLETE_REPLICAS_CONFIG();
}
if (!this.config.replicas.read.connection || !this.config.replicas.read.connection) {
throw new errors.E_INVALID_REPLICAS_CONFIG();
}
}
}
/**
* Cleans up reference for the write client and also the
* read client when not using replicas
*/
cleanupWriteClient() {
if (this.client === this.readClient) {
this.cleanupReadClient();
}
this.client = undefined;
}
/**
* Cleans up reference for the read client
*/
cleanupReadClient() {
this.roundRobinCounter = 0;
this.readClient = undefined;
this.readReplicas = [];
}
/**
* Does cleanup by removing knex reference and removing all listeners.
* For the same of simplicity, we get rid of both read and write
* clients, when anyone of them disconnects.
*/
monitorPoolResources() {
/**
* Pool has destroyed and hence we must cleanup resources
* as well.
*/
this.pool.on('poolDestroySuccess', () => {
this.logger.trace({ connection: this.name }, 'pool destroyed, cleaning up resource');
this.cleanupWriteClient();
this.emit('disconnect', this);
this.removeAllListeners();
});
if (this.readPool !== this.pool) {
this.readPool.on('poolDestroySuccess', () => {
this.logger.trace({ connection: this.name }, 'pool destroyed, cleaning up resource');
this.cleanupReadClient();
this.emit('disconnect', this);
this.removeAllListeners();
});
}
}
/**
* Returns normalized config object for write replica to be
* used by knex
*/
getWriteConfig() {
if (!this.config.replicas) {
/**
* Replacing string based libsql client with the
* actual implementation
*/
if (this.config.client === 'libsql') {
return {
...this.config,
client: LibSQLClient,
};
}
return this.config;
}
const { replicas, ...config } = this.config;
/**
* Give preference to the replica write connection when and merge values from
* the main connection object when defined.
*/
if (typeof replicas.write.connection === 'string' || typeof config.connection === 'string') {
config.connection = replicas.write.connection;
}
else {
config.connection = Object.assign({}, config.connection, replicas.write.connection);
}
/**
* Add pool to the config when pool config defined on main connection
* or the write replica
*/
if (config.pool || replicas.write.pool) {
config.pool = Object.assign({}, config.pool, replicas.write.pool);
}
return config;
}
/**
* Returns the config for read replicas.
*/
getReadConfig() {
if (!this.config.replicas) {
return this.config;
}
const { replicas, ...config } = this.config;
/**
* Reading replicas and storing them as a reference, so that we
* can pick a config from replicas as round robin.
*/
this.readReplicas = replicas.read.connection.map((one) => {
if (typeof one === 'string' || typeof config.connection === 'string') {
return one;
}
else {
return Object.assign({}, config.connection, one);
}
});
/**
* Add database property on the main connection, since knexjs needs it
* internally
*/
config.connection = {
database: this.readReplicas[0].database,
};
/**
* Add pool to the config when pool config defined on main connection
* or the read replica
*/
if (config.pool || replicas.read.pool) {
config.pool = Object.assign({}, config.pool, replicas.read.pool);
}
return config;
}
/**
* Resolves connection config for the writer connection
*/
writeConfigResolver(originalConfig) {
return originalConfig.connection;
}
/**
* Resolves connection config for the reader connection
*/
readConfigResolver(originalConfig) {
if (!this.readReplicas.length) {
return originalConfig.connection;
}
const index = this.roundRobinCounter++ % this.readReplicas.length;
this.logger.trace({ connection: this.name }, `round robin using host at ${index} index`);
return this.readReplicas[index];
}
/**
* Creates the write connection.
*/
setupWriteConnection() {
this.client = knex.knex(Object.assign({ log: new ConnectionLogger(this.name, this.logger) }, this.getWriteConfig(), {
debug: false,
}));
// @ts-ignore
patchKnex(this.client, this.writeConfigResolver.bind(this));
}
/**
* Creates the read connection. If there aren't any replicas in use, then
* it will use the write client instead.
*/
setupReadConnection() {
if (!this.hasReadWriteReplicas) {
this.readClient = this.client;
return;
}
this.logger.trace({ connection: this.name }, 'setting up read/write replicas');
this.readClient = knex.knex(Object.assign({ log: new ConnectionLogger(this.name, this.logger) }, this.getReadConfig(), {
debug: false,
}));
// @ts-ignore
patchKnex(this.readClient, this.readConfigResolver.bind(this));
}
/**
* Returns the pool instance for the given connection
*/
get pool() {
return this.client ? this.client.client.pool : null;
}
/**
* Returns the pool instance for the read connection. When replicas are
* not in use, then read/write pools are same.
*/
get readPool() {
return this.readClient ? this.readClient.client.pool : null;
}
/**
* Returns a boolean indicating if the connection is ready for making
* database queries. If not, one must call `connect`.
*/
get ready() {
return !!(this.client || this.readClient);
}
/**
* Opens the connection by creating knex instance
*/
connect() {
try {
this.setupWriteConnection();
this.setupReadConnection();
this.monitorPoolResources();
this.emit('connect', this);
}
catch (error) {
this.emit('error', error, this);
throw error;
}
}
/**
* Closes DB connection by destroying knex instance. The `connection`
* object must be free for garbage collection.
*
* In case of error this method will emit `close:error` event followed
* by the `close` event.
*/
async disconnect() {
this.logger.trace({ connection: this.name }, 'destroying connection');
/**
* Disconnect write client
*/
if (this.client) {
try {
await this.client.destroy();
}
catch (error) {
this.emit('disconnect:error', error, this);
}
}
/**
* Disconnect read client when it exists and both clients
* aren't same
*/
if (this.readClient && this.readClient !== this.client) {
try {
await this.readClient.destroy();
}
catch (error) {
this.emit('disconnect:error', error, this);
}
}
}
}