'Unexpected token' when recursively calling async function in Nodejs - node.js

My app contains posts with nested comments in Firebase Firestore structured such that each post/comment with docID has a sub collection postComments. Thus, a given post/comment can have an infinite number of nested comments.
comments
- docID
postComments
- docID
- docID
- docID
- docID
postComments
- docID
- docID
I am currently writing a Firebase cloud function to recursively query all documents and sub collection documents of a given docID and return all of those documents in an array. My plan was to define the getChildComments async function which takes in a docID and returns all of the documents in that document's postComments sub collection. I would then recursively call getChildComments until I have built an array with all of the nested comments in a thread.
exports.loadWholeCommentThread = functions.https.onCall(async (data, context) => {
let comments = await getChildComments(data.rootID);
return comments;
});
async function getChildComments(docID) {
try {
const db = admin.firestore();
const commentsRef = db.collection('comments').doc(docID).collection('postComments');
var comments = [];
const commentsQuerySnap = await commentsRef.get();
commentsQuerySnap.forEach((comment) => {
let commentData = comment.data();
comments.push(commentData);
if (commentData.commentCount > 0) {
let childComments = await getChildComments(commentData.commentID);
comments.concat(childComments);
}
});
return comments;
} catch (error) {
functions.logger.log(error);
throw new functions.https.HttpsError('unknown', 'ERROR0', { message: error.message } )
}
}
Unfortunately, when I try to deploy my code, I get the error Parsing error. Unexpected token getChildComments on the line where I recursively call getChildComments inside of getChildComments. Removing the await from this line fixes the build issue but then the recursive call doesn't finish.
How should I fix my issue? Or is there a better way to query all nested documents?

This is because you have used await outside of an async function (note that it is inside an arrow function!).
const comments = [];
const commentsQuerySnap = await commentsRef.get();
commentsQuerySnap.forEach((comment) => {
let commentData = comment.data();
comments.push(commentData);
if (commentData.commentCount > 0) {
let childComments = await getChildComments(commentData.commentID); // the keyword "await" here is invalid
comments = comments.concat(childComments);
}
});
But you can't just add async to this arrow function, because then your code won't properly wait for the comments array to be filled.
To properly fix this, you need to use .map() on the commentsQuerySnap.docs array in addition to using Promise.all to wait for each comment (and its children) to be retrieved.
const comments = [];
const commentsQuerySnap = await commentsRef.get();
await Promise.all(
commentsQuerySnap.docs.map(
async (comment) => {
let commentData = comment.data();
comments.push(commentData);
if (commentData.commentCount > 0) {
let childComments = await getChildComments(commentData.commentID);
comments = comments.concat(childComments);
}
})
)
);
While that above block works, the comments array may be out of order to what you were expecting. If you must maintain order of the comments fetched so they are in the same order as the query, you should return the built comments array for each document and then flatten them after they all have been retrieved.
// removed const comments = []; here
const commentsQuerySnap = await commentsRef.get();
const arrayOfCommentThreads = await Promise.all(
commentsQuerySnap.docs.map(
async (comment) => {
let commentData = comment.data();
const commentThread = [commentData];
if (commentData.commentCount > 0) {
let childComments = await getChildComments(commentData.commentID);
commentThread = commentThread.concat(childComments);
}
return commentThread;
})
)
);
const comments = arrayOfCommentThreads.flat();
Personally, I prefer the spread operator to using .concat like so:
const commentsQuerySnap = await commentsRef.get();
const arrayOfCommentThreads = await Promise.all(
commentsQuerySnap.docs.map(
async (comment) => {
const commentData = comment.data();
if (commentData.commentCount === 0) {
return [commentData];
}
const childComments = await getChildComments(commentData.commentID);
return [commentData, ...childComments];
})
)
);
const comments = arrayOfCommentThreads.flat();

Related

Facing issue with scooping in node JS

