Upsert in Firebase Firestore without the onFailure callback - firebase

There must be a better way to make upsert in Firebase Firestore in Kotlin.
I have collection of users that contains another collection userDocuments that contains field called highlights containing list of highlights.
I cannot use set and merge options as that will override the highlights list.
Any ideas how to make the code better. I do not like making two database requests on create and handling the failure like this. Maybe my database structure can be also optimized but I thought it is smart as all private userData will be stored in users collections with some subcollections.
My database structure is like this:
users -> {userId} -> userDocuments -> {docId} -> highlights ["this will be highlighted"]
users, and userDocuments are collections. Highlights is a field on userDocument.
docId might not yet be there, there will be 1000 of documents. And I do not want to add it to every user. I want it to be there, only when they make a change such as add or remove highlight to list of highlights.
usersCollection
.document(userId)
.collection("userDocuments")
.document(docId)
.update("highlights", FieldValue.arrayUnion(text))
.addOnFailureListener { err ->
// TODO should be handled differently
if (err is FirebaseFirestoreException &&
err.code === FirebaseFirestoreException.Code.NOT_FOUND
) {
val highlights = listOf(text)
usersCollection
.document(it)
.collection("userDocuments")
.document(docId)
.set(mapOf("highlights" to highlights), SetOptions.merge())
}
}

You can update using dictionary notation or dot notation too.
https://firebase.google.com/docs/firestore/manage-data/add-data#update_fields_in_nested_objects
db.collection("userDocuments")
.document(docId)
.update({
"highlights": FieldValue.arrayUnion(text)
});
You can consider using transactions as I mentioned in the comment above. But not sure if that is what you are looking for.

Related

How to only allow a unique document ID to be upload to Firebase Firestore using Rules [duplicate]

I want to create Firestore documents if they don't exist - if they do exist, skip them (don't update).
Here's the flow
var arrayOfRandomIds = [array of 500 random numbers];
for (var id of arrayOfRandomIds)
{
var ref = db.collection("tickets").doc(id);
batch.set(ref, {name: "My name", location: "Somewhere"}, { merge: true });
}
batch.commit();
I just want to know, would this overwrite any existing documents if they exist? I don't want anything overwritten, just skipped.
Thanks.
I think you can use security rules to accomplish that. That way you won't be charged for an additional document read to see if it already exists.
service cloud.firestore {
match /databases/{database}/documents {
match /tickets/{id} {
allow create;
}
}
}
Meanwhile there is a "create but don't overwrite" function.
Assuming you are using JavaScript here is the reference: https://googleapis.dev/nodejs/firestore/latest/DocumentReference.html#create
Here is the corresponding example code from the docs:
let documentRef = firestore.collection('col').doc();
documentRef.create({foo: 'bar'}).then((res) => {
console.log(`Document created at ${res.updateTime}`);
}).catch((err) => {
console.log(`Failed to create document: ${err}`);
});
Using .create() instead of .set() should do the trick for you without relying on security rules for application logic.
Firestore doesn't have a native "create but don't overwrite" operation. Here are the only available operations:
update: only change the contents of an existing document
set without merge: create or overwrite
set with merge: create or update if exists
Instead of a batch, what you can do instead is perform a transaction that checks to see if the document exists, then creates it conditionally if it does not already exist. You will have to write that logic inside your transaction handler.
I want to create Firestore documents if they don't exist - if they do exist, skip them (don't update).
In that case, you should check if a particular document actually exists in a collection, right before the write operation takes place. If it does not exist, create it, otherwise take no action.
So you should simply use set() function, without passing merge: true.

Firestore create documents if they don't exist, skip if they do

