I have the following function, which accepts an argument (an array of "names") and then checks data from my firebase database for each user against that array.
It uses that to compile an array of "settings" for each user with their email, and the names that the user shared with the list fed in as an argument. The function looks like this.
fbDaemon = ({ folderNames }) => {
const settings = [];
ref.once("value")
.then((snapshot) => {
snapshot.forEach(user => {
auth.getUser(user.key)
.then(function(userRecord) {
let email = userRecord.toJSON().email;
let zips = [];
user.forEach(setting => {
let dep = setting.val().department;
if(folderNames.includes(dep)){
zips.push(dep);
}
});
settings.push({ email, zips });
})
.catch(function(error) {
console.log("Error fetching user data:", error);
});
});
});
});
Essentially, it's going through my entire database and compiling a list of the settings that I will pass onto the next function. The end result should look something like this:
[ { email: 'example#example1.com',
zips: [ 'Drug Enforcement Administration', 'Executive Branch' ] },
{ email: 'example#example2.com',
zips: [ 'FEMA', 'Congress' ] },
];
The problem that I'm having right now is I'm not able to return the "settings" array at the appropriate time.
How can I reconfigure this function so that the settings array is only returned when the entire function has run?
In other words, I'd like to return a resolved promise with the settings array. How can I do this?
Perhaps you could use Promise.all() here, to resolve an array of promises (where each item in the array corresponds to a call to getUser for that item/user) ?
So, something along these lines:
fbDaemon = ({ folderNames, folderPaths }) => {
const settings = [];
return ref.once("value")
.then((snapshot) => {
// Collect all users from snapshot into an array
const users = []
snapshot.forEach(user => { users.push(user) })
// Create promise for each user, and use Promise.all to
// resolve when each "user promise" is complete
return Promise.all(users.map(user => {
// Be sure to add "return" here
return auth.getUser(user.key)
.then(function(userRecord) {
let email = userRecord.toJSON().email;
let zips = [];
user.forEach(setting => {
let dep = setting.val().department;
if(folderNames.includes(dep)){
zips.push(dep);
}
});
settings.push({ email, zips });
})
.catch(function(error) {
console.log("Error fetching user data:", error);
})
}));
}).then(function() {
return settings;
});
};
Related
I'm building something like a Messenger using React + Redux and Real-Time Database from Firebase.
I'm used to retrieve data from an API like this:
export const fetchContacts = () => {
return async dispatch => {
dispatch(fetchContactsStart());
try {
const response = await axios.get('....');
dispatch(fetchContactsSuccess(...));
} catch(e) {
dispatch(fetchContactsFail(...));
}
}
}
Now in firebase I have the following data:
enter image description here
What I need is to get all user contacts and then get user info associated with those contacts and build a structure like:
[{email: ..., username: ...}, {email: ..., username: ...}, ...]. After getting all the data in that format I want to dispatch the action to my reducer.
Right now I have the following code:
export const fetchContacts = () => {
return dispatch => {
dispatch(fetchContactsStart());
const userEmail = firebase.auth().currentUser.email;
let data = [];
firebase.database().ref(`users_contacts/${Base64.encode(userEmail)}`)
.on('value', contacts => contacts.forEach(contact => {
firebase.database().ref(`users/${Base64.encode(contact.val().contact)}`)
.on('value', user => {
data.push({ email: contact.val().contact, username: user.val().username });
console.log(data)
})
}
))
}
}
This works but I don't know how to dispatch the action only when the data is fully formed, is there any solution for this? Thanks for the help!
When you're waiting for multiple asynchronous operations to complete, the solution is usually to use Promise.all. In this case, that'd look something like this:
export const fetchContacts = () => {
return dispatch => {
dispatch(fetchContactsStart());
const userEmail = firebase.auth().currentUser.email;
let data = [];
firebase.database().ref(`users_contacts/${Base64.encode(userEmail)}`).once('value', contacts => {
let promises = [];
let map = {};
contacts.forEach(contact => {
const contactId = Base64.encode(contact.val().contact);
map[contactId] = contact.val().contact;
promises.push(firebase.database().ref(`users/${contactId}`).once('value'));
});
Promise.all(promises).then((users) => {
users.forEach((user) =>
data.push({ email: map[user.key], username: user.val().username });
})
// TODO: dispatch the result here
}
))
}
}
The main changes in here:
Now uses once() for loading the user data, so that is only loads once and returns a promise.
Uses Promise.all to wait until all profiles are loaded.
Added a map to look up the email address in the inner callback.
I have an array of objects and I have to add one property on each of the objects coming from and async function
I am doing an Array.reduce to iterate on each of the elements and return just one result: One array of objects with the new property.
I have this
const res = await resultOne.reduce(async (users = [], user, i) => {
let userName;
try {
let { name } = await names.getNames(user.id);
userName = name;
} catch (error) {
throw error;
}
delete user.id;
users.push({ ...user, userName });
return users;
}, []);
But I get the message
Push is not a function of users
And this is because I think is a promise.
How can I handle async requests in a reduce or a map
Yes, users is a promise. Don't use reduce with async functions. You could do something like await users in the first line, but that would be pretty inefficient and unidiomatic.
Use a plain loop:
const users = [];
for (const user of resultOne) {
const { name } = await names.getNames(user.id);
delete user.id;
users.push({ ...user, userName: user });
}
or, in your case where you can do everything concurrently and create an array anyway, the map function together with Promise.all:
const users = await Promise.all(resultOne.map(async user => {
const { name } = await names.getNames(user.id);
delete user.id;
return { ...user, userName: user };
}));
Because it's an async function, every time you return something it gets wrapped in a promise. To fix this you need to set the starting array as a promise and then await for the accumulator on each iteration.
const res = await resultOne.reduce(async (users, user, i) => {
try {
return [
...await users,
{ ...user, userName: await names.getNames(user.id.name) }
]
} catch (error) {
console.log(error)
}
}, Promise.resolve([]));
I am still working on a better understanding of promises. I started with some samples from Doug Stevenson's YouTube videos on promises and then modified it to use my collections. This code most closely resembles the sample using areas and cities.
Here's the code:
exports.getMatchesNew = functions.https.onCall((data, context) => {
console.log("In On Call", data);
return db.collection('matches').get()
.then(areaSnapshot => {
const promises = [];
areaSnapshot.forEach(doc => {
var area = doc.data();
// console.log("Area is ", area.mentees);
// console.log("area ID is ", area.id);
// Loop through each mentee for a mentor
for (const city in area.mentees)
{
// console.log("City is ", area.mentees[city]);
// User Information for current mentee
const p = db.collection('users').doc(area.mentees[city]).get();
//User Information for current mentor
const p2 = db.collection('users').doc(doc.id).get();
//console.log("Doc ",p);
// would like to combine this together, which will end up on one row
// mentor name, mentee name
promises.push(p, p2);
}
})
return Promise.all(promises);
//response.send(data);
})
.then(citySnapshots => {
const results = [];
citySnapshots.forEach(citySnap => {
const data = citySnap.data();
// this log entry is occuring once for each p and p2 from above.
// I tried an array reference like citySnap[0] for mentee info and citySnap[1] for mentor info, this did not work.
console.log("cSnap is: ", data);
results.push(data);
})
return Promise.all(results);
})
.catch(error => {
// Handle the error
console.log(error);
//response.status(500).send(error);
});
});
The output is that I get the mentee first name and last name, then I get the output for the mentor first name and last name (on a separate row).
In the Firestore, each doc in the matches collection is simply the mentor's id and an array of mentee IDs. All user info is stored in a 'users' collection.
So, I'm trying to loop through each matches doc, and produce one row of data for each mentor/mentee combination.
I still need to add some handling when p and/or p2 are not available.
My original intention for 'p' and 'p2' was to:
return first name and last name for p, rename them menteeFirstName and menteeLastName
return first name and last name for p2, rename them mentorFirstname and mentorLastName
combine this info and return an array of mentorFirstName, mentorLastName, menteeFirstName, menteeLastName.
However, I went down a rabbit hole with that. I decided to pare it back to working code, and post this.
So, can I combine the data from 'p' and 'p2'? Or am I doing this the wrong way?
I come from a relational db background, so the Firestore collections/documents concept with async calls is a new concept for me that I am becoming more familiar with (but not enough, yet).
I have tried to understand the various examples out there, but I think my unfamiliarity with promises is a major hurdle right now.
I have tried both Roamer, and Steven Sark's suggestions.
Roamer's suggestion does not error out, but I believe it is dropping promises.
exports.getMatchesNew = functions.https.onCall((data, context) => {
return db.collection('matches').get()
.then(areaSnapshot => {
const promises = [];
areaSnapshot.forEach(doc => {
var area = doc.data();
for (let city in area.mentees) {
const p = db.collection('users').doc(area.mentees[city]).get();
const p2 = db.collection('users').doc(doc.id).get();
promises.push(
Promise.all([p, p2]) // aggregate p and p2
.then(([mentee, mentor]) => {
var mentorInfo = mentor.data();
var menteeInfo = mentee.data();
console.log("Mentor: ", mentorInfo.lastName);
console.log("mentee: ", menteeInfo.lastName);
// return a plain object with all the required data for this iteration's doc
return {
// 'area': area, // for good measure
// 'city': city, // for good measure
'mentee': menteeInfo, // ???
'mentor': mentorInfo // ???
};
})
);
}
})
return Promise.all(promises);
})
.catch(error => {
console.log(error);
//response.status(500).send(error);
});
});
I see the data in the logs, but there are no records returned (or I am referencing them incorrectly in the .vue page
<template slot="mentorLastName" slot-scope="row">
{{row.item.mentor.lastName}}
</template>
<template slot="menteelastName" slot-scope="row">
{{row.item.mentee.lastName}}
</template>
This works in other instances where the results contain these same objects.
Steven Sark's code also runs, based on the log files, with no errors. The difference is that the vue page never returns (always showing as 'thinking'). In the past, this meant there was an error in the cloud function. There is no error in the cloud function logs, and the data is NOT showing in the console function logs (whereas it is in the Roamer version). So I cannot prove that is working.
exports.getMatchesNew = functions.https.onCall((data, context) => {
console.log("In On Call", data);
return db.collection('matches').get()
.then(areaSnapshot => {
const promises = [];
areaSnapshot.forEach(doc => {
var area = doc.data();
// console.log("Area is ", area.mentees);
// console.log("area ID is ", area.id);
// Loop through each mentee for a mentor
for (const city in area.mentees)
{
// console.log("City is ", area.mentees[city]);
// User Information for current mentee
const p = db.collection('users').doc(area.mentees[city]).get();
//User Information for current mentor
const p2 = db.collection('users').doc(doc.id).get();
//console.log("Doc ",p);
// would like to combine this together, which will end up on one row
// mentor name, mentee name
promises.push(p, p2);
}
})
return Promise.all(promises);
//response.send(data);
})
.then(citySnapshots => {
let mentee = citySnapshots[0];
let mentor = citySnapshots[1];
console.log("Mentor: ", mentor.lastName);
return {
mentee,
mentor
};
})
.catch(error => {
// Handle the error
console.log(error);
//response.status(500).send(error);
});
});
I look at both of these modified examples, and feel like I'm understanding it, and then I smack myself when I don't get the results. I feel like both of these are dropping promises based on the log entries, but I don't understand how. It looks to me like these are chained or connected. That none are by themselves.
Your approach could be made to work but is rather messy due to the formation of one promises array containing both ps and p2s.
As far as I can tell, you want to bundle the required data for each doc into an object.
There's a number of ways in which this could be done. Here's one using an inner Promise.all() to aggregate each [p,p2] pair of promises, then form the required object.
exports.getMatchesNew = functions.https.onCall((data, context) => {
return db.collection('matches').get()
.then(areaSnapshot => {
const promises = [];
areaSnapshot.forEach(doc => {
var area = doc.data();
for (let city in area.mentees) {
const p = db.collection('users').doc(area.mentees[city]).get();
const p2 = db.collection('users').doc(doc.id).get();
promises.push(
Promise.all([p, p2]) // aggregate p and p2
.then(([mentee, mentor]) => {
// return a plain object with all the required data for this iteration's doc
return {
'area': area, // for good measure
'city': city, // for good measure
'mentee': mentee.data(), // ???
'mentor': mentor.data() // ???
};
});
);
}
})
return Promise.all(promises);
})
.catch(error => {
console.log(error);
//response.status(500).send(error);
});
});
All on the assumption that .get() is asynchronous and .data() is synchronous
The returned promise's success path will deliver an array of {area,mentee,mentor} objects.
May not be 100% correct because I don't fully understand the data. Shouldn't be too hard to adapt.
Moved to an answer because it's too large for a comment:
I've never come across any examples that use Promise.all in that way ( using an array of data ), but maybe that's an example from an earlier Promise library like Bliebird, which is not applicable anymore.
here's a quick example for you:
Promis.all([ Promise.resolve('one'), Promise.reject(new Error('example failure') ])
.then( ( values ) => {
//values[0] = data from promise 1; 'one'
//values[1] = data from promise 2
})
.catch( ( error ) => {
// note that an error from any promise provided will resolve this catch
})
EDIT:
As requested, here is your code modified to use Promise.all:
exports.getMatchesNew = functions.https.onCall((data, context) => {
console.log("In On Call", data);
return db.collection('matches').get()
.then(areaSnapshot => {
const promises = [];
areaSnapshot.forEach(doc => {
var area = doc.data();
// console.log("Area is ", area.mentees);
// console.log("area ID is ", area.id);
// Loop through each mentee for a mentor
for (const city in area.mentees)
{
// console.log("City is ", area.mentees[city]);
// User Information for current mentee
const p = db.collection('users').doc(area.mentees[city]).get();
//User Information for current mentor
const p2 = db.collection('users').doc(doc.id).get();
//console.log("Doc ",p);
// would like to combine this together, which will end up on one row
// mentor name, mentee name
promises.push(p, p2);
}
})
return Promise.all(promises);
//response.send(data);
})
.then(citySnapshots => {
let mentee = citySnapshots[0];
let mentor = citySnapshots[1];
return {
mentee,
mentor
};
})
.catch(error => {
// Handle the error
console.log(error);
//response.status(500).send(error);
});
});
another example without your db logic that I'm not quite following:
var response1 = { "foo": "bar" }
var response2 = { "biz": "baz" }
return Promise.resolve( [ response1, response2 ] )
.then(areaSnapshot => {
const promises = [];
areaSnapshot.forEach(doc => {
promises.push(Promise.resolve( doc ));
})
return Promise.all(promises);
//response.send(data);
})
.then(citySnapshots => {
let mentee = citySnapshots[0];
let mentor = citySnapshots[1];
return {
mentee,
mentor
};
})
.catch(error => {
// Handle the error
console.log(error);
//response.status(500).send(error);
});
This is the final working code. Thanks to #Roamer-1888 and "#Steven Stark"
The final code is a bit is a cross between both of their offered solutions. Much appreciated help.
I am a little more comfortable with promises as a result. Hopefully I get to work with them a bit more to get more comfortable, and improve my chances of retaining it.
exports.getMatchesNew = functions.https.onCall((data, context) => {
return db.collection('matches').get()
.then(areaSnapshot => {
const promises = [];
areaSnapshot.forEach(doc => {
var area = doc.data();
for (let city in area.mentees) {
const p = db.collection('users').doc(area.mentees[city]).get();
const p2 = db.collection('users').doc(doc.id).get();
if ( typeof p !== 'undefined' && p && typeof p2 !== 'undefined' && p2)
{
promises.push(
Promise.all([p, p2]) // aggregate p and p2
);
}
}
})
return Promise.all(promises);
})
.then(citySnapshots => {
const results = [];
citySnapshots.forEach(citySnap => {
var mentor = citySnap[1].data();
var mentee = citySnap[0].data();
if ( typeof mentee !== 'undefined' && mentee && typeof mentor !== 'undefined' && mentor)
{
var matchInfo = {
mentor: {},
mentee: {}
}
matchInfo.mentor = mentor;
matchInfo.mentee = mentee;
results.push(matchInfo);
}
else
{
console.log("Error missing mentor or mentee record, Need to research: ");
}
})
return Promise.all(results);
})
.catch(error => {
console.log(error);
//response.status(500).send(error);
});
});
I'm having a hard time getting a promise chain to flow correctly in a firebase cloud function. It loops through a ref and returns an array of emails for sending out notifications. It has some nested children, and I think that's where I'm going wrong, but I can't seem to find the error.
/courses structure
{
123456id: {
..
members: {
uidKey: {
email: some#email.com
},
uidKey2: {
email: another#email.com
}
}
},
some12345string: {
// no members here, so skip
}
}
function.js
exports.reminderEmail = functions.https.onRequest((req, res) => {
const currentTime = new Date().getTime();
const future = currentTime + 172800000;
const emails = [];
// get the main ref and grab a snapshot
ref.child('courses/').once('value').then(snap => {
snap.forEach(child => {
var el = child.val();
// check for the 'members' child, skip if absent
if(el.hasOwnProperty('members')) {
// open at the ref and get a snapshot
var membersRef = admin.database().ref('courses/' + child.key + '/members/').once('value').then(childSnap => {
console.log(childSnap.val())
childSnap.forEach(member => {
var email = member.val().email;
console.log(email); // logs fine, but because it's async, after the .then() below for some reason
emails.push(email);
})
})
}
})
return emails // the array
})
.then(emails => {
console.log('Sending to: ' + emails.join());
const mailOpts = {
from: 'me#email.com',
bcc: emails.join(),
subject: 'Upcoming Registrations',
text: 'Something about registering here.'
}
return mailTransport.sendMail(mailOpts).then(() => {
res.send('Email sent')
}).catch(error => {
res.send(error)
})
})
})
The following should do the trick.
As explained by Doug Stevenson in his answer, the Promise.all() method returns a single promise that resolves when all of the promises, returned by the once() method and pushed to the promises array, have resolved.
exports.reminderEmail = functions.https.onRequest((req, res) => {
const currentTime = new Date().getTime();
const future = currentTime + 172800000;
const emails = [];
// get the main ref and grab a snapshot
return ref.child('courses/').once('value') //Note the return here
.then(snap => {
const promises = [];
snap.forEach(child => {
var el = child.val();
// check for the 'members' child, skip if absent
if(el.hasOwnProperty('members')) {
promises.push(admin.database().ref('courses/' + child.key + '/members/').once('value'));
}
});
return Promise.all(promises);
})
.then(results => {
const emails = [];
results.forEach(dataSnapshot => {
var email = dataSnapshot.val().email;
emails.push(email);
});
console.log('Sending to: ' + emails.join());
const mailOpts = {
from: 'me#email.com',
bcc: emails.join(),
subject: 'Upcoming Registrations',
text: 'Something about registering here.'
}
return mailTransport.sendMail(mailOpts);
})
.then(() => {
res.send('Email sent')
})
.catch(error => { //Move the catch at the end of the promise chain
res.status(500).send(error)
});
})
Instead of dealing with all the promises from the inner query inline and pushing the emails into an array, you should be pushing the promises into an array and using Promise.all() to wait until they're all done. Then, iterate the array of snapshots to build the array of emails.
You may find my tutorials on working with promises in Cloud Functions to be helpful in learning some of the techniques: https://firebase.google.com/docs/functions/video-series/
I need to retrieve a list of collections using express and mongodb module.
First, ive retrieved a list of collection names which works, I then retrieve the data of those given collections in a loop. My problem is in getColAsync():
getColAsync() {
return new Promise((resolve, reject) => {
this.connectDB().then((db) => {
var allCols = [];
let dbase = db.db(this.databaseName);
dbase.listCollections().toArray((err, collectionNames) => {
if(err) {
console.log(err);
reject(err);
}
else {
for(let i = 0; i < collectionNames.length; i++) {
dbase.collection(collectionNames[i].name.toString()).find({}).toArray((err, collectionData) => {
console.log("current collection data: " + collectionData);
allCols[i] = collectionData;
})
}
console.log("done getting all data");
resolve(allCols);
}
})
})
})
}
connectDB() {
if(this.dbConnection) {
// if connection exists
return this.dbConnection;
}
else {
this.dbConnection = new Promise((resolve, reject) => {
mongoClient.connect(this.URL, (err, db) => {
if(err) {
console.log("DB Access: Error on mongoClient.connect.");
console.log(err);
reject(err);
}
else {
console.log("DB Access: resolved.");
resolve(db);
}
});
});
console.log("DB Access: db exists. Connected.");
return this.dbConnection;
}
}
In the forloop where i retrieve every collection, the console.log("done getting all data") gets called and the promise gets resolved before the forloop even begins. for example:
done getting all data
current collection data: something
current collection data: something2
current collection data: something3
Please help
The Problem
The problem in your code is this part:
for (let i = 0; i < collectionNames.length; i++) {
dbase.collection(collectionNames[i].name.toString()).find({}).toArray((err, collectionData) => {
console.log("current collection data: " + collectionData);
allCols[i] = collectionData;
})
}
console.log("done getting all data");
resolve(allCols);
You should notice that resolve(allCols); is called right after the for loop ends, but each iteration of the loop doesn't wait for the toArray callback to be called.
The line dbase.collection(collectionNames[i].name.toString()).find({}).toArray(callback) is asynchronous so the loop will end, you'll call resolve(allCols);, but the .find({}).toArray code won't have completed yet.
The Solution Concept
So, basically what you did was:
Initialize an array of results allCols = []
Start a series of async operations
Return the (still empty) array of results
As the async operations complete, fill the now useless results array.
What you should be doing instead is:
Start a series of async operations
Wait for all of them to complete
Get the results from each one
Return the list of results
The key to this is the Promise.all([/* array of promises */]) function which accepts an array of promises and returns a Promise itself passing downstream an array containing all the results, so what we need to obtain is something like this:
const dataPromises = []
for (let i = 0; i < collectionNames.length; i++) {
dataPromises[i] = /* data fetch promise */;
}
return Promise.all(dataPromises);
As you can see, the last line is return Promise.all(dataPromises); instead of resolve(allCols) as in your code, so we can no longer execute this code inside of a new Promise(func) constructor.
Instead, we should chain Promises with .then() like this:
getColAsync() {
return this.connectDB().then((db) => {
let dbase = db.db(this.databaseName);
const dataPromises = []
dbase.listCollections().toArray((err, collectionNames) => {
if (err) {
console.log(err);
return Promise.reject(err);
} else {
for (let i = 0; i < collectionNames.length; i++) {
dataPromises[i] = new Promise((res, rej) => {
dbase.collection(collectionNames[i].name.toString()).find({}).toArray((err, collectionData) => {
console.log("current collection data: " + collectionData);
if (err) {
console.log(err);
reject(err);
} else {
resolve(collectionData);
}
});
});
}
console.log("done getting all data");
return Promise.all(dataPromises);
}
});
})
}
Notice now we return a return this.connectDB().then(...), which in turn returns a Promise.all(dataPromises); this returning new Promises at each step lets us keep alive the Promise chain, thus getColAsync() will itself return a Promise you can then handle with .then() and .catch().
Cleaner Code
You can clean up your code a bit as fallows:
getColAsync() {
return this.connectDB().then((db) => {
let dbase = db.db(this.databaseName);
const dataPromises = []
// dbase.listCollections().toArray() returns a promise itself
return dbase.listCollections().toArray()
.then((collectionsInfo) => {
// collectionsInfo.map converts an array of collection info into an array of selected
// collections
return collectionsInfo.map((info) => {
return dbase.collection(info.name);
});
})
}).then((collections) => {
// collections.map converts an array of selected collections into an array of Promises
// to get each collection data.
return Promise.all(collections.map((collection) => {
return collection.find({}).toArray();
}))
})
}
As you see the main changes are:
Using mondodb functions in their promise form
Using Array.map to easily convert an array of data into a new array
Below I also present a variant of your code using functions with callbacks and a module I'm working on.
Promise-Mix
I'm recently working on this npm module to help get a cleaner and more readable composition of Promises.
In your case I'd use the fCombine function to handle the first steps where you select the db and fetch the list of collection info:
Promise.fCombine({
dbase: (dbURL, done) => mongoClient.connect(dbURL, done),
collInfos: ({ dbase }, done) => getCollectionsInfo(dbase, done),
}, { dbURL: this.URL })
This results in a promise, passing downstream an object {dbase: /* the db instance */, collInfos: [/* the list of collections info */]}. Where getCollectionNames(dbase, done) is a function with callback pattern like this:
getCollectionsInfo = (db, done) => {
let dbase = db.db(this.databaseName);
dbase.listCollections().toArray(done);
}
Now you can chain the previous Promise and convert the list of collections info into selected db collections, like this:
Promise.fCombine({
dbase: ({ dbURL }, done) => mongoClient.connect(dbURL, done),
collInfos: ({ dbase }, done) => getCollectionsInfo(dbase, done),
}, { dbURL: this.URL }).then(({ dbase, collInfos }) => {
return Promise.resolve(collInfos.map((info) => {
return dbase.collection(info.name);
}));
})
Now downstream we have a the list of selected collections from our db and we should fetch data from each one, then merge the results in an array with collection data.
In my module I have a _mux option which creates a PromiseMux which mimics the behaviour and composition patterns of a regular Promise, but it's actually working on several Promises at the same time. Each Promise gets in input one item from the downstream collections array, so you can write the code to fetch data from a generic collection and it will be executed for each collection in the array:
Promise.fCombine({
dbase: ({ dbURL }, done) => mongoClient.connect(dbURL, done),
collInfos: ({ dbase }, done) => getCollectionsInfo(dbase, done),
}, { dbURL: this.URL }).then(({ dbase, collInfos }) => {
return Promise.resolve(collInfos.map((info) => {
return dbase.collection(info.name);
}));
})._mux((mux) => {
return mux._fReduce([
(collection, done) => collection.find({}).toArray(done)
]).deMux((allCollectionsData) => {
return Promise.resolve(allCollectionsData);
})
});
In the code above, _fReduce behaves like _fCombine, but it accepts an array of functions with callbacks instead of an object and it passes downstream only the result of the last function (not a structured object with all the results). Finally deMux executes a Promise.all on each simultaneous Promise of the mux, putting together their results.
Thus the whole code would look like this:
getCollectionsInfo = (db, done) => {
let dbase = db.db(this.databaseName);
dbase.listCollections().toArray(done);
}
getCollAsync = () => {
return Promise.fCombine({
/**
* fCombine uses an object whose fields are functions with callback pattern to
* build consecutive Promises. Each subsequent functions gets as input the results
* from previous functions.
* The second parameter of the fCombine is the initial value, which in our case is
* the db url.
*/
dbase: ({ dbURL }, done) => mongoClient.connect(dbURL, done), // connect to DB, return the connected dbase
collInfos: ({ dbase }, done) => getCollectionsInfo(dbase, done), // fetch collection info from dbase, return the info objects
}, { dbURL: this.URL }).then(({ dbase, collInfos }) => {
return Promise.resolve(collInfos.map((info) => {
/**
* we use Array.map to convert collection info into
* a list of selected db collections
*/
return dbase.collection(info.name);
}));
})._mux((mux) => {
/**
* _mux splits the list of collections returned before into a series of "simultaneous promises"
* which you can manipulate as if they were a single Promise.
*/
return mux._fReduce([ // this fReduce here gets as input a single collection from the retrieved list
(collection, done) => collection.find({}).toArray(done)
]).deMux((allCollectionsData) => {
// finally we can put back together all the results.
return Promise.resolve(allCollectionsData);
})
});
}
In my module I tried to avoid most common anti-pattern thought there still is some Ghost Promise which I'll be working on.
Using the promises from mongodb this would get even cleaner:
getCollAsync = () => {
return Promise.combine({
dbase: ({ dbURL }) => { return mongoClient.connect(dbURL); },
collInfos: ({ dbase }) => {
return dbase.db(this.databaseName)
.listCollections().toArray();
},
}, { dbURL: this.URL }).then(({ dbase, collInfos }) => {
return Promise.resolve(collInfos.map((info) => {
return dbase.collection(info.name);
}));
}).then((collections) => {
return Promise.all(collections.map((collection) => {
return collection.find({}).toArray();
}))
});
}