NodeJS await/async making data retrieval synchronous [duplicate] - javascript

This question already has answers here:
Using async/await with a forEach loop
(33 answers)
Closed 1 year ago.
I know this has been asking thousands of times, but I still haven't been able to find an answer. I have a forEach loop that I am using to loop through a list of users and retrieving specific location data for each. Inside the forEach the first function call gets the branch then the second gets information on that branch.
The problem is that the forEach loop is not completing prior to returning data from the function, which is causing all sorts of issues.
Here is my code:
//getAllUserData() - this is the function which returns prematurely
const getAllUserData = async () => {
const sequelize = DB.GetDB('mcaintranet');
const userDB = MCAIntranet.initModels(sequelize);
userDB.tblUsers.belongsTo(userDB.tblSMSSent, { foreignKey: 'collector', targetKey: 'user' });
userDB.tblSMSSent.hasMany(userDB.tblUsers);
let users = await userDB.tblUsers.findAll({
attributes: ['collector', 'id'],
include: {
model: userDB.tblSMSSent,
attributes: [[sequelize.fn('COUNT', 'tblSMSSent.id'), 'numMessages']]
},
raw: true,
group: ['collector', 'tblUsers.id'],
})
//return list of all users and texts sent
//UserName, User Province Code, # Messages Sent, user Number
try {
await users.forEach(async user => {
const branch = await Users.getUserBranch(user);
const province = getKey(branch);
user.province = province;
// console.log("User: ", [user, branch])
//clean up JSON from join results
user.numMessages = user["tblSMSSent.numMessages"];
delete user["tblSMSSent.numMessages"];
})
const obj = { 'users': users };
console.log("DONE: ", obj)
return obj;
}
catch (ex) {
console.log("ERROR: ", ex)
const msg = ex.Message;
return { results: "False", message: "SMS.js 61" + msg }; //is user allowed to use SMS page)
}
}
//getUserBranch() - accessor function to return branch number
const getUserBranch = async (user) => {
// console.log("Branch User: ", user)
let branch = getUserProps(user.collector ? user.collector : user, 'branch');
// console.log(props)
if (!branch) branch = 0;
return branch;
}
//getUserProps - pulls all user properties from database and filters as necessary
const getUserProps = async (user, propName = null) => {
let ret = {};
if (user.collector) user = user.collector;
user = user.trim();
let filter = {
collector: user
}
if (propName) {
filter.propertyName = propName
}
// console.log("Filter: ", filter)
const sequelize = DB.GetDB('MCAIntranet');
const propsDB = MCAIntranet.initModels(sequelize);
//get all values contained for user
let props = await propsDB.tblUserProperties.findAll({
attributes: ['id', 'propertyName', 'propertyValue', 'updated'],
where: filter,
order: [['updated', 'DESC']],
})
// console.log("Props: ", props);
let distinctProps = await propsDB.tblUserProperties.findAll({
attributes: ['propertyName'],
DISTINCT: true,
where: filter,
})
//the SMS Credits item is sometimes added as a second row
//so loop through all potential value rows for user
distinctProps.forEach(dr => {
// console.log(dr);
var name = dr.propertyName.toLowerCase();
// console.log("name: ", name)
try {
//get individual values for each property user possesses
let tmpRows = props.filter(row => row.propertyName.toLowerCase() == name);
// console.log("tmpRows: ", tmpRows)
//get the most recent value
let row = tmpRows[tmpRows.length - 1];
var item = row.propertyName.trim();
var value = row.propertyValue.trim();
//add to returned JSON
ret[item] = value;
} catch (ex) {
console.log("USERS.js 157 " + ex.message);
return ({ error: "USERS.js 158" + ex.Message });
}
})
// console.log("Ret: ", ret)
return ret;
}
//getKey() - returns necessary data about the user province
const getKey = (data) => {
let branch;
try {
branch = data.branch ? data.branch : data;
}
catch (ex) {
console.log(data)
console.log(ex)
}
branch = parseInt(branch);
// console.log(branch)
try {
let acctKey = "";
let province = "";
let abbr = "";
// console.log("Branch: ", branch)
switch (branch) {
case 4: //vancouver
case '4':
abbr = "BC";
acctKey = key["BC"];
province = "British Columbia";
break;
case 7: //Montreal
case '7':
abbr = "QC";
acctKey = key["QC"];
province = "Quebec";
break;
case 9: //Toronto
case '9':
abbr = "ON";
acctKey = key["ON"];
province = "Ontario";
break;
default: //Edmonton
abbr = "AB";
acctKey = key["AB"];
province = "Alberta";
break;
}
return { key: acctKey, province: province, abbr: abbr };
}
catch (ex) {
throw ex;
}
}
Somewhere in this conglomeration of awaits and asyncs something is lacking causing the getAllData() function to return user names without the location data. How can I block execution until I have all the data? Please ignore the extraneous awaits I have put in while endeavoring to solve this problem.
Thanks.

