Ho do I generate a value automatically in Firestore document? - firebase

My documents require an integer value which is used as index for queries. Each document contains an index field ( number) where I am assigning the values manually one by one. Maybe somewhere I can put something that stores the current index value and increments it and assigns it to the new document as it's index value whenever I create the new document.

There is no such feature in Cloud Firestore. You will need to come up with all the values yourself. The only thing that Firestore can generate for you automatically is a timestamp based on the server's sense of time.

I can think of two ways to handle this, though I don't know that you want to use an integer based index for your document id. If you delete one, your index is now off. And what about write failures? Race conditions? Etc. You may want to rethink your data structure & organization.
If using an integer is not required as the document id:
// Create a reference to a new document inside your collection
const ref = firebase.firestore().collection('myCollectionName').doc()
// Now you have an auto-generated document id you can use for your code
const myData = {...}
const setDoc = await firebase.firestore().collection('myCollectionName').doc(ref.id).set(myData)
If using an integer is required:
You'll want a separate collection/object that keeps track of the latest index so you don't run into collisions. Then, you'll want to increment that value to get the next index, and then use that as your id. This comes with inherent problems like...what if the data is bad as you try to enter it, but after you've incremented the value...etc.
// Collection: myIndex
// Doc: index
// Value: {lastIndex: 1}
const doc = await firebase.firestore().collection('myIndex').doc('index')
// You now have the last index value using:
const lastIndex = doc.val().lastIndex
const nextIndex = lastIndex + 1
const myData = {...}
// Now run a batched operation to write to both documents
const batch = firebase.firestore().batch()
// Update the index document
const indexUpdateRef = firebase.firestore().collection('myIndex').doc('index')
batch.update(indexUpdateRef, {lastIndex: nextIndex})
// Add your new myData document
const newDataRef = firebase.firestore().collection('myCollectionName').doc(nextIndex)
batch.set(newDataRef, myData)
// Commit the batch
await batch.commit()
As I said - I think this is a really bad idea and workflow, but it's doable. Lots missing from keeping this in sync as well.
In either case above...
You can take advantage of FieldValue.increment() to help auto increment your integer values, but that will add to more reads & writes, longer processing time, and higher charges for all of that. Which is why I started with and maintain that you should probably rethink your data structure or consider a RDB if you want autoincremented indices.

Related

Firestore rule to only add/remove one item of array

To optimize usage, I have a Firestore collection with only one document, consisting in a single field, which is an array of strings.
This is what the data looks like in the collection. Just one document with one field, which is an array:
On the client side, the app is simply retrieving the entire status document, picking one at random, and then sending the entire array back minus the one it picked
var all = await metaRef.doc("status").get();
List tokens=all['all'];
var r=new Random();
int numar=r.nextInt(tokens.length);
var ales=tokens[numar];
tokens.removeAt(numar);
metaRef.doc("status").set({"all":tokens});
Then it tries to do some stuff with the string, which may fail or succeed. If it succeeds, then no more writing to the database, but if it fails it fetches that array again, adds the string back and pushes it:
var all = await metaRef.doc("status").get();
List tokens=all['all'];
List<String> toate=(tokens.map((element) => element as String).toList());
toate.add(ales.toString());
metaRef.doc("status").set({"all":toate});
You can use the methods associated with the Set object.
Here is an example to check that only 1 item was removed:
allow update: if checkremoveonlyoneitem()
function checkremoveonlyoneitem() {
let set = resource.data.array.toSet();
let setafter = request.resource.data.array.toSet();
return set.size() == setafter.size() + 1
&& set.intersection(setafter).size() == 1;
}
Then you can check that only one item was added. And you should also add additional checks in case the array does not exist on your doc.
If you are not sure about how the app performs the task i.e., successfully or not, then I guess it is nice idea to implement this logic in the client code. You can just make a simple conditional block which deletes the field from the document if the operation succeeds, either due to offline condition or any other issue. You can find the following sample from the following document regarding how to do it. Like this, with just one write you can delete the field which the user picks without updating the whole document.
city_ref = db.collection(u'cities').document(u'BJ')
city_ref.update({
u'capital': firestore.DELETE_FIELD
})snippets.py

How to query an array of objects in a Firebase Cloud Function, to get a matching object and then update

