Let's say I have a data model with some optional properties. This could be for example a user object with a "firstname", a "lastname" and an optional "website" property.
In Cloud Firestore only user documents with a known website would have the "website" property set, for all other user documents this property would not exist.
My questions is now, how to query for all user documents without a "website" property?
Documents can contain properties with a null value data type (see data types documentation). This will then allow you to construct a query to limit results where the website property is null.
This is not quite the same as a missing property, but if you use custom objects to write data to Firestore, empty properties will automatically be saved as null rather than not at all. You can also manually/programmatically write a null value to the database.
In Android, I tested this using the following:
FirebaseFirestore.getInstance().collection("test").whereEqualTo("website", null).get();
Where my database structure looked like:
This returned only the document inuwlZOvZNTHuBakS6GV, because document 9Hf7uwORiiToOKz6zcsX contains a string value in the website property.
I believe you usually develop in Swift, where unfortunately custom objects aren't supported, but you can use NSNull() to write a null value to Firestore. For example (I'm not proficient in Swift, so feel free to correct any issues):
// Writing data
let docData: [String: Any] = [
"firstname": "Example",
"lastname": "User",
"website": NSNull()
]
db.collection("data").document("one").setData(docData) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
// Querying for null values
let query = db.collection("test").whereField("website", isEqualTo: NSNull())
The documentation doesn't mention a method to query for values that don't exist, so this seems like the next best approach. If anyone can improve or suggest alternatives, please do.
Related
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.
I have the following subcollection structure:
/projects/{projectId}/documents/{documentId}
Where each document has an access map used for checking security rules for each document:
{
title: "A Great Story",
content: "Once upon a time ...",
access: {
alice: true,
bob: false,
david: true,
jane: false
// ...
}
}
How the security rules are set:
match /{path=**}/documents/{documentId} {
allow list, get: if resource.data.access[request.auth.uid] == true;
}
When I want to query all documents alice can access across all projects:
db.collectionGroup("documents").where(`access.${uid}`, "==", true);
I get an error telling me that I need to create a collection group index for access.alice, access.david, etc.
How can I overcome this indexing issue with collection group queries?
As a quick overview of indexes, there are two types - a composite index (used for connecting multiple fields together) or a single-field index. By default, single-field indexes are automatically created for each field of a document for ordinary collection queries and disabled for collection group queries. If a field's value is an array, the values of that array will be added to an Array Contains index. If a field's value is primitive (string, number, Timestamp, etc), the document will be added to the Ascending and Descending indexes for that field.
To enable querying of the access field for a collection group query, you must create the index. While you could click the link in the error message you get when making the query, this would only add an index for the user you were querying (e.g. access.alice, access.bob, and so on). Instead, you should use the Firebase Console to tell Cloud Firestore to create the index for access (which will create indexes for each of its children). Once you've got the console open, use the "Add Exemption" button (consider this use of "exemption" to mean "override default settings") to define a new indexing rule:
In the first dialog, use these settings:
Collection ID: "documents"
Field Path: "access"
Collection: Checked ✔
Collection group: Checked ✔
In the second dialog, use these settings:
Collection Scope
Collection Group Scope
Ascending
Enabled
Enabled
Descending
Enabled
Enabled
Array Contains
Enabled
Enabled
In your security rules, you should also check if the target user is in the access map before doing the equality. While accessing a missing property throws a "Property is undefined on object." error which denies access, this will become a problem if you later combine statements together with ||.
To fix this, you can either use:
match /{path=**}/documents/{documentId} {
allow list, get: if request.auth.uid in resource.data.access
&& resource.data.access[request.auth.uid] == true;
}
or provide a fallback value when the desired key is not found:
match /{path=**}/documents/{documentId} {
allow list, get: if resource.data.access.get(request.auth.uid, false) == true;
}
As an example of the rules breaking, let's say you wanted a "staff" user to be able to read a document, even if they aren't in that document's access map.
These rules would always error-out and fail (if the staff member's user ID wasn't in the access map):
match /{path=**}/documents/{documentId} {
allow list, get: if resource.data.access[request.auth.uid] == true
|| get(/databases/$(database)/documents/users/$(request.auth.uid)).data.get("staff", false) == true;
}
whereas, these rules would work:
match /{path=**}/documents/{documentId} {
allow list, get: if resource.data.access.get(request.auth.uid, false) == true
|| get(/databases/$(database)/documents/users/$(request.auth.uid)).data.get("staff", false) == true;
}
As a side note, unless you need to query for documents where a user does not have access to it, it is simpler to simply omit that user from the access map.
{
title: "A Great Story",
content: "Once upon a time ...",
access: {
alice: true,
david: true,
// ...
}
}
As noted on https://cloud.google.com/firestore/docs/query-data/queries#collection-group-query "Before using a collection group query, you must create an index that supports your collection group query. You can create an index through an error message, the console, or the Firebase CLI."
In this case you'll want to enable CollectionGroup queries on the access map (https://cloud.google.com/firestore/docs/query-data/indexing#add_a_single-field_index_exemption)
You would have to create an index for it as mentioned by #Jim.
If you don't want to get much into it, you can just catch the error and it'll have a direct link to to create one as shown below:
db.collectionGroup("documents").where(`access.${uid}`, "==", true).get().then((snap) => {
//
}).catch((e) => console.log(e))
Other ways as mentioned in the documentation include:
Before using a collection group query, you must create an index that
supports your collection group query. You can create an index through
an error message, the console, or the Firebase CLI.
Let's say I have a data model with some optional properties. This could be for example a user object with a "firstname", a "lastname" and an optional "website" property.
In Cloud Firestore only user documents with a known website would have the "website" property set, for all other user documents this property would not exist.
My questions is now, how to query for all user documents without a "website" property?
Documents can contain properties with a null value data type (see data types documentation). This will then allow you to construct a query to limit results where the website property is null.
This is not quite the same as a missing property, but if you use custom objects to write data to Firestore, empty properties will automatically be saved as null rather than not at all. You can also manually/programmatically write a null value to the database.
In Android, I tested this using the following:
FirebaseFirestore.getInstance().collection("test").whereEqualTo("website", null).get();
Where my database structure looked like:
This returned only the document inuwlZOvZNTHuBakS6GV, because document 9Hf7uwORiiToOKz6zcsX contains a string value in the website property.
I believe you usually develop in Swift, where unfortunately custom objects aren't supported, but you can use NSNull() to write a null value to Firestore. For example (I'm not proficient in Swift, so feel free to correct any issues):
// Writing data
let docData: [String: Any] = [
"firstname": "Example",
"lastname": "User",
"website": NSNull()
]
db.collection("data").document("one").setData(docData) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
// Querying for null values
let query = db.collection("test").whereField("website", isEqualTo: NSNull())
The documentation doesn't mention a method to query for values that don't exist, so this seems like the next best approach. If anyone can improve or suggest alternatives, please do.
Let's say I have a data model with some optional properties. This could be for example a user object with a "firstname", a "lastname" and an optional "website" property.
In Cloud Firestore only user documents with a known website would have the "website" property set, for all other user documents this property would not exist.
My questions is now, how to query for all user documents without a "website" property?
Documents can contain properties with a null value data type (see data types documentation). This will then allow you to construct a query to limit results where the website property is null.
This is not quite the same as a missing property, but if you use custom objects to write data to Firestore, empty properties will automatically be saved as null rather than not at all. You can also manually/programmatically write a null value to the database.
In Android, I tested this using the following:
FirebaseFirestore.getInstance().collection("test").whereEqualTo("website", null).get();
Where my database structure looked like:
This returned only the document inuwlZOvZNTHuBakS6GV, because document 9Hf7uwORiiToOKz6zcsX contains a string value in the website property.
I believe you usually develop in Swift, where unfortunately custom objects aren't supported, but you can use NSNull() to write a null value to Firestore. For example (I'm not proficient in Swift, so feel free to correct any issues):
// Writing data
let docData: [String: Any] = [
"firstname": "Example",
"lastname": "User",
"website": NSNull()
]
db.collection("data").document("one").setData(docData) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
// Querying for null values
let query = db.collection("test").whereField("website", isEqualTo: NSNull())
The documentation doesn't mention a method to query for values that don't exist, so this seems like the next best approach. If anyone can improve or suggest alternatives, please do.
I am stuck trying to allow an an array of admins access to their data.
I have a database structure like this:
{
"Respondents": {
"Acme Corp": {
"admins": ["mMK7eTrRL4UgVDh284HntNRETmx1", ""mx1TERNmMK7eTrRL4UgVDh284Hnt"],
"data": {data goes here...}
},
"Another Inc": {
"admins": ["Dh284HmMK7eTrRL4UgVDh284HntN", ""x1TERNmx1TERNmMK7eTrRL4UgVDh"],
"data": {their data goes here...}
}
}
}
And then I tried to set my rules like this
{
"rules": {
"Respondents": {
"$organisation" : {
".read": "root.child('Respondents').child($organisation).child('admins').val().includes(auth.id)",
".read": "root.child('Respondents').child($organisation).child('admins').val().includes(auth.id)"
}
}
}
}
..but that won't parse in the Firebase Database Rules editor
I get "Error saving rules - Line 7: No such method/property 'includes'", but I need something to match the user id with the array of admins.
Any experience or suggestions?
As you've found, there is no includes() operation in Firebase's security rules. This is because Firebase doesn't actually store the data as an array. If you look in the Firebase Database console or read this blog post you will see that Firebase stores it as a regular object:
"admins": {
"0": "mMK7eTrRL4UgVDh284HntNRETmx1",
"1": "mx1TERNmMK7eTrRL4UgVDh284Hnt"
}
And since that is a regular JavaScript object, there is no contains() method on it.
In general creating arrays are an anti-pattern in the Firebase Database. They're often the wrong data structure and when used are regularly the main cause of scalability problems.
In this case: you're not really looking to store a sequence of UIDs. In fact: the order of the UIDs doesn't matter, and each UID can be meaningfully present in the collection at most once. So instead of an array, you're looking to store set of uids.
To implement a set in Firebase, you use this structure:
"admins": {
"mMK7eTrRL4UgVDh284HntNRETmx1": true,
"mx1TERNmMK7eTrRL4UgVDh284Hnt": true
}
The value doesn't matter much. But since you must have a value to store a key, it is idiomatic to use true.
Now you can test whether a key with the relevant UID exists under admins (instead of checking whether it contains a value):
"root.child('Respondents').child($organisation).child('admins').child(auth.uid).exists()",