Instead of forEach, you want this:
await Promise.all(users.map(async (user) => {...}))
map returns the promise for each inner operation, and await-ing Promise.all waits until all of those promises are finished

Related

Node js , cannot retrive data from sqlight3 table

async function get_info(compName) {
let company = {
name:""
, activityNumber:""
, siret :""
, adresse :""
, tva :""
, logo:0
};
buff = await db.all("SELECT * FROM company WHERE name = ?", [compName], (err, rows) => {
if (err) {
console.log(err)
return err;
}
rows.forEach(element => {
console.log(element.name) // WORK
company.name = element.name;
company.activityNumber = element.activityNumber;
company.adresse = element.adresse;
company.logo = element.logo;
company.siret = element.siret;
company.tva = element.tva;
});
});
console.log(" ... " + company.name) // DOSENT WORK
return company;
}
I'm trying to get company fill and get data out of my database.
The first console.log() is good but not the second one, it's empty, and the object it returns have defaults values, there will be only one element who will match "WHERE name = ?" so I don't worry about erasing the value.

Why does firebase cloud-function javascript promise run more than the number of loop invocations?

I have a cloud function that is triggered when a sale/purchase is committed into firestore. This function's purpose is to update the inventory level centrally.
The function works just fine if I'm updating an item's inventory at only 1 warehouse, but doing so for multiple warehouses has unexpected behavior. I'm looping through all the warehouses that are affected to calculate the total inventory level changes, and every iteration kicks-off a javascript promise.
The problem seems to occur with the way the promises are invoked. E.g: if I want to update 3 warehouses and loop 3 times, somehow 5 promises are being kicked-off. This is visible through the logs. I've researched similar questions here, but the solutions were suggested while firestore was still in beta and might not be the right way forward. (Firestore transactions getting triggered multiple times resulting in wrong data)
Here is the code
export const onTransactionCreate = functions.firestore
.document('/companies/{companyId}/sub_transactions/{transId}')
.onCreate(async (snapshot, context) => {
const transId = context.params.transId
const stock_transaction: IStockTransaction = <IStockTransaction>snapshot.data()
const trans_type: TRANS_TYPE = stock_transaction.trans_type
const promises: any[] = []
stock_transaction.lineItems.forEach((element, index) => {
const ITEM_GUID = element.item_guid
const is_increasing = isIncreasingTransaction(element.line_trans_type)
const delta_stock = element.qty_transaction * (is_increasing ? 1 : -1)
const TARGET_BRANCH_ID = element.target_branch_guid
const itemRef = db.collection(FIRESTORE_PATHS.COL_COMPANIES).doc(companyId).
collection(FIRESTORE_PATHS.SUB_COMPANIES_ITEMS).
doc("" + ITEM_GUID)
const item_promise = db.runTransaction(async t => {
try {
const item_doc = await t.get(itemRef)
const item_branch_quantities: IBranchQuantity[] = (item_doc.data()!.branch_quantities || new Array())
const item_branch_ids: string[] = (item_doc.data()!.available_branch_ids || new Array())
const branch_index = item_branch_ids.indexOf(TARGET_BRANCH_ID)
console.log(`${transId} Line Item ${index}, after document.get(), search branch index: ${branch_index}`)
if (branch_index !== -1) {
const prev_qty = item_branch_quantities[branch_index]
const updated_qty = prev_qty.quantity + delta_stock
item_branch_quantities[branch_index] = {
item_guid: prev_qty.item_guid,
branch_guid: prev_qty.branch_guid,
quantity: updated_qty
}
console.log(`${transId} Line Item ${index} Updating qty # item ${delta_stock}, prev qty ${prev_qty.quantity}`)
} else {
item_branch_ids.push(TARGET_BRANCH_ID)
item_branch_quantities.push({
item_guid: element.item_guid,
branch_guid: TARGET_BRANCH_ID,
quantity: delta_stock
})
console.log(`${transId} Line Item ${index} Adding qty # item ${delta_stock}`)
}
t.update(itemRef, {
branch_quantities: item_branch_quantities,
available_branch_ids: item_branch_ids
})
} catch (err) {
throw new Error(err)
}
})
promises.push(item_promise)
});
return Promise.all(promises)
})
we have found the solution by reading this article.
A transaction consists of any number of get() operations followed by any number of write operations such as set(), update(), or delete(). In the case of a concurrent edit, Cloud Firestore runs the entire transaction again. For example, if a transaction reads documents and another client modifies any of those documents, Cloud Firestore retries the transaction. This feature ensures that the transaction runs on up-to-date and consistent data.
lineItems.forEach(element => {
const delta_transaction = element.qty * (isLineTransIncrease(element.line_trans_type) ? 1 : -1)
const itemRef = db.collection('companies').doc(companyId).collection('sub_items').doc("" + element.item_guid)
const p = db.runTransaction(t => {
return t.get(itemRef)
.then(doc => {
let item_branch_quantities: IBranchQuantity[] = doc.data()!.branch_quantities
let item_branch_ids: string[] = doc.data()!.available_branch_ids
if (!item_branch_quantities)
item_branch_quantities = new Array()
if (!item_branch_ids)
item_branch_ids = new Array()
const branch_index = item_branch_ids.indexOf(current_branch_id)
if (branch_index !== -1) {
const prev_qty = item_branch_quantities[branch_index]
const updated_qty: number = prev_qty.quantity + delta_transaction
item_branch_quantities[branch_index] = {
item_guid: prev_qty.item_guid,
branch_guid: prev_qty.branch_guid,
quantity: updated_qty
}
} else {
item_branch_ids.push(current_branch_id)
item_branch_quantities.push({
item_guid: element.item_guid,
branch_guid: current_branch_id,
quantity: delta_transaction
})
}
t.update(itemRef, {
branch_quantities: item_branch_quantities,
branch_ids: item_branch_ids
})
})
})
item_update_transactions.push(p)
});
return Promise.all(item_update_transactions)
})
function isLineTransIncrease(line_trans: number): boolean {
return (line_trans === 1) || (line_trans === 2)
}

