UNPKG

@athenna/database

Version:

The Athenna database handler for SQL/NoSQL.

954 lines (953 loc) 26.7 kB
/* eslint-disable @typescript-eslint/ban-ts-comment */ /** * @athenna/database * * (c) João Lenon <lenon@athenna.io> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ import { Exec, Is, Json, Options } from '@athenna/common'; import { debug } from '#src/debug'; import { Log } from '@athenna/logger'; import { Driver } from '#src/database/drivers/Driver'; import { ConnectionFactory } from '#src/factories/ConnectionFactory'; import { Transaction } from '#src/database/transactions/Transaction'; import { MigrationSource } from '#src/database/migrations/MigrationSource'; import { WrongMethodException } from '#src/exceptions/WrongMethodException'; import { PROTECTED_QUERY_METHODS } from '#src/constants/ProtectedQueryMethods'; import { NotConnectedDatabaseException } from '#src/exceptions/NotConnectedDatabaseException'; export class MySqlDriver extends Driver { /** * Connect to database. */ connect(options = {}) { options = Options.create(options, { force: false, saveOnFactory: true, connect: true }); if (!options.connect) { return; } if (this.isConnected && !options.force) { return; } const knex = this.getKnex(); const configs = Config.get(`database.connections.${this.connection}`, {}); const knexOpts = { client: 'mysql2', migrations: { tableName: 'migrations' }, pool: { min: 2, max: 20, acquireTimeoutMillis: 60 * 1000 }, debug: false, useNullAsDefault: false, ...Json.omit(configs, ['driver', 'validations']) }; debug('creating new connection using Knex. options defined: %o', knexOpts); if (Config.is('rc.bootLogs', true)) { Log.channelOrVanilla('application').success(`Successfully connected to ({yellow} ${this.connection}) database connection`); } this.client = knex.default(knexOpts); this.isConnected = true; this.isSavedOnFactory = options.saveOnFactory; if (this.isSavedOnFactory) { ConnectionFactory.setClient(this.connection, this.client); } this.qb = this.query(); } /** * Close the connection with database in this instance. */ async close() { if (!this.isConnected) { return; } await this.client.destroy(); this.qb = null; this.tableName = null; this.client = null; this.isConnected = false; ConnectionFactory.setClient(this.connection, null); } /** * Creates a new instance of query builder. */ query() { if (!this.isConnected) { throw new NotConnectedDatabaseException(); } const query = this.useSetQB ? this.qb.table(this.tableName) : this.client.queryBuilder().table(this.tableName); const handler = { get: (target, propertyKey) => { if (PROTECTED_QUERY_METHODS.includes(propertyKey)) { this.qb = this.query(); } return target[propertyKey]; } }; return new Proxy(query, handler); } /** * Sync a model schema with database. */ async sync() { debug(`database sync with ${MySqlDriver.name} is not available yet, use migration instead.`); } /** * Create a new transaction. */ async startTransaction() { const trx = await this.client.transaction(); return new Transaction(this.clone().setClient(trx)); } /** * Commit the transaction. */ async commitTransaction() { const client = this.client; await client.commit(); this.tableName = null; this.client = null; this.isConnected = false; } /** * Rollback the transaction. */ async rollbackTransaction() { const client = this.client; await client.rollback(); this.tableName = null; this.client = null; this.isConnected = false; } /** * Run database migrations. */ async runMigrations() { await this.client.migrate.latest({ migrationSource: new MigrationSource(this.connection) }); } /** * Revert database migrations. */ async revertMigrations() { await this.client.migrate.rollback({ migrationSource: new MigrationSource(this.connection) }); } /** * List all databases available. */ async getDatabases() { const [databases] = await this.raw('SHOW DATABASES'); return databases.map(database => database.Database); } /** * Get the current database name. */ async getCurrentDatabase() { return this.client.client.database(); } /** * Verify if database exists. */ async hasDatabase(database) { const databases = await this.getDatabases(); return databases.includes(database); } /** * Create a new database. */ async createDatabase(database) { await this.raw('CREATE DATABASE IF NOT EXISTS ??', database); } /** * Drop some database. */ async dropDatabase(database) { await this.raw('DROP DATABASE IF EXISTS ??', database); } /** * List all tables available. */ async getTables() { const [tables] = await this.raw('SELECT table_name FROM information_schema.tables WHERE table_schema = ?', await this.getCurrentDatabase()); return tables.map(table => table.TABLE_NAME); } /** * Verify if table exists. */ async hasTable(table) { return this.client.schema.hasTable(table); } /** * Create a new table in database. */ async createTable(table, closure) { await this.client.schema.createTable(table, closure); } /** * Drop a table in database. */ async dropTable(table) { await this.client.schema.dropTableIfExists(table); } /** * Remove all data inside some database table * and restart the identity of the table. */ async truncate(table) { try { await this.raw('SET FOREIGN_KEY_CHECKS = 0'); await this.raw('TRUNCATE TABLE ??', table); } finally { await this.raw('SET FOREIGN_KEY_CHECKS = 1'); } } /** * Make a raw query in database. */ raw(sql, bindings) { return this.client.raw(sql, bindings); } /** * Calculate the average of a given column. */ async avg(column) { const [{ avg }] = await this.qb.avg({ avg: column }); return avg; } /** * Calculate the average of a given column using distinct. */ async avgDistinct(column) { const [{ avg }] = await this.qb.avgDistinct({ avg: column }); return avg; } /** * Get the max number of a given column. */ async max(column) { const [{ max }] = await this.qb.max({ max: column }); return max; } /** * Get the min number of a given column. */ async min(column) { const [{ min }] = await this.qb.min({ min: column }); return min; } /** * Sum all numbers of a given column. */ async sum(column) { const [{ sum }] = await this.qb.sum({ sum: column }); return sum; } /** * Sum all numbers of a given column in distinct mode. */ async sumDistinct(column) { const [{ sum }] = await this.qb.sumDistinct({ sum: column }); return sum; } /** * Increment a value of a given column. */ async increment(column) { await this.qb.increment(column); } /** * Decrement a value of a given column. */ async decrement(column) { await this.qb.decrement(column); } /** * Calculate the average of a given column using distinct. */ async count(column = '*') { const [{ count }] = await this.qb.count({ count: column }); return `${count}`; } /** * Calculate the average of a given column using distinct. */ async countDistinct(column) { const [{ count }] = await this.qb.countDistinct({ count: column }); return `${count}`; } /** * Find a value in database. */ async find() { return this.qb.first(); } /** * Find many values in database. */ async findMany() { const data = await this.qb; this.qb = this.query(); return data; } /** * Find many values in database and return as paginated response. */ async paginate(page = { page: 0, limit: 10, resourceUrl: '/' }, limit = 10, resourceUrl = '/') { if (Is.Number(page)) { page = { page, limit, resourceUrl }; } const [{ count }] = await this.qb .clone() .clearOrder() .clearSelect() .count({ count: '*' }); const data = await this.offset(page.page).limit(page.limit).findMany(); return Exec.pagination(data, parseInt(count), page); } /** * Create a value in database. */ async create(data = {}) { if (Is.Array(data)) { throw new WrongMethodException('create', 'createMany'); } const created = await this.createMany([data]); return created[0]; } /** * Create many values in database. */ async createMany(data = []) { if (!Is.Array(data)) { throw new WrongMethodException('createMany', 'create'); } const ids = []; const promises = data.map(data => { return this.qb .clone() .insert(data) .then(([id]) => ids.push(data[this.primaryKey] || id)); }); await Promise.all(promises); return this.whereIn(this.primaryKey, ids).findMany(); } /** * Create data or update if already exists. */ async createOrUpdate(data = {}) { const query = this.qb.clone(); const hasValue = await query.first(); if (hasValue) { await this.qb .where(this.primaryKey, hasValue[this.primaryKey]) .update(data); return this.where(this.primaryKey, hasValue[this.primaryKey]).find(); } return this.create(data); } /** * Update a value in database. */ async update(data) { await this.qb.clone().update(data); const result = await this.findMany(); if (result.length === 1) { return result[0]; } return result; } /** * Delete one value in database. */ async delete() { await this.qb.delete(); } /** * Set the table that this query will be executed. */ table(table) { if (!this.isConnected) { throw new NotConnectedDatabaseException(); } this.tableName = table; this.qb = this.query(); return this; } /** * Log in console the actual query built. */ dump() { console.log(this.qb.toSQL().toNative()); return this; } /** * Set the columns that should be selected on query. */ select(...columns) { this.qb.select(...columns); return this; } /** * Set the columns that should be selected on query raw. */ selectRaw(sql, bindings) { return this.select(this.raw(sql, bindings)); } /** * Set the table that should be used on query. * Different from `table()` method, this method * doesn't change the driver table. */ from(table) { this.qb.from(table); return this; } /** * Set the table that should be used on query raw. * Different from `table()` method, this method * doesn't change the driver table. */ fromRaw(sql, bindings) { return this.from(this.raw(sql, bindings)); } /** * Set a join statement in your query. */ join(table, column1, operation, column2) { return this.joinByType('join', table, column1, operation, column2); } /** * Set a left join statement in your query. */ leftJoin(table, column1, operation, column2) { return this.joinByType('leftJoin', table, column1, operation, column2); } /** * Set a right join statement in your query. */ rightJoin(table, column1, operation, column2) { return this.joinByType('rightJoin', table, column1, operation, column2); } /** * Set a cross join statement in your query. */ crossJoin(table, column1, operation, column2) { return this.joinByType('crossJoin', table, column1, operation, column2); } /** * Set a full outer join statement in your query. */ fullOuterJoin(table, column1, operation, column2) { // TODO https://github.com/knex/knex/issues/3949 return this.joinByType('leftJoin', table, column1, operation, column2); } /** * Set a left outer join statement in your query. */ leftOuterJoin(table, column1, operation, column2) { return this.joinByType('leftOuterJoin', table, column1, operation, column2); } /** * Set a right outer join statement in your query. */ rightOuterJoin(table, column1, operation, column2) { return this.joinByType('rightOuterJoin', table, column1, operation, column2); } /** * Set a join raw statement in your query. */ joinRaw(sql, bindings) { this.qb.joinRaw(sql, bindings); return this; } /** * Set a group by statement in your query. */ groupBy(...columns) { this.qb.groupBy(...columns); return this; } /** * Set a group by raw statement in your query. */ groupByRaw(sql, bindings) { this.qb.groupByRaw(sql, bindings); return this; } /** * Set a having statement in your query. */ having(column, operation, value) { if (operation === undefined) { this.qb.having(column); return this; } if (value === undefined) { this.qb.having(column, '=', operation); return this; } this.qb.having(column, operation, value); return this; } /** * Set a having raw statement in your query. */ havingRaw(sql, bindings) { this.qb.havingRaw(sql, bindings); return this; } /** * Set a having exists statement in your query. */ havingExists(closure) { const driver = this.clone(); // @ts-ignore this.qb.havingExists(function () { closure(driver.setQueryBuilder(this, { useSetQB: true })); }); return this; } /** * Set a having not exists statement in your query. */ havingNotExists(closure) { const driver = this.clone(); // @ts-ignore this.qb.havingNotExists(function () { closure(driver.setQueryBuilder(this, { useSetQB: true })); }); return this; } /** * Set a having in statement in your query. */ havingIn(column, values) { this.qb.havingIn(column, values); return this; } /** * Set a having not in statement in your query. */ havingNotIn(column, values) { this.qb.havingNotIn(column, values); return this; } /** * Set a having between statement in your query. */ havingBetween(column, values) { this.qb.havingBetween(column, values); return this; } /** * Set a having not between statement in your query. */ havingNotBetween(column, values) { this.qb.havingNotBetween(column, values); return this; } /** * Set a having null statement in your query. */ havingNull(column) { this.qb.havingNull(column); return this; } /** * Set a having not null statement in your query. */ havingNotNull(column) { this.qb.havingNotNull(column); return this; } /** * Set an or having statement in your query. */ orHaving(column, operation, value) { if (operation === undefined) { this.qb.orHaving(column); return this; } if (value === undefined) { this.qb.orHaving(column, '=', operation); return this; } this.qb.orHaving(column, operation, value); return this; } /** * Set an or having raw statement in your query. */ orHavingRaw(sql, bindings) { this.qb.orHavingRaw(sql, bindings); return this; } /** * Set an or having exists statement in your query. */ orHavingExists(closure) { const driver = this.clone(); // @ts-ignore this.qb.orHavingExists(function () { closure(driver.setQueryBuilder(this, { useSetQB: true })); }); return this; } /** * Set an or having not exists statement in your query. */ orHavingNotExists(closure) { const driver = this.clone(); // @ts-ignore this.qb.orHavingNotExists(function () { closure(driver.setQueryBuilder(this, { useSetQB: true })); }); return this; } /** * Set an or having in statement in your query. */ orHavingIn(column, values) { // @ts-ignore this.qb.orHavingIn(column, values); return this; } /** * Set an or having not in statement in your query. */ orHavingNotIn(column, values) { this.qb.orHavingNotIn(column, values); return this; } /** * Set an or having between statement in your query. */ orHavingBetween(column, values) { this.qb.orHavingBetween(column, values); return this; } /** * Set an or having not between statement in your query. */ orHavingNotBetween(column, values) { this.qb.orHavingNotBetween(column, values); return this; } /** * Set an or having null statement in your query. */ orHavingNull(column) { // @ts-ignore this.qb.orHavingNull(column); return this; } /** * Set an or having not null statement in your query. */ orHavingNotNull(column) { // @ts-ignore this.qb.orHavingNotNull(column); return this; } /** * Set a where statement in your query. */ where(statement, operation, value) { if (Is.Function(statement)) { const driver = this.clone(); this.qb.where(function () { statement(driver.setQueryBuilder(this, { useSetQB: true })); }); return this; } if (operation === undefined) { this.qb.where(statement); return this; } if (value === undefined) { this.qb.where(statement, operation); return this; } this.qb.where(statement, operation, value); return this; } /** * Set a where not statement in your query. */ whereNot(statement, value) { if (Is.Function(statement)) { const driver = this.clone(); this.qb.whereNot(function () { statement(driver.setQueryBuilder(this, { useSetQB: true })); }); return this; } if (value === undefined) { this.qb.whereNot(statement); return this; } this.qb.whereNot(statement, value); return this; } /** * Set a where raw statement in your query. */ whereRaw(sql, bindings) { this.qb.whereRaw(sql, bindings); return this; } /** * Set a where exists statement in your query. */ whereExists(closure) { const driver = this.clone(); this.qb.whereExists(function () { closure(driver.setQueryBuilder(this, { useSetQB: true })); }); return this; } /** * Set a where not exists statement in your query. */ whereNotExists(closure) { const driver = this.clone(); this.qb.whereNotExists(function () { closure(driver.setQueryBuilder(this, { useSetQB: true })); }); return this; } /** * Set a where like statement in your query. */ whereLike(column, value) { this.qb.whereLike(column, value); return this; } /** * Set a where ILike statement in your query. */ whereILike(column, value) { this.qb.whereILike(column, value); return this; } /** * Set a where in statement in your query. */ whereIn(column, values) { this.qb.whereIn(column, values); return this; } /** * Set a where not in statement in your query. */ whereNotIn(column, values) { this.qb.whereNotIn(column, values); return this; } /** * Set a where between statement in your query. */ whereBetween(column, values) { this.qb.whereBetween(column, values); return this; } /** * Set a where not between statement in your query. */ whereNotBetween(column, values) { this.qb.whereNotBetween(column, values); return this; } /** * Set a where null statement in your query. */ whereNull(column) { this.qb.whereNull(column); return this; } /** * Set a where not null statement in your query. */ whereNotNull(column) { this.qb.whereNotNull(column); return this; } /** * Set a or where statement in your query. */ orWhere(statement, operation, value) { if (Is.Function(statement)) { const driver = this.clone(); this.qb.orWhere(function () { statement(driver.setQueryBuilder(this, { useSetQB: true })); }); return this; } if (operation === undefined) { this.qb.orWhere(statement); return this; } if (value === undefined) { this.qb.orWhere(statement, operation); return this; } this.qb.orWhere(statement, operation, value); return this; } /** * Set an or where not statement in your query. */ orWhereNot(statement, value) { if (Is.Function(statement)) { const driver = this.clone(); this.qb.orWhereNot(function () { statement(driver.setQueryBuilder(this, { useSetQB: true })); }); return this; } if (value === undefined) { this.qb.orWhereNot(statement); return this; } this.qb.orWhereNot(statement, value); return this; } /** * Set a or where raw statement in your query. */ orWhereRaw(sql, bindings) { this.qb.orWhereRaw(sql, bindings); return this; } /** * Set an or where exists statement in your query. */ orWhereExists(closure) { const driver = this.clone(); this.qb.orWhereExists(function () { closure(driver.setQueryBuilder(this, { useSetQB: true })); }); return this; } /** * Set an or where not exists statement in your query. */ orWhereNotExists(closure) { const driver = this.clone(); this.qb.orWhereNotExists(function () { closure(driver.setQueryBuilder(this, { useSetQB: true })); }); return this; } /** * Set an or where like statement in your query. */ orWhereLike(column, value) { this.qb.orWhereLike(column, value); return this; } /** * Set an or where ILike statement in your query. */ orWhereILike(column, value) { this.qb.orWhereILike(column, value); return this; } /** * Set an or where in statement in your query. */ orWhereIn(column, values) { this.qb.orWhereIn(column, values); return this; } /** * Set an or where not in statement in your query. */ orWhereNotIn(column, values) { this.qb.orWhereNotIn(column, values); return this; } /** * Set an or where between statement in your query. */ orWhereBetween(column, values) { this.qb.orWhereBetween(column, values); return this; } /** * Set an or where not between statement in your query. */ orWhereNotBetween(column, values) { this.qb.orWhereNotBetween(column, values); return this; } /** * Set an or where null statement in your query. */ orWhereNull(column) { this.qb.orWhereNull(column); return this; } /** * Set an or where not null statement in your query. */ orWhereNotNull(column) { this.qb.orWhereNotNull(column); return this; } /** * Set an order by statement in your query. */ orderBy(column, direction = 'ASC') { this.qb.orderBy(column, direction.toUpperCase()); return this; } /** * Set an order by raw statement in your query. */ orderByRaw(sql, bindings) { this.qb.orderByRaw(sql, bindings); return this; } /** * Order the results easily by the latest date. By default, the result will * be ordered by the table's "createdAt" column. */ latest(column = 'createdAt') { return this.orderBy(column, 'DESC'); } /** * Order the results easily by the oldest date. By default, the result will * be ordered by the table's "createdAt" column. */ oldest(column = 'createdAt') { return this.orderBy(column, 'ASC'); } /** * Set the skip number in your query. */ offset(number) { this.qb.offset(number); return this; } /** * Set the limit number in your query. */ limit(number) { this.qb.limit(number); return this; } }