I’m working through some inherited code using promises and having a major issue. I have two functions, both returning a promise (or a series of promises in the findOne case), where findAll makes calls to findOne to loop through a number of records to query the data from those records in other tables using Sequelize. I've tried to lay out the code so that all promises passed to findOne are resolved or rejected before returning to the parent function, findAll.
Given a number of matching records in findAll, I pass each record to findOne, which by the time findOne resolves it calls another function, constructJson, which returns a value and not a promise. The issue I'm seeing is that once all records pass through findOne and are returned to findAll, the code actually goes back to the resolve(constructJson(_map, scrub)) line the findOne function a number of times. In my test use case where I’m supposed to be returned four distinct record IDs (that are passed into findOne to be queried), findOne is returning duplicated or out-of-order IDs. I very rarely get the same four sequential IDs returned in the same order they were passed in to findOne, though based on the flow of the function it seems like this should be the case. There is a piece to the asynchronicity I am missing in order to ensure the promise returned by findOne is fully completed the the associated IDs (queries object) passed into it are returned in order. What am I not seeing?
const Sequelize = require('sequelize');
function findAll(dbConn, _map, queries, joins, primaryTable, primaryKey, whereClause, limit, offset, scrub) {
return new Promise((resolve, reject) => {
const sequelize = new Sequelize(dbConn.db, dbConn.username, dbConn.password, dbConn.options);
offset = isNaN(offset) ? 0 : offset;
whereClause = !!whereClause ? ` WHERE true${whereClause}` : ' WHERE true';
sequelize.query(`SELECT count(${primaryKey}) FROM ${primaryTable}${whereClause}`, {type: sequelize.QueryTypes.SELECT})
.then(result => {
totalRows = result[0]["count"];
});
whereClause = isNaN(limit) ? whereClause : whereClause + ` LIMIT ${limit} OFFSET ${offset}`;
sequelize.query(`SELECT ${primaryKey} FROM ${primaryTable}${whereClause}`, {type: sequelize.QueryTypes.SELECT})
.then(matchingRecords => {
let promiseStack = [];
matchingRecords.forEach(record => {
const cloneOfMap = JSON.parse(JSON.stringify(_map));
promiseStack.push(
findOne(dbConn, cloneOfMap, queries, joins, primaryTable, primaryKey, record.id, scrub)
.then(result => {
return result;
})
.catch(error => {
return {err: `error collecting data for id = ${record.id}\n${error}`};
})
)
});
Promise.all(promiseStack).then(bundleEntries => {
sequelize.close();
let errors = bundleEntries.filter(r => {return !!r.err}).map(r => r.err);
if (errors.length === 0) {
resolve(bundleEntries);
} else {
reject("The following queries failed:\n" + errors.join('\n'));
}
});
})
.catch(error => {
sequelize.close();
reject(error);
});
});
}
function findOne(dbConn, _map, queries, joins, primaryTable, primaryKey, id, scrub) {
return new Promise((resolve, reject) => {
let promiseStack = [];
let noMatchesReturned = true;
const sequelize = new Sequelize(dbConn.db, dbConn.username, dbConn.password, dbConn.options);
let queryString;
queries.forEach(query => {
if (!!query.sql && query.sql !== '') {
switch (dbConn.options.dialect) {
case "postgres":
queryString = query.sql + ` WHERE ${primaryTable}.${primaryKey}='${id}' LIMIT 1`;
break;
case "mssql":
queryString = query.sql + ` WHERE ${primaryTable}.${primaryKey}='${id}'`;
break;
}
}
else {
let otherTable = '';
let joinString = '';
if (query.table !== primaryTable) {
otherTable = ", " + primaryTable;
joins.filter(j => {return j.fkTable === primaryTable && j.pkTable === query.table}).forEach(j => {
joinString += ` AND ${j.fkTable}.${j.fkColumn} = ${j.pkTable}.${j.pkColumn}`;
});
}
switch (dbConn.options.dialect) {
case "postgres":
queryString = `SELECT ${query.table}.${query.column} FROM ${query.table}${otherTable} WHERE ${primaryTable}.${primaryKey}='${id}'${joinString} LIMIT 1`;
break;
case "mssql":
queryString = `SELECT TOP 1 ${query.table}.${query.column} FROM ${query.table}${otherTable} WHERE ${primaryTable}.${primaryKey}='${id}'${joinString}`;
break;
}
}
// console.log(queryString);
promiseStack.push(
sequelize.query(queryString, {type: sequelize.QueryTypes.SELECT})
.then(row => {
let newData;
if (row.length === 0) {
newData = null;
} else {
noMatchesReturned = false;
const x = row[0];
const y = Object.keys(x)[0];
newData = x[y];
}
return Object.assign(query, {data: newData});
})
.catch(error => {
return {err: `Error executing query ${error.sql}\n\t${error}`};
})
);
});
Promise.all(promiseStack).then(rows => {
// identify queries that returned an error
sequelize.close();
let errors = rows.filter(r => {return !!r.err}).map(r => r.err);
if (errors.length === 0) {
if (noMatchesReturned) {
resolve([]);
}
else {
// all queries completed without error
rows.forEach(result => {
_map.rows[result.id].data = result.data;
});
resolve(constructJson(_map, scrub));
}
}
else {
sequelize.close();
reject("The following queries failed:\n" + errors.join('\n'));
}
}, error => {
sequelize.close();
reject(error);
});
});
}