Mongoose update function is only saves doc changes half of the time - node.js

The route below first runs a middleware which gives me access to req.user so I can get that users cart_id. Then I get the item which is being added to the cart through req.body (an item is uniquely identified by its id and color). Then I get the cart from the database and try to update it.
If the findIndex() function returns any number but -1, it means the item already is in the cart and I want to increase its quantity. If the function did not find the item, then I want to add it to the cart.
The strange problem I am getting is that when I try adding a new item (the code runs the else block where I push a new item to the cart) everything works and the changes are being saved. HOWEVER, when I try to add an item which is already in the cart, the changes are NOT being saved.
I have tried to console.log the quantity both before and after updating it and it is being updated correctly, but for some reason the changes aren't being saved to the database. Please help.
router.get("/add-to-cart", get_user, async (req, res) => {
try {
const { item, color, quantity } = req.body;
await Cart.findById(req.user.cart_id).then(doc => {
const i = doc.line_items.findIndex(elm => { return (elm.item == item && elm.color == color) });
if (i !== -1) {
console.log(doc.line_items[i].quantity) // 1
doc.line_items[i].quantity += quantity;
console.log(doc.line_items[i].quantity) // 2, but not being saved to database
}
else {
doc.line_items.push({ item, color, quantity }); // does get saved to database
}
doc.save();
}).catch(err => {
throw err.message;
});
res.json({ success: true });
}
catch (err) {
console.log(err);
res.status(500).json({ error: "unknown error" });
}
});
// the front end:
await fetch('/cart/add-to-cart', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ item: 101, color: 'white', quantity: 1 })
}).then(res => res.json())
// the cart schema
{
_id: 123456,
line_items: [
{ item: 101, color: 'white', quantity: 1 },
{ item: 101, color: 'green', quantity: 1 },
{ item: 102, color: 'white', quantity: 1 },
]
}

Related

Mongoose Update Doesn't Run Before My Res.Send()

