@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
text/typescript
/**
* @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())
}
}