Query documents in firestore from an array of ID's - javascript

I was wondering how I can query documents from a firestore collection from an array of ID's? I only want the documents in the collection that are in the array of ID's. I looked at another answer and think my approach is correct, however, I am getting an error.
(node:15105) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'data' of undefined
> at /Users/username/SideProjects/projectname/functions/index.js:40:38
> at processTicksAndRejections (internal/process/task_queues.js:95:5)
The error happens because the function is not finding any documents in the collection from that array of ID's. However, I double-checked the database and know that there are documents in the collection with ID's from the array.
const admin = require('firebase-admin')
....
let feedItems = db.collection(feedItemsCollection)
feedItemsList = feedItems.where(admin.firestore.FieldPath.documentId(), 'in', ['HPOorsSnbHpTYwwXxfWw']).get().then(snapshot2 => {
console.log(admin.firestore.FieldPath.documentId())
console.log("In feed Items")
//console.log(feedItemIds)
console.log(snapshot2[0])
//error happens on this line because snapshot2[0] returns undefined
console.log(snapshot2[0].data())
})
Snapshot2[0] returns undefined which I'm assuming means that no data was returned. I think I'm not properly calling documentId(), but don't know the fix.

There "maybe" two problems with your code. Follow both points to make sure things are working
Data inside snapshot2 maybe empty
You'll first have to fix your code to test this theory. You're not accessing data from snapshot2 correctly. To do it right, one way is this:
// `snapshot2` will have a `docs` property that you can leverage
const snapshot2Data = snapshot2.docs.map((doc) => doc.data());
.documentId() may not be doing what it's supposed to (as you said)
To test this theory, check if snapshot2Data is empty. Run :
console.log(snapshot2Data); // what do you get ?
If no, it's not empty and you got data back, then you're all set. Nothing more to do
If yes, it is empty, then run :
console.log(admin.firestore.FieldPath.documentId()); // what do you get ?
Did you get back a string? If no, then we have another problem. You'll need to take a closer look at your firebase-admin setup, as well.

Related

Best way to batch create if not exists in firestore

I am working with a project where we create a bunch of entries in firestore based on results from an API endpoint we do not control, using a firestore cloud function. The API endpoint returns ids which we use for the document ids, but it does not include any timestamp information. Since we want to include a createdDate in our documents, we are using admin.firestore.Timestamp.now() to set the timestamp of the document.
On subsequent runs of the function, some of the documents will already exist so if we use batch.commit with create, it will fail since some of the documents exist. However, if we use batch.commit with update, we will either not be able to include a timestamp, or the current timestamp will be overwritten. As a final requirement, we do update these documents from a web application and set some properties like a state, so we can't limit the permissions on the documents to disallow update completely.
What would be the best way to achieve this?
I am currently using .create and have removed the batch, but I feel like this is less performant, and I occasionally do get the error Error: 4 DEADLINE_EXCEEDED on the firestore function.
First prize would be a batch that can create or update the documents, but does not edit the createdDate field. I'm also hoping to avoid reading the documents first to save a read, but I'd be happy to add it in if it's the best solution.
Thanks!
Current code is something like this:
const createDocPromise = docRef
.create(newDoc)
.then(() => {
// success, do nothing
})
.catch(err => {
if (
err.details &&
err.details.includes('Document already exists')
) {
// doc already exists, ignore error
} else {
console.error(`Error creating doc`, err);
}
});
This might not be possible with batched writes as set() will overwrite the existing document, update() will update the timestamp and create() will throw an error as you've mentioned. One workaround would be to use create() for each document with Promise.allSettled() that won't run catch() if any of the promise fails.
const results = [] // results from the API
const promises = results.map((r) => db.doc(`col/${r.id}`).create(r));
const newDocs = await Promise.allSettled(promises)
// either "fulfilled" or "rejected"
newDocs.forEach((result) => console.log(result.status))
If any documents exists already, create() will throw an error and status for that should be rejected. This way you won't have to read the document at first place.
Alternatively, you could store all the IDs in a single document or RTDB and filter out duplicates (this should only cost 1 read per invocation) and then add the data.
Since you prefer to keep the batch and you want to avoid reading the documents, a possible solution would be to store the timestamps in a field of type Array. So, you don't overwrite the createdDate field but save all the values corresponding to the different writes.
This way, when you read one of the documents you sort this array and take the oldest value: it is the very first timestamp that was saved and corresponds to the document creation.
This way you don't need any extra writes or extra reads.

Firestore orderBy and where filters not working together

