Cannot read property 'map' of undefined with promise all - node.js

I was initially just running one query in node.js but I now need two sets of data so I ran two queries and used Promise.all like this:
Promise.all([products, subcats]);
res.status(200).json({
products,
subcats
});
In React I have:
class ProductList extends Component {
state = {
products: []
};
async componentDidMount() {
const catslug = this.props.match.params.catslug;
const { data: products } = await getCatProducts(catslug);
this.setState({ products: products });
}
When I was only running the one query I was running this without any issue:
this.state.products.map(product => (
Now because I have the 2 queries I need to change it to:
this.state.products.products.map(product => (
But as soon as I do that I get the error:
Cannot read property 'map' of undefined
So, I changed it to this and now it works with no errors:
{this.state.products.products &&
this.state.products.products.map(product => (
My question is why did it work before without having to put in the ... && bit but now I have to use it with the Promise.all in node.js?

It's because of the initial shape of your state
state = {
products: []
};
You see this.state.products is already defined as an array, so it can be mapped over (despite being empty). However this.products.products is NOT an array, it is undefined, so trying to map will return the error you are seeing. The line you have entered
{this.state.products.products &&
this.state.products.products.map(product => (
checks that the array exists before attempting to map it (which wont evaluate to true until your async code finishes, and then the array is defined).
An alternative fix would be to set your initial state shape to match your final state shape. i.e
state ={
products:{
products:[]
}
};
Or if you don't want the nested products property you can change
async componentDidMount() {
const catslug = this.props.match.params.catslug;
const { data } = await getCatProducts(catslug);
this.setState({ products: data.products });
}
and return to your old this.state.products.map()

The issue is in the following line:
const { data: products } = await getCatProducts(catslug);
If I understand correctly, when you were sending one value, you were sending it like:
res.status(200).json(products); // an array
But now you are sending an object, which further contains 2 arrays products and subcats.
What you have 2 do is add below changes to make it work:
const obj = await getCatProducts(catslug);
const products = obj.products
const subcats = obj.subcats

Related

Firestore promise wait for product details before returning products

I know similar questions like this have been asked 1000 times but for the life of me I am struggling with something I feel is quite simple.
We have 2 tables, one called order_lines the other called order_lines_meta, I need to first query order_lines and for each line get the order_lines_meta and return that
I have tried a lot of variations, here is where I am at and stuck, I need it to wait for the order_lines_meta to come back because otherwise I get blank metaData as the data comes after nodejs has already outputted the order_lines
At the end an object that contains order info, line items of objects and within line items a meta data object
Appreciate the help, I just can't seem to wrap my brain on this one , and I am certainly open to other ways of doing this as well
Using nodejs, express, typescript, firestore
const orderNumber = req.query.orderNumber as string;
const customerName = req.query.customerName as string;
const orderDate = req.query.orderDate as string;
const pickListObj = {
orderNumber: orderNumber,
customerName: customerName,
orderDate: orderDate,
line_items: <any>[],
};
db.collection('order_lines').where('number', '==', orderNumber).get().then((snap) => {
const promises = <any>[];
snap.forEach(async (order: any) => {
// get meta data
const metaDataObj = <any>[];
const productName = order.data().name;
const productQty = order.data().quantity;
promises.push(db.collection('worder_line_meta').where('lineId', '==', order.data().lineId).get().then((doc: any) => {
if (doc.display_value != '') {
const meta = [{display_key: doc.data().display_key, display_value: doc.data().display_value}];
metaDataObj.push(meta);
}
}));
});
return Promise.all(promises);
}).then(() => {
pickListObj.line_items.push({name: productName, quantity: productQty, meta_data: metaDataObj});
});
Move the push statement from the last .then inside the previous .then:
promises.push(db.collection('worder_line_meta')...then((doc: any) => {
if (doc.display_value != '') {
...
}
pickListObj.line_items.push({name: productName,
quantity: productQty,
meta_data: metaDataObj});
}));
In the last .then, you will then find the complete pickListObj.
However, I wonder whether it might be simpler and faster to join the two database collections right on the database and retrieve everything with one db.collection operation.

Error in returning new value from Mongoose Database (Nodejs)

When I execute a find query in mongodb after executing a findOneAndUpdate query the data that is returned is not the updated one eventhough the data has been correctly updated in the database.
I find the list of all entries which have a particular code/codes,
let codeList:Array<CodeBase> = await codebaseModel.find({codeBaseId: codes});
I loop through each entry and where the fileTree needs to be updated, I get the directory tree and update it
if (codeList.length > 0) {
codeList.forEach( (e): void => {
if (e.fileTree === 'To be updated') {
let tree = directoryTree(src/uploads/${e.codeBaseId}/unzipped/);
let treeData = JSON.stringify(tree);
let value = codebaseModel.findOneAndUpdate(
{ codeBaseId: e.codeBaseId },
{ fileTree: treeData },
{ upsert:true,new:true, }
);
}
});
}
I need get the updated list again (so as to ensure that I have the entries with the updated fileTree values.
let codeListNext:Array<CodeBase> | null | void = [];
await codebaseModel.find({
codeBaseId: codes,
},).then((value:Array<CodeBase>)=>{
codeListNext = value;
});
console.log(Firstlist length is ${codeListNext});
However, the list that is returned is the old list. However, the mongodb database has updated values. I tried using async await but was not successful. Can someone help me understand where am I going wrong ?

API call in ReactJS and setting response to state

I have an array with ids like
[123,345,212,232.....]
I have a Rest API that responds the particular product with respect to an id .
So in ReactJS, how would I loop through the array and get the products and store in state
I tried doing this in useEffect{running once as in componentDidMount} but it only stores the first product in array.
Are there better ways to accomplish this?
My Actual Code that has the problem is
useEffect(() => {
const getBought = async () => {
user.bought.map(async (boughtID) => {
let bought = await get(`/bought/${boughtID}`, {}, true);
let boughtDate = bought.createdAt.slice(0, 10);
bought.products.map(async (pr) => {
let product = await get(`product/${pr.product}`);
let quantityBought = pr.quantity;
setAllBoughts({
...allBoughts,
[boughtDate]: [
...(allBoughts.boughtDate || []),
{
product,
quantityBought,
},
],
});
});
});
};
getBought();
}, []);
So I have a user which has arrays of boughtIds in bought key...
so I am looping throughout those bought keys
A bought schema has products which has product id and number of product bought (pr.product and pr.quantity).. So I am hitting the API , and save all the products in a state object.
so allBoughts is state which is an object that has keys as (dates) as in the code .createdAt.slice(0,10) which gives me date removing rest of stuffs mongodb added.
But everytime I do it, there are few things that happen.
Either only the first item is saved, and rest are not
Either the same item gets overwritten
or the page reloads infinitely(especially whrn I try to remove dependency array from useEffect)
setAllBoughts({
...allBoughts,
[boughtDate]: [
...(allBoughts.boughtDate || []),
{
product,
quantityBought,
},
],
});
allBoughts is the state that existed when this effect ran. Ie, it's the empty state, with no results in it yet. So every time a result comes back, you are copying the empty state, and adding one entry to it. This erases any results you have.
Instead, you need to use the most recent version of the state, which you can do with the function version of set state:
setAllBoughts(prevState => {
return {
...prevState,
[boughtDate]: [
...(prevState.boughtDate || []),
{
product,
quantityBought,
},
],
};
})
From what you're saying, it seems to me that you're erasing the previous stored result in the state (You get result for id 123, store it, then result of 345, store it and erase the previous value). Or you're just not looping through the array.
Here is an example of how to GET multiple times in a useEffect and assign all the results in the state.
Fully working example at Codesandbox
export default function App() {
const ids = useRef([1, 3, 5]);
const [items, setItems] = useState([]);
useEffect(() => {
const promises = ids.current.map((id) => fetchData(id));
Promise.all(promises).then((responses) => {
setItems(responses);
});
}, []);
async function fetchData(id) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
return response.json();
}
return (
<div className="App">
{items.map((item) => (
<div key={item.id}>
{item.id} - {item.title}
</div>
))}
</div>
);
}

Cloud firestore trigger query by documentID for array of ids

I am trying to write a transaction that first query documents by documentId from a list of ids, then makes some updates.
I am getting the error:
The corresponding value for FieldPath.documentId() must be a string or a DocumentReference.
For example:
const indexArray = [..list of doc ids...]
const personQueryRef = db.collection("person").where(admin.firestore.FieldPath.documentId(), "in", indexArray)
return db.runTransaction(transaction => {
return transaction.get(personQueryRef).then(personQuery => {
return personQuery.forEach(personRef => {
transaction.update(personRef, { ...update values here })
//more updates etc
})
})
})
I am wanting to do this in an onCreate and onUpdate trigger. Is there another approach I should be taking?
Update
The error still persists when not using a transaction, so this is unrelated to the problem.
The problem does not occur when the query is .where(admin.firestore.FieldPath.documentId(), "==", "just_one_doc_id"). So, the problem is with using FieldPath.documentId() and in.
It sounds like the type of query you're trying to do just isn't supported by the SDK. Whether or not that's intentional, I don't know. But if you want to transact with multiple documents, and you already know all of their IDs, you can use getAll(...) instead:
// build an array of DocumentReference objects
cost refs = indexArray.map(id => db.collection("person").doc(id))
return db.runTransaction(transaction => {
// pass the array to getAll()
return transaction.getAll(refs).then(docs => {
docs.forEach(doc => {
transaction.update(doc.ref, { ...update values here })
})
})
})

How do I use transactions or batches to read the value of an update, then perform more updates using that value?

What is the best approach to do a batch update or transaction, that reads a value of the first update, then uses this value to make further updates?
Here is an example:
//create person
const id = await db
.collection("person")
.add({ ...person })
.then(ref => ref.id)
//then do a series of updates
let batch = db.batch()
const private_doc = db
.collection("person")
.doc(id)
.collection("private")
.doc("data")
batch.set(private_doc, {
last_modified,
version: 1,
versions: []
})
const some_index = db.collection("data").doc("some_index")
batch.update(some_index, {
[id]: { first_name: person.first_name, last_name: person.last_name, last_modified }
})
const another_helpful_doc = db.collection("some_other_collection").doc("another_helpful_doc")
batch.update(another_helpful_doc, {
[id]: { first_name: person.first_name, last_name: person.last_name, image: person.image }
})
return batch.commit().then(() => {
person.id = id
return person
})
You can see here if there is an error any of the batch updates, the person doc will still be created - which is bad. I could add in a catch to delete the person doc if anything fails, however interested to see if this is possible with transactions or batches.
You can call the doc() method, without specifying any path, in order to create a DocumentReference with an auto-generated ID and, then, use the reference later. Note that the document corresponding to the DocumentReference is NOT created.
So, the following would do the trick, since all the writes/updates are included in the batched write:
const new_person_ref = db.collection("person").doc();
const id = new_person_ref.id;
let batch = db.batch()
batch.set(new_person_ref, { ...person })
const private_doc_ref = db // <- note the addition of ref to the variable name, it could help avoiding errors, as this is not a DocumentSnapshot but a DocumentReference.
.collection("person")
.doc(id)
.collection("private")
.doc("data")
batch.set(private_doc_ref, {
last_modified,
version: 1,
versions: []
})
//....

Resources