JS - Knex, Pass function to Transaction - javascript

I created a node.js application that uses the knex library to make database operations. The database is Microsoft SQL Server. I created a script called db.js that returns the knex object, and i have a controller.js script that makes the actually needed database operations. I have all the operations wrapped inside a translation statement, which brings me to my question. What i would like to do, is pass in a parameter that tells the transaction to rollback or commit. However, whenever i try to pass this function in, it just fails. Does anyone know if this feature achievable ? I was able to do this feature with the catch function.
I don't think its db specific, so anyone can download knex, hook it up a db and give it a shot with the code below.
example.js
/**
* #param {userID} Int
* #param {rollback} Boolean
*/
const getUsers = (userID, rollback) => {
// Using tran as a transaction object:
return db('master').transaction((tran) => {
db('master')
.select()
.from('users')
.where({ 'user_id': userID })
.transacting(tran)
.then(tran.rollback) // Works
// .then(transact(tran, rollback)) throws error
.catch((error) => {
logError(error, tran); // Works
});
// .catch(tran.rollback);
});
};
const logError = (error, transaction) => {
transaction.rollback;
console.log('transaction error: ',error);
console.log('transaction log: ',transaction);
};
const transact = (transaction, rollback) => {
try {
if (rollback) return transaction.rollback;
else return transaction.commit;
} catch (error) {
console.log(error);
}
};
const user = await getUsers(1, true); // error is thrown
assert.strictEqual(user.constructor === Array, true);
assert.strictEqual(user.length == 0, true);
Error Message
Error: the array [ {
"user_id": 1
"user_name": "JonnyBoy"
"zip": 1200
"email": "jjboy#test.com"
} ] was thrown, throw an Error :)

then takes a function as its first parameter. If the Promise resolves, the function that's passed to then is called with the value the Promise resolved to. The rollback property on the transaction object is a function so you can write:
someQuery().then(trans.rollback)
and rollback will be called when someQuery resolves. You could also write:
someQuery.then(result => trans.rollback(result))
These statements are equivalent.
That said, minimally, there's two changes that you need to make. One, fix how you're calling transact. For example:
.then(() => transact(tran, rollback))
And change how transact calls rollback:
const transact = (trx, rollback) => {
const action = rollback ? trx.rollback : trx.commit
return action()
}
Also bear in mind that rollback itself will return a rejected Promise with a generic error. You can pass a custom error to rollback and it will reject with that error instead.

Related

Node throws UnhandledPromiseRejectionWarning for Mongoose requests using Promise.all with .catch() statement

