daemonraco/dfdb

View on GitHub
src/includes/connection/connection.dfdb.ts

Summary

Maintainability
A
1 hr
Test Coverage
/**
 * @file connection.dfdb.ts
 * @author Alejandro D. Simi
 */
import { Promise } from 'es6-promise';
import * as JSZip from 'jszip';
import * as fs from 'fs';

import { BasicConstants } from '../constants.dfdb';
import { BasicDictionary } from '../basic-types.dfdb';
import { ConnectionDBValidationResult, ConnectionSavingQueueResult } from './types.dfdb';
import { DocsOnFileDB } from '../manager.dfdb';
import { Collection } from '../collection/collection.dfdb';
import { IErrors } from '../errors.i.dfdb';
import { Initializer } from './initializer.dfdb';
import { IResource } from '../resource.i.dfdb';
import { Rejection } from '../rejection.dfdb';
import { RejectionCodes } from '../rejection-codes.dfdb';
import { SubLogicCollections } from './collections.sl.dfdb';
import { SubLogicConnect } from './connect.sl.dfdb';
import { SubLogicErrors } from '../errors.sl.dfdb';
import { SubLogicFile } from './file.sl.dfdb';
import { SubLogicInit } from './init.sl.dfdb';

export { ConnectionDBValidationResult, ConnectionSavingQueueResult } from './types.dfdb';

/**
 * This class represents an active connection to a database on file.
 *
 * @class Connection
 */