Sequelize query SELECT * FROM Table only returning one row

I'm currently using PostgreSQL and Sequelize.js to query some data. When I'm using sequelize.query(), it only returns one row data but when I enter it through pgAdmin it works as expected.
Here is the code I use in sequelize.query().
SELECT table2.student_id,
s.canvasser_name,
l.level_name,
table2.total_score
FROM (SELECT table1.student_id,
sum(table1.max_score) total_score
FROM (SELECT sq.student_id,
max(sq.score) max_score
FROM public.student_quiz sq
GROUP BY sq.quiz_id, sq.student_id) table1
GROUP BY table1.student_id) table2
INNER JOIN public.student s
ON s.id = table2.student_id
INNER JOIN public.level l
ON l.id = s.level_id
ORDER BY table2.total_score DESC
LIMIT 10;
And here is the nodejs code
const getRank = (option, logs = {}) => new Promise(async (resolve, reject) => {
try {
let { offset, limit } = option;
if (!limit) limit = 10;
const result = await sequelize.query(
`SELECT table2.student_id,
s.canvasser_name,
l.level_name,
table2.total_score
FROM (SELECT table1.student_id,
sum(table1.max_score) total_score
FROM (SELECT sq.student_id,
max(sq.score) max_score
FROM public.student_quiz sq
GROUP BY sq.quiz_id, sq.student_id) table1
GROUP BY table1.student_id) table2
INNER JOIN public.student s
ON s.id = table2.student_id
INNER JOIN public.level l
ON l.id = s.level_id
ORDER BY table2.total_score DESC
LIMIT 10;`,
{ plain: true }
);
return resolve(result);
} catch (error) {
let customErr = error;
if (!error.code) customErr = Helpers.customErrCode(error, null, undefinedError);
logger.error(logs);
return reject(customErr);
}
});
And here is the code that consume the function above
const getRankController = async (req, res) => {
try {
const { offset, limit } = req.query;
const result = await getRank({ offset, limit });
if (result.length < 1) {
return Helpers.response(res, {
success: false,
message: 'cannot get score list'
}, 404);
}
return Helpers.response(res, {
success: true,
result
});
} catch (error) {
return Helpers.error(res, error);
}
};
In the meantime, I'm trying another approach using the sequelize built in function, here is the code.
const getRank = (
option,
logs = {}
) => new Promise(async (resolve, reject) => {
try {
// eslint-disable-next-line prefer-const
let { offset, limit } = option;
if (!limit) limit = 10;
const result2 = await StudentQuiz.findAll({
attributes: ['studentId', [sequelize.fn('sum', sequelize.fn('max', sequelize.col('score'))), 'totalPrice'], 'quizId'],
group: 'studentId',
include: [
{
model: Student,
include: [{
model: Level
}],
},
],
offset,
limit
});
return resolve(result2);
} catch (error) {
let customErr = error;
if (!error.code) customErr = Helpers.customErrCode(error, null, undefinedError);
logger.error(logs);
return reject(customErr);
}
});
This one does not work since it is nested function, I kinda don't get it how to reproduce it.
I've tried to do some simple query like SELECT * FROM table and it returns one row, and then I've found out that I need to add "public" to table name so it became SELECT * FROM public.table and it works out. Well, until I'm trying the code in the second code block.
Any answer or advice will be appreciated, thank you.
I suppose you need to indicate a query type and remove plain: true option like this:
const result = await sequelize.query(
`SELECT table2.student_id,
s.canvasser_name,
l.level_name,
table2.total_score
FROM (SELECT table1.student_id,
sum(table1.max_score) total_score
FROM (SELECT sq.student_id,
max(sq.score) max_score
FROM public.student_quiz sq
GROUP BY sq.quiz_id, sq.student_id) table1
GROUP BY table1.student_id) table2
INNER JOIN public.student s
ON s.id = table2.student_id
INNER JOIN public.level l
ON l.id = s.level_id
ORDER BY table2.total_score DESC
LIMIT 10;`,
{
type: Sequelize.QueryTypes.SELECT
}
);
From Sequelize documentation:
options.plain - Sets the query type to SELECT and return a single row

