@athenna/database
Version:
The Athenna database handler for SQL/NoSQL.
954 lines (953 loc) • 26.7 kB
JavaScript
/* 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;
}
}