Handling Callbacks in Classes - node.js

I'm building classes to find and quickly operate actions on mongodb documents. This is the UserCursor class. (Not talking about MongoDB's cursor)
exports { UserCursor };
class UserCursor {
private __id: object;
constructor(query: { _id?: object, otherId?: number }) {
let { _id, otherId } = query; // Shortens the vars' name
if (!_id && !otherId) return; // Checks if 1 identifier is provided
if (_id) { // If a _id is provided
Users.findOne({ _id }, (err, user) => {
this.__id = user._id;
});
} else if (otherId) { // If a otherId is provided
Users.findOne({ otherId }, (err, user) => {
console.log(1); // Debug, you'll see later
this.__id = user._id;
});
}
}
// Returns this.__id (which should have been initialized in the constructor)
get _id() {
console.log(2)
return this.__id;
}
}
When run, the console returns
2
1
I think you got the problem: the mongo callback in the constructor gets on after _id operates. How could I manage that, since the constructor gets activated each time the class is used?

It's not entirely clear to me, what exactly you want to happen and how you use this class but I assume you want to instantiate it and then be able to get _id instantaneously. If it's not the case, you may still get some useful info from my answer. Feel free to provide more details, I will update it.
So mongodb operations are asynchronous, if you do
const cursor = new UserCursor(...)
console.log(cursor._id)
(I assume you want this), first all operations in this thread will run, including the call to get _id(), then the callback code will. The problem with such asynchronous things is that now to use this _id you will have to make all of your code asynchronous as well.
So you will need to store a Promise that resolves with _id from mongodb and make a method getId that returns this promise like this:
private __id: Promise<object>
constructor(...) {
// ...
if(_id) {
this.__id = new Promise((resolve, reject) => {
Users.findOne({ _id }, (err, user) => {
if(err) return reject(err)
resolve(user._id)
});
})
} else {
// Same here
}
}
public getId() {
return this.__id;
}
Then use it:
const cursor = new UserCursor(...)
cursor.getId().then(_id => {
// Do smth with _id
})
Or
async function doStuff() {
const cursor = new UserCursor()
const _id = await cursor.getId()
}
doStuff()
If you now do it inside some function, you'll also have to make that function async
Also you could leave a getter like you have now, that will return a promise, but I find it less readable than getId():
cursor._id.then(_id => { ... })
const _id = await cursor._id

Related

nodejs mongodb - how to return the inserted document using insertOne

I am using "mongodb": "^3.1.6",.
I have a method using the drivers insertOne method (shops is my mongoDb database collection):
/**
* Adds a new shop to the shops collection
* #param {Shop} doc - the new shop to add
*/
static async addShop(shop) {
try {
return await shops.insertOne(shop, {}, (err, result) => {
if (err) {
throw e
}
return result
})
// TODO this should return the new shop
} catch (e) {
console.error(`Something went wrong in addShop : ${e}`)
throw e
}
}
Now the method inserts the document into the collection as expected, but does not return the insert result. How do I return the result value of the callback function?
For reference - I wrote this unit test that I want to get to pass:
test("addShop returns the added shop", async () => {
const testShop = {
name: "Test shop for jest unit tests",
}
const newShop = await ShopsDAO.addShop(testShop)
const shoppingCart = global.DBClient.db(process.env.NS)
const collection = shoppingCart.collection("shops")
expect(newShop.name).toEqual(testShop.name)
await collection.remove({ name: testShop.name })
})
Thanks for the help.
I suggest you not to mix promises and callbacks as it is a bad way to organize your code. According to docs, if you do not pass callback insertOne will return Promise. So I suggest you to rewrite function smth like that:
static async addShop(shop) {
try {
return shops.insertOne(shop)
} catch (insertError) {
console.error(`Something went wrong in addShop : ${insertError}`)
throw insertError
}
}
This addShop will return promise, use await to retrieve data from it:
const newShop = await ShopsDAO.addShop(testShop)
P.S. You can also omit await with return statement

