Get the first document from Firestore subcollection with primary document? - node.js

I am using Node.js (which I am very new at) and Google Cloud Firestore database to save documents according to:
Users
Tweets
One Users document, i.e. a User, has many Tweets in a subcollection 'Tweets'. I am interested in retrieving a User together with the last Tweet in the subcollection so I get a JSON-file like this. In other words, this is what I want to get:
users: {
{
name:'john',
twitter_username:'johnny',
tweets: {
text: "I am called johnny, this is my tweet",
created_at: "2021-06-29 12:00:00"
},
},
{
name:'anne',
twitter_username:'anne',
tweets: {
text: "I am called anne, this is another tweet",
created_at: "2019-06-28 12:00:00"
},
}
}
I have this function:
function getUserData() {
return db.collection('users').get()
.then((querySnapshot) => {
var docs = querySnapshot.docs.map(doc => [doc.data(), doc.id]);
//console.log(docs);
return docs
which, if I could fetch and replace doc.id (i.e. the User document ID) with the last tweet, would solve it I guess. But how can I do that?
Any other solution, possibly with for loops, would be fine as well. I have spent hours on this seemingly easy problem but can't get it to return both the User-data and the tweet-data.
Edit 2:
I realized I could do this:
function getUserData() {
return db.collection('users').get()
.then((querySnapshot) => {
var docs = querySnapshot.docs.map(doc => [doc.data(), doc.id, getStuff(doc.id)])
console.log(docs)
return docs
});
}
function getStuff(doc_id) {
return db.collection('users').doc(doc_id).collection('tweets').limit(1).get()
.then((querySnapshot) => {
var docs = querySnapshot.docs.map(doc => doc.data());
console.log("TWEETS", doc_id, docs[0]['text']);
return docs[0]['text']
});
}
which produces a log result as:
TWEETS DAU86mxIhmD6qQQpH4F God’s country!
TWEETS JQHTO0jUjAodMQMR6wI I’m almost there Danny!
from the getStuff-function.
The only issue now is that I can't get the map function to wait for getStuff so the return docs return a Promise { <pending> } for getStuff(doc.id).
I am not to familiar with Promises and await/async and I can't get that to work. Solving this Promise pending -> twitter text would then solve my problem. How do I do that?

If you want to get the data of a single user you could write the code like this:
const getUserData = async (userUid) => {
const userSnap = await db.collection("users").doc(userUid).get();
const tweetSnaps = await db
.collection("tweets")
.orderBy("created_at", "desc")
.limit(1)
.get();
let tweet = {};
tweetSnaps.forEach((doc) => {
tweet = doc.data();
});
return {
...userSnap.data(),
...tweet,
};
};
We first get the user and then query for the last tweet and get that. We sort the tweets collection by created_at and limit it for a single doc.
If you want to get the same data for all users at once we would need to change the code a little bit but the logic would be the same.
If the data is saved in separate collections you can't get them in a single database request.
UPDATE for Edit 2
Here your code how it should look like with correct async/await:
const getUserData = async () => {
const querySnapshot = await db.collection("users").get();
const docs = querySnapshot.docs;
for (let i = 0; i < docs.length; i++) {
const element = docs[i];
doc.data(),
doc.id,
await getStuff(doc.id),
}
console.log(docs);
return docs;
};
const getStuff = async (doc_id) => {
const querySnapshot = await db
.collection("users")
.doc(doc_id)
.collection("tweets")
.limit(1)
.get();
var docs = querySnapshot.docs.map((doc) => doc.data());
console.log("TWEETS", doc_id, docs[0]["text"]);
return docs[0]["text"];
};

Related

How to get inner collection in firebase firestore

I'm trying to get the device token of a particular user in firestore which is stored in tokens collection inside either "clients" or "lawyers" collection.
When i remove the second .collection("tokens") from the chain i get the user object back but with the token collection in the chain i just can't seem to get any user (client or lawyer) back, even though the user and it's token exist. what am i doing wrong
exports.onReceiveChatMessage = functions.database
.ref("/messages/{uid}")
.onCreate(async (snapshot, context) => {
const newMessage = snapshot.val();
console.log("NEW_MESSAGE", newMessage);
const senderName = newMessage.sender_name;
const messageContent = newMessage.content;
console.log("SENDER'S_NAME", senderName);
console.log("MESSAGE_BODY", messageContent);
const uid = context.params.uid;
console.log("RECEIVERS_ID", uid);
if (newMessage.sender_id == uid) {
//if sender is receiver, don't send notification
console.log("sender is receiver, dont send notification...");
return;
} else if (newMessage.type === "text") {
console.log(
"LETS LOOK FOR THIS USER, STARTING WITH CLIENTS COLLECTION..."
);
let userDeviceToken;
await firestore
.collection("clients")
.doc(uid)
.collection("tokens")
.get()
.then(async (snapshot) => {
if (!snapshot.exists) {
console.log(
"USER NOT FOUND IN CLIENTS COLLECTION, LETS CHECK LAWYERS..."
);
await firestore
.collection("lawyers")
.doc(uid)
.collection("tokens")
.get()
.then((snapshot) => {
if (!snapshot.exists) {
console.log(
"SORRY!!!, USER NOT FOUND IN LAWYERS COLLECTION EITHER"
);
return;
} else {
snapshot.forEach((doc) => {
console.log("LAWYER_USER_TOKEN=>", doc.data());
userDeviceToken = doc.data().token;
});
}
});
} else {
snapshot.forEach((doc) => {
console.log("CLIENT_USER_TOKEN=>", doc.data());
userDeviceToken = doc.data().token;
});
}
});
// console.log("CLIENT_DEVICE_TOKEN", userDeviceToken);
} else if (newMessage.type === "video_session") {
}
})
This line
if (!snapshot.exists) {
should be:
if (snapshot.empty) {
because you're calling get() on a CollectionReference (which returns a QuerySnapshot), not on a DocumentReference (which returns a DocumentSnapshot).
If you remove the .collection('tokens') from the chain in your example, it does work because a DocumentSnapshot does have the member exists, but a CollectionReference doesn't.
Take a look at their members here:
https://googleapis.dev/nodejs/firestore/latest/CollectionReference.html#get
Then:
https://googleapis.dev/nodejs/firestore/latest/QuerySnapshot.html
As a suggestion, I used to confuse snapshots and got that problem because of working with Javascript instead of Typescript. So I got used to calling the result snap when called on a document, and snaps when called on collections. That reminds me of what kind of response I'm working on. Like this:
// single document, returns a DocumentSnapshot
const snap = await db.collection('xyz').doc('123').get();
if (snap.exists) {
snap.data()...
}
// multiple documents, returns a QuerySnapshot
const snaps = await db.collection('xyz').get();
if (!snaps.empty) { // 'if' actually not needed if iterating over docs
snaps.forEach(...);
// or, if you need to await, you can't use the .forEach loop, use a plain for:
for (const snap of snaps.docs) {
await whatever(snap);
}
}

How to use async await inside another async function

i have a question about using async await inside another promise. I have a function call another function to get a transaction details.
When i running the function LastTransactions the field details do not show results. Anyone can help me ?
LastTransactions: async (transactionKey, page) => {
const api = `https://api.pagar.me/1/payables?recipient_id=${transactionKey}&count=${totalResults}&page=${page}&api_key=${PagarmeApiKey}`;
const response = await axios.get(api);
transactions = response.data.map((item) => {
return {
id : item.id,
transactionId : item.transaction_id,
trxDetails : [transactionDetails(item.transaction_id)],
}
});
return transactions;
},
and a detail function
async function transactionDetails(id){
const response = await axios.get(`https://api.pagar.me/1/transactions/${id}?api_key=${PagarmeApiKey}`)
const data = response.data;
return data;
}
You need to utilize the Promise.all method to take an array of promises and return an array with your transactions once each individual call for transaction details finishes.
async (transactionKey, page) => {
const api =
`https://api.pagar.me/1/payables?recipient_id=${transactionKey}&count=${totalResults}&page=${page}&api_key=${PagarmeApiKey}`;
const response = await axios.get(api);
// create an array of promises and wait for
// all of them to resolve before continuing
const transactions = await Promise.all(
response.data.map(async item => {
const { id, transaction_id } = item;
// get transaction details for each item in the array
const trxDetails = await transactionDetails(transaction_id);
return {
id,
trxDetails,
transactionId: transaction_id,
};
})
);
return transactions;
};
References:
Promise.all() - MDN
Since transactionDetails(item.transaction_id) is Asynchronous, you need to await that as well, otherwise it will return immediately and trxDetails will contain a promise object, and not response.data.
try this:
transactions = response.data.map(async (item) => {
return {
id : item.id,
transactionId : item.transaction_id,
trxDetails : [await transactionDetails(item.transaction_id)],
}
});

Chaining multiple Promises, loop over array using map and create mongoose document?

I'm retrieving a list of people from the database using getPeople(). As soon as I receive them as res, I want to prepare them to be stored in my local mongodb if they do not exist. Sometimes there're duplicate entries (for one id) within res. My issues is that it's not waiting for Person.create(pers) to finish, continues searching if this id is already in mongodb, can't find any since Person.create(pers) is still creating it and starts the second Person.create(pers)..
this.getPeople()
.then(res => {
return Promise.all(res.map(pers => {
pers.birthday = df(pers.birthday, 'dd.mm.yyyy')
pers.pickedUp = false
console.log(pers.id)
return Person
.find({ id: pers.id })
.exec()
.then(found => {
if (found === undefined || found.length == 0)
return pers
})
.then(pers => {
return Person
.create(pers)
.then(console.log('created'))
.catch(err => console.log(err))
})
}))
}).catch(err => console.log(err))
I expected the console output to be like this:
940191
created
940191
created
Instead, I'm getting this:
940191
940191
created
created
That's because Promise.all simply awaits all the promises you're mapping. It does not guarantee any order in which the promises are resolved.
If you want to sequentially process the elements of your res-array, you can simply use a for .. of loop in combination with async/await (note that this still needs some error handling, but it should give you something to start with):
async function getPeopleAndCreateIfNotExisting() {
const persons = [];
const res = await this.getPeople();
for (const pers of res) {
pers.birthday = df(pers.birthday, 'dd.mm.yyyy');
pers.pickedUp = false;
console.log(pers.id)
const found = await Person
.find({ id: pers.id }).exec();
if (found) {
persons.push(pers);
} else {
persons.push(await Person.create(pers));
}
}
return person;
}

Synchronously iterate through firestore collection

I have a firebase callable function that does some batch processing on documents in a collection.
The steps are
Copy document to a separate collection, archive it
Run http request to third party service based on data in document
If 2 was successful, delete document
I'm having trouble with forcing the code to run synchronously. I can't figure out the correct await syntax.
async function archiveOrders (myCollection: string) {
//get documents in array for iterating
const currentOrders = [];
console.log('getting current orders');
await db.collection(myCollection).get().then(querySnapshot => {
querySnapshot.forEach(doc => {
currentOrders.push(doc.data());
});
});
console.log(currentOrders);
//copy Orders
currentOrders.forEach (async (doc) => {
if (something about doc data is true ) {
let id = "";
id = doc.id.toString();
await db.collection(myCollection).doc(id).set(doc);
console.log('this was copied: ' + id, doc);
}
});
}
To solve the problem I made a separate function call which returns a promise that I can await for.
I also leveraged the QuerySnapshot which returns an array of all the documents in this QuerySnapshot. See here for usage.
// from inside cloud function
// using firebase node.js admin sdk
const current_orders = await db.collection("currentOrders").get();
for (let index = 0; index < current_orders.docs.length; index++) {
const order = current_orders.docs[index];
await archive(order);
}
async function archive(doc) {
let docData = await doc.data();
if (conditional logic....) {
try {
// await make third party api request
await db.collection("currentOrders").doc(id).delete();
}
catch (err) {
console.log(err)
}
} //end if
} //end archive
Now i'm not familiar with firebase so you will have to tell me if there is something wrong with how i access the data.
You can use await Promise.all() to wait for all promises to resolve before you continue the execution of the function, Promise.all() will fire all requests simultaneously and will not wait for one to finish before firing the next one.
Also although the syntax of async/await looks synchronous, things still happen asynchronously
async function archiveOrders(myCollection: string) {
console.log('getting current orders')
const querySnapshot = await db.collection(myCollection).get()
const currentOrders = querySnapshot.docs.map(doc => doc.data())
console.log(currentOrders)
await Promise.all(currentOrders.map((doc) => {
if (something something) {
return db.collection(myCollection).doc(doc.id.toString()).set(doc)
}
}))
console.log('copied orders')
}

Node.js Sequalize creating new rows in forEach loop

I'm trying to create new rows in the database using Sequalize ORM. I receive an array of collections from req.query.collections. For each of those collections I need to create a new userCollection. If none userCollections were created, I wanna respond with internal server error (line 41), otherwise return an array of objects with newly created userCollections.
The problem is, I keep getting an internal server error when I make test requests from Postman. When I check my database, I see that those userCollections were created, so no error occurred.
I know why this happens: because userCollection.build({ stuff }).save() returns a promise. So when I try to console.log userCollections from within .then() statement, I get an array with a newly created collections, just like I should. But by that time server has already responded with internal server error.
Here's my function code:
exports.addCollections = async (req, res, next) => {
const libraryId = req.params.libraryId;
const collections = req.query.collections;
if (!collections)
next(Boom.forbidden());
const userCollections = [];
collections.forEach(async (collectionId, index) => {
const collection = await Collection.findByPk(collectionId);
if (!collection)
return next(Boom.notFound());
userCollection.build({
user_id: req.user.id,
library_id: libraryId,
public_collection_id: collection.id,
title: collection.title,
description: collection.description
})
.save()
.then(newUserCollection => {
userCollections.push(newUserCollection.get({ plain: true }));
// should be printed first, but comes second
// prints out the array with newly created record
console.log(userCollections);
})
.catch(error => {
console.log(error);
});
});
// should be printed second, but comes first
// prints out empty array
console.log(userCollections);
if (userCollections.length === 0) {
next(Boom.internal());
}
res.json(userCollections);
}
Posting the solution
Thanks to Sebastien Chopin who created this tutorial:
https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404
So I added this function:
const asyncForEach = async (array, callback) => {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array)
}
}
And instead of collections.forEach...(blah blah) (line 10 of the code posted in the question) I do:
try {
await asyncForEach(collections, async (collectionId, index) => {
const collection = await Collection.findByPk(collectionId);
if (!collection)
return next(Boom.notFound());
userCollection.build({
user_id: req.user.id,
library_id: libraryId,
public_collection_id: collection.id,
title: collection.title,
description: collection.description
})
.save()
.then(newUserCollection => {
userCollections.push(newUserCollection.get({ plain: true }));
console.log(userCollections);
})
.catch(error => {
console.log(error);
});
})
} catch (err) {
return next(Boom.internal(err.message));
}

Resources