I'm creating a rest api for CRUD operations using Sequelize and MySql. I'm using a controller to run an update on a PATCH request to update fields of a product. It technically works, but I feel like there is a more elegant way to handle this.
Sequelize's update method will return an array of objects depending on the results. Array[0] is the number of rows affected by the update (should just be one in my case, as I'm updating by id). Array[1] will return an object with details about the update as well as all the old values and new values. Here's how I'm handling that currently:
//products.controller.js
//Update a single product using id (PUT/PATCH)
const patch = (req, res) => {
const id = req.params.id;
Product.update(req.body, { where: { id }, individualHooks: true })
.then((rowsAffected) => {
//Item not found
if (Object.entries(rowsAffected[1]).length === 0) {
res.status(404).send({
success: false,
status: 404, //Not found
message: `Product with id ${id} not found. Update failed.`,
});
return;
}
//if rowsAffected[0] === 1 then success
if (rowsAffected[0] === 1) { //row changed
res.status(200).send({
success: true,
status: 200,
message: `Product updated.`,
id: id,
payload: req.body,
});
} else {
// if rowsAffected[0] !== 1 then it failed.
res.status(200).send({
success: false,
status: 200, //Not Modified
message: `No fields have changed. Product not updated.`,
});
}
})
.catch((err) => {
res.status(500).send({
success: false,
status: 500,
message:
err.message || "Something went wrong while updating the product.",
});
});
}
As you can see, first I'm checking to see if the the update function returns the product details (meaning it successfully found it in the database). If not then sending 404. Then I check the affected rows. If 1 then success, if 0 then nothing changed. Finally I'm catching any server errors.
I feel like there is a better way rather than having to break down the update function's return (like Object.entries(rowsAffected[1]).length === 0)
This is ok if this is the only way you can check the effects of the update. What I can suggest is putting an abstraction above it.
First thing that checking (rowsAffected[0] === 1) does not make much sense, since the update is idempotent and you end up with the same resource state no matter what the actual values are. If you insist, then I would not pair success: false with a 200 ok status, because failure is failure and it requires an error message and 4xx or 5xx status. So either delete it or convert it into a proper error. Hard to find such a status code, but maybe using 409 conflict is ok in these cases https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409 though I would just remove this part of the code. I keep it for the sake of the example.
As of the success and status properties in the body, they don't make much sense either, because they travel in the header, and it is evident from the HTTP standard that 2xx means success, 4xx and 5xx means error. So I would remove those too.
If you don't want to support detailed error codes and exception types and parameters, then just send the error messages and the body can be even a string instead of an object.
Sending the err.message to the consumers is a bad idea by unexpected errors. You don't know what you send out. You need to log them and send something general instead. Communicating errors is always a higher abstraction level stuff, many times. As of the Product with id ${id} not found. Update failed. here adding the id is not necessary, because the request contains it.
So atm. the code looks like this:
const patch = (req, res) => {
const id = req.params.id;
Product.update(req.body, { where: { id }, individualHooks: true })
.then((rowsAffected) => {
if (Object.entries(rowsAffected[1]).length === 0) {
res.status(404).send({message: `Product not found. Update failed.`});
return;
}
//if rowsAffected[0] === 1 then success
if (rowsAffected[0] === 1) { //row changed
res.status(200).send({
message: `Product updated.`,
id: id,
payload: req.body,
});
} else {
res.status(409).send({message: "No fields have changed. Product not updated."});
}
})
.catch((err) => {
res.status(500).send({message: "Something went wrong while updating the product."});
});
}
We can go further by mapping status codes to status messages and extracting the possibly repeating parts of the story into separate functions.
const patch = (req, res) => {
const id = req.params.id;
const statusMessages = {
200: "Product updated."
404: "Product not found. Update failed."
409: "No fields have changed. Product not updated.",
500: "Something went wrong while updating the product."
};
Product.update(req.body, { where: { id }, individualHooks: true })
.then(updateStatusVerification)
.then(successHandler(res, statusMessages, () => {
return {
id: id,
payload: req.body,
};
}))
.catch(apiErrorHandler(res, statusMessages));
}
function successHandler(res, statusMessages, callback){
return function (){
let body = callback();
body.message = statusMessages[200];
res.status(200).send(body);
};
}
function apiErrorHandler(res, statusMessages){
return function (err){
let statusCode = 500;
if (err instanceof NotFoundError)
statusCode = 404;
else if (err instanceof NotUpdatedError)
statusCode = 409;
res.status(statusCode).send({
message: statusMessages[statusCode]
});
};
}
function updateStatusVerification(rowsAffected){
return new Promise((resolve, reject) => {
if (Object.entries(rowsAffected[1]).length === 0)
reject(new NotFoundError);
else if (rowsAffected[0] !== 1)
reject(new NotUpdatedError);
else
resolve();
});
}
class ApiError extends Error {}
class NotFoundError extends ApiError {}
class NotUpdatedError extends ApiError {}
We can move the status messages to the documentation. So you will end up with something like this and some utility functions:
const patch = (req, res) => {
const id = req.params.id;
statusMessages = docs.product.update.statusMessages;
Product.update(req.body, { where: { id }, individualHooks: true })
.then(updateStatusVerification)
.then(successHandler(res, statusMessages, () => {
return {
id: id,
payload: req.body,
};
}))
.catch(apiErrorHandler(res, statusMessages));
}
We can go even further if this is a frequent pattern:
const patch = (req, res) => {
const id = req.params.id;
handleUpdate(
Product.update(req.body, { where: { id }, individualHooks: true }),
() => {id: id, payload: req.body},
docs.product.update.statusMessages
);
}
function handleUpdate(dbUpdatePromise, successCallback, statusMessages){
dbUpdatePromise.then(updateStatusVerification)
.then(successHandler(res, statusMessages, successCallback))
.catch(apiErrorHandler(res, statusMessages));
}
So it can be as abstract as you like, it really depends on your needs and what the current usage allows. You can decide how many and what kind of layers you need based on actual use cases and repetitions.
Related
I am using a JS script to implement the Stripe payment provider on a website. The code mainly comes from this tutorial from Stripe. The full code can be found here.
Part of the script involves promises, which I am very unfamiliar with. Essentially, I need to go through a number of .then blocks to handle a customer's checkout and subscribe them to the service. When I run the code, I get the following error:
TypeError: Cannot read property 'subscription' of undefined
Below is the function causing the error, with the problematic .then block noted:
var createSubscription = async function ({ customerIdInput, paymentMethodIdInput, priceIdInput }) {
var handlePaymentThatRequiresCustomerAction = function ({
subscription,
invoice,
priceId,
paymentMethodId,
}) {
{
if (subscription && subscription.status === 'active') {
// Subscription is active, no customer actions required.
return { subscription, priceId, paymentMethodId };
}
// If it's a first payment attempt, the payment intent is on the subscription latest invoice.
// If it's a retry, the payment intent will be on the invoice itself.
let paymentIntent = invoice ? invoice.payment_intent : subscription.latest_invoice.payment_intent;
if (
paymentIntent.status === 'requires_action'
) {
return stripe
.confirmCardPayment(paymentIntent.client_secret, {
payment_method: paymentMethodId,
})
.then((result) => {
if (result.error) {
// Start code flow to handle updating the payment details.
// Display error message in your UI.
// The card was declined (i.e. insufficient funds, card has expired, etc).
throw result;
} else {
if (result.paymentIntent.status === 'succeeded') {
// Show a success message to your customer.
return {
priceId: priceId,
subscription: subscription,
invoice: invoice,
paymentMethodId: paymentMethodId,
};
}
}
})
.catch((error) => {
displayError(error);
});
} else {
// No customer action needed.
return { subscription, priceId, paymentMethodId };
}
} };
var handleRequiresPaymentMethod = function () {
};
var onSubscriptionComplete = function (result) {
if (result.subscription.status === 'active') {
orderComplete();
}
};
var subscriptionParams = {
"paymentMethodId": `${paymentMethodIdInput}`,
"customerId": `${customerIdInput}`,
"priceId": `${priceIdInput}`
};
await fetch('/create-subscription', {
method: 'post',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify(subscriptionParams),
})
.then((response) => {
return response.json()
})
// If the card is declined, display an error to the user.
.then((result) => {
if (result.error) {
showError(result);
// The card had an error when trying to attach it to a customer.
throw result;
}
var output = Promise.resolve(result);
return output;
})
// Normalize the result to contain the object returned by Stripe.
// Add the additional details we need.
.then((output) => {
return new Promise(function (resolve) {
resolve({
paymentMethodId: paymentMethodIdInput,
priceId: priceIdInput,
subscription: output,
});
});
})
// Some payment methods require a customer to be on session
// to complete the payment process. Check the status of the
// payment intent to handle these actions.
//THIS IS THE THEN BLOCK CAUSING THE ERROR
.then((value) => {
handlePaymentThatRequiresCustomerAction({
subscription: value.subscription,
invoice: value.subscription.latest_invoice,
priceId: value.priceId,
paymentMethodId: value.paymentMethodId,
});
})
// If attaching this card to a Customer object succeeds,
// but attempts to charge the customer fail, you
// get a requires_payment_method error.
.then(() => handleRequiresPaymentMethod())
// No more actions required. Provision your service for the user.
.then(() => {
onSubscriptionComplete();
})
.catch((error) => {
// An error has happened. Display the failure to the user here.
// We utilize the HTML element we created.
showError(error);
});
}
I'm 90% certain that the .then block preceding the error (below) is not fully executing before the problematic block tries to reference value.subscription, and that that is causing the error. However, I'm not sure how to fix that. I've tried using a Jasmine test to isolate the problem, but I've been unsuccessful in that avenue.
.then((output) => {
return new Promise(function (resolve) {
resolve({
paymentMethodId: paymentMethodIdInput,
priceId: priceIdInput,
subscription: output,
});
});
})
//THIS IS THE THEN BLOCK CAUSING THE ERROR
.then((value) => {
handlePaymentThatRequiresCustomerAction({
subscription: value.subscription,
invoice: value.subscription.latest_invoice,
priceId: value.priceId,
paymentMethodId: value.paymentMethodId,
});
})
What can I do to ensure that everything executes in the proper order and avoid this error? Thanks for your help.
UPDATE: as Zac stated in the comments, the issue was the OnSubscriptionComplete() function, which I neglected to pass a value to.
So basically, I'm trying to separate my code that handles data (mongoose) from my express Router code, since I might want to use it elsewhere too.
The first thing I did was, I got rid of the res.json() calls, since I don't want the code to only work returning a http response. I want it to return data, so I can then return that data from my router as a http response, but still use it as regular data elsewhere.
Here is a function I wrote to get data from mongoose.
module.exports.user_login = data => {
console.log(data);
ModelUser.findOne({email: data.email}).then(user => {
if(!user){
console.log({email: 'E-mail address not found'});
return {
status: response_code.HTTP_404,
response: {email: 'E-mail address not found'}
}
}
bcrypt.compare(data.password, user.password).then(isMatch => {
if(!isMatch){
console.log({password: 'Invalid password'});
return {
status: response_code.HTTP_400,
response: {password: 'Invalid password'}
}
}
const payload = {
id: user.id,
email: user.email
};
jwt.sign(
payload,
config.PASSPORT_SECRET,
{
expiresIn: "1h"
},
(err, token) => {
console.log({
status: response_code.HTTP_200,
response: {
success: true,
token: token
}
});
return {
status: response_code.HTTP_200,
response: {
success: true,
token: token
}
}
}
);
});
});
};
When this code gets executed in my route like so:
router.post("/login", (req, res) => {
const { errors, isValid } = validateLogin(req.body);
if(!isValid) return res.status(400).json(errors);
console.log("ret", dm_user.user_login(req.body));
});
The log says the return value of user_login() is undefined, even though right before the return statement in user_login() I am logging the exact same values and they are getting logged.
Before I changed it to a log, I tried to store the return value in a variable, but obviously that remained undefined as well, and I got the error: 'Cannot read propery 'status' of undefined' when trying to use the value.
I am definitely missing something..
Well you have an small callback hell here. It might be a good idea to go with async / await and splitting up your code into smaller chunks instead of putting everyhing in 1 file.
I rewrote your user_login function:
const { generateToken } = require("./token.js");
module.exports.user_login = async data => {
let user = await ModelUser.findOne({ email: data.email });
if (!user) {
console.log({ email: "E-mail address not found" });
return {
status: response_code.HTTP_404,
response: { email: "E-mail address not found" }
};
}
let isMatch = await bcrypt.compare(data.password, user.password);
if (!isMatch) {
console.log({ password: "Invalid password" });
return {
status: response_code.HTTP_400,
response: { password: "Invalid password" }
};
}
const payload = {
id: user.id,
email: user.email
};
let response = await generateToken(
payload,
config.PASSPORT_SECRET,
response_code
);
return response;
};
I have moved your token signing method into another file and promisfied it:
module.exports.generateToken = (payload, secret, response_code) => {
return new Promise((res, rej) => {
jwt.sign(
payload,
secret,
{
expiresIn: "1h"
},
(err, token) => {
if (err) {
rej(err);
}
res({
status: response_code.HTTP_200,
response: {
success: true,
token: token
}
});
}
);
});
};
Now you need to change your router function into an async:
router.post("/login", async (req, res) => {
const { errors, isValid } = validateLogin(req.body);
if(!isValid) return res.status(400).json(errors);
let result = await dm_user.user_login(req.body);
console.log(result);
});
In addition: You get undefined because you return your value to an callback function
I also would seperate your routes from your controllers instead of writing your code inside an anonymous function
Please notice that whenever you are trying to return any value you are always present in the callback function and that is definitely not going to return any value to its intended place.
There are a couple of things you can improve about your code :
1.Donot use jwt inside your code where you are making database calls, instead move it where your routes are defined or make a separate file.
2.If you are intending to re-use the code, I would suggest you either use async-await as shown in the answer above by Ifaruki or you can use something like async.js. But the above shown approach is better.
Also always use 'error' field when you are making db calls like this:
ModelUser.findOne({email: data.email}).then((error,user) => {
I have a route '/login' which has controller to verify user is valid. For now, i am just verifying password entered from route params equals to password i stored in my DB.
I have few methods to get job done for me.
findUser() - verifies user entered mandatory fields or not and resolves Mongoose object when data is found
ValidatePassword() - It is chained to findUser() and converts mongoose object to JS object
In validatePassword method, i want to delete unwanted fields like password before sending data to client. As a result i am converting mongoose object to javascript object and performing delete operation.
Problem: Whenever i am converting in to JS object, i am getting 'toObject() not defined' error.
Attaching snippets for above controller methods.
Please suggest any changes!
let findUser = (req, res) => {
return new Promise((resolve, reject) => {
if (req.body.email) {
userModel.find({ email: req.body.email }, (error, userDetails) => {
if (error) {
let response = apiResponse.generate(
true,
"Unable to reach out to server",
500,
null
);
reject(response);
} else if (checkLib.isEmpty(userDetails)) {
let response = apiResponse.generate(
true,
"Unable to reach out to server",
500,
null
);
reject(response);
} else {
resolve(userDetails);
}
});
} else {
let response = apiResponse.generate(
true,
"Please provide emailID and password",
404,
null
);
reject(response);
}
});
};
This method retrieves userDetails which i am chaining in validatePassword()
let validatePassword = (retrievedUserDetails) => {
return new Promise((resolve, reject) => {
console.log("passw:" + retrievedUserDetails);
if (req.body.password) {
console.log(retrievedUserDetails.password);
console.log(req.body.password);
if (retrievedUserDetails[0].password != req.body.password) {
let response = apiResponse.generate(
true,
"Password or email is invalid",
404,
null
);
reject(response);
} else {
let retrievedUserDetailsObj = retrievedUserDetails.toObject();
delete retrievedUserDetailsObj.password;
let response = apiResponse.generate(
false,
"Signed in successfully",
200,
retrievedUserDetails
);
resolve(response);
}
} else {
let response = apiResponse.generate(
true,
"Please provide password",
404,
null
);
reject(response);
}
});
}
Chaining:
findUser(req, res)
.then(validatePassword)
.then((resolve) => {
let response = apiResponse.generate(
false,
"user is signed in successfully",
200,
resolve
);
res.send(response);
})
.catch((err) => {
console.log(err);
res.send(err);
});
};
It seems you're using the .find method of the model and not the .findOne method. The first one will always return an array of documents that match the query while the second one will return the first object that matches. What you're basically trying to do is [{something}].toObject() and that is indeed undefined.
Use findOne, instead of find because .toObject works on a single object, not an array.
If you want to run .toObject on the array, just loop the array and run .toObject on single object of the array in the loop.
exports.update_activity_status = (req, res) => {
const {
campaign_id,
leadId,
leadActivity,
status,
} = req.body;
const client = mongoClient.connect(`${process.env.MONGO_URL}`, {
useUnifiedTopology: true,
});
client.then((cli) => {
cli
.db(`${process.env.DATABASE_NAME}`)
.collection(`${process.env.ACTIVITY_COLLECTION}`)
.findOneAndUpdate(
{
"data.campaign_id": mongoose.Types.ObjectId(campaign_id),
"data.leads.$._id": mongoose.Types.ObjectId(leadId),
// "data.leads._id" : leadId
},
{
$set: {
"data.leads.$.leadActivity": leadActivity,
"data.leads.$.status": status,
},
},
{
returnNewDocument: true,
}
)
.then((result) => {
console.log("UPDATED RESULT", result);
res.json(result)
})
.catch((err) => console.log("new err", err));
});
};
In my query, I need to update the status and leadActivity of the users whose leadId matches the following conditions:
campaign_id that matches the document (this is working fine).
leadsId that matches inside that particular document (not working).
First I tried using only db.find({"data.campaign_id": mongoose.Types.ObjectId(campaign_id)}) and it returns me the data that matches only the first condition as specified above. As soon as I try it along with second condition and findOneAndUpdate({....}), accessing the nested data after including secondary filter condition returns null.
Here's is what my document object looks like:
Any help to resolve this is appreciated. Thanks in advance.
The second condition for matching the _id in the leads-array is incorrect, you need to change it to:
"data.leads._id": mongoose.Types.ObjectId(leadId)
Hi I'm new to mongodb and node.js. I have the following abbreviated schema:
const PostSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'user'
},
likes: [
{
user: {
type: Schema.Types.ObjectId,
ref: 'user'
}
}
],...
I want to access each Post by id (passed through request params) and then remove a like based on the user id. Presently, it removes the like from the array but does not then throw any errors when I try again to remove the same id from the db. Here is my code at the moment:
const like = await Post.findByIdAndUpdate(
req.params.id,
{ $pull: { likes: { user: req.user.id } } },
(error, result) => {
if (!error) {
return res.json('Post unliked');
}
return res.status(400).send('You have not liked this post');
}
);
Find the correct post with findById() instead of findByIdAndUpdate and use the higher order function map() to access the specific index of the like with the required user property.
await Post.findById(req.params.id, async (error, result) => {
if (!error) {
const index = result.likes
.map(like => {
return like.user;
})
.indexOf(req.user.id);
if (index > -1) {
console.log('found');
//was found
result.likes.splice(index, 1);
await result.save();
return res.json('Post unliked');
} else {
console.log('not found');
return res.status(400).send('You have not liked this post');
}
}
return res.json(error);
});
If the Id doesn't exist's then it return null so you can check if it return null then you can send the response with status 400 else you can send response with status 200. I think this can be the solution to your problem
hope it work, thanks!
Try something like this, and please let me know if it works:
await Post.findByIdAndUpdate(
req.params.id, (error, result) => {
if (!error) {//if error
return res.json(error);
}
const index = result.likes.indexOf(`${req.user.id}` )//find the index, I am not sure if you will need to add it as a string, make some tests!
if (index > -1) {//was found
result.likes.splice(index, 1);//remove
result.save();//save the doc to mongo
return res.json('Post unliked');
}
else return res.status(400).send('You have not liked this post');// it was not found
}
);
You may need to adjust something since I am writing the code from my head!
I hope that helps!
References:
How can I remove a specific item from an array?
https://www.w3schools.com/jsref/jsref_indexof.asp
Mongoose indexOf in an ObjectId array <- this one explains that indexof do work for mongoose ids!