How to query nested data in Firebase Database? - swift

How can I make a query to get all bookings of all users that have timestamp in the range of 1519912278 ...1520689878 ? In Firebase docs it states that I can query deeply nested children.
According to this answer, it should work https://stackoverflow.com/a/47805607 , but it return null.
What is the flow of my User app? :
1. User singn up
2. When booking is made, it is saved at /Users/UID/stripe_customer_created_with_card/bookingNumber{booking object}
Why do I need all bookings located under /Users?:
There is a second app designed for Workers which can observe /Users node and each worker should be able to get bookings from /Users based on a timeStamp. For example a worker1382 can read bookings made by /Users from 16 March 2018 to 24 March 2018.
dbRef = FIRDatabase.database().reference().child("Users")
dbRef.queryOrdered(byChild: "TimeStampDateAndTime")
.queryStarting(atValue: "1519912278")
.queryEnding(atValue: "1520689878")
.observeSingleEvent(of: .value, with: { (snapshot: FIRDataSnapshot) in
print(snapshot.value) //returns null
}
Users
UID
cus_CRC50bSq7rXuHv //stripe customer id for the user
617643762 //booking number
TimeStampDateAndTime: "1521309610"
Postcode: "E14 9DS"
BookingNumber:"617643762"
Another question: The parent key of the booking object is equal to BookingNumber. If I replace the parent key 617643762 with TimeStampDateAndTime value 1521309610, would I be able to order the bookings by time stamp and ultimately get only the ones that are within a certain range 1519912278 ...1520689878? So, the path would look like Users/UID/cus/1521309610: {object goes here}
From Firebase Documentation
Queries can also be ordered by deeply nested children, rather than only children one level down. This is useful if you have deeply nested data like this:
{
"lambeosaurus": {
"dimensions": {
"height" : 2.1,
"length" : 12.5,
"weight": 5000
}
},
}
To query the height now, we use the full path to the object rather than a single key.
Now, my question is, what can't I do if I don't know the full path?
let ref = Firebase(url:"https://dinosaur-facts.firebaseio.com/dinosaurs")
ref.queryOrderedByChild("dimensions/height").observeEventType(.ChildAdded, withBlock: { snapshot in
if let height = snapshot.value["height"] as? Double {
println("\(snapshot.key) was \(height) meters tall")
}
})
EDIT 2
Initial Structure looks like this:
{
"Users" : {
"I50dHN7wPqPJClPfYDG76gXizdD2" : { //-> user?.uid of a logged in user
"cus_CRC50bSq7rXuHv" : {
"617643762" : {
"TimeStampDateAndTime" : "1521309610"
}
}
}
}
}
Modified structure:
{
"Users" : {
"I50dHN7wPqPJClPfYDG76gXizdD2" : {
"1521309610" : { // -> store timeStamp as the immediate child key of UID rather than the booking number
"TimeStampDateAndTime" : "1521309610",
"BookingNumber": "617643762"
}
}
}
}
{
"UsersProfile":{
"I50dHN7wPqPJClPfYDG76gXizdD2":{
"StripeCustomer":"cus_CRC50bSq7rXuHv", // -> store StripeCustomer on the UsersProfile
"Name": "John Doe"
}
}
}

You can query properties at a known path under each child node. So you can query dimensions/weight in the dinosaur sample, and you can query 617643762/TimeStampDateAndTime from your data.
But you can't query */TimeStampDateAndTime, since that's not a known path.
If you need to get a list of bookings in a date range across all users, you will need to modify your data structure to allow that query. For example, by adding a top-level list of bookings:
"Bookings": {
"book_617643762"
Customer: "cus_CRC50bSq7rXuHv"
TimeStampDateAndTime: "1521309610"
Postcode: "E14 9DS"
}
With this additional structure, you can then query:
dbRef = FIRDatabase.database().reference().child("Bookings")
dbRef.queryOrdered(byChild: "TimeStampDateAndTime")
.queryStarting(atValue: "1519912278")
.queryEnding(atValue: "1520689878")
.observeSingleEvent(of: .value, with: { (snapshot: FIRDataSnapshot) in
print(snapshot.value) //returns null
}

Related

Updating Firebase Data for Any User in Swift

I am trying to allow documents (reports) that are created by a user to be viewed by other users and then updated with changes. The documents show up in a tableView that when selected show the appropriate data for that specific report loaded up in a ViewController. However when I go to select the update button within that ViewController of the individual report, the error comes back that no document can be found.
Below is the current variation of code I am working with for the update function.
private let database = Firestore.firestore()
private init() {}
// UPDATE REPORT
public func updateTheData(
reportPost: ReportPost,
thisReport: String,
completion: #escaping (Bool) -> Void
){
let data = [
"id": reportPost.identifier,
"title": reportPost.title,
"timestamp1": reportPost.timestamp1,
"address": reportPost.address,
"customerPO": reportPost.customerPO,
"authNum": reportPost.authNum,
"contactName": reportPost.contactName,
"contactPhone": reportPost.contactPhone,
"modelNum": reportPost.modelNum,
"serialNum": reportPost.serialNum,
"addInfo": reportPost.addInfo,
"equipProblem": reportPost.equipProblem,
"action": reportPost.action,
"followUp": reportPost.followUp,
"techName1": reportPost.techName1,
"techName2": reportPost.techName2,
"techName3": reportPost.techName3,
"techName4": reportPost.techName4,
"timestamp2": reportPost.timestamp2,
"timestamp3": reportPost.timestamp3,
"timestamp4": reportPost.timestamp4,
"milesTraveled1": reportPost.milesTraveled1,
"milesTraveled2": reportPost.milesTraveled2,
"milesTraveled3": reportPost.milesTraveled3,
"milesTraveled4": reportPost.milesTraveled4,
"travelTime1": reportPost.travelTime1,
"travelTime2": reportPost.travelTime2,
"travelTime3": reportPost.travelTime3,
"travelTime4": reportPost.travelTime4,
"siteTime1": reportPost.siteTime1,
"siteTime2": reportPost.siteTime2,
"siteTime3": reportPost.siteTime3,
"siteTime4": reportPost.siteTime4,
"serviceType1": reportPost.serviceType1,
"serviceType2": reportPost.serviceType2,
"serviceType3": reportPost.serviceType3,
"serviceType4": reportPost.serviceType4,
"timeTotal1": reportPost.timeTotal1,
"timeTotal2": reportPost.timeTotal2,
"timeTotal3": reportPost.timeTotal3,
"timeTotal4": reportPost.timeTotal4,
"partNum1": reportPost.partNum1,
"partNum2": reportPost.partNum2,
"partNum3": reportPost.partNum3,
"partNum4": reportPost.partNum4,
"quantity1": reportPost.quantity1,
"quantity2": reportPost.quantity2,
"quantity3": reportPost.quantity3,
"quantity4": reportPost.quantity4,
"price1": reportPost.price1,
"price2": reportPost.price2,
"price3": reportPost.price3,
"price4": reportPost.price4,
"priceTotal1": reportPost.priceTotal1,
"priceTotal2": reportPost.priceTotal2,
"priceTotal3": reportPost.priceTotal3,
"priceTotal4": reportPost.priceTotal4,
]
let docRef = database.collection("reports").document(thisReport)
docRef.updateData(data) { error in
if let error = error {
print("Error updating document: \(error)")
} else {
print("Document successfully updated")
}
}
}
For Reference, this is how a report is saved and uploaded to the database in Firebase.
// FSR REPORT POSTING
public func insert(
reportPost: ReportPost,
email: String,
completion: #escaping (Bool) -> Void
) {
let userEmail = email
.replacingOccurrences(of: ".", with: "_")
.replacingOccurrences(of: "#", with: "_")
let data = [
"id": reportPost.identifier,
"title": reportPost.title,
"timestamp1": reportPost.timestamp1,
"address": reportPost.address,
"customerPO": reportPost.customerPO,
"authNum": reportPost.authNum,
"contactName": reportPost.contactName,
"contactPhone": reportPost.contactPhone,
"modelNum": reportPost.modelNum,
"serialNum": reportPost.serialNum,
"addInfo": reportPost.addInfo,
"equipProblem": reportPost.equipProblem,
"action": reportPost.action,
"followUp": reportPost.followUp,
"techName1": reportPost.techName1,
"techName2": reportPost.techName2,
"techName3": reportPost.techName3,
"techName4": reportPost.techName4,
"timestamp2": reportPost.timestamp2,
"timestamp3": reportPost.timestamp3,
"timestamp4": reportPost.timestamp4,
"milesTraveled1": reportPost.milesTraveled1,
"milesTraveled2": reportPost.milesTraveled2,
"milesTraveled3": reportPost.milesTraveled3,
"milesTraveled4": reportPost.milesTraveled4,
"travelTime1": reportPost.travelTime1,
"travelTime2": reportPost.travelTime2,
"travelTime3": reportPost.travelTime3,
"travelTime4": reportPost.travelTime4,
"siteTime1": reportPost.siteTime1,
"siteTime2": reportPost.siteTime2,
"siteTime3": reportPost.siteTime3,
"siteTime4": reportPost.siteTime4,
"serviceType1": reportPost.serviceType1,
"serviceType2": reportPost.serviceType2,
"serviceType3": reportPost.serviceType3,
"serviceType4": reportPost.serviceType4,
"timeTotal1": reportPost.timeTotal1,
"timeTotal2": reportPost.timeTotal2,
"timeTotal3": reportPost.timeTotal3,
"timeTotal4": reportPost.timeTotal4,
"partNum1": reportPost.partNum1,
"partNum2": reportPost.partNum2,
"partNum3": reportPost.partNum3,
"partNum4": reportPost.partNum4,
"quantity1": reportPost.quantity1,
"quantity2": reportPost.quantity2,
"quantity3": reportPost.quantity3,
"quantity4": reportPost.quantity4,
"price1": reportPost.price1,
"price2": reportPost.price2,
"price3": reportPost.price3,
"price4": reportPost.price4,
"priceTotal1": reportPost.priceTotal1,
"priceTotal2": reportPost.priceTotal2,
"priceTotal3": reportPost.priceTotal3,
"priceTotal4": reportPost.priceTotal4,
]
database
.collection("users")
.document(userEmail)
.collection("reports")
.document(reportPost.identifier)
.setData(data) { error in
completion(error == nil)
}
}
When the report is first inserted, it's stored at this path
database.collection("users")
.document(userEmail)
.collection("reports")
.document(reportPost.identifier)
.setData(data)
which is
users/the_email/reports/some id
but then when you're attempting to update that report it's at this path
database.collection("reports")
.document(thisReport)
.updateData(data)
which is
database/reports/some id
and that's two different locations.
To update data, it needs to point to the same document reference. so the data needs to be updated at the same location it was initially written to
database.collection("users")
.document(userEmail)
.collection("reports")
.document(reportPost.identifier)
.updateData(data) //<- same path as it was initially
As a side note, the documentId is both being stored as the documentId as well as within the document in the "id" field and there's no reason to do that. A documentId is static and if it's known, you can get to that data directly without a query.
I would also suggest not using users emails as documentIds'; user emails can change and if that happens you won't be able to find the report. Additionally, if you want to update that, you'll have to find every occurance of that old email in the entire database, read it and it's child data in, delete it and re-write it. What a pain.
Also, substituting _ for # and . in the email address may work to a point but if want to get back that email address, there could be multiple _ in the address; how would you know which _ was the # and which one was actually part of the email address.
some_person#thing.com -> some_person_thing_com = some#person_thing.com?
EDIT
Per the question, suppose there are a series of reports that are 1) shared amongst users 2) the creating user can edit their own reports
Here's one option for storing the reports
reports (collection)
document_0 //a document with an auto-generated documentId
created_by_uid: "uid_0"
report_info: "some info about this report, fields etc"
document_1
created_by_uid: "uid_1"
report_info: "some info about this report"
The reports collection can be read by any user, which meets criteria 1. It's easy to get the reports created by user with uid_0 as a simple query can be run against the reports collection where created_by_uid = uid_0.
Here's the code with an object to hold the report data: documentId the report_info (the report data) and a field to track who created the report. Suppose user with uid_0 is logged in and wants to edit a report. We'll load all of the reports uid_0 created and store them in an array:
class ReportClass {
var id = ""
var report_info = ""
var created_by_uid = ""
}
var reportArray = [ReportClass]()
func readReports(forUid: String) { //pass in uid_0
let reportsCollection = self.db.collection("reports")
reportsCollection.whereField("created_by_uid", isEqualTo: forUid)
reportsCollection.getDocuments(completion: {documentSnapshot, error in
for doc in documentSnapshot!.documents {
//note we don't need the created_by_uid because it's not going to change
let report = ReportClass()
//keep track of the document Id
report.id = doc.documentID as! String
//the report info which the user is changing
report.report_info = doc.get("report_info") as! String
self.reportArray.append(report)
}
})
}
now the reports are presented to the user so they can select one and edit it. Upon saving we update the ReportClass object report_info field with the new data the user input and pass the object to a function to update that report
func updateReport(theReport: ReportClass) {
let reportsCollection = self.db.collection("reports")
let thisReport = reportsCollection.document(theReport.id)
//the report document path is reports/documentId
thisReport.updateData(["report_info": theReport.info]) //only update the report_info field
}
The key here is we load in and store the users reports, keeping the report documentId in the object. That keeps track of the path to the report
reports/report id/id, report_info and created_by_uid fields
then when the report is ready to be written, we derive the path from the documentId stored in the object so the path is the same.
I did a lot of this long-hand, but you should be using Codable objects as it makes populating them a snap
See Mapping Firestore Data in Swift

