Home Reference Source

src/core.js

const CustomError = require('./util/error');
const Database = require('better-sqlite3');
const path = require('path');
const { DataTypeDefaults } = require('./util/constants');
const Util = require('./util/util');
const Table = require('./table');

/**
 * The base provider
 */
class Core {
    /** @param {string} filepath The filepath to the DB */
    constructor(filepath) {
        /**
         * The resolved path to the file
         * @type {string}
         */
        if (filepath !== null && typeof filepath !== 'undefined' && filepath.length > 0) this.filepath = path.resolve(path.dirname(require.main.filename), filepath);
        else this.filepath = ':memory:';

        /**
         * The raw database
         * @type {external:Database}
         * @private
         */
        this._db = null;

        /**
         * The names of the tables
         * @type {Map<string, Table>}
         */
        this.tables = new Map();

        /**
         * A map of prepared statements to use internally
         * @type {Map<string, external:Statement>}
         * @private
         */
        this.prepared = new Map();
    }

    /**
     * The raw database
     * @type {external:Database}
     * @readonly
     */
    get db() {
        return this._db;
    }

    /**
     * Initializes the wrapper, and pulls all the table names into memory
     * @returns {Core}
     */
    init() {
        this._db = new Database(this.filepath);

        this.prepared.set('getTable', this.db.prepare('SELECT * FROM sqlite_master WHERE name=? AND type=\'table\''));
        const tables = this.db.prepare('SELECT * FROM sqlite_master WHERE type=\'table\'').all();

        for (const table of tables) {
            this.tables.set(table.name, new Table({ core: this, name: table.name })._init());
        }
        return this;
    }

    /**
     * Creates a new table
     * @param {string} tableName The name of the table
     * @param {TableOptions} options Options for the table
     * @returns {Table}
     */
    create(tableName, options) {
        Util.checkTableName(tableName);
        const parsedOptions = this._parseOptions(options);
        this.db.prepare(`CREATE TABLE IF NOT EXISTS ${tableName}${parsedOptions}`).run();
        const tableInfo = this.prepared.get('getTable').get(tableName);
        this.tables.set(tableInfo.name, new Table({
            core: this,
            name: tableInfo.name
        }));
        return this.get(tableName);
    }

    /**
     * Parses options for creating a table
     * @param {TableOptions} options Options to parse
     * @returns {ParsedTableOptions}
     * @private
     */
    _parseOptions(options) {
        if (typeof options !== 'object' && !(options instanceof Array)) throw new CustomError(`The table options must be an array, found type "${typeof options}"`, 'INVALID_OPTIONS');
        if (!options.length) throw new CustomError('Options must be provided', 'INVALID_OPTIONS');
        const optArr = [];
        for (let keyOptions of options) {
            const keyArr = [];
            keyOptions = Util.mergeDefault(DataTypeDefaults, keyOptions);
            if (!keyOptions.name) throw new CustomError('A key must have a name', 'INVALID_KEY_NAME');
            if (typeof keyOptions.name !== 'string') throw new CustomError(`A name must be of type string, found type "${typeof keyOptions.name}"`, 'INVALID_KEY_NAME');
            keyArr.push(keyOptions.name);
            keyArr.push(keyOptions.type);
            if (keyOptions.primary) keyArr.push('PRIMARY KEY');
            if (keyOptions.unique) keyArr.push('UNIQUE');
            if (keyOptions.nullable != null) { // eslint-disable-line eqeqeq
                keyArr.push(keyOptions.nullable ? 'NULL' : 'NOT NULL');
            }
            optArr.push(keyArr.join(' '));
        }
        let optStr = optArr.join(', ');
        optStr = `(${optStr})`;
        return optStr;
    }

    /**
     * Gets a table
     * @param {string} tableName The name of the table
     * @returns {Table}
     */
    get(tableName) {
        Util.checkTableName(tableName);
        const table = this.tables.get(tableName);
        if (!table) throw new CustomError(`The table ${tableName} doesn't exists`, 'INVALID_TABLE');
        return table;
    }

    /**
     * Deletes a table, and drops it from the database
     * @param {string} tableName The name of the table
     * @param {boolean} [drop=false] Whether to drop the table from the database
     * @returns {null}
     */
    delete(tableName, drop = false) {
        Util.checkTableName(tableName);
        const table = this.tables.get(tableName);
        if (!table) throw new CustomError(`The table ${tableName} doesn't exists`, 'INVALID_TABLE');
        this.db.prepare(`DELETE FROM ${tableName}`).run();
        if (drop) this.db.prepare(`DROP TABLE ${tableName}`).run();
        this.tables.delete(tableName);
        return null;
    }

    /**
     * When concatenated with a string, this returns the raw databases name
     * @returns {string}
     */
    toString() {
        return this.db.name;
    }

    /**
     * Defines the JSON.stringify() behavior
     * @private
     */
    toJSON() {
        return Util.flatten(this);
    }

    valueOf() {
        return this.db.name;
    }
}

module.exports = Core;

/**
 * @external Database
 * @see {@link https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/api.md#class-database}
 */

/**
 * The parsed table options
 * @private
 * @typedef {string} ParsedTableOptions
 */

/**
 * Options when creating a table
 * @typedef {Array<KeyOptions>} TableOptions
 */

/**
 * @external Statement
 * @see {@link https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/api.md#class-statement}
 */