If I query my users collection and filter by 2 different properties as seen below, it returns the expected data.
return this.afs
.collection<Employee>('users', ref => {
return ref
.where('accountId', '==', accountId)
.where('isEmployee', '==', true);
})
.valueChanges()
However, if I try to order that data by name, as seen below, it returns an empty array.
return this.afs
.collection<Employee>('users', ref => {
return ref
.where('accountId', '==', accountId)
.where('isEmployee', '==', true)
.orderBy('name');
})
.valueChanges()
I figured I must need to manage the way indexing is being handled or something, but did some research and my understanding is that if that were the case, I would get an error message in the console with a link to setup the composite indexing that I need. I don't get any errors though, just an empty array. What am I doing wrong? Is it possible to order a collection of data that is filtered by 2 properties? I believe I am using Firebase version 6.5, but am willing to update if that will solve my problem.
I would get an error message in the console with a link to setup the composite indexing that I need.
Yes, that's correct. When you are using a query like yours, a warning message should be displayed in the console.
I don't get any errors though, just an empty array.
In this case, you should create that index manually in the Firebase console.
Is it possible to order a collection of data that is filtered by 2 properties?
Sure it is. Just create the required index and then you should be able to execute the query.
It doesn’t look like you’re ready to catch errors. May want to do something like:
.catch(error => console.log(error)
(Will format that when on desktop)

Trying to get a snapshot of a variable from firebase gives me an error

Problem
In a social media app I am making with react native and firebase, I am trying to grab the number of comments a post has using the snapshot function of a variable I have saved on my servers, then I am going to add one to this variable when a user adds a new comment. My code to do so is right here:
firebase.database().ref('posts').child(this.state.passKey).update({
comments: firebase.database().ref('posts/'+this.state.passKey).child('comments').snapshot.val() + 1
})
When I actually run this code, I get an error saying:
Reference.child failed: First argument was an invalid path = "undefined".
Paths must be non-empty strings and can't contain ".","#","$","[", or "["
At first I thought this might be that the "this.state.passKey" wasn't actually passing the key, but putting in a key I copied from the server didn't fix the problem.
My Server
-
To get the comments of particular post you should do like this
let postId='someId'
postRef=`/posts/${postId}`
firebase.database().ref(postRef).once("value", dataSnapshot => {
comment=dataSnapshot.val().comments
});
It looks like you're expecting this bit of code to query the database:
firebase.database().ref('posts/'+this.state.passKey).child('comments').snapshot.val() + 1
Unfortunately, it doesn't work that way. There's no snapshot property on a database Reference object returned by child() or ref().
Instead, you'll need to query the database at that reference, then when you're called back with its value, you can apply it elsewhere.
var ref = firebase.database().ref('posts/'+this.state.passKey+'/comments')
ref.once('value', function(snapshot) {
// use the snapshot here
})

Mongo DB - Why my Users.findOne is Undefined?

I'm working on Meteor, trying to find some values from Mongodb collection.
here is the code:
var sameLogins = Users.findOne({login: 'a'});
console.log(sameLogins);
But it's returning and "undefined".
But record exists in collection:
So, can anybody tell what I'm missing?
Also, in mongo console - everything is working fine:
I was looking in Publish/Subsribe stuff, but i'm using autopublish module yet.
Thank you!
I will leave the answer for this issue for new users having the same problem.
If you're using autopublish package then you should be aware that it's publishing the result of .find() for every collection.
But, Meteor.users.find(), be default, will return only _id and profile fields, so documents in your Meteor.users client collection will have these two fields only.
The most easy workaround for this would be to create your own publication (allUsers, for example) and in it to return those fields you need:
Server:
Meteor.publish('allUsers', () => {
// check for Meteor.userId() is omitted, put it here, if needed
return Meteor.users.find({}, { fields: { ... } });
});
Don't forget to subscribe to it:
Client:
Meteor.subscribe('allUsers');
Update for Meteor:
Right now you are storing a cursor in your variable sameLogins. In order to retrieve the results you want, you must actually execute this query by either calling fetch(). What is returned from findOne without fetch is essentially an object that you could use to iterate over and find mongoDB documents - (called a collection cursor). The cursor is not your result itself.
Calling fetch would like something like:
Users.findOne({login: 'a'}).fetch()

Meteor collection trouble

First off, sorry for being a complete javascript noob, I am more of a PHP guy and am just testing out the meteor framework.
I am trying to loop through a collection of objects and trying to add a property from another collections as so :
Template.host.hosts = function() {
var hosts = Hosts.find();
hosts.forEach(function(host) {
host.lastPing = Pings.findOne({id: host.id}, {sort: {timestamp : -1}});
// This works fine
// console.log(host.lastPing.id);
});
for (host in hosts) {
// This results in "TypeError: Cannot read property 'id' of undefined"
console.log(host.lastPing.id);
}
return hosts;
};
I don't understand why the second console.log is not working.
I have tried searching but I don't know if the problem is specific to the way meteor handles collections or the way I should be adding properties to a javascript object or someting completely unrelated (scope etc...)
I have simplified my problem to try to understand what is happening, my real problem is obviously looping in a template as per :
{{#each hosts}}
{{this.lastPing.id}}
{{/each}}
Thanks
Three things:
MongoDB and Meteor ids are stored in _id rather than id.
In the context of your forEach method, host iterates through the query set returned by Hosts.find(), but it doesn't actually give you access to the documents themselves. Essentially, it's a copy of the information in the MongoDB rather than the document in the database.
The correct (and only) way to update the actual document is by using the Collection.update method:
Hosts.update({_id: host._id}, {$set: {lastPing: Pings.findOne({id: host.id}, {sort: {timestamp : -1}}) }});
(note that you can only update by _id on the client which is why that's what I've used here, whereas you can supply any query on the server.)
The hosts object is a cursor rather than an array. This means that when you use for host in hosts, you're actually iterating through the properties of the cursor object (which are inherited from the prototype) rather than an array of hosts, and none of them has an id property. One way to make this work is to fetch the query set and put it into hosts like this:
var hosts = Hosts.find().fetch();
Alternatively, you can stick with the cursor and use forEach again, although you'll either have to rewind it with hosts.rewind(), or repeat the line above to reset it to the start of the query set.
Hope that's helpful.

Categories

Resources