This code is showing empty object ( {} )
// declared at top
let mainData = {};
let trainStations = {};
let routes = {};
let trainNo = {};
data["data"].forEach(async (element) => {
const response2 = await fetch(
`https://india-rail.herokuapp.com/trains/getRoute?trainNo=${element["train_base"]["train_no"]}`
);
const data2 = await response2.json();
data2["data"].forEach((ele) => {
routes[ele["source_stn_code"]] = true;
});
trainNo[element["train_base"]["train_no"]] = routes;
});
console.log(trainNo);
if i do this then i will give response with data
data["data"].forEach(async (element) => {
const response2 = await fetch(
`https://india-rail.herokuapp.com/trains/getRoute?trainNo=${element["train_base"]["train_no"]}`
);
const data2 = await response2.json();
data2["data"].forEach((ele) => {
routes[ele["source_stn_code"]] = true;
});
trainNo[element["train_base"]["train_no"]] = routes;
console.log(trainNo);
});
maybe there is some scooping issue please kindly help me to solve this problem :)
Please refer here and also check this.
As a short note, using await inside a forEach() loop will give unexpected results. This is because the forEach() does not wait until the promise to settled (either fulfilled or rejected).
A simple solution for this could be using either the traditional for loop or the for..of loop.
for(let element of data["data"]){
const response2 = await fetch(
`https://india-rail.herokuapp.com/trains/getRoute?trainNo=${element["train_base"]["train_no"]}`
);
const data2 = await response2.json();
data2["data"].forEach((ele) => {
routes[ele["source_stn_code"]] = true;
});
trainNo[element["train_base"]["train_no"]] = routes;
}
console.log(trainNo);
NOTE: Make sure to wrap the above for..of loop inside an async function because the await keyword is allowed inside a function only when the function is defined with async keyword.

why my data returning the state before get updated in async function nodejs typescript

i dont know why variable result in console log is returning the state before get updated in this line
result[msisdn] = "customer exist"
the code itself it executed but why keep returning the state before get modified, i think i already give await keyword on all async code that i write
code.ts
async migrateCustomerImageUseCase(request: ListCustomerForMigration, response: Response): Promise<any>{
const result = {} as any
const lists = request as any
await lists.forEach( async (element: any[]) => {
const msisdn = element[0] as string
const isCustomerExist = await this.ekycHandler.isCustomerExist(msisdn)
if(isCustomerExist){
result[msisdn] = "customer exist" as string
this.logger.info(`${msisdn} = ${result[msisdn]}`);
// tslint:disable-next-line:no-console
// console.log(result[msisdn])
// const customerImageData:CustomerImageData = {
// customerIdCardImage: element[2],
// customerImage: element[3],
// msisdn: element[0] as string
// };
// const localFileName = await this.uploadBase64ImageToLocalUseCase(customerImageData, response)
// const ossFileName = await this.uploadImageToOssUseCase(localFileName, response)
// ossFileName["customerId"] = isCustomerExist.id
// await this.ekycPortalRepo.createOrUpdateCustomerDetailCifFromMigrationData(ossFileName)
} else {
result[msisdn] = "customer not exist" as string
this.logger.info(`${msisdn} = ${result[msisdn]}`);
}
})
return result
}
and the code.ts is called in another async function with this line
const migrateResult = await this.ekycPortalUseCase.migrateCustomerImageUseCase(listCustomers, response)
I think your case is mostly connected with this - Using async/await with a forEach loop
You cannot use forEach indeed. Each of the async callback function calls does return a promise, but you're throwing them away instead of awaiting them. Just use map instead, and you can await the array of promises that you'll get with Promise.all
You should do something like that:
const promises = lists.map(() => {
return (async () => {
const msisdn = element[0] as string
const isCustomerExist = await this.ekycHandler.isCustomerExist(msisdn)
if(isCustomerExist){
result[msisdn] = "customer exist" as string
this.logger.info(`${msisdn} = ${result[msisdn]}`);
} else {
result[msisdn] = "customer not exist" as string
this.logger.info(`${msisdn} = ${result[msisdn]}`);
}
})()
})
await Promise.all(promises)
Typescript doesn't wait for a forEach to be fully completed before moving on, that's why the return result statement gets executed before it is filled with the results from inside the loop.
One solution might be to change from using forEach to a basic for loop to avoid this issue entirely.