I'm new to Node/Mongoose and am trying to handle errors correctly in a script to add players to a league. In the below code, explicitly thrown and non-Promise related errors are caught correctly by the .catch() statement, but rejected Promises are not.
For example, trying to pass an invalid userID throws User not found.
But if I test Promise rejection by disconnecting the database, I get the following:
(node:6252) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): MongoNetworkError: failed to connect to server [localhost:27017] on first connect [MongoNetworkError: connect ECONNREFUSED 127.0.0.1:27017]
Am I using Promise.all() and .catch() incorrectly somehow?
Just to be clear, I'm trying to work out why the error isn't being handled, not why the error is being thrown.
My script:
const
mongoose = require('mongoose'),
User = require('./models/users'),
League = require('./models/leagues'),
dbUrl = process.env.DBURL || 'mongodb://localhost/predictor';
mongoose.connect(dbUrl, { useNewUrlParser: true });
const addUserToLeague = (userId, leagueId) => {
let foundUser = User.findById(userId);
let foundLeague = League.findById(leagueId);
return Promise.all([foundUser, foundLeague])
.then(arr => {
if(!arr[0]){
throw 'User not found';
}else if(!arr[1]){
throw 'League not found';
}
return arr;
})
.then(arr => {
arr[0].leagueMemberships.push(arr[1]);
arr[1].users.push(arr[0]);
return arr;
})
.then(updatedArr => {
updatedArr[0].save();
updatedArr[1].save();
return updatedArr;
})
.then(updatedArr => { console.log(`User ${updatedArr[0]._id} added to league ${updatedArr[1]._id}`) })
.catch(err => { console.log('Error:', err) });
};
addUserToLeague(process.argv[2], process.argv[3]); // Needs 2 args: User ID and League ID
As Bergi pointed out, the error would appear to be from connect, which returns a promise that you're not handling at all — including not waiting for it to finish. So at a minimum, you need to handle that:
const connectionPromise = mongoose.connect(dbUrl, { useNewUrlParser: true })
.catch(error => {
// Handle connection error
});
Then in addUserToLeague:
const addUserToLeague = (userId, leagueId) => {
return connectionPromise.then(connection => {
// ...logic here
});
};
...but, I question whether you should be connecting when the module is loaded like that, rather than passing a connection into addUserToLeague.
Aside from that, the actual use of Promise.all is okay, but:
One hopes that findById doesn't resolve the promise with a falsy value if the item isn't found, so that whole first then handler seems unnecessary.
Presumably save returns a promise. You're not handling rejection or waiting for resolution of those if so.
I'd use destructuring to avoid arr[0] and arr[1], as it's easy to forget the order.
There's no reason for the then handler with the push calls to be separate from the then handler doing the saving.
addUserToLeague should return the result of the promise chain, so that code calling it A) Knows when it's finished, and B) Knows when it fails.
Errors shouldn't be handled in addUserToLeague; instead, handle them in its caller.
There's also the issue that the data is denormalized: You're storing the membership information in both the user object and the league object. Maybe that's relatively normal in document databases (I wouldn't know); in an RDBMS you'd store the information in a single place. The reason is clear from the code in addUserToLeague: What if saving the user succeeds but saving the league fails? Then the user object says it's a member of a league the league object doesn't say it's a member of. There's also the problem that since it's stored in two places, even if nothing goes wrong, for a brief period one of the (the user or the league) will have been saved but the other won't have been. Both are integrity problems. If you can normalize it to storing this information in one place, that would be good. If you can't, you need to update the code so that it saves one of them, waits for that to succeed, saves the other, and if that fails attempts to undo the change to the first.
Something like this (I don't attempt to address the normalization issue here, that's a big picture thing):
const
mongoose = require('mongoose'),
User = require('./models/users'),
League = require('./models/leagues'),
dbUrl = process.env.DBURL || 'mongodb://localhost/predictor';
const addUserToLeague = (connection, userId, leagueId) => {
return Promise.all([
User.findById(userId),
League.findById(leagueId)
])
.then(([user, league]) => {
user.leagueMemberships.push(league);
league.users.push(user);
return Promise.all([user.save(), league.save()]);
})
.then((([user, league]) => {
console.log(`User ${user._id} added to league ${league._id}`);
});
};
mongoose.connect(dbUrl, { useNewUrlParser: true })
.then(connection => addUserToLeague(connection, process.argv[2], process.argv[3]) // Needs 2 args: User ID and League ID
.catch(error => {
// Handle/report error
});
If you're using any recent verson of Node, you can use an async function:
const
mongoose = require('mongoose'),
User = require('./models/users'),
League = require('./models/leagues'),
dbUrl = process.env.DBURL || 'mongodb://localhost/predictor';
const addUserToLeague = async (connection, userId, leagueId) => {
let [user, league] = await Promise.all([
User.findById(userId),
League.findById(leagueId)
]);
user.leagueMemberships.push(league);
league.users.push(user);
[user, league] = await Promise.all([user.save(), league.save()]);
console.log(`User ${user._id} added to league ${league._id}`);
};
mongoose.connect(dbUrl, { useNewUrlParser: true })
.then(connection => addUserToLeague(connection, process.argv[2], process.argv[3]) // Needs 2 args: User ID and League ID
.catch(error => {
// Handle/report error
});

Is creating a new promise with a async function call bad practice?

Snippets are from a node.js and mongoDB CRUD application.Github repo for full code. The code is working fine but unsure if my structure and use of promises and async await are bad practice.
handlers._newbies = {};
handlers._newbies.post = (parsedReq, res) => {
const newbie = JSON.parse(parsedReq.payload);
databaseCalls.create(newbie)
.then((result) => {
res.writeHead(200,{'Content-Type' : 'application/json'});
const resultToString = JSON.stringify(result.ops[0]);
res.write(resultToString);
res.end();
})
.catch(err => console.log(err));
};
const databaseCalls = {};
databaseCalls.create = (newbie) => {
return new Promise(async (resolve, reject) => {
try {
const client = await MongoClient.connect('mongodb://localhost:27017', { useNewUrlParser: true });
console.log("Connected correctly to server");
const db = client.db('Noob-List');
const result = await db.collection('newbies').insertOne(newbie);
client.close();
resolve(result);
} catch(err) {
console.log(err);
}
});
};
When the node server gets a POST request with the JSON payload, it calls the handlers._newbies.post handler which takes the payload and passed it to the
const newbie = JSON.parse(parsedReq.payload);
databaseCalls.create(newbie)
call. I want this database call to return a promise that holds the result of the db.collection('newbies').insertOne(newbie);
call. I was having trouble doing this with just returning the promise returned by the insertOne because after returning I cant call client.close();.
Again maybe what I have done here is fine but I haven't found anything online about creating promises with promises in them. Thank you for your time let me know what is unclear with my question.
It is considered an anti-pattern to be wrapping an existing promise in a manually created promise because there's just no reason to do so and it creates many an opportunities for error, particular in error handling.
And, in your case, you have several error handling issues.
If you get an error anywhere in your database code, you never resolve or reject the promise you are creating. This is a classic problem with the anti-pattern.
If you get an error after opening the DB, you don't close the DB
You don't communicate back an error to the caller.
Here's how you can do your .create() function without the anti-pattern and without the above problems:
databaseCalls.create = async function(newbie) {
let client;
try {
client = await MongoClient.connect('mongodb://localhost:27017', { useNewUrlParser: true });
console.log("Connected correctly to server");
const db = client.db('Noob-List');
return db.collection('newbies').insertOne(newbie);
} catch(err) {
// log error, but still reject the promise
console.log(err);
throw err;
} finally {
// clean up any open database
if (client) {
client.close();
}
}
}
Then, you would use this like:
databaseCalls.create(something).then(result => {
console.log("succeeded");'
}).catch(err => {
console.log(err);
});
FYI, I also modified some other things:
The database connection is closed, even in error conditions
The function returns a promise which is resolved with the result of .insertOne() (if there is a meaningful result there)
If there's an error, the returned promise is rejected with that error
Not particularly relevant to your issue with promises, but you will generally not want to open and close the DB connection on every operation. You can either use one lasting connection or create a pool of connections where you can fetch one from the pool and then put it back in the pool when done (most DBs have that type of feature for server-side work).

my graphql server mutation return null value

I am having challenges retrieving the results of my mutation. I need to create a db record and send an email notifying to user that the registration was successful. since both the sending of the email and the db update is server side I want to do both in the same mutation. If the email message fail the db must not be updated. So I have the following Mutation:
Mutation: {
createDealer(_, params) {
console.log("params: " + JSON.stringify(params));
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash(params.dealer.password, salt, function(err, hash) {
// Store hash in your password DB.
console.log("hashed password " + params.dealer.password)
params.dealer.password = hash;
console.log("hashed password " + params.dealer.password + " Hash: " + hash);
let session = driver.session();
let query = "CREATE (d:Dealer {email:$dealer.email}) SET d += $dealer RETURN d";
let here = "here".link("mymail#example.com");
let messageObj = {
to: params.dealer.email,
subject: 'Dealer Registration',
text: `Thank you for signing up. To complete and activate your registration please click ${here}.`
}
return (sendEmail(messageObj))
.then(data => {
console.log('SendMail data....' + JSON.stringify(data));
return session.run(query, params)
})
.then(result => {
console.log('SendNeo4j data....' + JSON.stringify(result));
return result.records[0].get("d").properties
})
.catch((err) => {
console.log(err);
});
//});
});
}); // genSalt
} // Create Dealer
}, // Mutation
Even thought both actions are successful I can't seem to retrieve the results. I get 'undefined' for:
console.log('SendMail data....' + JSON.stringify(data));
while
console.log('SendNeo4j data....' + JSON.stringify(result));
does display the correct data
but graphiql returns 'null' for the mutate.
this is the graphiql mutation:
mutation CreateDealer($dealer: DealerInput!) {
createDealer(dealer: $dealer) {
email
name
}
}
with the DealerInput variables of course.
I have read where you can retrieve multiple results from a query/mutation but I am not sure how it works. Here I need both the results of the sendEmail and the db update for my Angular/apollo front-end....I would imaging graphiql knows nothing of the sendEmail but I expected it to return the properties I requested.
SendEmail:
module.exports = (message) =>
new Promise((resolve, reject) => {
const data = {
from: 'mymail#example.com',
to: message.to,
subject: message.subject,
text: message.text
};
mailgun.messages().send(data, (error) => {
if (error) {
return reject(error);
}
return resolve();
});
});
Can someone with a little more experience than I help me out here...thanks
Couple of things to fix here. Returning a Promise (or any other value) inside a callback doesn't do anything, and doing so won't let you chain additional Promises like you want. Instead, your promise gets fired off inside the callback and isn't awaited.
As a general rule of thumb, don't mix Promises and callbacks. If you absolutely have to use callbacks, always wrap the callback in a Promise (like you did inside sendMail). Luckily, most popular libraries today support both callbacks and Promises. Here's how you could refactor the code above to correctly chain all your Promises:
createDealer(_, params) {
return bcrypt.hash(params.dealer.password, 10) // note the return here!
.then(hash => {
params.dealer.password = hash
const session = driver.session()
const query = "CREATE (d:Dealer {email:$dealer.email}) SET d += $dealer RETURN d"
const here = "here".link("mymail#example.com")
const messageObj = {
to: params.dealer.email,
subject: 'Dealer Registration',
text: `Thank you for signing up. To complete and activate your registration please click ${here}.`
}
return sendEmail(messageObj) // note the return here!
}).then(data => {
return session.run(query, params) // note the return here!
}).then(result => {
result.records[0].get("d").properties // note the return here!
})
bcrypt.hash will autogenerate the salt for you if you don't pass one in -- there's no need to call two separate functions
We kick off our Promise chain with bcrypt.hash, so we need to return the Promise it returns. A resolver must return a value or a Promise that will resolve to a value, otherwise it returns null.
Inside each then, we return a Promise. This way we "chain" our Promises, allowing the final value we return in the resolver to be the value the very last Promise in the chain resolves to.
We need to also fix your sendMail function to actually return the value. You're correctly returning the new Promise inside the function, but you also need to pass the returned data object to resolve. That tells the Promise to resolve to that value.
module.exports = (message) => new Promise((resolve, reject) => {
const data = // ...etc
mailgun.messages().send(data, (error) => {
if (error) reject(error) // no need to return, it's pointless
resolve(data) // pass data to resolve
})
})
Side note: looks like the official mailgun library supports Promises.
Additionally, I would strongly encourage you to look into using async/await, especially when dealing with a long Promise chain. It's less error prone and more readable:
createDealer async (_, params) {
const hash = await bcrypt.hash(params.dealer.password)
params.dealer.password = hash
const session = driver.session()
const query = "CREATE (d:Dealer {email:$dealer.email}) SET d += $dealer RETURN d"
const here = "here".link("mymail#example.com")
const messageObj = {
to: params.dealer.email,
subject: 'Dealer Registration',
text: `Thank you for signing up. To complete and activate your registration please click ${here}.`
}
const emailResult = await sendEmail(messageObj)
const result = await session.run(query, params)
return result.records[0].get("d").properties // still need to return!
}
EDIT: With regard to catching errors, GraphQL will catch any errors thrown by your resolver, which means you can often skip using catch yourself. For example, if your mailgun request fails, it'll generate some kind of error and your query will return null for data and the error details inside of the errors array.
That may be sufficient, although 1) you may want to log your error's stack elsewhere; and 2) in production, you probably don't want to expose internal error details to the public.
That means you'll probably want to use custom errors. As a bonus, you can add some custom properties to your errors to help the client deal with them eloquently. So your code may end up looking more like this:
class DeliveryFailureError extends Error {}
DeliveryFailureError.code = 'DELIVERY_FAILURE'
DeliveryFailureError.message = 'Sorry, we could not deliver the email to your account'
try {
await mailgun.messages.create()
} catch (err) {
logger.error('Mailgun request failed:', err.stack)
throw new DeliveryFailureError()
}

How to send value from Promise 'then' to 'catch'?

I just want to ask how should I pass the resolve promise to catch if the value on the resolve is not intended.
e.g.
let prom = getPromise();
prom.then(value => {
if (value.notIWant) {
// Send to catch <-- my question is here, I want to pass it on the catch.
}
// Process data.
}).catch(err => {
// Pass the error through ipc using json, for logging.
});
I tried to using throw but the object cant be parsed to json and just got an empty object.
ANSWER:
#BohdanKhodakivskyi first comment below is the answer I want.
#31py answer is also correct but the #BohdanKhodakivskyi solution is much simplier and will render the same result.
Simply use throw value;. In your case:
prom.then(value => {
if (value.notIWant) {
// Send to catch
throw value;
}
// Process data.
}).catch(err => {
// Pass the error through ipc using json, for logging.
});
Please also note the difference and limitations between using Promise.reject() and throw which is perfectly described in this question. For example, throw will not work in some async scenarios.
You can simply return a rejected promise:
prom.then(value => {
if (value.notIWant) {
return Promise.reject('your custom error or object');
}
// Process data.
}).catch(err => {
console.log(err); // prints 'your custom error or object'
});
.catch actually handles any promise rejection in the chain, so if you're returning a rejected promise, the control automatically flows to catch.
why you just not rethrow the error? throw new Error("something");
You can use outside functions to do it:
var processData = function(data) {
// process data here
}
var logIt = function(data) {
// do logging here..
}
let prom = getPromise();
prom.then(value => {
if (value.notIWant) {
// Send to catch <-- my question is here, I want to pass it on the catch.
logIt(/*pass any thing*/);
}
// Process data.
processData(data);
}).catch(err => {
logIt(/*pass any thing*/);
});

FeathersJS - How to cancel hook on error

In my registration service I've created a 'before' hook to check if the user is already registered to a tournament. If the user is found to be registered, I have it throwing an error.
Per feathersjs documentation:
If a promise fails, the error will be propagated immediately and will
exit out of the promise chain.
The problem I'm having is the error is being "triggered" and showing the user has already registered, but the registration still takes place. Meaning, the hook is never "exited".
Here is my function
exports.checkRegistered = () => {
return (hook) => {
let tourneyId = hook.data.tourneyId
let userId = hook.data.userId
return hook.service.find({
query: {
tourneyId: tourneyId,
userId: userId
}
}).then(reg => {
// User already registered
if (reg.data.length != 0) {
// Also tried to return a promise with same result.
// return Promise.reject(new Error('Already registered.'))
throw new Error('Already registered');
}
}).catch(err => {})
}
}
Here is my "before hook" object
exports.before = {
create: [
api.checkRegistered()
]
};
The problem is the .catch(err => {}). It will return a promise that just resolves successfully with undefined which will then continue the hook chain as if no error happened. Removing it should fix your issue.

Categories