@athenna/database
Version:
The Athenna database handler for SQL/NoSQL.
277 lines (276 loc) • 8.86 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 SqliteDriver 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: 'better-sqlite3',
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('PRAGMA database_list');
return databases.map(database => database.name);
}
/**
* 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 tables = await this.raw("SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%'", await this.getCurrentDatabase());
return tables.map(table => table.name);
}
/**
* Remove all data inside some database table
* and restart the identity of the table.
*/
async truncate(table) {
await this.raw('DELETE FROM ??', 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, '*');
}
/**
* Find a value in database.
*/
async find() {
const data = await super.find();
return this.normalizeRow(data);
}
/**
* Find many values in database.
*/
async findMany() {
const data = await super.findMany();
return data.map(row => this.normalizeRow(row));
}
/**
* 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_extract(??, ?) ' + normalized.operator + ' ?', [
parsed.column,
this.parseJsonSelectorToSqlitePath(parsed.path),
normalized.value
]);
return this;
}
const wildcard = this.parseJsonSelectorToWildcardParts(parsed.path);
this.qb.whereRaw('exists (select 1 from json_each(??, ?) where json_extract(json_each.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_extract(??, ?) ' + normalized.operator + ' ?', [
parsed.column,
this.parseJsonSelectorToSqlitePath(parsed.path),
normalized.value
]);
return this;
}
const wildcard = this.parseJsonSelectorToWildcardParts(parsed.path);
this.qb.orWhereRaw('exists (select 1 from json_each(??, ?) where json_extract(json_each.value, ?) ' +
normalized.operator +
' ?)', [parsed.column, wildcard.arrayPath, wildcard.valuePath, normalized.value]);
return this;
}
/**
* Convert a json selector path to sqlite json path.
*/
parseJsonSelectorToSqlitePath(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
};
}
/**
* Normalize json strings returned by sqlite into arrays/objects.
*/
normalizeRow(row) {
if (!row || !Is.Object(row)) {
return row;
}
return Object.entries(row).reduce((normalized, [key, value]) => {
normalized[key] = this.normalizeJsonValue(value);
return normalized;
}, {});
}
/**
* Parse stringified json objects/arrays returned by sqlite.
*/
normalizeJsonValue(value) {
if (!Is.String(value)) {
return value;
}
const trimmed = value.trim();
if (!(trimmed.startsWith('{') && trimmed.endsWith('}')) &&
!(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
return value;
}
try {
return JSON.parse(trimmed);
}
catch {
return value;
}
}
/**
* Set a where ILike statement in your query.
*/
whereILike(column, value) {
this.qb.whereLike(column, value);
return this;
}
/**
* Set a where ILike statement in your query.
*/
orWhereILike(column, value) {
this.qb.orWhereLike(column, value);
return this;
}
}