In WIX how do you sort a referenced field in a table

I'm using WIX.
I have created a membersTable that is connected to my MembersListDetails dataset.
MembersListDetails dataset has a field FirstName that is a reference field from members/privateMembersData dataset.
MembersListDetails dataset also has a membersTags field dataset that is a reference field from membersTags dataset it can be filtered.
This all works, however I can't find the code to allow me to sort my membersTable by firstName.
I have found in the WIX documents that WIX can't sort a reference field. But that you can query a reference field
Code Examples:
import wixData from 'wix-data';
let options = {
"order": "asc"
};
$w.onReady(function () {
wixData.queryReferenced("members/privateMembersData", "firstName", options)
.then((results) => {
if (results.items.length > 0) {
let firstItem = results.items[0]; //see item below
} else {
// handle case where no matching items found
}
})
.catch((err) => {
let errorMsg = err;
});
});
I also tried this:
export function membersListDetails_beforeQuery(query, context) {
let hookContext = context; // see below
let newQuery = wixData.query("#membersListDetails")
.ascending("firstName", "lastName")
.find()
.then( (results) => {
if(results.items.length > 0) {
let items = results.items;
let firstItem = items[0];
let totalCount = results.totalCount;
let pageSize = results.pageSize;
let currentPage = results.currentPage;
let totalPages = results.totalPages;
let hasNext = results.hasNext();
let hasPrev = results.hasPrev();
let length = results.length;
//let query = results.query;
} else {
// handle case where no matching items found
}
} )
.catch( (error) => {
let errorMsg = error.message;
let code = error.code;
} );
return newQuery;
}
I don't get any errors but the First Name field never sorts.
Can someone please help me out!!!
Thank you!
Sylvia

Batch get DocumentReferences?

I'm trying to improve a firestore get function, I have something like:
return admin.firestore().collection("submissions").get().then(
async (x) => {
var toRet: any = [];
for (var i = 0; i < 10; i++) {
try {
var hasMedia = x.docs[i].data()['mediaRef'];
if (hasMedia != null) {
var docData = (await x.docs[i].data()) as MediaSubmission;
let submission: MediaSubmission = new MediaSubmission();
submission.author = x.docs[i].data()['author'];
submission.description = x.docs[i].data()['description'];
var mediaRef = await admin.firestore().doc(docData.mediaRef).get();
submission.media = mediaRef.data() as MediaData;
toRet.push(submission);
}
}
catch (e) {
console.log("ERROR GETTIGN MEDIA: " + e);
}
}
return res.status(200).send(toRet);
});
The first get is fine but the performance is worst on the line:
var mediaRef = await admin.firestore().doc(docData.mediaRef).get();
I think this is because the call is not batched.
Would it be possible to do a batch get on an array of mediaRefs to improve performance?
Essentially I have a collection of documents which have foreign references stored by a string pointing to the path in a separate collection and getting those references has been proven to be slow.
What about this? I did some refactoring to use more await/async code, hopefully my comments are helpful.
The main idea is to use Promise.all and await all the mediaRefs retrieval
async function test(req, res) {
// get all docs
const { docs } = await admin
.firestore()
.collection('submissions')
.get();
// get data property only of docs with mediaRef
const datas = await Promise.all(
docs.map(doc => doc.data()).filter(data => data.mediaRef),
);
// get all media in one batch - this is the important change
const mediaRefs = await Promise.all(
datas.map(({ mediaRef }) =>
admin
.firestore()
.doc(mediaRef)
.get(),
),
);
// create return object
const toRet = datas.map((data: MediaSubmission, i) => {
const submission = new MediaSubmission();
submission.author = data.author;
submission.description = data.description;
submission.media = mediaRefs[i].data() as MediaData;
return submission;
});
return res.status(200).send(toRet);
}

Categories