@athenna/database
Version:
The Athenna database handler for SQL/NoSQL.
220 lines (219 loc) • 7.66 kB
JavaScript
/**
* @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 { debug } from '#src/debug';
import { Log } from '@athenna/logger';
import { Is, Json, Options } from '@athenna/common';
import { ConnectionFactory } from '#src/factories/ConnectionFactory';
import { BaseKnexDriver } from '#src/database/drivers/BaseKnexDriver';
import { WrongMethodException } from '#src/exceptions/WrongMethodException';
import { EmptyColumnException } from '#src/exceptions/EmptyColumnException';
export class MySqlDriver extends BaseKnexDriver {
/**
* 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();
}
/**
* List all databases available.
*/
async getDatabases() {
const [databases] = await this.raw('SHOW DATABASES');
return databases.map(database => database.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);
}
/**
* 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');
}
}
/**
* Create many values in database.
*/
async createMany(data = []) {
if (!Is.Array(data)) {
throw new WrongMethodException('createMany', 'create');
}
const preparedData = data.map(data => this.prepareInsert(data));
const ids = [];
const promises = preparedData.map((prepared, index) => {
return this.qb
.clone()
.insert(prepared)
.then(([id]) => ids.push(data[index][this.primaryKey] || id));
});
await Promise.all(promises);
return this.whereIn(this.primaryKey, ids).findMany();
}
/**
* Set a where json statement in your query.
*/
whereJson(column, operator, value) {
if (Is.Undefined(column) || !Is.String(column)) {
throw new EmptyColumnException('whereJson');
}
const parsed = this.parseJsonSelector(column);
if (!parsed) {
throw new Error(`Invalid JSON selector: ${column}`);
}
const normalized = this.normalizeJsonOperation(operator, value);
if (!parsed.path.includes('*')) {
this.qb.whereRaw('JSON_UNQUOTE(JSON_EXTRACT(??, ?)) ' + normalized.operator + ' ?', [
parsed.column,
this.parseJsonSelectorToMySqlPath(parsed.path),
normalized.value
]);
return this;
}
const wildcard = this.parseJsonSelectorToWildcardParts(parsed.path);
this.qb.whereRaw("exists (select 1 from json_table(json_extract(??, ?), '$[*]' columns (value json path ?)) as jt where JSON_UNQUOTE(jt.value) " +
normalized.operator +
' ?)', [parsed.column, wildcard.arrayPath, wildcard.valuePath, normalized.value]);
return this;
}
/**
* Set an or where json statement in your query.
*/
orWhereJson(column, operator, value) {
if (Is.Undefined(column) || !Is.String(column)) {
throw new EmptyColumnException('orWhereJson');
}
const parsed = this.parseJsonSelector(column);
if (!parsed) {
throw new Error(`Invalid JSON selector: ${column}`);
}
const normalized = this.normalizeJsonOperation(operator, value);
if (!parsed.path.includes('*')) {
this.qb.orWhereRaw('JSON_UNQUOTE(JSON_EXTRACT(??, ?)) ' + normalized.operator + ' ?', [
parsed.column,
this.parseJsonSelectorToMySqlPath(parsed.path),
normalized.value
]);
return this;
}
const wildcard = this.parseJsonSelectorToWildcardParts(parsed.path);
this.qb.orWhereRaw("exists (select 1 from json_table(json_extract(??, ?), '$[*]' columns (value json path ?)) as jt where JSON_UNQUOTE(jt.value) " +
normalized.operator +
' ?)', [parsed.column, wildcard.arrayPath, wildcard.valuePath, normalized.value]);
return this;
}
/**
* Convert a json selector path to mysql json path.
*/
parseJsonSelectorToMySqlPath(path) {
const parts = path
.split('->')
.map(part => part.trim())
.filter(Boolean);
return this.toJsonPath(parts);
}
/**
* Split a json selector around the wildcard.
*/
parseJsonSelectorToWildcardParts(path) {
const parts = path
.split('->')
.map(part => part.trim())
.filter(Boolean);
const wildcardIndex = parts.indexOf('*');
return {
arrayPath: this.toJsonPath(parts.slice(0, wildcardIndex)),
valuePath: this.toJsonPath(parts.slice(wildcardIndex + 1))
};
}
/**
* Convert path parts to a valid json path.
*/
toJsonPath(parts) {
return parts.reduce((jsonPath, part) => {
if (/^\d+$/.test(part)) {
return `${jsonPath}[${part}]`;
}
return `${jsonPath}.${part}`;
}, '$');
}
/**
* Normalize operator/value pairs from the whereJson overloads.
*/
normalizeJsonOperation(operator, value) {
if (Is.Undefined(value)) {
return {
operator: '=',
value: operator
};
}
return {
operator,
value
};
}
}