I am using a scheduled task in a Firebase Cloud Function to query an array which contains a number of objects that need to be updated if a matching condition exists. My current attempt is using the 'array-contains' method to get the objects, then loop over them to find a matching condition which will then batch update the items. This is my data structure:
I need to find an object that is <= the current time, and also if the 'active' value = false.
export const liveMeetingsTrigger = functions.runWith( { memory: '1GB' }).pubsub
.schedule('every 1 minutes').onRun(async context => {
const now = admin.firestore.Timestamp.now();
const liveMeetings = await admin.firestore().collection('fl_content').where('meeting', 'array-contains', 'liveMeetingDate').get();
const batch = admin.firestore().batch();
liveMeetings.forEach(doc => {
if(doc.data().liveMeetingDate <= now && doc.data().active == false){
batch.update(doc.ref,'active',true);
}
});
return await batch.commit();
});
I have also tried using an exact object in the query instead of just using 'liveMeetingDate', but still get no results back, any help would be great - thanks.
Debugging: As the array I am trying to reach is inside of the (map) object 'liveMeetings' i have tried the dot notation (liveMeetings.meeting) with no success. Also trying a new collection with the the 'meeting' array at top level has provided no success.
Simple logging in the console (liveMeetings.size) shows that nothing is being returned on the query, so therefore the logging does not even reach the loop in the code.
As explained in this anwser the following query will not work:
const liveMeetings = await admin.firestore().collection('fl_content').where('meeting', 'array-contains', 'liveMeetingDate').get();
because the meetings array contain some objects, instead of "simple" or primitive data (e.g. string, number...).
You could query it with the exact objects, like:
const obj = {active: false, liveMeetingDate: ..., meetingId: ..., ....};
const liveMeetings = await admin.firestore().collection('fl_content').where('meeting', 'array-contains', 'obj').get();
Another approach would be to create a new collection which contains the similar documents (same Document ID) but with a meeting Array that contains only the liveMeetingDate property.
Finally, note that since your Array is within a map, you need to do
await admin.firestore().collection('fl_content').where('liveMeetings.meeting', 'array-contains', ...).get();
(PS: I don't mark this question as duplicate since you expressly ask for more help in the comments of the duplicate question/answer)

Fetch collection startAfter documentID

Is there a way to fetch document after documentID like
private fun fetchCollectoionnAfterDocumentID(limit :Long){
val db = FirebaseFirestore.getInstance()
var query:Query = db.collection("questionCollection")
.startAfter("cDxXGLHlP56xnAp4RmE5") //
.orderBy("questionID", Query.Direction.DESCENDING)
.limit(limit)
query.get().addOnSuccessListener {
var questions = it.toObjects(QuestionBO::class.java)
questions.size
}
}
I want to fetch sorted questions after a given Document ID. I know I can do it using DocumentSnapShot. In order to fetch the second time or after the app is resume I have to save this DocumentSnapshot in Preference.
Can It be possible to fetch after document ID?
startAfter - > cDxXGLHlP56xnAp4RmE5
Edit
I know I can do it using lastVisible DocumentSnapshot . But I have to save lastVisible DocumentSnapshot in sharedPreference.
When app launch first time 10 question are fetched from questionCollection. Next time 10 more question have to be fetched after those lastVisible. So for fetching next 10 I have to save DocumentSnapshot object in sharedPreference. Suggest me a better approach after seeing my database structure.
And one more thing questionID is same as Document reference ID.
There is no way you can pass only the document id to the startAfter() method and simply start from that particular id, you should pass a DocumentSnapshots object, as explained in the official documentation regarding Firestore pagination:
Use the last document in a batch as the start of a cursor for the next batch.
first.get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot documentSnapshots) {
=// Get the last visible document
DocumentSnapshot lastVisible = documentSnapshots.getDocuments()
.get(documentSnapshots.size() -1);
// Construct a new query starting at this document,
Query next = db.collection("cities")
.orderBy("population")
.startAfter(lastVisible) //Pass the DocumentSnapshot object
.limit(25);
// Use the query for pagination
}
});
See, here the lastVisible is a DocumentSnapshot object which represents the last visible object. You cannot pass only a document id. For more information, you can check my answer from the following post:
How to paginate Firestore with Android?
It's in Java but I'm confident you can understand it and write it in Kotlin.
Edit:
Please consider defining an order of your results so that all your pages of data can exist in a predictable way. So you need to either specify a startAt()/startAfter() value to indicate where in the ordering to begin receiving ordered documents or use a DocumentSnapshot to indicate the next document to receive, as explained above.
Another solution might be to put the document id into the document itself (as a value of a property) and order on it, or you can use FieldPath.documentId() to order by the id without having to add one.
You can also check this and this out.
There is one way to let startAfter(documentID) works.
Making one more document "get", then using the result as startAfter input.
val db = FirebaseFirestore.getInstance()
// I use javascript await / async here
val afterDoc = await db.collection("questionCollection").doc("cDxXGLHlP56xnAp4RmE5").get();
var query:Query = db.collection("questionCollection")
.startAfter(afterDoc)
.orderBy("questionID", Query.Direction.DESCENDING)
.limit(limit)
A simple way to think of this: if you order on questionID you'll need to know at least the value of questionID of the document to start after. You'll often also want to know the key, to disambiguate between documents with the same values. But since it sounds like your questionID values are unique within this collection, that might not be needed here.
But just knowing the key isn't enough, as that would require Firestore to scan its entire index to find that document. Such an index scan would break the performance guarantees of Firestore, which is why it requires you to give you the information it needs to perform a direct lookup in the index.

