UNPKG

@adonisjs/lucid

Version:

SQL ORM built on top of Active Record pattern

319 lines (318 loc) 10.3 kB
/* * @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); } } } }