UNPKG

@athenna/database

Version:

The Athenna database handler for SQL/NoSQL.

277 lines (276 loc) 8.86 kB
/** * @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; } }