@athenna/database
Version:
The Athenna database handler for SQL/NoSQL.
219 lines (218 loc) • 7.05 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 PostgresDriver 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: 'pg',
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);
}
/**
* List all databases available.
*/
async getDatabases() {
const { rows: databases } = await this.raw('SELECT datname FROM pg_database');
return databases.map(database => database.datname);
}
/**
* Create a new database.
*/
async createDatabase(database) {
/**
* Catching the error to simulate IF NOT EXISTS
*/
try {
await this.raw('CREATE DATABASE ??', database);
}
catch (_err) { }
}
/**
* Drop some database.
*/
async dropDatabase(database) {
/**
* Catching the error to simulate IF EXISTS
*/
try {
await this.raw('DROP DATABASE ??', database);
}
catch (_err) { }
}
/**
* List all tables available.
*/
async getTables() {
const { rows: tables } = await this.raw('SELECT table_name FROM information_schema.tables WHERE table_schema = current_schema() AND table_catalog = ?', 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) {
await this.raw('TRUNCATE TABLE ?? CASCADE', table);
}
/**
* 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));
return this.qb.insert(preparedData, '*');
}
/**
* 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 path = this.parseJsonSelectorToWildcardPath(parsed.path);
const normalized = this.normalizeJsonOperation(operator, value);
this.qb.whereRaw('jsonb_path_exists(??, ?::jsonpath, ?::jsonb)', [
parsed.column,
`${path} ? (@ ${normalized.operator} $value)`,
JSON.stringify({ value: 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 path = this.parseJsonSelectorToWildcardPath(parsed.path);
const normalized = this.normalizeJsonOperation(operator, value);
this.qb.orWhereRaw('jsonb_path_exists(??, ?::jsonpath, ?::jsonb)', [
parsed.column,
`${path} ? (@ ${normalized.operator} $value)`,
JSON.stringify({ value: normalized.value })
]);
return this;
}
/**
* Convert a json selector path to a valid postgres json path.
*/
parseJsonSelectorToWildcardPath(path) {
const parts = path
.split('->')
.map(part => part.trim())
.filter(Boolean);
return parts.reduce((jsonPath, part) => {
if (part === '*') {
return `${jsonPath}[*]`;
}
if (/^\d+$/.test(part)) {
return `${jsonPath}[${part}]`;
}
return `${jsonPath}.${part}`;
}, '$');
}
/**
* Normalize operator/value pair for postgres json path comparisons.
*/
normalizeJsonOperation(operator, value) {
if (Is.Undefined(value)) {
return {
operator: '==',
value: operator
};
}
return {
operator: this.getJsonPathOperator(operator),
value
};
}
/**
* Convert query operators to postgres json path operators.
*/
getJsonPathOperator(operator) {
const operators = {
'=': '==',
'==': '==',
'!=': '!=',
'<>': '!=',
'>': '>',
'>=': '>=',
'<': '<',
'<=': '<='
};
return operators[operator] || operator;
}
}