From 1006554f730a348e75875c67acc01246cb24d280 Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 23 Jan 2019 23:16:06 -0800 Subject: [PATCH] [Performance] Suggest Profiles, Find Users By Phone and Endpoint Names --- src/config/router.js | 13 +++----- src/ups/auth/dao.js | 46 ++++++++++++++++++--------- src/ups/auth/handlers.js | 21 +++++++++++++ src/ups/profiles/dao.js | 44 +++++++++++++++++++++++++- src/ups/profiles/handlers.js | 23 ++++++++++++-- src/ups/users/dao.js | 60 ------------------------------------ src/ups/users/handlers.js | 46 --------------------------- 7 files changed, 122 insertions(+), 131 deletions(-) delete mode 100644 src/ups/users/dao.js delete mode 100644 src/ups/users/handlers.js diff --git a/src/config/router.js b/src/config/router.js index acd5407..b99968a 100644 --- a/src/config/router.js +++ b/src/config/router.js @@ -1,5 +1,4 @@ const AuthHandlers = require('../ups/auth/handlers'); -const UserHandlers = require('../ups/users/handlers'); const ProfileHandlers = require('../ups/profiles/handlers'); /** @@ -16,18 +15,16 @@ function router(app) { app.post('/auth/phone/code', AuthHandlers.sendPhoneCode); app.post('/auth/phone/verify', AuthHandlers.verifyPhoneCode); app.post('/auth/signin', AuthHandlers.signin); - app.put('/auth/signup', AuthHandlers.signup); app.post('/auth/signout', AuthHandlers.signout); app.post('/auth/passwordReset', AuthHandlers.sendPasswordResetEmail); - - // Users Endpoints - app.post('/users/by/phone', UserHandlers.findUsersByPhone); - app.get('/users/suggest', UserHandlers.suggestUsers); + app.post('/auth/findUsers/byPhone', AuthHandlers.findUsersByPhoneNumber); + app.put('/auth/signup', AuthHandlers.signup); // Profile Endpoints - app.post('/profiles/:profileNumber', ProfileHandlers.updateUserProfile); - app.get('/profiles/:profileNumber', ProfileHandlers.getUserProfile); + app.post('/profiles/me/:profileNumber', ProfileHandlers.updateUserProfile); + app.get('/profiles/me/:profileNumber', ProfileHandlers.getUserProfile); app.put('/profiles/uploadImage/:profileNumber', ProfileHandlers.updateUserProfileImage); + app.get('/profiles/suggest', ProfileHandlers.suggestNProfiles); } module.exports = router; \ No newline at end of file diff --git a/src/ups/auth/dao.js b/src/ups/auth/dao.js index 127af32..e790551 100644 --- a/src/ups/auth/dao.js +++ b/src/ups/auth/dao.js @@ -61,6 +61,29 @@ function createNewUser(user) { }); } +/** + * Filters contacts into three batches: + * 1. Contacts that are in the tagfer network. + * 2. Contacts that are out of the network. + * 3. Contacts that failed to be processed. + * @param {Array} phoneNumbers + * @returns {Object} groups of inNetwork, outNetwork, failed + */ +async function getUsersByPhoneNumber(phoneNumbers) { + const promises = []; + const inNetwork = []; + const outNetwork = []; + const failed = []; + + for (let i = 0; i < phoneNumbers.length; i++) { + if (phoneNumbers[i]) promises.push(_getTagferIdByPhoneNumber(phoneNumbers[i],inNetwork, outNetwork, failed)); + } + + await Promise.all(promises); + + return { inNetwork, outNetwork, failed }; +} + /** * Sends a password reset email * @param {email} email @@ -182,6 +205,12 @@ function _doesKeyValueExist({email, tagferId, phoneNumber}) { }); } +function _getTagferIdByPhoneNumber(phoneNumber, inNetwork, outNetwork, failed) { + return admin.database().ref(`mapper/phoneNumbers/${phoneNumber}`).once('value') + .then( data => data.exists() ? inNetwork.push(data.val()) : outNetwork.push(phoneNumber)) + .catch( () => failed.push(phoneNumber)); +} + module.exports = { doesEmailExist, doesTagferIdExist, @@ -194,17 +223,6 @@ module.exports = { createNewSessionId, getSession, deleteSession, - resetPassword -}; - -// function signinWithTagferId1(tagferId, password, callback) { -// const path = `users/${tagferId}/hash`; - -// db.ref(path).once('value').then( data => { -// if(!data.exists()) { -// callback({error: errors.AUTH_USER_NOT_FOUND}); -// } -// const hash = data.val(); -// bcrypt.compare(password, hash).then(result => callback({result})); -// }); -// } \ No newline at end of file + resetPassword, + getUsersByPhoneNumber +}; \ No newline at end of file diff --git a/src/ups/auth/handlers.js b/src/ups/auth/handlers.js index d1c7bc6..e71b2c6 100644 --- a/src/ups/auth/handlers.js +++ b/src/ups/auth/handlers.js @@ -1,3 +1,5 @@ +const phone = require('phone'); + const authDao = require('./dao'); const profileDao = require('./../profiles/dao'); const utils = require('../utils/utils'); @@ -156,6 +158,24 @@ async function signup(req, res) { } } +/** + * Endpoint: auth/findUsers/byPhone + * Seperates the phone numbers into batches in/out of Tagfer network, receiver should use this information to either + * add people who already in network, or send invites or repeat calls for failed batches. + * @param {Object} req {phoneNumbers: Array} + * @param {Object} res {inNetwork : Array, outNetwork: Array, failed: Array } + */ +async function findUsersByPhoneNumber(req, res) { + const phoneNumbers = req.body.phoneNumbers; + const verifier = () => phoneNumbers && Array.isArray(phoneNumbers); + if (!utils.isAppSecretValid(req,res) || !utils.isBodyValid(verifier, res)) { + return; + } + + const batches = await authDao.getUsersByPhoneNumber(phoneNumbers.map((number) => phone(number)[0])); + res.json(batches); +} + /** * Endpoint: auth/passwordReset * Send a reset password link to the email @@ -200,6 +220,7 @@ function getTwitterUsername(req, res) { module.exports = { doesAttributeExist, doesSessionExist, + findUsersByPhoneNumber, sendPhoneCode, verifyPhoneCode, signin, diff --git a/src/ups/profiles/dao.js b/src/ups/profiles/dao.js index 77a78b5..f6d6fc4 100644 --- a/src/ups/profiles/dao.js +++ b/src/ups/profiles/dao.js @@ -54,9 +54,51 @@ async function updateProfileImage(profileImageData, profileNumber, tagferId) { } } +var lastRetrievedProfile = undefined; +/** + * Returns a list of user profiles,that work as 'suggested contacts'. It works in a cyclic manner, starting from the top + * node of the children of the profiles it fetches N profiles and saves the last retrieved profile. The next call will + * start from the last retrieved profile and fetch the next N profiles. If the number of profiles fetched is ever less + * than N, we make a recursive call to start at the top of the tree again and fetch the X remaining profiles. + * + * @param {Number} N number of profiles to retrieve + */ +function suggestNProfiles(N) { + let promise = null; + + if (lastRetrievedProfile) { + promise = database.ref('profiles').orderByKey().startAt(lastRetrievedProfile).limitToFirst(N).once('value'); + } else { + promise = database.ref('profiles').limitToFirst(N).once('value'); + } + + return promise.then(data => { + const list = new Array(data.numChildren()); + let index = 0; + + data.forEach(profile => { list[index++] = _toLiteProfile(profile.val().profile1, profile.key); }); + if (index > 0) { + lastRetrievedProfile = list[index-1].tagferId; + } + + if (data.hasChildren && N !== data.numChildren()) { + lastRetrievedProfile = undefined; + return suggestNProfiles(N - index).then(list2 => list2.concat(list)); + } + + return list; + }); +} + +function _toLiteProfile(profile, tagferId) { + const { fullName, jobTitle, companyName, photoURL } = profile; + return { tagferId, fullName, jobTitle, companyName, photoURL }; +} + module.exports = { updateProfile, createInitialProfiles, updateProfileImage, - getProfile + getProfile, + suggestNProfiles }; diff --git a/src/ups/profiles/handlers.js b/src/ups/profiles/handlers.js index 2e64c3f..0be1497 100644 --- a/src/ups/profiles/handlers.js +++ b/src/ups/profiles/handlers.js @@ -3,6 +3,7 @@ const authDao = require('../auth/dao'); const utils = require('../utils/utils'); const errors = require('../../config/errors'); const http = require('../../config/http'); +const appConfig = require('../../config/app.json'); // Handlers /** @@ -23,7 +24,7 @@ async function updateUserProfile(req, res) { const tagferId = authDao.getSession(sessionId).tagferId; profileDao.updateProfile(profileObj, profileNumber, tagferId).then(() => { res.status(http.CREATED).json({}); - }).catch((error) => { + }).catch(() => { res.status(http.INTERNAL_SERVER_ERROR).json({ error: errors.APP_FIREBASE_DATABASE_ERROR }); }); } catch (error) { @@ -73,8 +74,26 @@ async function getUserProfile(req, res) { } } +/** + * Endpoint: profiles/suggest + * @param {Object} req {} + * @param {Object} res { profiles: LiteProfileObject } + * + * LiteProfileObject = { tagferId, fullName, photoURL, jobTitle, companyName } + */ +function suggestNProfiles(req, res) { + if (!utils.isAppSecretValid(req,res)) { + return; + } + + profileDao.suggestNProfiles(appConfig.suggestedUsersCount) + .then(profiles => res.json({ profiles })) + .catch(() => res.json({ error: errors.APP_FIREBASE_DATABASE_ERROR })); +} + module.exports = { updateUserProfile, updateUserProfileImage, - getUserProfile + getUserProfile, + suggestNProfiles }; \ No newline at end of file diff --git a/src/ups/users/dao.js b/src/ups/users/dao.js deleted file mode 100644 index a0a3811..0000000 --- a/src/ups/users/dao.js +++ /dev/null @@ -1,60 +0,0 @@ -const admin = require('firebase-admin'); - -const errors = require('../../config/errors'); - -/** - * Filters contacts into three batches: - * 1. Contacts that are in the tagfer network. - * 2. Contacts that are out of the network. - * 3. Contacts that failed to be processed. - * @param {Array} contacts - * @returns {Object} groups inNetwork, outNetwork, failedBatch - */ -async function filterContactsIntoBatches(contacts) { - const promises = []; - const inNetworkBatch = []; - const outNetworkBatch = []; - const failedBatch = []; - - const addToBatch = (contact) => contact.inNetwork? inNetworkBatch.push(contact.tagferId) : contact.outNetwork? outNetworkBatch.push(contact.phoneNumber) : failedBatch.push(contact.phoneNumber); - - for (let i = 0; i < contacts.length; i++) { - if (contacts[i]) { - promises.push(_getContactStatus(contacts[i])); - } - } - - const results = await Promise.all(promises); - - for(let i = 0; i < results.length; i++) { - if (results[i]) - addToBatch(results[i]); - } - - return {inNetworkBatch, outNetworkBatch, failedBatch}; -} - -var pageToken = undefined; -/** - * Returns a list of N users. Used to suggest contacts. - * @param {Number} N is the number of contacts you want to get - */ -function getNextNUsers(N) { - return admin.auth().listUsers(N, pageToken).then( result => { - pageToken = !result.pageToken? undefined : result.pageToken; - return result.users.length === 0? getNextNUsers(N): result.users; - }).catch( error => { throw { error: error.code }; }); -} - -function _getContactStatus(phoneNumber) { - return admin.auth().getUserByPhoneNumber(phoneNumber).then( user => { - return {inNetwork: true, tagferId: user.uid}; - }).catch( error => { - return error.code === errors.AUTH_USER_NOT_FOUND ? { outNetwork: true, phoneNumber } : { error: error.code, phoneNumber}; - }); -} - -module.exports = { - filterContactsIntoBatches, - getNextNUsers -}; \ No newline at end of file diff --git a/src/ups/users/handlers.js b/src/ups/users/handlers.js deleted file mode 100644 index 1980a4c..0000000 --- a/src/ups/users/handlers.js +++ /dev/null @@ -1,46 +0,0 @@ -const phone = require('phone'); - -const dao = require('./dao'); -const utils = require('../utils/utils'); -const appConfig = require('../../config/app.json'); - -/** - * Endpoint: users/by/phone - * Seperates the phone numbers into batches in/out of Tagfer network, receiver should use this information to either - * add people who already in network, or send invites or repeat calls for failed batches. - * @param {Object} req {contacts: Array} - * @param {Object} res {inNetworkBatch : Array, outNetworkBatch: Array, failedBatch: Array } - */ -async function findUsersByPhone(req, res) { - const contacts = req.body.contacts; - const verifier = () => contacts && Array.isArray(contacts); - if (!utils.isAppSecretValid(req,res) || !utils.isBodyValid(verifier, res)) { - return; - } - - const batches = await dao.filterContactsIntoBatches(contacts.map((number) => phone(number)[0])); - res.json(batches); -} - -/** - * Endpoint: users/suggest - * @param {Object} req {} - * @param {Object} res { users: UserObject } - * - * UserObject = { tagferId: String, displayName: String, photoURL: String } - */ -function suggestUsers(req, res) { - if (!utils.isAppSecretValid(req,res)) { - return; - } - - dao.getNextNUsers(appConfig.suggestedUsersCount).then( listUsers => { - const users = listUsers.map( user => ({ tagferId: user.uid, displayName: user.displayName, photoURL: user.photoURL }) ); - res.json({ users }); - }).catch(error => res.json({ error }) ); -} - -module.exports = { - findUsersByPhone, - suggestUsers -};