If you have a firebase snapshot with a filter applied to the current user’s child, how can one do that for all users in the database?

If you have a firebase snapshot that you are filtering for the current user, how do you do that for all users in the database?
Path: (people.child(uid).child(peopleWhoLike2)
It works for the current user, but I want it for all users. Probably something like for peopleArray in snapshot.children.allObjects as! [DataSnapshot] { needs to be used. Below is code for current
let thisUserRef1 = Database.database().reference().child("people").child(uid)
let myPeopleRef1 = thisUserRef1.child("peopleWhoLike2")
myPeopleRef1.observeSingleEvent(of:DataEventType.value, with: { snapshot in
print("klk")
let peopleArray = snapshot.children.allObjects as! [DataSnapshot]
let filteredResults = peopleArray.filter { person in
let personUid = person.value as! Int
let coordSnap12 = personUid
print("kjk", coordSnap12 )
let date = Date(timeIntervalSince1970: TimeInterval(coordSnap12)/1000.0)
//let secondsInDay = 86400
return Calendar.current.isDateInToday(date)
}
print(filteredResults, "ppp")
let countb = filteredResults.compactMap({$0}).count
print(countb, "ccc")
JSON DB Structure
"people" : {
"1ZWT7FAE2qThNQfBj7tbMO7BnMo1" : {
"Coordinates" : {
"latitude" : 50.054738,
"longitude" : 8.226809826085624
"peopleWhoLike2" : {
"1vLVFwrXrHUoakmDrnQKwbv08Yj1" : 1581548952597,
"F9NX0UCG4fVHCKFk2VZ1NZKsLro2" : 1586210112155,
"IrrBgFY9C1ekMmHUkQRzc5LhbDu1" : 1581547417432,
Firebase Realtime Database doesn't have a direct way to get the count of number of children in a node without reading in that nodes data. So there are two methods.
Observe the node, iterating over the children with .childAdded to get a count. Or, along those same lines load in all of the children with .value - to which you can get a count of the snapshots children. The downside there is it could be a LOT of data so with larger datasets, or unknown amount of children this is discouraged. It's also very read heavy obviously as if you have 1000 users, it's a lot of data to parse through.
Keep a separate node of the child count (this is the winner!)
The additional node would look like this
like_me_counts
"1ZWT7FAE2qThNQfBj7tbMO7BnMo1": 3
"another user id": 20
"yet another user id": 12
So as a user has another person like them, add the 'liker' to the "peopleWhoLike2" node as shown in your question, and then increment that users matching node within the like_me_counts node.
In this case if another person like user 1ZWT7... read that in from the like_me_counts node to get the value of 3, add one and then write out 4. If a user unlikes them, read it in, subtract 1 and write out the new value.
You can wrap all of that into a transaction so it either all succeeds or all fails.
EDIT
I would even go so far to say that it may be best to break out the people who like you into it's own node
like_me
uid_0
user_10: true
user_11: true
uid_1
user_39: true
uid_2
user_46: true
What that structure, each user would add a child added observer to their like_me node, and when someone likes them... say user_52 likes user_2
uid_2
user_46: true
user_52: true
then uid_2 will receive that notification and can handle it. Obviously, I am just using true as a placeholder but there could be other info such as a time stamp or even child nodes that can be used for filtering (for todays likes only for example)
uid_2
user_46: 20200624
user_52: 20200629

How can I retrieve data with queryEqualToValue in auto id child. Firebase-iOS

As my question. I can't retrieve data from firebase when I try to use "queryEqualToValue" with auto id key.
self.ref.child(..my child..).queryOrderByChild("name").queryEqualToValue("my name")
auto child id above child "name".
Edit: for my json tree
Students
(auto id)
name
first name
nick name
My real data doesn't like this but this's for example structure.
I really want to check equal to "first name".
Here's my code
let ref = FIRDatabase.database().reference()
ref.child("Students").queryOrderedByChild("name").queryEqualToValue("Jason bourne").observeEventType(.Value, withBlock: { snapshot in
print("value : " + snapshot.value)
}
Given your realtime database looks something like this:
{
"students": {
1: {
"name": {
"first_name": "Nathapong",
"nick_name": "Oniikal3"
}
}
}
}
You can observe the students path with the event type ChildAdded and order the query by child key name/first_name. Then you can use queryEqualToValue to find students with a particular first name.
let ref = FIRDatabase.database().referenceWithPath('students').queryOrderByChild("name/first_name").queryEqualToValue("Nathapong")
ref.observeSingleEventOfType(.ChildAdded, block: { snapshot in
print(snapshot)
})
This code should work. Check for names if I made any mistakes and insert your data.
self.ref.child("Students").child("name").queryOrderedByChild("first name").queryEqualToValue("my name").observeSingleEventOfType(.Value) { (snapshot: FIRDataSnapshot) in
print(snapshot.value)
}
Before use queries with child, you need to specify this using ".indexOn":["name"] in Database Rules. See documentation for examples
{
"rules":{
".write": "true",
".read": "true",
"Students": {
"$key": {
".indexOn":["name"]
}
}
}
}
Your read/write rules can go where you need it. .indexOn is just like another rule, alongside with .validate

Structuring data for chat app in Firebase

Im following Firebase guide to structuring data for a chat app. They suggest the structure as seen below.
{
// Chats contains only meta info about each conversation
// stored under the chats's unique ID
"chats": {
"one": {
"title": "Historical Tech Pioneers",
"lastMessage": "ghopper: Relay malfunction found. Cause: moth.",
"timestamp": 1459361875666
},
"two": { ... },
"three": { ... }
},
// Conversation members are easily accessible
// and stored by chat conversation ID
"members": {
// we'll talk about indices like this below
"one": {
"ghopper": true,
"alovelace": true,
"eclarke": true
},
"two": { ... },
"three": { ... }
},
// Messages are separate from data we may want to iterate quickly
// but still easily paginated and queried, and organized by chat
// converation ID
"messages": {
"one": {
"m1": {
"name": "eclarke",
"message": "The relay seems to be malfunctioning.",
"timestamp": 1459361875337
},
"m2": { ... },
"m3": { ... }
},
"two": { ... },
"three": { ... }
}
}
How do I structure my user data so that I can easily display a list of all of the chats they are part of and for each one of them display the last message and timestamp. If I do the following structure:
"users": {
"ghopper": {
"name": "Gary Hopper",
"chats": {
"one: true",
"two": true
}
},
"alovelace" { ... }
},
I can easily get a list of each chat group for a specific user, for example ghopper, by doing (in swift):
ref.child("users").child("ghopper").child("chats").observeSingleEventOfType(.Value, withBlock: { (snapshot) in
//do something with data
}
However I won't have the lastMessage and timestamp in this snapshot. What do I need to do to access this data?
Duplicate all this data for each user? i.e adding users/ghopper/chats/one/ {"lastMessage": "ghopper: Relay malfunction found. Cause: moth.", "timestamp" : 1459361875666}
Make a query for "chats/specificGroupId" for each chat that the user is part of (adding multiple listners)?
Some other way?
How do I structure my user data so that I can easily display a list of
all of the chats they are part of and for each one of them display the
last message and timestamp.
Change the chats structure a tad by adding users who are in the chat node
"chats": {
"one": {
"title": "Historical Tech Pioneers",
"lastMessage": "ghopper: Relay malfunction found. Cause: moth.",
"timestamp": 1459361875666
users
uid_1: true
uid_3: true
},
"two": { ... },
Then you can deep query for all chats a particular user is part of - this will return the chats uid_3 is involved in
chatsRef.queryOrderedByChild("users/uid_3").queryEqualToValue(true)
.observeSingleEventOfType(.Value, withBlock: { snapshot in
//.Value can return multiple nodes within the snapshot so iterate over them
for child in snapshot.children {
let lastmsg = child.value["lastMessage"] as! String
let timestamp = child.value["timestamp"] as! String
print(lastmsg)
print(timestamp)
}
})
Note that each firebase user has a discreet user id obtained when the user is created via auth.uid. This should (generally) be used as the key for each user.
In the block where you have a list of all the chats a user is in, can you do:
var dictionary: [String: Long]
var lastMessage: String
for chat in listOfChatsUserIsIn
ref.child("chats").child("\(chat)").child("lastMessage").observeSingleEventOfType(.Value, withBlock: { (snapshot) in
lastMessage = snapshot
ref.child("chats").child("\(chat)").child("timestamp").observeSingleEventOfType(.Value, withBlock: { (snapshot) in
//add timestamp and last message to dictionary
}
}
I don't know how correct my syntax is. Still learning firebase. But, I think this is basically your second suggestion. Don't know how else you would get the data. This would be O(2n) though which isn't bad.
[[Update 1]]
I was being lazy with my code. I put lastMessage = snapshot to save it so you could add it to the dictionary in the next block.
As for Firebase being asynchronous. I think this would still work as long as you use either the timestamp or message as the key and the other as the value in the dictionary. It may be populated out of order, but you could always sort it by timestamp later. Although, yes, this probably is not best practice.
Jay, I like your solution. Wouldn't you have to also list uid_2: false?
Unfortunately, it seems like both these database structures grow by n^2 as users -> inf and as chats -> inf.

How should you query a collection via nested arrays in mongodb (Meteor)?

I'm dont think that this is a Meteor specific question, but rather around mongo and building mongo queries.
If I have the following structure,
{
username : someName,
contacts : [
{
userid : asdfae33rtqqxxx,
name : contactName,
status : friend
}
{
userid : asdfae33rtqqxxx,
name : anotherName,
status : pending
}
{
userid : asdfae33rtqqxxx,
name : contactName,
status : blocked
}
]
}
How could I pass in values from this array into a query against the users collection, to a) get the users in this array, or b) get all users in this array from the users collection with a particular status.
If this is not possible, how should I adjust my schema in order to make these sorts of queries possible?
This function will return a Meteor.users cursor based on an array of contacts and an optionally required status:
var usersByContacts = function(contacts, requiredStatus) {
var userIds = _.chain(contacts)
.map(function(c) {
if (requiredStatus) {
if (c.status === requiredStatus)
return c.userid;
} else {
return c.userid;
}
})
.compact()
.value();
return Meteor.users.find({_id: {$in: userIds}});
};
You can use it like this:
var users1 = usersByContacts(thing.contacts);
var users2 = usersByContacts(thing.contacts, 'pending');
This assumes thing has the schema referenced in your question. Also note that if you find this is a common pattern in your code, you should consider turning usersByContacts into a transform.