export class Connection implements IErrors, IResource {
    //
    // Protected properties.
    protected _collections: { [name: string]: Collection } = {};
    protected _connected: boolean = false;
    protected _dbFile: JSZip = null;
    protected _dbFullPath: string = null;
    protected _dbLockFullPath: string = null;
    protected _dbName: string = null;
    protected _dbPath: string = null;
    protected _fileAccessQueue: any = null;
    protected _manifest: BasicDictionary = {
        collections: {},
        initializer: null,
        initializerMD5: null
    };
    protected _manifestPath: string = null;
    protected _subLogicCollections: SubLogicCollections = null;
    protected _subLogicConnect: SubLogicConnect = null;
    protected _subLogicErrors: SubLogicErrors<Connection> = null;
    protected _subLogicFile: SubLogicFile = null;
    protected _subLogicInit: SubLogicInit = null;
    //
    // Constructor.
    /**
     * @constructor
     * @param {string} dbname Name of the database to connect.
     * @param {string} dbpath Directory where the requested database is stored.
     * @param {any} options List of extra options to use when connecting.
     */
    constructor(dbName: string, dbPath: string, options: any = {}) {
        //
        // Shortcuts.
        this._dbName = dbName;
        this._dbPath = dbPath;
        //
        // Main paths.
        this._manifestPath = `manifest`;
        this._dbFullPath = DocsOnFileDB.GuessDatabasePath(this._dbName, this._dbPath);
        this._dbLockFullPath = `${this._dbFullPath}${BasicConstants.DBLockExtension}`;
        //
        // Sub-logics.
        this._subLogicCollections = new SubLogicCollections(this);
        this._subLogicConnect = new SubLogicConnect(this);
        this._subLogicErrors = new SubLogicErrors<Connection>(this);
        this._subLogicFile = new SubLogicFile(this);
        this._subLogicInit = new SubLogicInit(this);
    }
    //
    // Public methods.
    /**
     * Provides access to a collection inside current database connection. If such
     * collection doesn't exist, it is created.
     *
     * @method collection
     * @param {string} name Collection to look for.
     * @returns {Promise<Collection>} Returns a connection as a promise.
     */
    public collection(name: string): Promise<Collection> {
        //
        // Forwarding to sub-logic.
        return this._subLogicCollections.collection(name);
    }
    /**
     * Provides access to the list of collection this connections knows.
     *
     * @method collections
     * @returns {BasicDictionary} Returns a list of collections this connection
     * knows.
     */
    public collections(): BasicDictionary {
        //
        // Forwarding to sub-logic.
        return this._subLogicCollections.collections();
    }
    /**
     * Connects this object to the physicial database file. If the database file
     * doesn't exist it is created.
     *
     * @method connect
     * @returns {Promise<void>} Returns TRUE or FALSE as a promise indicating
     * if it's connected or not.
     */
    public connect(): Promise<void> {
        //
        // Forwarding to sub-logic.
        return this._subLogicConnect.connect();
    }
    /**
     * Provides access to the connection status.
     *
     * @method connected
     * @returns {boolean} Returns TRUE when it's connected.
     */
    public connected(): boolean {
        //
        // Forwarding to sub-logic.
        return this._subLogicConnect.connected();
    }
    /**
     * Closes current connection, but first it closes all its collections and also
     * saves all pending changes.
     *
     * @method close
     * @returns {Promise<void>} Returns a promise that gets resolved when this
     * operation is finished.
     */
    public close(): Promise<void> {
        //
        // Forwarding to sub-logic.
        return this._subLogicConnect.close();
    }
    /**
     * Provides a way to know if there was an error in the last operation.
     *
     * @method error
     * @returns {boolean} Returns TRUE when there was an error.
     */
    public error(): boolean {
        //
        // Forwarding to sub-logic.
        return this._subLogicErrors.error();
    }
    /**
     * Ask this connection to forget a tracked collection.
     *
     * @method forgetCollection
     * @param {string} name Collection name.
     * @param {boolean} drop Forgetting a collection is simple assuming that it's
     * not loaded, but it will still have an entry in the manifest. This parameter
     * forces this connection to completelly forget it.
     * @returns {Promise<void>} Returns a promise that gets resolved when this
     * operation is finished.
     */
    public forgetCollection(name: string, drop: boolean = false): Promise<void> {
        //
        // Forwarding to sub-logic.
        return this._subLogicCollections.forgetCollection(name, drop);
    }
    /**
     * Provides a way to know if this connection stores certain collection.
     *
     * @method hasCollection
     * @param {string} name Name of the collection to check.
     * @returns {boolean} Returns TRUE when it does.
     */
    public hasCollection(name: string): boolean {
        //
        // Forwarding to sub-logic.
        return this._subLogicCollections.hasCollection(name);
    }
    /**
     * This method allows to know if current database connection has an
     * initializer assigned.
     *
     * @method hasInitializer
     * @returns {boolean} Returns TRUE when it has.
     */
    public hasInitializer(): boolean {
        //
        // Forwarding to sub-logic.
        return this._subLogicInit.hasInitializer();
    }
    /**
     * This method allows access to current database connection's  assigned
     * initializer.
     *
     * @method initializer
     * @returns {Initializer} Returns a copy of this connection's initializer.
     */
    public initializer(): Initializer {
        //
        // Forwarding to sub-logic.
        return this._subLogicInit.initializer();
    }
    /**
     * Provides access to the error message registed by the last operation.
     *
     * @method lastError
     * @returns {string|null} Returns an error message.
     */
    public lastError(): string | null {
        //
        // Forwarding to sub-logic.
        return this._subLogicErrors.lastError();
    }
    /**
     * Provides access to the rejection registed by the last operation.
     *
     * @method lastRejection
     * @returns {Rejection} Returns an rejection object.
     */
    public lastRejection(): Rejection {
        //
        // Forwarding to sub-logic.
        return this._subLogicErrors.lastRejection();
    }
    /**
     * This method centralizes all calls to load a file from inside the database
     * zip file.
     *
     * @method loadFile
     * @param {string} zPath Internal path to load.
     * @returns {Promise<ConnectionSavingQueueResult>} Returns a standarized
     * result object.
     */
    public loadFile(zPath: string): Promise<ConnectionSavingQueueResult> {
        //
        // Forwarding to sub-logic.
        return this._subLogicFile.loadFile(zPath);
    }
    /**
     * This method tries to reapply the initial database structure an recreates
     * does assets that may be missing.
     *
     * @method reinitialize
     * @returns {Promise<void>} Returns a promise that gets resolved when this
     * operation is finished.
     */
    public reinitialize(): Promise<void> {
        //
        // Forwarding to sub-logic.
        return this._subLogicInit.reinitialize();
    }
    /**
     * This method centralizes all calls to remove a file from inside the database
     * zip file.
     *
     * @method removeFile
     * @param {string} zPath Internal path to remove.
     * @returns {Promise<ConnectionSavingQueueResult>} Returns a standarized
     * result object.
     */
    public removeFile(zPath: string): Promise<ConnectionSavingQueueResult> {
        //
        // Forwarding to sub-logic.
        return this._subLogicFile.removeFile(zPath);
    }
    /**
     * This method actually saves zip information physically.
     *
     * @method save
     * @returns {Promise<void>} Returns a promise that gets resovled when this
     * operation is finished.
     */
    public save(): Promise<void> {
        //
        // Forwarding to sub-logic.
        return this._subLogicFile.save();
    }
    /**
     * This method assigns an initialization structure to this connection's
     * database and applies it creating all required and missing assets.
     *
     * @method setInitializer
     * @param {Initializer} specs Specifications to associate with this database
     * connection.
     * @returns {Promise<void>} Returns a promise that gets resovled when this
     * operation is finished.
     */
    public setInitializer(specs: Initializer): Promise<void> {
        //
        // Forwarding to sub-logic.
        return this._subLogicInit.setInitializer(specs);
    }
    /**
     * This method assigns an initialization structure to this connection's
     * database and applies it creating all required and missing assets.
     * Similar to 'setInitializer()' but based on a JSON.
     *
     * @method setInitializerFromJSON
     * @param {any} specs Specifications to associate with this database
     * connection given as a JSON.
     * @returns {Promise<void>} Returns a promise that gets resovled when this
     * operation is finished.
     */
    public setInitializerFromJSON(specs: any): Promise<void> {
        const initializer: Initializer = new Initializer();
        initializer.loadFromJSON(specs);
        return this.setInitializer(initializer);
    }
    /**
     * This method assigns an initialization structure to this connection's
     * database and applies it creating all required and missing assets.
     * Similar to 'setInitializer()' but based on a string.
     *
     * @method setInitializerFromJSON
     * @param {string} specs Specifications to associate with this database
     * connection given as a string.
     * @returns {Promise<void>} Returns a promise that gets resovled when this
     * operation is finished.
     */
    public setInitializerFromString(specs: string): Promise<void> {
        const initializer: Initializer = new Initializer();
        initializer.loadFromString(specs);
        return this.setInitializer(initializer);
    }
    /**
     * This methods provides a proper value for string auto-castings.
     *
     * @method toString
     * @returns {string} Returns a simple string identifying this connection.
     */
    public toString = (): string => {
        return `connection:${this._dbName}[${this._dbPath}]`;
    }
    /**
     * This method centralizes all calls to update contents of a file inside the
     * database zip file.
     *
     * @method updateFile
     * @param {string} zPath Internal path to remove.
     * @param {any} data Information to be stored.
     * @param {boolean} skipPhysicalSave After virtually updating the file, should
     * physical storage be updated?
     * @returns {Promise<ConnectionSavingQueueResult>} Returns a standarized
     * result object.
     */
    public updateFile(zPath: string, data: any, skipPhysicalSave: boolean = false): Promise<ConnectionSavingQueueResult> {
        //
        // Forwarding to sub-logic.
        return this._subLogicFile.updateFile(zPath, data, skipPhysicalSave);
    }
    //
    // Public class methods.
    /**
     * This method takes the basic values that represent a database and checks if
     * it exists and if it's valid or not.
     *
     * @static
     * @method IsValidDatabase
     * @param {string} dbname Name of the database.
     * @param {string} dbpath Directory where the requested database is stored.
     * @returns {Promise<ConnectionDBValidationResult>}
     */
    public static IsValidDatabase(dbname: string, dbpath: string): Promise<ConnectionDBValidationResult> {
        //
        // Building promise to return.
        return new Promise<ConnectionDBValidationResult>((resolve: (res: ConnectionDBValidationResult) => void, reject: (err: Rejection) => void) => {
            //
            // Default values.
            const results: ConnectionDBValidationResult = new ConnectionDBValidationResult();
            //
            // Guessing database zip file's full path.
            const dbFullPath = DocsOnFileDB.GuessDatabasePath(dbname, dbpath);
            //
            // Checking its existence.
            let stat: any = null;
            try { stat = fs.statSync(dbFullPath); } catch (e) { }
            //
            // Does it exist?
            if (stat) {
                results.exists = true;
                //
                // Loading its information for further analysis.
                fs.readFile(dbFullPath, (error: any, data: any) => {
                    //
                    // Was it read?
                    if (error) {
                        results.errorCode = RejectionCodes.DatabaseDoesntExist;
                        results.error = RejectionCodes.Message(results.errorCode);
                        resolve(results);
                    } else {
                        //
                        // Does the header indicates it's a zip file?
                        if (data.toString().substring(0, 2) === 'PK') {
                            results.valid = true;
                        } else {
                            results.errorCode = RejectionCodes.DatabaseNotValid;
                            results.error = RejectionCodes.Message(results.errorCode);
                        }
                        resolve(results);
                    }
                });
            } else {
                results.errorCode = RejectionCodes.DatabaseDoesntExist;
                results.error = RejectionCodes.Message(results.errorCode);
                resolve(results);
            }
        });
    }
}