I currently have a controller which is handling the onboarding of a user. When the user completes their onboarding flow, I update their status in Mongo from New to Active, then send them to a new page. As a method of security, I also have a middleware function on every authenticated route which checks if the user is logged in, as well as their status. If their status is New, I send them to the onboarding flow (because theoretically they haven't seen the onboarding flow).
As I run through my experience, when I submit the onboarding flow, I get redirected back to the beginning of the flow. I check Mongo and my status is no longer New, so I was confused why this was happening. Eventually I realized when I am sending the user to a new page, the authentication route is checking the user's status before my findOneAndUpdate() has had a chance to complete. So the user gets redirected back to the onboarding flow because the last query didn't finish in time.
Any idea how to fix this? I assume it has something to do with async/await but I'm not sure. Here's my code below, I'm working in Node.JS with an express framework. Also, in my post onboarding I am using a mapbox api to get the lat/long of their zip code, which is why I have the request.get() in the code.
Onboarding Controller
exports.postOnboarding = (req, res, next) => {
var data = req.params.data;
var final = data.split(',');
location = final[4].toString();
url = "https://api.mapbox.com/geocoding/v5/mapbox.places/" + location + ".json";
request.get(url)
.query({access_token: "private_key"})
.end(function(err, result) {
User.findOneAndUpdate(
{"credentials.userId": req.session.user.credentials.userId },
{ practiceSettings: {
businessType: final[2],
experienceType: final[0],
fullFee: final[3]
},
credentials: {
userType: "Active",
active: true,
userId: req.session.user.credentials.userId,
provider: "local"
},
paySettings: {
q1: "undeternmined"
},
license: final[1],
zip: final[4],
latLong: result.body.features[0].center
}, (err, result) => {
if (err) {
console.log(err);
} else {
console.log("settings updated");
res.redirect('/dashboard');
}
}
)
}) };
Dashboard Route
router.get('/dashboard', isAuth, adminController.getDashboard);
isAuth Middleware
const User = require('../models/user');
module.exports = (req, res, next) => {
if (!req.session.isLoggedIn) {
return res.redirect('/login');
} else if (req.session.user.credentials.userType == 'Unverified') {
return res.redirect('/login?verified=false');
} else if (req.url == '/onboarding') {
return next();
}
User.findOne({"credentials.userId" : req.session.user.credentials.userId})
.then(result => {
res.locals.user = result;
if (req.session.sidebarStatus == 'closed') {
res.locals.sidebarStatus = 'closed';
}
if (result.credentials.userType == 'New') {
return res.redirect('/onboarding');
}
next();
})
}
And for reference, below is a snippet of my onboarding.ejs file which calls the post route. This isn't the whole thing, I have a lot of nested Sweet Alert modals, but this is the important part.
Swal.fire({
text: "Question",
width: "90%",
input: "text",
inputPlaceholder: "92805",
inputValidator: (value) => {
if (!value) {
return 'You must fill in this field.'
}
if (value.length != 5) {
return 'Please use a 5 digit zip-code as your answer.'
}
},
showCancelButton: false,
confirmButtonText: 'Submit',
backdrop: '#FFFFFF',
allowOutsideClick: false
})
.then((result5) => {
res3 = result3.value.replace(",", "");
final = [result1.value, result2.value, res3, result4.value, result5.value];
$.ajax({
url: "/post-onboarding/" + final,
dataType: 'json',
type: 'post',
success: function (data) {
if ( data.length ) {
Swal.fire({
title: 'Error!',
text: 'Something bad happened',
icon: 'error',
confirmButtonText: 'OK'
});
} else {
//redirect user
}
}
});

Stripe "no such price"

I created a product with tiered pricing in my Stripe dashboard. I copied the price API IDs and put them in my app's frontend. When I run my code, the backend generates the error: No such price: 'PRICE_1HPYAGLJZYBC5S5KGBKT8UDY'. This price id matches one of the prices on my Stripe dashboard, but I never set the product so I'm wondering if that's the issue. Here is my client js:
function createSubscription({ customerId, paymentMethodId, priceId }) {
return (
fetch('/create-subscription', {
method: 'post',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify({
customerId: customerId,
paymentMethodId: paymentMethodId,
priceId: priceId,
}),
})
.then((response) => {
return response.json();
})
// If the card is declined, display an error to the user.
.then((result) => {
if (result.error) {
// The card had an error when trying to attach it to a customer
throw result;
}
return result;
})
// Normalize the result to contain the object returned
// by Stripe. Add the addional details we need.
.then((result) => {
console.log("RETURNING SUBSCRIPTION")
return {
// Use the Stripe 'object' property on the
// returned result to understand what object is returned.
subscription: result,
paymentMethodId: paymentMethodId,
priceId: priceId,
};
})
);
}
And here is my backend code:
app.post('/create-subscription', async function(req, res) {
console.log(req.body);
User.findOne({"_id": req.session.auth_user._id}, async function(err, user) {
if (user.stripe_id) {
console.log("RETRIEVING CUSTOMER");
var customer = await stripe.customers.retrieve(user.stripe_id);
if (user.stripe_subscription) {
console.log("RETRIEVING SUBSCRIPTION");
var subscription = await stripe.subscriptions.retrieve(user.stripe_subscription);
update_customer(customer, subscription);
}
else {
console.log("CREATING SUBSCRIPTION");
var subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{
price: req.body.priceId,
}]
});
user.stripe_subscription = subscription.id;
user.save(function(err) {
update_customer(customer, subscription);
})
}
}
else {
console.log("CREATING CUSTOMER");
var customer = await stripe.customers.create({
email: req.body.email,
});
user.stripe_id = customer.id;
user.save( async function(err, user) {
if (user.stripe_subscription) {
console.log("RETRIEVING SUBSCRIPTION");
var subscription = await stripe.subscriptions.retrieve(user.stripe_subscription);
update_customer(customer, subscription);
}
else {
console.log("CREATING SUBSCRIPTION");
var subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{
price: req.body.priceId,
}]
});
user.stripe_subscription = subscription.id;
user.save(function(err) {
update_customer(customer, subscription);
});
}
});
}
});
async function update_customer(customer, subscription) {
const paymentMethod = await stripe.paymentMethods.attach(
req.body.paymentMethodId,
{customer: customer.id}
);
console.log(subscription);
res.send(subscription);
}
});
Check the price ID, it looks like something in your frontend converts all the string to uppercase. Usually price id start in lowercase ('price....') and then the string is a mix between numbers and lowercase and uppercase characters.
Incase anyone faces this issue in the future. I had the same issue, but mine was caused by the stripe secret being wrong.
It's wise to:
Double check the price,
Trim the string,
Check your config keys all through
Hope this helps someone 🚀
In my case I was following the docs and
the docs had it like this: Price = "{{price_1234}}"
So I changed it to this: Price = "price_1234" and it worked.
In my case, the API keys were not correct.
If like me, you just followed the tutorial from the docs, the API keys from the code snippets that are proposed are not correct.
You have to setup the ones from the dashboard page (for the test environment: https://dashboard.stripe.com/test/dashboard)

MERN - delete item/row in React Data Table Component

I have a MERN stack application that is modified from a great tutorial I completed. In the original app, transactions were rendered in a list populated from an API call to Mongo Atlas DB. I converted the list to a react-data-table-component and am now trying to figure out how to delete a table row/transaction. The original app had this as part of the transaction component with an onClick button. When I attempt to use the deleteTransaction function, I receive a "TypeError: Cannot read property '_id' of undefined". I can see that the data table renders via the object {transactions}, but cannot figure out why it does not recognize the _id.
Other info: state is managed through the React Context API, with a Router.js and Reducer.js.
TransactionTable.js
import React, { useContext, useEffect } from "react";
// Data table imports
import IconButton from "#material-ui/core/IconButton";
import DeleteIcon from "#material-ui/icons/Delete";
import Card from "#material-ui/core/Card";
import DataTable from "react-data-table-component";
// import transaction component and context provider
import { GlobalContext } from "../context/GlobalState";
// create data table component
export const TransactionTable = () => {
const { transactions, getTransactions, deleteTransaction } = useContext(
GlobalContext
);
// react-data-table-component Columns for back-end data
const columns = [
{
name: "Transaction",
selector: "text",
sortable: true
},
{
name: "Amount",
selector: "amount",
sortable: true,
// conditionally render amount if positive or negative
conditionalCellStyles: [
{
when: row => row.amount > 0,
style: {
color: "green"
}
},
{
when: row => row.amount < 0,
style: {
color: "red"
}
}
]
},
{
// where I'm attempting to pass the transactions prop and apply the deleteTransaction function
// using the delete button that renders in each row
cell: ({ transactions }) => (
<IconButton
aria-label="delete"
color="secondary"
onClick={() => deleteTransaction(transactions._id)}
>
<DeleteIcon />
</IconButton>
)
}
];
useEffect(() => {
getTransactions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div>
<Card style={{ height: "100%" }} p={2} mx="auto">
<DataTable
title="Transactions"
columns={columns}
data={transactions}
defaultSortField="Transactions"
//actions={actions}
pagination={true}
highlightOnHover={true}
dense={true}
/>
</Card>
</div>
);
};
./controllers/transactions.js - this is where the deleteTransaction function is
const Transaction = require('../models/Transaction');
// #desc Get all transactions
// #route GET /api/v1/transactions
// #access Public
exports.getTransactions = async (req, res, next) => {
try {
const transactions = await Transaction.find();
//const result = result.transaction.toString()
return res.status(200).json({
success: true,
count: transactions.length,
data: transactions
});
} catch (err) {
return res.status(500).json({
success: false,
error: 'Server Error'
});
}
}
// #desc Add transaction
// #route POST /api/v1/transactions
// #access Public
exports.addTransaction = async (req, res, next) => {
try {
const { text, amount } = req.body;
const transaction = await Transaction.create(req.body);
return res.status(201).json({
success: true,
data: transaction
});
} catch (err) {
if(err.name === 'ValidationError') {
const messages = Object.values(err.errors).map(val => val.message);
return res.status(400).json({
success: false,
error: messages
});
} else {
return res.status(500).json({
success: false,
error: 'Server Error'
});
}
}
}
// #desc Delete transaction
// #route DELETE /api/v1/transactions/:id
// #access Public
exports.deleteTransaction = async (req, res, next) => {
try {
const transaction = await Transaction.findById(req.params.id);
if(!transaction) {
return res.status(404).json({
success: false,
error: 'No transaction found'
});
}
await transaction.remove();
return res.status(200).json({
success: true,
data: {}
});
} catch (err) {
return res.status(500).json({
success: false,
error: 'Server Error'
});
}
}
According to the docs https://www.npmjs.com/package/react-data-table-component#custom-cells, each cell is passed an object named row by convention (you can name it to whatever you want)..
This row object should have the _id you need..
// react-data-table-component Columns for back-end data
const columns = [
// ... column items,
{
cell: row => (
<IconButton
aria-label="delete"
color="secondary"
onClick={() => deleteTransaction(row._id)}
>
<DeleteIcon />
</IconButton>
)
}
]
Each row basically represents a single transaction.

MEAN stack how to find _id from database to send a PUT request

I'm having a problem identifying a 'task' in mongoDB from my frontend angular.
This question is the most similar to my question but here it just says req.body.id and doesn't really explain how they got that.
This question involves what I am trying to do: update one document in a collection upon a click. What it does in the frontend isn't important. I just want to change the status text of the Task from "Active" to "Completed" onclick.
First I create a task and stick it in my database collection with this code:
createTask(): void {
const status = "Active";
const taskTree: Task = {
_id: this._id,
author: this.username,
createdBy: this.department,
intendedFor: this.taskFormGroup.value.taskDepartment,
taskName: this.taskFormGroup.value.taskName,
taskDescription: this.taskFormGroup.value.taskDescription,
expectedDuration: this.taskFormGroup.value.expectedDuration,
status: status
};
this.http.post("/api/tasks", taskTree).subscribe(res => {
this.taskData = res;
});
}
When I make this post to the backend, _id is magically filled in!
I'm just not sure how I can pass the id to the put request in nodejs router.put('/:id') when I'm pushing it from the frontend like this:
completeTask(): void {
const status = "Completed";
const taskTree: Task = {
_id: this._id,
author: this.username,
createdBy: this.department,
intendedFor: this.taskFormGroup.value.taskDepartment,
taskName: this.taskFormGroup.value.taskName,
taskDescription: this.taskFormGroup.value.taskDescription,
expectedDuration: this.taskFormGroup.value.expectedDuration,
status: status
};
console.log(taskTree);
this.http.put("/api/tasks/" + taskTree._id, taskTree).subscribe(res => {
this.taskData = res;
console.log(res);
});
}
In the template I have a form that's filled in and the data is immediately outputted to a task 'card' on the same page.
When I send the put request from angular, I get a response in the backend just fine of the response I ask for in task-routes.js:
router.put("/:id", (req, res, next) => {
const taskData = req.body;
console.log(taskData);
const task = new Task({
taskId: taskData._id,
author: taskData.author,
createdBy: taskData.createdBy,
intendedFor: taskData.intendedFor,
taskName: taskData.taskName,
taskDescription: taskData.taskDescription,
expectedDuration: taskData.expectedDuration,
status: taskData.status
})
Task.updateOne(req.params.id, {
$set: task.status
},
{
new: true
},
function(err, updatedTask) {
if (err) throw err;
console.log(updatedTask);
}
)
});
The general response I get for the updated info is:
{
author: 'there's a name here',
createdBy: 'management',
intendedFor: null,
taskName: null,
taskDescription: null,
expectedDuration: null,
status: 'Completed'
}
Now I know _id is created automatically in the database so here when I click create task & it outputs to the 'card', in the console log of task after I save() it on the post request, taskId: undefined comes up. This is all fine and dandy but I have to send a unique identifier from the frontend Task interface so when I send the 'put' request, nodejs gets the same id as was 'post'ed.
I'm quite confused at this point.
So I finally figured this out...In case it helps someone here's what finally worked:
First I moved my update function and (patch instead of put) request to my trigger service:
Trigger Service
tasks: Task[] = [];
updateTask(taskId, data): Observable<Task> {
return this.http.patch<Task>(this.host + "tasks/" + taskId, data);
}
I also created a get request in the trigger service file to find all the documents in a collection:
getTasks() {
return this.http.get<Task[]>(this.host + "tasks");
}
Angular component
Get tasks in ngOnInit to list them when the component loads:
ngOnInit() {
this.triggerService.getTasks().subscribe(
tasks => {
this.tasks = tasks as Task[];
console.log(this.tasks);
},
error => console.error(error)
);
}
Update:
completeTask(taskId, data): any {
this.triggerService.updateTask(taskId, data).subscribe(res => {
console.log(res);
});
}
Angular template (html)
<button mat-button
class="btn btn-lemon"
(click)="completeTask(task._id)"
>Complete Task</button>
// task._id comes from `*ngFor="task of tasks"`, "tasks" being the name of the array
//(or interface array) in your component file. "task" is any name you give it,
//but I think the singular form of your array is the normal practice.
Backend Routes
GET all tasks:
router.get("", (req, res, next) => {
Task.find({})
.then(tasks => {
if (tasks) {
res.status(200).json(tasks);
} else {
res.status(400).json({ message: "all tasks not found" });
}
})
.catch(error => {
response.status(500).json({
message: "Fetching tasks failed",
error: error
});
});
});
Update 1 field in specified document (status from "Active" to "Completed"):
router.patch("/:id", (req, res, next) => {
const status = "Completed";
console.log(req.params.id + " IT'S THE ID ");
Task.updateOne(
{ _id: req.params.id },
{ $set: { status: status } },
{ upsert: true }
)
.then(result => {
if (result.n > 0) {
res.status(200).json({
message: "Update successful!"
});
}
})
.catch(error => {
res.status(500).json({
message: "Failed updating the status.",
error: error
});
});
});
Hope it helps someone!

async await with promise all

I want to know if I am using promise.all correctly with async await.
Basically, I need to get the house data based on the ID, then I need to get all the reviews for that house as well as the count of reviews.
server.get("/api/houses/:id", async (req, res) => {
const { id } = req.params;
const house = await House.findByPk(id);
if (!house) {
return res.status(400).send("No house found");
}
const reviews = await Review.findAndCountAll({
where: {
houseId: house.id
}
});
house.dataValues.reviewsCount = reviews.count;
const results = await Promise.all([house.dataValues, reviews.rows]);
console.log(results);
res.send(results);
});
In the front end when I console.log the response after making the http request, I get back the below which seems okay since Promise.all gives you arrays. But I don't know if this is the best way to do this or if there is a better way.
[
{
id: 2329,
host: 2,
picture: '/img/houses/1.jpg',
type: 'Entire house',
town: 'Some town',
title: 'Some title',
price: 50,
description: 'Some description',
guests: 4,
bedrooms: 1,
beds: 2,
baths: 1,
wifi: true,
reviewsCount: 2
},
[
{
id: 1,
houseId: 2329,
userId: 1,
comment: 'An awesome review',
createdAt: '2019-01-11T22:00:00.000Z',
updatedAt: '2019-01-11T22:00:00.000Z'
},
{
id: 2,
houseId: 2329,
userId: 2,
comment: 'Another awesome review',
createdAt: '2019-01-11T22:00:00.000Z',
updatedAt: '2019-01-11T22:00:00.000Z'
}
]
]
You're not using Promise.all correctly. The code is working, because you're awaiting each promise individually.
Since Review.findAndCountAll depends on House.findByPk result, Promise.all won't do any good here.
You're using Promise.all with the already resolved values of the both promises, so you can just drop it.
server.get("/api/houses/:id", async (req, res) => {
const { id } = req.params;
const housePromise = await House.findByPk(id);
const reviews = await Review.findAndCountAll({
where: {
houseId: house.id
}
});
house.dataValues.reviewsCount = reviews.count;
res.send([house.dataValues, reviews.rows]);
});
Basically you're doing:
const res = await Promise.all([1, 5]); // [1, 5]
Which can be translated directly to:
const res = [1, 5];
Instead of sending it in an array, I think it's better to send an object:
{
house: house.dataValues,
reviews: reviews.rows
}
You can ignore async await and use Promise. You can try following code
server.get("/api/houses/:id", async (req, res) => {
const { id } = req.params;
return House.findByPk(id)
.then( house => {
// !house might be 'true' if house is 'undefined'
if( house === undefined || !house ) {
return res.status(400).send("No house found");
}
return Review.findAndCountAll({ where: { houseId: house.id } })
.then(reviews => {
house.dataValues.reviewsCount = reviews.count;
return {house, reviews};
})
.catch(error => res.send(error));
})
.then( result => {
return res.send(results);
})
.catch(error => {
return res.send(error);
});
})

Resources