Error using foreach and Promisse.all in node

I am stuck with this code:
await Promise.all(auxusers.forEach(async (element: any) => {
const userPopulated: any = new Object();
userPopulated.hours = await this.hoursRepository.find({ where: { userId : { like: element.id}}});
userPopulated.generalinfo = element;
usersArray.push(userPopulated);
}
));
return usersArray
It´s return me: Promise.all for each, error: Cannot read property 'Symbol(Symbol.iterator)' of undefined
Any idea? I am really don´t know what more can I trie
Thanks in advance.
Best,
The problem is that foreach does not return an array.
I would suggest to use map instead.
Check the code below:
const users = [1,2,3,4,5];
Promise.all(
users.map(async (element) => {
const userPopulated = {};
userPopulated.hours = Promise.resolve(element);
userPopulated.generalinfo = element;
return element;
})
).then((a) => console.log(a))

How to make Mongoose update work with await?

I'm creating a NodeJS backend where a process reads in data from a source, checks for changes compared to the current data, makes those updates to MongoDB and reports the changes made. Everything works, except I can't get the changes reported, because I can't get the Mongoose update action to await.
The returned array from this function is then displayed by a Koa server. It shows an empty array, and in the server logs, the correct values appear after the server has returned the empty response.
I've digged through Mongoose docs and Stack Overflow questions – quite a few questions about the topic – but with no success. None of the solutions provided seem to help. I've isolated the issue to this part: if I remove the Mongoose part, everything works as expected.
const parseJSON = async xmlData => {
const changes = []
const games = await Game.find({})
const gameObjects = games.map(game => {
return new GameObject(game.name, game.id, game)
})
let jsonObj = require("../sample.json")
Object.keys(jsonObj.items.item).forEach(async item => {
const game = jsonObj.items.item[item]
const gameID = game["#_objectid"]
const rating = game.stats.rating["#_value"]
if (rating === "N/A") return
const gameObject = await gameObjects.find(
game => game.bgg === parseInt(gameID)
)
if (gameObject && gameObject.rating !== parseInt(rating)) {
try {
const updated = await Game.findOneAndUpdate(
{ _id: gameObject.id },
{ rating: rating },
{ new: true }
).exec()
changes.push(
`${updated.name}: ${gameObject.rating} -> ${updated.rating}`
)
} catch (error) {
console.log(error)
}
}
})
return changes
}
Everything works – the changes are found and the database is updated, but the reported changes are returned too late, because the execution doesn't wait for Mongoose.
I've also tried this instead of findOneAndUpdate():
const updated = await Game.findOne()
.where("_id")
.in([gameObject.id])
.exec()
updated.rating = rating
await updated.save()
The same results here: everything else works, but the async doesn't.
As #Puneet Sharma mentioned, you'll have to map instead of forEach to get an array of promises, then await on the promises (using Promise.all for convenience) before returning changes that will then have been populated:
const parseJSON = async xmlData => {
const changes = []
const games = await Game.find({})
const gameObjects = games.map(game => {
return new GameObject(game.name, game.id, game)
})
const jsonObj = require("../sample.json")
const promises = Object.keys(jsonObj.items.item).map(async item => {
const game = jsonObj.items.item[item]
const gameID = game["#_objectid"]
const rating = game.stats.rating["#_value"]
if (rating === "N/A") return
const gameObject = await gameObjects.find(
game => game.bgg === parseInt(gameID)
)
if (gameObject && gameObject.rating !== parseInt(rating)) {
try {
const updated = await Game.findOneAndUpdate(
{ _id: gameObject.id },
{ rating: rating },
{ new: true }
).exec()
changes.push(
`${updated.name}: ${gameObject.rating} -> ${updated.rating}`
)
} catch (error) {
console.log(error)
}
}
})
await Promise.all(promises)
return changes
}
(The diff, for convenience:
9,10c9,10
< let jsonObj = require("../sample.json")
< Object.keys(jsonObj.items.item).forEach(async item => {
---
> const jsonObj = require("../sample.json")
> const promises = Object.keys(jsonObj.items.item).map(async item => {
33a34
> await Promise.all(promises)
)
EDIT: a further refactoring would be to use that array of promises for the change descriptions themselves. Basically changePromises is an array of Promises that resolve to a string or null (if there was no change), so a .filter with the identity function will filter out the falsy values.
This method also has the advantage that changes will be in the same order as the keys were iterated over; with the original code, there's no guarantee of order. That may or may not matter for your use case.
I also flipped the if/elses within the map function to reduce nesting; it's a matter of taste really.
Ps. That await Game.find({}) will be a problem when you have a large collection of games.
const parseJSON = async xmlData => {
const games = await Game.find({});
const gameObjects = games.map(game => new GameObject(game.name, game.id, game));
const jsonGames = require("../sample.json").items.item;
const changePromises = Object.keys(jsonGames).map(async item => {
const game = jsonGames[item];
const gameID = game["#_objectid"];
const rating = game.stats.rating["#_value"];
if (rating === "N/A") {
// Rating from data is N/A, we don't need to update anything.
return null;
}
const gameObject = await gameObjects.find(game => game.bgg === parseInt(gameID));
if (!(gameObject && gameObject.rating !== parseInt(rating))) {
// Game not found or its rating is already correct; no change.
return null;
}
try {
const updated = await Game.findOneAndUpdate(
{ _id: gameObject.id },
{ rating: rating },
{ new: true },
).exec();
return `${updated.name}: ${gameObject.rating} -> ${updated.rating}`;
} catch (error) {
console.log(error);
}
});
// Await for the change promises to resolve, then filter out the `null`s.
return (await Promise.all(changePromises)).filter(c => c);
};

Async does not seem to wait for await

I use node js and postgres as well as chai and mocha for tdd, and now I have encountered a problem when I try to update an item in my database with a wrong foreign key. When this happens I want to basically get the old item from the database with the valid values.
this is the update method in the Item class
async update() {
if (this.description.length === 0) {
throw new Error("Description can not be deleted");
}
try {
const updateItem = await this.tryUpdate();
this.copyToThis(updateItem);
} catch (e) {
const oldItem = await Item.getById(this.id);
this.copyToThis(oldItem);
console.log(this);
throw new Error("Updating did not work");
}
}
This is the test that fails
it('should throw an error if you update with wrong category or project id and get the old values from the server', async function () {
const newProject = "3b4e092e-1dd9-40a5-8357-69696b3e35ba";
const newCategory = "3cf87368-9499-4af1-9af0-10ccf1e84088";
const item = await Item.getById(updateId);
expect(item).to.exist;
const oldProjectId = item.projectId;
const oldCategoryId = item.categoryId;
item.projectId = newProject;
expect(item.update()).to.be.rejectedWith(Error);
item.categoryId = newCategory;
expect(item.update()).to.be.rejectedWith(Error);
expect(item.categoryId).to.equal(oldCategoryId);
expect(item.projectId).to.equal(oldProjectId);
});
this is the AssertionError
-3cf87368-9499-4af1-9af0-10ccf1e84088
+3cf87368-9499-4af1-9af0-10ccf1e84087
As you can see the item still has the wrong categoryId and not the one from the server. Eventhough the log has the correct item.
I solved it myself
I needed to add an await in the test
it('should throw an error if you update with wrong category or project id and get the old values from the server', async function () {
const newProject = "3b4e092e-1dd9-40a5-8357-69696b3e35ba";
const newCategory = "3cf87368-9499-4af1-9af0-10ccf1e84088";
const item = await Item.getById(updateId);
expect(item).to.exist;
const oldProjectId = item.projectId;
const oldCategoryId = item.categoryId;
item.projectId = newProject;
await expect(item.update()).to.be.rejectedWith(Error);
item.categoryId = newCategory;
await expect(item.update()).to.be.rejectedWith(Error);
expect(item.categoryId).to.equal(oldCategoryId);
expect(item.projectId).to.equal(oldProjectId);
});

Resources