UNPKG

@magic.batua/account

Version:

The Account modules powers the user account management features of the Magic Batua platform.

294 lines (264 loc) 10.9 kB
/** * @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. */ import * as Chai from "chai" import * as Mongo from "mongodb" import { Account } from "./Account" import { Source } from "./Source" import { ExternalError } from "@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. */ export var 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. */ export async function Find(phone: string, db: Mongo.Db): Promise<Account | null> { 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 } // If a match is found but it has been soft deleted, return null else if(document.isDeleted && document.recoverBy <= Date.now()) { return null } // If a match is found and it hasn't been soft deleted, return an `Account` instance else { return new Account(document) } } catch(exception) { throw new ExternalError("Database search crashed unexpectedly.", "Azure CosmosDB", exception.toString()) } } /** * 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. */ export async function FindAndReplace(account: Account, db: Mongo.Db): Promise<Account> { 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(response.value) } // If update wasn't successful, throw the update error. else { throw response.lastErrorObject } } catch(exception) { throw new ExternalError("Database search crashed unexpectedly.", "Azure CosmosDB", exception.toString()) } } /** * 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. */ export async function GetAccountByID(id: string, db: Mongo.Db): Promise<Account | null> { 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 } // If a match is found but it has been soft deleted, return null else if(document.isDeleted && document.recoverBy <= Date.now()) { return null } // If a match is found and it hasn't been soft deleted, return an `Account` instance else { return new Account(document) } } catch(exception) { throw new ExternalError("Database search crashed unexpectedly.", "Azure CosmosDB", exception.toString()) } } /** * 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. */ export async function GetReferrer(inviteCode: string, db: Mongo.Db): Promise<Account | null> { 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 } // If a referrer is found, but it has been soft-deleted else if(result.isDeleted && result.recoverBy <= Date.now()) { return null } // Referrer found, not soft-deleted, instantiate and return a `Account` object else { return new Account(result) } } catch(exception) { throw new ExternalError("Database search crashed unexpectedly.", "Azure CosmosDB", exception.toString()) } } /** * 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. */ export async function Insert(account: Account, db: Mongo.Db): Promise<Account> { 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(response.ops.pop(), Source.Database) } catch(exception) { throw new ExternalError("Couldn't write object to database.", "Azure CosmosDB", exception.toString()) } } /** * 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. */ export async function IsDuplicate(account: Account, db: Mongo.Db): Promise<boolean> { 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 } // If a matching document is found but the account has been soft-deleted // return false else if (result.isDeleted && result.recoverBy <= Date.now()) { return false } // Otherwise return true else { return true } } catch (exception) { throw new ExternalError("Database search crashed unexpectedly.", "Azure CosmosDB", exception.toString()) } } /** * 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. */ export async function UpdateInPlace(id: string, query: any, db: Mongo.Db) { try { let filter = { _id: id } let updates: any let operations: any 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 ExternalError("Database update crashed unexpectedly.", "Azure CosmosDB", exception.toString()) } }