The problem
At web app startup, the main task is to retrieve all user informations that are stored in various Firestore's collections and sub-collections.
The problem is that, even if I uses promises and a Loading State to prevent an empty app rendering, the app is rendered with all collections data, except for collections that have sub-collections.
The process
- If user is logged
-- Set Loading Status Active
-- Load Collection A
-- Load Collection B and forEach, load all sub-collection
-- Load Collection C
-- Set Loading Status Inactive
At this point, the app is rendered, but only with Collection A and Collection C. The collection B is loaded (I can see it by Redux Logs) but can't be seen in the app.
Those datas appears only if I change the component status (open/close a menu, for example).
Some code
Here's how I retrieve a collection with sub-collections:
export function setCompanyJobs(user) {
return {
type: "SET_COMPANY_JOBS",
payload: loadCompanyJobs(user),
};
}
Function that retrieve the main collection
export function loadCompanyJobs(user) {
return new Promise((resolve, reject) => {
let companyJobs = [];
db.collection("company").doc(user.selectedCompany).collection("jobs").get().then((jobs) => {
jobs.forEach((job) => {
loadJobLinkedServices(user, job).then((jobLinkedServices) => {
companyJobs.push({
id: job.id,
...
});
});
});
resolve(companyJobs);
}).catch(function (error) {
...
});
});
}
Function that retrieve all collection's sub-collections
export function loadJobLinkedServices(user, job){
return new Promise((resolve, reject) => {
let jobLinkedServices = [];
db.collection("company").doc(user.selectedCompany).collection("jobs").doc(job.id).collection("linkedServices").get().then((linkedServices) => {
linkedServices.forEach((linkedService) => {
jobLinkedServices.push({
id: linkedService.id,
...
});
});
resolve(jobLinkedServices)
}).catch(function (error) {
...
});
})
When you do
return new Promise((resolve, reject) => {
let companyJobs = [];
db.collection("company").doc(user.selectedCompany).collection("jobs").get().then((jobs) => {
jobs.forEach((job) => {
loadJobLinkedServices(user, job).then((jobLinkedServices) => {
companyJobs.push({
id: job.id,
...
});
});
});
resolve(companyJobs);
}).catch(function (error) {
...
});
});
nothing ensure that your Promise resolves only after ALL the queries triggered in the jobs.forEach() loop are done (i.e. the promises returned by the calls to the loadJobLinkedServices function have resolved).
I don't know reactjs but I think you can use the JavaScript Promise.all() method along the following lines:
return new Promise((resolve, reject) => {
let promises = [];
let companyJobs = [];
db.collection("company").doc(user.selectedCompany).collection("jobs").get().then((jobs) => {
jobs.forEach((job) => {
promises.push(loadJobLinkedServices(user, job));
});
Promise.all(promises).
then(results => {
//Loop over the results array to populate the companyJobs array
resolve(companyJobs);
})
}).catch(function (error) {
...
});
});
Also, don't forget to correctly chain your calls to the different asynchronous functions, i.e. something like:
query Collection A
THEN query Collection B
THEN query all sub-collections (with Promise.all())
THEN query Collection C
THEN set Loading Status Inactive
Finally, a last remark: Note that the get() method returns a promise, so I am not sure that you need to wrap the calls to the get() method into some new Promises (Again, I am not versed in reactjs, so this remark may be wrong).
In other words, I think you could do something like the following (for example for the loadJobLinkedServices function):
export function loadJobLinkedServices(user, job){
let jobLinkedServices = [];
return db.collection("company").doc(user.selectedCompany).collection("jobs").doc(job.id).collection("linkedServices").get()
.then((linkedServices) => {
linkedServices.forEach((linkedService) => {
jobLinkedServices.push({
id: linkedService.id,
...
});
});
return jobLinkedServices;
}).catch(function (error) {
...
});
})
Related
Say I have this minimal database stored in Cloud Firestore. How could I retrieve the names of subCollection1 and subCollection2?
rootCollection {
aDocument: {
someField: { value: 1 },
anotherField: { value: 2 }
subCollection1: ...,
subCollection2: ...,
}
}
I would expect to be able to just read the ids off of aDocument, but only the fields show up when I get() the document.
rootRef.doc('aDocument').get()
.then(doc =>
// only logs [ "someField", "anotherField" ], no collections
console.log( Object.keys(doc.data()) )
)
It is not currently supported to get a list of (sub)collections from Firestore in the client SDKs (Web, iOS, Android).
In server-side SDKs this functionality does exist. For example, in Node.js you'll be after the ListCollectionIds method:
var firestore = require('firestore.v1beta1');
var client = firestore.v1beta1({
// optional auth parameters.
});
// Iterate over all elements.
var formattedParent = client.anyPathPath("[PROJECT]", "[DATABASE]", "[DOCUMENT]", "[ANY_PATH]");
client.listCollectionIds({parent: formattedParent}).then(function(responses) {
var resources = responses[0];
for (var i = 0; i < resources.length; ++i) {
// doThingsWith(resources[i])
}
})
.catch(function(err) {
console.error(err);
});
It seems like they have added a method called getCollections() to Node.js:
firestore.doc(`/myCollection/myDocument`).getCollections().then(collections => {
for (let collection of collections) {
console.log(`Found collection with id: ${collection.id}`);
}
});
This example prints out all subcollections of the document at /myCollection/myDocument
Isn't this detailed in the documentation?
/**
* Delete a collection, in batches of batchSize. Note that this does
* not recursively delete subcollections of documents in the collection
*/
function deleteCollection(db, collectionRef, batchSize) {
var query = collectionRef.orderBy('__name__').limit(batchSize);
return new Promise(function(resolve, reject) {
deleteQueryBatch(db, query, batchSize, resolve, reject);
});
}
function deleteQueryBatch(db, query, batchSize, resolve, reject) {
query.get()
.then((snapshot) => {
// When there are no documents left, we are done
if (snapshot.size == 0) {
return 0;
}
// Delete documents in a batch
var batch = db.batch();
snapshot.docs.forEach(function(doc) {
batch.delete(doc.ref);
});
return batch.commit().then(function() {
return snapshot.size;
});
}).then(function(numDeleted) {
if (numDeleted <= batchSize) {
resolve();
return;
}
// Recurse on the next process tick, to avoid
// exploding the stack.
process.nextTick(function() {
deleteQueryBatch(db, query, batchSize, resolve, reject);
});
})
.catch(reject);
}
This answer is in the docs
Sadly the docs aren't clear what you import.
Based on the docs, my code ended up looking like this:
import admin, { firestore } from 'firebase-admin'
let collections: string[] = null
const adminRef: firestore.DocumentReference<any> = admin.firestore().doc(path)
const collectionRefs: firestore.CollectionReference[] = await adminRef.listCollections()
collections = collectionRefs.map((collectionRef: firestore.CollectionReference) => collectionRef.id)
This is of course Node.js server side code. As per the docs, this cannot be done on the client.
I have an array in which project _id is stored in the user collection. I'm initially fetching that array and then I'm trying to retrieve the data corresponding to the _id (stored in user collection) stored if projects collection. The data retrieved is pushed on to the array and that array is returned as an output.
But I'm unable to do so. An empty array is being returned.
var projectInfo= new Array();
users.post('/retrieveProjects', function(user, res, next) {
MongoClient.connect(url, function(err,db) {
if (err) throw err;
var dbo = db.db("EMWorks");
dbo.collection("users").findOne({_id:user.body.userid})
.then(response => {
if(response)
{
console.log(response);
(response.project).forEach(element => {
dbo.collection("projects").findOne({_id:element})
.then(respo => {
console.log(respo);
projectInfo.push(respo);
});
console.log(projectInfo) ;
});
}else{
console.log("No Projects created yet...!");
}
});
});
});
Output of the code
The problem is that dbo.collection("projects").findOne is async and returns a promise.
The forEach loop runs and generates these promises for each elemnt, but does not wait for its execution.
The promises are not resolved when the loop finishes and an empty array is returned.
You can wait for the promises to be executed and return the array only after that:
const promises = [];
(response.project).forEach(element => {
promises.push(dbo.collection("projects").findOne({_id:element})
.then(respo => {
console.log(respo);
projectInfo.push(respo);
});
}));
Promise.all(promises).then(function(values) {
console.log(projectInfo) ;
});
This can also be simplified with the map function and async/await :
const promises = (response.project).map(async element => {
const respo = await dbo.collection("projects").findOne({_id:element});
projectInfo.push(respo);
});
Promise.all(promises).then(function(values) {
console.log(projectInfo) ;
});
I am writing a Chrome extension in ReactJS.
I am looping through an array of URLs and trying to get the the HTML content of those pages.
this.state.advertData.map(function(e, i) {
common.updateTabUrl(e.url).then((tab) => {
common.requestHTML(tab).then((response) => {
console.log(response.content);
})
});
})
common.js:
let requestHTML = function(tab) {
return new Promise(function(resolve, reject) {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tab.id, {'req': 'source-code'}, function (response) {
resolve(response)
})
})
})
}
let updateTabUrl = function(url) {
return new Promise(function(resolve, reject) {
let update = chrome.tabs.update({
url: url
}, function(tab) {
chrome.tabs.onUpdated.addListener(function listener (tabId, info) {
if (info.status === 'complete' && tabId === tab.id) {
chrome.tabs.onUpdated.removeListener(listener);
resolve(tab);
}
});
})
})
}
content_script.js
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
let response = '';
if (request.req === 'source-code') {
response = document.documentElement.innerHTML;
}
sendResponse({content: response});
});
My issue is that the response.content always seems to be the same. More importantly, the tab that updates seems to only ever display the last url in my array. I think it is a problem with the way I am handling Promises.
Any help is appreciated.
The problem with your code is that it doesn't wait for the previous URL to load before proceeding to the next one so only the last one gets actually loaded in a tab.
I suggest using 1) Mozilla's WebExtension polyfill, 2) await/async syntax, 3) executeScript that automatically runs when a tab is complete by default 4) a literal code string in executeScript so you don't need neither a separate file nor to declare the content script in manifest.json.
async function getUrlSourceForArray({urls, tabId = null}) {
const results = [];
for (const url of urls) {
await browser.tabs.update(tabId, {url});
const [html] = await browser.tabs.executeScript(tabId, {
code: 'document.documentElement.innerHTML',
});
results.push(html);
}
return results;
}
Invoking inside an async function:
const allHtmls = await getUrlSourceForArray({
urls: this.state.advertData.map(d => d.url),
tabId: null, // active tab
});
P.S. you can also open all the URLs at once in a new window in background, assuming there won't be more than say 10 URLs, otherwise you would risk exhausting the user's RAM.
How do I process the results of more than one Promise?
Say the results a of DoA and b of DoB in DoNext
I read https://developer.mozilla.org/de/docs/Web/JavaScript/Guide/Using_promises
It introduces the "then"-Syntax which is supposed to replace the "callback pyramid of doom" however I don't understand the following:
DoA.then(function(a){
return DoB(a)
})
.then(function(b){
DoNext(a,b);
})
In the call of DoNext, a is unknown. I understand that this is because a is only defined in the anonymous function in line 2. However in the "callback pyramid of doom" I can access a, because in that pattern DoNext is within the anonymous function that is the success callback of DoA.
How do I handle this in then-Syntax?
There are multiple ways of handling multiple Promise situation.
1- Chain Promise (ugly though)
DoA.then(a => {
DoB(a).then(.....)
})
.catch(error => console.log(error));
2- Promise.all()
var promise1 = Promise.resolve(3);
var promise2 = 42;
var promise3 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then(function(values) {
console.log(values);
});
// expected output: Array [3, 42, "foo"]
3- async function (but read the details)
(async () => {
const a = await DoA(); // DoA must be a Promise
const b = await DoB(a); // DoB must be a Promise
// rest of the code
})();
I have a piece of code inside an angular controller that requests three services for data. Once the last service returns, I'm taking the data from all and merge it into a dataset. Out of the nested loops, my "vm" instance of this have the values I want the way I want (using a $log.debug to the console) but when I try to access in the view it has the value of initialization.
Here is my code:
function loadConfigs(userId) {
// Load the apps the user have access to
aimsApps.getAccessibleApps()
.then((resApps) => {
vm.apps = resApps.data;
// Load notifications types
aimsNotificationTypes.getNotificationTypes()
.then((resTypes) => {
vm.types = resTypes;
// Load the configurations for a user
aimsNotificationConfigs.getNotificationConfigs(userId)
.then((resConfigs) => {
vm.configs = resConfigs;
// Create a store like notification[app][type] and load the configs
$log.debug('ALL RESOLVED', vm.apps, vm.types, vm.configs);
vm.apps.forEach((eApp) => {
vm.notifSettings[eApp.name] = [];
vm.types.forEach((eType) => {
vm.configs.forEach((eConfig) => {
if ((eConfig.app === eApp.name) && (eConfig.notificationType === eType.name)) {
vm.notifSettings[eApp.name][eType.name] = eConfig;
} else {
vm.notifSettings[eApp.name][eType.name] = {
app: eApp.name,
user: userId,
notificationType: eType.name,
sendToWeb: true,
sendToEmail: true,
sendToSMS: true,
};
}
});
});
});
$log.debug('NOTIF CONFIG', vm.notifSettings);
});
});
});
}
When this line $log.debug('NOTIF CONFIG', vm.notifSettings); is reached, I can see in the console the values for vm.notifSettings but my view doesn't reflect those changes.
Any suggestions?