I want to create Firestore documents if they don't exist - if they do exist, skip them (don't update).
Here's the flow
var arrayOfRandomIds = [array of 500 random numbers];
for (var id of arrayOfRandomIds)
{
var ref = db.collection("tickets").doc(id);
batch.set(ref, {name: "My name", location: "Somewhere"}, { merge: true });
}
batch.commit();
I just want to know, would this overwrite any existing documents if they exist? I don't want anything overwritten, just skipped.
Thanks.
I think you can use security rules to accomplish that. That way you won't be charged for an additional document read to see if it already exists.
service cloud.firestore {
match /databases/{database}/documents {
match /tickets/{id} {
allow create;
}
}
}
Meanwhile there is a "create but don't overwrite" function.
Assuming you are using JavaScript here is the reference: https://googleapis.dev/nodejs/firestore/latest/DocumentReference.html#create
Here is the corresponding example code from the docs:
let documentRef = firestore.collection('col').doc();
documentRef.create({foo: 'bar'}).then((res) => {
console.log(`Document created at ${res.updateTime}`);
}).catch((err) => {
console.log(`Failed to create document: ${err}`);
});
Using .create() instead of .set() should do the trick for you without relying on security rules for application logic.
Firestore doesn't have a native "create but don't overwrite" operation. Here are the only available operations:
update: only change the contents of an existing document
set without merge: create or overwrite
set with merge: create or update if exists
Instead of a batch, what you can do instead is perform a transaction that checks to see if the document exists, then creates it conditionally if it does not already exist. You will have to write that logic inside your transaction handler.
I want to create Firestore documents if they don't exist - if they do exist, skip them (don't update).
In that case, you should check if a particular document actually exists in a collection, right before the write operation takes place. If it does not exist, create it, otherwise take no action.
So you should simply use set() function, without passing merge: true.

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.

Efficiently storing and getting likes in FireStore / Document DB

I have a page with posts and likes for each post.
In FireStore a collection of posts and a collection of likes, and I update the total_likes and recent likes array when a user likes or unlikes a post with cloud functions.
However, I can't figure out how to show for each post if the currently logged in user liked it or not. What's an efficient way to do that for.
Any pointers?
I believe you might need to look at data aggregration. Even though this example is with Angular, I also use the same principle in a different application: https://angularfirebase.com/lessons/firestore-cloud-functions-data-aggregation/
Alternatively, you could store the post_id's that your user likes in their own 'like_array'. Knowing which posts the user currently sees, you can cross reference the shown post_id's with the (single object) 'like_array' from the user to determine if he/she has liked a particular post. In the long run, you could disambiguate like_arrays based on days or weeks, and only query the like_arrays of this and last day/week - based on what post you are showing. If you are working with categories of posts, similar applies: different like_arrays for different categories.
Hope this helps!
One solution would be to have another collection in your Firestore database where you create a document by user, in which you save (and update) an object containing all the posts this user has liked.
Like
- likers (Collection)
- UserUID (doc)
- postIds {
post1_UID: true,
post2_UID: true
}
The idea is to use the technique described in the doc, here: https://firebase.google.com/docs/firestore/solutions/arrays#solution_a_map_of_values
I don't know which language you use in the front end but in JavaScript you would do:
var postToTestId = ....; <- You set this value as you need (e.g. as a function parameter)
firebase.auth().signInWithEmailAndPassword("...", ".....")
.then(function (info) {
var postId = 'azer';
return db.collection('likers')
.where('postIds.'+ postToTestId, '==', true)
.get();
})
.then(function(querySnapshot) {
if (querySnapshot.size > 0) {
console.log("USER LIKES THIS POST!!!");
}
})
.catch(function (error) {
console.log(error);
});
I don't think there is any solution without storing somewhere all the posts each user liked...

Firestore: remove sensitive fields on documents

I'm trying to figure it out how to remove a sensitive field on a firestore document. For example, my collection is a group information. The group is protected with a pin code field. Any one wants to join the group has to know the pin code.
In the meantime, I want to let users query what group is available to join. For query part, I don't want return group information with pin code information. Do we have anyway to remove sensitive fields from a document for Firestore for reading event?
Cloud function only supports write event. 1 possible solution is use cloud function on write event, and put pin code in a separate document. Is there a better solution? THanks.
My group schema is:
group: {
name: string,
pinCode: string
}
A user can either access a document, or they can't. There is no property-level access control in Firestore.
So to accomplish what you want, you will need to store the public and private information in separate documents.
You could either create a second document with the private information in the same collection and then secure them using:
match /databases/{database}/documents {
match /groups/{group} {
allow read: if resource.data.visibility != "private"
}
}
Alternatively (and simpler to secure) you could create a separate collection for the private documents.
You can create a Firebase Function that returns only the fields that you need (non sensitive), here an example:
exports.getTopUsers = functions.https.onCall(async (data) => {
const users = [];
return db.collection('users').orderBy('bids').limit(data.limit).get()
.then((querySnapshot) => {
querySnapshot.forEach((user) => {
users.push({
diplayName: user.get('displayName'),
});
});
return {
topUsers: users,
};
})
.catch((err) => {
console.error(err);
});
});
So, you need to create a separate array (that will be returned) and filling it with only the field that you want while iterating your Firestore collection.

Resources