Usage of exec() method on create query

I have a situation where I want to put all the business logic and callbacks in one place and mongoose queries in one place. So I'm making use of .exec() method for that purpose and handle its callback in service module. I'm successful with find query with exec()
repository module:
const findAUser = userName => {
return Users.findOne({username: userName});
}
Service Module
repository.findAUser(user.username).exec((error, document) => {
console.log(document);
if(error) {
rejectGeneric(reject);
} else {
..............................
But i'm not able to achieve the same with create query of mongoose
const createAUser = user => {
return Users.create(user);
}
And the below code doesn't work
repository.createAUser(user).exec((error, document) => {
....................................
}
How to use exec() method on mongoose create query? Is there any way to achieve this?
I have solved this by using callback option, and my resolution looks like this:
repository
const createAUser = (user, callback) => {
return Users.create(user, (error, document) => callback(error, document));
}
Service
repository.createAUser(user, (error, document) => {
if(error) {
// Do stuff for error handling
} else {
// Do stuff for success scenario
}
});

How do I use promises and loops together without promise.defer?

I'm fairly new to promises, and I'm having a problem avoiding some of the things I see described as promise anti-patterns (like Q.defer()). I have a loop that is mostly synchronous, but may occasionally need to make an asynchronous call. The code was very simple before I added the asynchronous calls, but the changes I had to make in order to keep it working with asynchronous calls is very messy.
I would like some advice on how to refactor my code. The code is trying to take selected properties of one object and add them to another. A simplified version is as follows:
function messyFunction(user, fieldArray) {
return Promise.fcall(() => {
var deferred = Promise.defer();
if (!fieldArray) {
deferred.resolve(user);
} else {
var temp = {};
var fieldsMapped = 0;
for (var i = 0; i < fieldArray.length; i++) {
var field = fieldArray[i];
if (field === 'specialValue') {
doSomethingAsync().then((result) => {
temp.specialField = result;
fieldsMapped++;
if (fieldsMapped === fieldArray.length) {
deferred.resolve(temp);
}
});
} else {
temp[field] = user[field];
fieldsMapped++;
if (fieldsMapped === fieldArray.length) {
deferred.resolve(temp);
}
}
}
}
return deferred.promise;
});
}
Here is how I would refactor it. I'm not super familiar with q, so I just used native Promises, but the principal should be the same.
To hopefully make things more clear, I've de-generalized your code a bit to turn messyFunction into getUserFields, asynchronously calculating the age.
Instead of using a for loop and using a counter to keep track of how many fields have been collected, I put them in an array and then pass it into Promise.all. Once all of the values for the fields are collected, the then on the Promise.all promise resolves to the new object.
// obviously an age can be calculated synchronously, this is just an example
// of a function that might be asynchronous.
let getAge = (user) => {
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve((new Date()).getFullYear() - user.birthYear);
}, 100);
});
};
function getUserFields(user, fieldArray) {
if (!fieldArray) {
// no field filtering/generating, just return the object as
// a resolved promise.
return Promise.resolve(user);
// Note: If you want to make sure this is copy of the data,
// not just a reference to the original object you could instead
// use Object.assign to return it:
//
// return Promise.resolve(Object.assign({}, user));
} else {
// Each time we grab a field, we are either store it in the array right away
// if it is synchronous, if it is asyncronous, create a thenable representing
// its eventual value and store that.
// This array will hold them so we can later pass them into Promise.all
let fieldData = [];
// loop over all the fields we want to collect
fieldArray.forEach(fieldName => {
// our special 'age' field doesn't exist on the object but can
// be generated using the .birthYear
if (fieldName === 'age') {
// getAge returns a promise, we attach a then to it and store
// that then in fieldData array
let ageThenable = getAge(user).then((result) => {
// returning a value inside of then will resolve this
// then to that value
return [fieldName, result];
});
// add our thenable to the promise array
fieldData.push(ageThenable);
} else {
// if we don't need to do anything asyncronous, just add the field info
// to the array
fieldData.push([fieldName, user[fieldName]]);;
}
});
// Promise.all will wait until all of the thenables to be resolved before
// firing it's then. This means if none of the fields we were looking for
// is asyncronous, it will fire right away. If some of them were
// asyncronous, it will wait until they all return a value before firing.
// We are returning the then, which will transform the collected data into
// an object.
return Promise.all(fieldData).then(fields => {
// fields is an array of 2-element arrays containing the field name
// and field value for each field we are collecting. We will loop over
// this array with reduce and transform them into an object containing
// all of the properties we collected.
let newUserObj = fields.reduce((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
// Above I am using destructuring to get the key/value if the browsers
// you are targeting don't support destructuring, you can instead just
// give it a name as an array and then get the key/value from that array:
//
// let newUserObj = fields.reduce((acc, fieldData) => {
// let key = fieldData[0],
// value = fieldData[1];
// acc[key] = value;
// return acc;
// }, {});
// return new object we created to resolve the then we returned
return newUserObj;
});
}
}
let users = [
{
name: 'Tom',
birthYear: 1986
},
{
name: 'Dick',
birthYear: 1976
},
{
name: 'Harry',
birthYear: 1997
}
];
// generate a new user object with an age field
getUserFields(users[0], ['name', 'birthYear', 'age']).then(user => {
console.dir(user);
});
// generate a new user object with just the name
getUserFields(users[1], ['name']).then(user => {
console.dir(user);
});
// just get the user info with no filtering or generated properties
getUserFields(users[2]).then(user => {
console.dir(user);
});

Sequelize update

I'm trying to update a model in Sequelize using the following code:
exports.updateItem = function(item) {
return new Promise((fulfill, reject) => {
models.TimesheetItem.update(item,{where: {id: item.id}})
.then(fulfill)
.catch(console.dir);
});
};
Where item is the result of doing models.TimeSheetItem.find()
The call never executes the .then and instead passes an empty object to the .catch.
I've looked over the documentation and it seems that this is the way to update a row, but I can't get it to work. What am I doing wrong?
Thank you!
According to the documentation the update method takes two parameters - first one is values which will be used to perform the update, and second one is options - so in your case this is the where clause. If you want to perform an update on a single instance only, you can do it in two ways - use Model.update() method, which can update multiple instances of this model at once matching the where clause, or perform instance.update() in order to update only the single instance. First option will look like that:
let updateValues = { name: 'changed name' };
models.Model.update(updateValues, { where: { id: 1 } }).then((result) => {
// here your result is simply an array with number of affected rows
console.log(result);
// [ 1 ]
});
The first option is not very useful when you want to update only single instance. So that is why there is a possibility of performing update() on Sequelize model instance
let updateValues = { name: 'changed name' };
instance.update(updateValues).then((self) => {
// here self is your instance, but updated
});
In your case, if the item parameter is a Sequelize model instance (not a plain javascript JSON object), your update function could be like that
exports.updateItem = function(item){
return item.update(values).then((self) => {
return self;
}).catch(e => {
console.log(e);
});
};
However, if the item is not a sequelize model instance but only a plain object with values you want to update, it could be done in two ways - first is to use Model.update() (just like you did), or second one is to retrieve TimesheetItem with id = item.id and then perform instance.update() as shown above
exports.updateItem = function(item){
models.TimesheetItem.update(item, { where: { id: item.id } }).then((result) => {
// here result will be [ 1 ], if the id column is unique in your table
// the problem is that you can't return updated instance, you would have to retrieve it from database once again
return result;
}).catch(e => {
console.log(e);
});
};
Or the second option with returning instance and performing update on it
exports.updateItem = function(item) {
return models.TimesheetItem.findById(item.id).then((itemInstance) => {
return itemIstance.update(item).then((self) => {
return self;
});
}).catch(e => {
console.log(e);
});
}
The difference is that you do not need to create and return your own Promise - sequelize methods like update() return promises by themselves.
{ error: type "where" does not exist}
Just an update to the solution, Sequelize now gives the error above when you include the 'where' option(as shown below). So take it out and it should work perfectly.
exports.updateItem = function(item){
models.TimesheetItem.update(item, { id: item.id }).then((result) => {
// here result will be [ 1 ], if the id column is unique in your table
// the problem is that you can't return updated instance, you would have to retrieve it from database once again
return result;
}).catch(e => {
console.log(e);
});
};

sharing a transaction or task object between multiple queries across multiple controllers in pg-promise

I am relatively new to node.js, postgresql, promises and infact stackoverflow so apologies in advance if things sound a little disjointed!
I am currently trying to run multiple queries within chained promises spread across various controllers. I want to run all the queries within the same transaction or task to eliminate multiple connects and disconnects to the database.
I have tried the following where I am adding a student and assigning two mentors for that student. The HTTP request is routed to the student controller which adds a student via the student repository. The student repository is where the task starts and is returned to the controller which forwards it to the mentor controller and along the chain it goes...
# HttpPost("/api/students/create")
addStudent( # Req()request) {
var studentid;
var mentorids= [];
//Add the student
return this.studentRepository.addStudent(request.body.student)
.then(newStudentId => {
studentid = newStudentId;
//Add the mentors, passing the transaction object received back from
studentRepository
return this.mentorController.addMentor(request.body.schoolid, request.body.mentors, newStudentId.transaction)
.then(result => {
var data = [];
console.log(result);
for (var role in result) {
data.push({
mentorid: result[role].roleid,
studentid: studentid
});
}
//Assigns the student to mentors in the link table
return this.studentRepository.assignMentor(data)
.then(result => {
return result;
})
})
});
}
Student repository
addStudent(student): any {
return this.collection.task(t => {
return this.collection.one(this.sql.addStudent, student)
.then(studentid => {
return {
studentid: studentid.studentid,
transaction: t
}
});
})
}
Mentor controller
addMentor(institutionid: number, mentors, t): any {
var promises = [];
var mentorIds = [];
for (var role in mentors) {
promises.push(this.roleController.registerRole(institutionid,mentors[role].role,t));
}
return t.batch(promises)
.then(result => {
return Promise.resolve(result);
})
}
Role controller
# HttpPost("/api/roles/register")
registerRole(institutionid, # Req()request, t ? ) : any {
console.log(request);
return this.roleRepository.checkRoleEnrollment(institutionid, request.email, request.roletype, t)
.then(result => {
return this.roleRepository.addRoleEnrollment(institutionid, request, t)
.then(data => {
return this.roleRepository.updateRoleEnrollment(data.roleenrollmentid, data.roleid)
.then(d => {
return data;
})
})
})
.catch (error => {
return Promise.reject(error);
});
}
I am getting the following error when I call checkEnrollment in the Role Controller:
"name": "Error",
"message": "Unexpected call outside of task.",
"stack": "Error: Unexpected call outside of task. at Task.query
(\api\node_modules\pg-promise\lib\task.js:118:19)
at Task.obj.oneOrNone (\api\node_modules\pg-promise\lib\database.js:491:31)
at RoleRepository.checkRoleEnrollment....
Any help would be much appreciated. Thanking you in advance.
As per my earlier comment:
That error means you are trying to access connection t allocated by a task somewhere outside of the task's callback function, i.e. the task's callback has returned, the connection was released, and then you are using the connection object allocated by the task from somewhere else, which is, of course, invalid.
b.t.w. I'm the author of pg-promise ;)
Below is what your code effectively doing, in a simplified form:
var cnReference;
db.task(t => {
cnReference = t;
// can only use `t` connection while executing the callback
})
.then(data => {
// we are now outside of the task;
// the task's connection has been closed already,
// and we can do nothing with it anymore!
return cnReference.query('SELECT...');
})
.catch(error => {
// ERROR: Unexpected call outside of task.
// We cannot use a task connection object outside of the task's callback!
});
You need to correct the implementation to make sure this doesn't happen.

Resources