How to Remove Arrays of Element in Firebase Cloud Firestore using Ionic 4

In My Cloud Firestore database structure looks like this. Now, I'd like to delete index positions based on Index 0, Index 1 like this.
const arrayLikedImagesRef = {imageurl: image, isliked: true};
const db = firebase.firestore();
const deleteRef = db.collection('userdata').doc(`${phno}`);
deleteRef.update({
likedimages: firebase.firestore.FieldValue.arrayRemove(arrayLikedImagesRef)
});
});
As explained here, “bad things can happen when trying to update or delete array elements at specific indexes”. This is why the Firestore official documentation indicates that the arrayRemove() function will take elements (strings) as arguments, but not indexes.
As suggested in this answer, if you prefer using indexes then you should get the entire document, get the array, modify it and add it back to the database.
You can't use FieldValue to remove array items by index. Instead, you could use a transaction to remove the array items. Using a transaction ensures you are actually writing back the exact array you expect, and can deal with other writers.
For example (the reference I use here is arbitrary, of course, you would need to provide the correct reference):
db.runTransaction(t => {
const ref = db.collection('arrayremove').doc('targetdoc');
return t.get(ref).then(doc => {
const arraydata = doc.data().likedimages;
// It is at this point that you need to decide which index
// to remove -- to ensure you get the right item.
const removeThisIndex = 2;
arraydata.splice(removeThisIndex, 1);
t.update(ref, {likedimages: arraydata});
});
});
Of course, as noted in the above code, you can only be sure you are about to delete the correct index when you are actually inside the transaction itself -- otherwise the array you fetch might not line up with the array data that you originally selected the index at. So be careful!
That said, you might be asking what to do given that FieldValue.arrayRemove doesn't support nested arrays (so you can't pass it multiple maps to remove). In that case, you just want a variant of the above that actually checks values (this example only works with a single value and a fixed object type, but you could easily modify it to be more generic):
const db = firebase.firestore();
const imageToRemove = {isliked: true, imageurl: "url1"};
db.runTransaction(t => {
const ref = db.collection('arrayremove').doc('byvaluedoc');
return t.get(ref).then(doc => {
const arraydata = doc.data().likedimages;
const outputArray = []
arraydata.forEach(item => {
if (!(item.isliked == imageToRemove.isliked &&
item.imageurl == imageToRemove.imageurl)) {
outputArray.push(item);
}
});
t.update(ref, {likedimages: outputArray});
});
});
(I do note that in your code you are using a raw boolean, but the database has the isliked items as strings. I tested the above code and it appears to work despite that, but it'd be better to be consistent in your use of types).

Firestore Cloud Functions - Keeping Count of Amount of Documents in Collection

I am trying to write a cloud function that will keep track of the amount of Documents in the Collection. There isn't a ton of documentation on this probably because of Firestore is so now.. so I was trying to think of the best way to do this.. this is the solution I come up with.. I can't figure out how to return the count
Document 1 -> Collection - > Documents
In Document 1 there would ideally store the Count of Documents in the Collection, but I can't seem to figure out how to relate this
Let's just assume Document1 is a Blog post and the subcollection is comments.
Trigger the function on comment doc create.
Read the parent doc and increment its existing count
Write the data to the parent doc.
Note: If your the count value changes faster than once-per-second, you may need a distributed counter https://firebase.google.com/docs/firestore/solutions/counters
exports.aggregateComments = functions.firestore
.document('posts/{postId}/comments/{commentId}')
.onCreate(event => {
const commentId = event.params.commentId;
const postId = event.params.postId;
// ref to the parent document
const docRef = admin.firestore().collection('posts').doc(postId)
return docRef.get().then(snap => {
// get the total comment count and add one
const commentCount = snap.data().commentCount + 1;
const data = { commentCount }
// run update
return docRef.update(data)
})
});
I put together a detailed firestore aggregation example if you need to run advanced aggregation calculations beyond a simple count.

Resources