Source/Database.js

"use strict";
/**
 *  @module     Database
 *  @overview   Defines the database manipulation functions used by the `Registry` module.
 *
 *  @author     Animesh Mishra <hello@animesh.ltd>
 *  @copyright  © 2018 Animesh Ltd. All Rights Reserved.
 */
Object.defineProperty(exports, "__esModule", { value: true });
const Chai = require("chai");
const Mongo = require("mongodb");
const Account_1 = require("./Account");
const Source_1 = require("./Source");
const error_1 = require("@magic.batua/error");
const expect = Chai.expect;
const AccountCollection = "Users";
/**
 *  @exports Database
 *  The `Database` module powers the database read/write capabilities of the Magic Batua
 *  platform.
 */
exports.description = "Powers the database read/write capabilities of the Magic Batua platform.";
/**
 *  Looks up the `phone` number in the user account registry.
 *
 *  @param {string} phone   Phone number to be searched
 *  @param {Mongo.Db} db    Database to be scoured
 *
 *  @returns {Promise<Account | null>}  An `Account` object if a match is found. If a
 *  match is found but the referrer account has been soft-deleted, returns `null`. If
 *  no match is found, returns `null`.
 *
 *  @throws {ExternalError} If the search operation fails, a `424 Failed Dependency`
 *                          error is thrown.
 */
async function Find(phone, db) {
    try {
        let filter = { phone: phone };
        let document = await db.collection(AccountCollection).findOne(filter);
        // If no document is found, return null
        if (document == null) {
            return null;
        }
        else if (document.isDeleted && document.recoverBy <= Date.now()) {
            return null;
        }
        else {
            return new Account_1.Account(document);
        }
    }
    catch (exception) {
        throw new error_1.ExternalError("Database search crashed unexpectedly.", "MongoDB", exception.toString());
    }
}
exports.Find = Find;
/**
 *  Finds a user account registry entry that matches the given account information and
 *  replaces that document with the `account` document provided.
 *
 *  @param {Account} account  Phone number to be searched
 *  @param {Mongo.Db} db      Database to be scoured
 *
 *  @returns {Promise<Account>} The updated `Account` object if a match is found and
 *                              successfully replaced by the document provided.
 *
 *  @throws {ExternalError} If the operation fails, a `424 Failed Dependency` error is thrown.
 */
async function FindAndReplace(account, db) {
    try {
        let filter = { _id: account._id };
        let response = await db.collection(AccountCollection).findOneAndReplace(filter, account);
        // If update was successful, return the updated Account instance
        if (response.ok) {
            return new Account_1.Account(response.value);
        }
        else {
            throw response.lastErrorObject;
        }
    }
    catch (exception) {
        throw new error_1.ExternalError("Database search crashed unexpectedly.", "MongoDB", exception.toString());
    }
}
exports.FindAndReplace = FindAndReplace;
/**
 *  Given a user `id`, retrieves the full account information stored against that `id` in
 *  the database.
 *
 *  @param {string} id      Magic Batua user ID
 *  @param {Mongo.Db} db    Database to be searched
 *
 *  @returns {Promise<Account | null>}  An `Account` object if a match is found. If a
 *  match is found but the referrer account has been soft-deleted, returns `null`. If
 *  no match is found, returns `null`.
 *
 *  @throws {ExternalError} If the search operation fails, a `424 Failed Dependency`
 *                          error is thrown.
 */
async function GetAccountByID(id, db) {
    try {
        let filter = { _id: new Mongo.ObjectId(id) };
        let document = await db.collection(AccountCollection).findOne(filter);
        // If no document is found, return null
        if (document == null) {
            return null;
        }
        else if (document.isDeleted && document.recoverBy <= Date.now()) {
            return null;
        }
        else {
            return new Account_1.Account(document);
        }
    }
    catch (exception) {
        throw new error_1.ExternalError("Database search crashed unexpectedly.", "MongoDB", exception.toString());
    }
}
exports.GetAccountByID = GetAccountByID;
/**
 *  When a user is referred to Magic Batua by their friends and family, their signup
 *  request contains an `inviteCode` that helps us identify the referrer in our systems.
 *  This is the method that does that heavy lifting.
 *
 *  This method searches the database to find the `Account` having the same `referralCode`
 *  property as the given `inviteCode`. If a match is found, it is deemed the rightful
 *  referrer and returned to the calling scope.
 *
 *  @param {string} inviteCode  Referral code used at signup
 *  @param {Mongo.Db} db        Database to be scoured
 *
 *  @returns {Promise<Account | null>}  An `Account` object if a referrer is found. If
 *  a match is found but the referrer account has been soft-deleted, returns `null`. If
 *  no match is found, returns `null`.
 *
 *  @throws {ExternalError} If the search operation fails, a `424 Failed Dependency`
 *                          error is thrown.
 */
