[Performance] Suggest Profiles, Find Users By Phone and Endpoint Names

This commit is contained in:
Omar 2019-01-23 23:16:06 -08:00
parent 0b432b99f7
commit 1006554f73
7 changed files with 122 additions and 131 deletions

View File

@ -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;

View File

@ -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}));
// });
// }
resetPassword,
getUsersByPhoneNumber
};

View File

@ -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<PhoneNumber>}
* @param {Object} res {inNetwork : Array<PhoneNumber>, outNetwork: Array<PhoneNumber>, failed: Array<PhoneNumber> }
*/
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,

View File

@ -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
};

View File

@ -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
};

View File

@ -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
};

View File

@ -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<PhoneNumber>}
* @param {Object} res {inNetworkBatch : Array<PhoneNumber>, outNetworkBatch: Array<PhoneNumber>, failedBatch: Array<PhoneNumber> }
*/
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
};