async function GetReferrer(inviteCode, db) {
    try {
        let filter = { referralCode: inviteCode };
        let result = await db.collection(AccountCollection).findOne(filter);
        // If no referrer is found, return null
        if (result == null) {
            return null;
        }
        else if (result.isDeleted && result.recoverBy <= Date.now()) {
            return null;
        }
        else {
            return new Account_1.Account(result);
        }
    }
    catch (exception) {
        throw new error_1.ExternalError("Database search crashed unexpectedly.", "MongoDB", exception.toString());
    }
}
exports.GetReferrer = GetReferrer;
/**
 *  Inserts the given `account` document into the MongoDB database instance
 *  held by `db`. No validation checks are run before writing the object to
 *  database.
 *
 *  @param {Account} account    `Account` object to be written to database
 *  @param {Mongo.Db} db        The recipient MongoDB database
 *
 *  @returns {Promise<Account>} A fully instantiated `Account` object
 *  @throws {ExternalError} If the write operation fails, a `424 Failed Dependency`
 *                          error is thrown.
 */
async function Insert(account, db) {
    try {
        let response = await db.collection(AccountCollection).insertOne(account);
        // Check whether the operation executed correctly
        expect(response.result.ok).to.be.equal(1, "Account: Database.Insert() returned a non-ok result.");
        // Return the account object
        return new Account_1.Account(response.ops.pop(), Source_1.Source.Database);
    }
    catch (exception) {
        throw new error_1.ExternalError("Couldn't write object to database.", "MongoDB", exception.toString());
    }
}
exports.Insert = Insert;
/**
 *  Checks whether the given `account` information is already registered with us.
 *
 *  @param {Account} account    Account information
 *  @param {Mongo.Db} db        Database to be scoured
 *
 *  @returns {Promise<boolean>}  `true` if a matching account is found. If a match is found
 *  but the account has been soft-deleted, returns `false`. If no match is found,
 *  returns `false`.
 *
 *  @throws {ExternalError} If the search operation fails, a `424 Failed Dependency`
 *                          error is thrown.
 */
async function IsDuplicate(account, db) {
    try {
        let filter = { phone: account.phone };
        let fields = { fields: {
                isDeleted: true,
                recoverBy: true
            } };
        let result = await db.collection(AccountCollection).findOne(filter, fields);
        // If no matching document is found, return false
        if (result == null) {
            return false;
        }
        else if (result.isDeleted && result.recoverBy <= Date.now()) {
            return false;
        }
        else {
            return true;
        }
    }
    catch (exception) {
        throw new error_1.ExternalError("Database search crashed unexpectedly.", "MongoDB", exception.toString());
    }
}
exports.IsDuplicate = IsDuplicate;
/**
 *  Updates the database object corresponding to the given `_id` in place. Doesn't run data
 *  validation checks, so use very carefully and sparsely. Most of the update operations in the
 *  software are done through the `Database.FindAndReplace()` method. Use that.
 *
 *  **Only allows updates for `name`, `email` or `phone`.**
 *
 *  If the `query` parameter contains a Magic Points transaction entry, then the entry is
 *  pushed to the `pointsLedger.transactions` array.
 *
 *  @param {string}     id       Magic Batua user `_id`
 *  @param {any}        query    A JSON containing key-value pairs that should be updated.
 *  @param {Mongo.Db}   db       MongoDB database instance
 *
 *  @throws {ExternalError} If the operation fails, a `424 Failed Dependency` error is thrown.
 */
async function UpdateInPlace(id, query, db) {
    try {
        let filter = { _id: id };
        let updates;
        let operations;
        if (query.name) {
            updates.name = query.name;
        }
        if (query.email) {
            updates.email = query.email;
        }
        if (query.phone) {
            updates.phone = query.phone;
        }
        // If a points transaction is supplied in the query, then push it to the transaction array
        if (query.pointsTransaction) {
            operations = {
                $set: updates,
                $push: { "pointsLedger.transactions": query.pointsTransaction }
            };
        }
        else {
            operations = {
                $set: updates
            };
        }
        let response = await db.collection(AccountCollection).updateOne(filter, operations);
        expect(response.result.ok).to.equal("1", "Database update returned a non-true result.");
    }
    catch (exception) {
        throw new error_1.ExternalError("Database update crashed unexpectedly.", "MongoDB", exception.toString());
    }
}
exports.UpdateInPlace = UpdateInPlace;
//# sourceMappingURL=Database.js.map