How to move Firebase relational queries server side - javascript

This is my first project with Firebase, and I've created a relational data structure. Now I see why this isn't the best way to do things!
In this part of my app, users can add multiple items to an outfit - here's a diagram of the data structure/relationship I have in Firebase now.
I've included code from a Redux action creator in my React Native app. When a user edits an outfit - removing some items - this code:
takes an array with the new list of items from the client
compares this array with the current saved items server side
creates a new array of the diff (removed items)
loops through the outfits for each of those items, matching against the outfit being edited
removes references that match
This code works, but is pretty deeply nested and messy:
export const updateTagReferences = (localTags, outfitId) => {
//localTags represents the new set of items from application state
//outfitId is the uid for the outfit where those items appear
const {currentUser} = firebase.auth();
return dispatch => {
firebase
.database()
.ref(`users/${currentUser.uid}/outfits/${outfitId}/taggedItems`)
.once('value')
.then(snapshot => {
var serverTags = snapshot.val();
// Work out the diff (ie, which items have been removed locally)
return _.differenceWith(serverTags, localTags, _.isEqual);
})
.then(toDelete => {
toDelete.map(item => {
firebase
.database()
.ref(`users/${currentUser.uid}/items/${item.item.uid}/outfits`)
.once('value')
.then(snapshot => {
var outfits = snapshot.val();
for (var taggedItem in outfits) {
if (outfits.hasOwnProperty(taggedItem)) {
var i = outfits[taggedItem];
return i.uid === outfitId
? firebase
.database()
.ref(
`users/${currentUser.uid}/items/${item.item.uid}/outfits/${taggedItem}`,
)
.remove()
: null;
}
}
});
});
})
.then(console.log('Done'));
};
};
As you can see, I'm trying to use Firebase promises to loop through the outfits nested in each item server side.
My issues are:
I'm making lots of queries to Firebase, which isn't ideal
console.log('Done') fires before .remove() - I need to follow up with a second action once this action completes
I've tried the following code, based on this answer, but I can't seem to get it to work:
export const updateTagReferences = (localTags, outfitId) => {
const {currentUser} = firebase.auth();
return dispatch => {
// Create a ref for the outfit the user is editing
firebase
.database()
.ref(`users/${currentUser.uid}/outfits/${outfitId}/taggedItems`)
.once('value')
.then(snapshot => {
// Compare local (app state) with remote (Firebase) to work out which items have been removed
var serverTags = snapshot.val();
return _.differenceWith(serverTags, localTags, _.isEqual);
})
.then(toDelete => {
var promises = [];
// Create a ref for each item to delete
toDelete.map(item => {
firebase
.database()
.ref(`users/${currentUser.uid}/items/${item.item.uid}/outfits`)
.once('value')
.then(snapshot => {
var outfits = snapshot.val();
for (var taggedItem in outfits) {
// Loop through the outfits that item appears in
if (outfits.hasOwnProperty(taggedItem)) {
var i = outfits[taggedItem];
// Match against the outfit the user is editing
return i.uid === outfitId
? promises.push(
firebase
.database()
.ref(
`users/${currentUser.uid}/items/${item.item.uid}/outfits/${taggedItem}`,
)
// Remove the reference
.remove(),
)
: null;
}
}
})
// Execute all the promises, removing firebase references for each item removed locally
.then(Promise.all(promises).then(console.log('Done')));
});
});
};
};
I'm trying to make my code more efficient overall, so I can replicate it elsewhere in my project. I'd love an explanation of the best way to move this kind of query server side.
Currently, changing the whole data structure is out of scope (that's for next time!)
Thanks.

Related

How to retrieve nested data using Javascript (Firebase Realtime Database)?

I want to retrieve the data from donation and display it in a table. I was able to retrieve the user data from Users and displayed it on a table. But now I don't know how I will be able to retrieve the data from donation.
This is my database structure in Firebase. Note: All of the data that was entered came from a mobile app created in Android Studio.
This is the code that I made when retrieving the User data.
function AddAllITemsToTable(User) {
id=0;
tbody.innerHTML="";
User.forEach(element => {
AddItemToTable(element.uid, element.fullName, element.organization, element.contactPerson, element.contactNo, element.location, element.emailAddress, element.status);
});
}
function GetAllDataRealtime() {
const dbRef = ref(database, 'Users');
onValue(dbRef,(snapshot) => {
var Users = [];
snapshot.forEach(childSnapshot => {
Users.push(childSnapshot.val());
});
AddAllITemsToTable(Users);
})
}
window.onload = GetAllDataRealtime;
Since you're calling onValue on /Users, you already get all data for all users and all of their donations. To process the donations in your code:
const dbRef = ref(database, 'Users');
onValue(dbRef,(snapshot) => {
var Users = [];
snapshot.forEach(userSnapshot => {
Users.push(userSnapshot.val());
userSnapshot.child("donation").forEach((donationSnapshot) => {
console.log(donationSnapshot.key, donationSnapshot.val());
});
});
AddAllITemsToTable(Users);
})
As I said in my comment, I recommend reading the Firebase documentation on structuring data, as the way you nest donations under each user does not follow the guidance on nesting data and keeping your structure flat.

Update 2 children in firebase realtime database with react native

Im trying to update two children in my database (using realtime database firebase), but when database is updated, my application go back to the home screen for no reason.
When I update only in "Tasks" it works (the app doesnt go back to the home screen) but when I combine update in "Tasks" and "Users" there is this problem..
Maybe i dont do it the good way.. Any ideas?
statusPlayback = async (status) => {
const { navigation } = this.props
const task = navigation.getParam('task')
//console.log("task = ", task);
//to check if we arrived to the end of the
if (status.didJustFinish) {
const CountVideoRef = firebase
.database()
.ref("Tasks")
.child(firebase.auth().currentUser.uid).child(task.AssignPerson)
.child(task.taskname);
CountVideoRef.once("value").then((snapshot) => {
CountVideoRef.update({
countViewVideo: snapshot.val().countViewVideo + 1,
});
})
const PointEndVideoRef = firebase
.database()
.ref("Users")
.child(firebase.auth().currentUser.uid);
PointEndVideoRef.once("value").then((snapshot1) => {
PointEndVideoRef.update({
Points: snapshot1.val().Points + 10,
});
const points = (snapshot1.val().Points) + 10
//console.log("points = ", points)
this.props.updatePoints({ points: points })
})
this.setState({ showButtonVisible: true });
}
};
I doubt this is the cause of the navigation problem, but this style of updating is fraught with problems:
CountVideoRef.once("value").then((snapshot) => {
CountVideoRef.update({
countViewVideo: snapshot.val().countViewVideo + 1,
});
})
If you need to update an existing value in the database based on its existing value, you should use a transaction to prevent race conditions when multiple users perform the same action around the same time (or while offline).
In this case, you can use the simpler atomic server-side increment operation, which means the above becomes:
CountVideoRef.set(firebase.database.ServerValue.increment(1));

Paginating data causing unusual behavior?

I am displaying "global posts" on one of my tabs. Currently, there are only 11 posts in the database:
In the app Some of the posts are being duplicated, and I have no idea why these SPECIFIC posts are being duplicated, as it seems to me like it is happening at random.
Here is the code for how I paginate the data.
When the component mounts, I query firestore and pull 5 posts using getCollection().
.
async componentDidMount() {
this.unsubscribe = Firebase.firestore()
.collection('globalPosts')
.orderBy("date_created", "desc")
.limit(5)
.onSnapshot(this.getCollection);
}
I get the posts successfully in getCollection(), and set an index, lastItemIndex, so I know where to query for the next posts
.
getCollection = (querySnapshot) => {
const globalPostsArray = [];
querySnapshot.forEach((res) => {
const {
..fields
} = res.data();
globalPostsArray.push({
..fields
});
});
this.setState({
globalPostsArray,
isLoading: false,
lastItemIndex: globalPostsArray.length - 1
});
}
This gets the first 5 items, no problem, ordered by date_created, descending.
If the user scrolls down the flatlist, I have logic in the flatlist to handle fetching more data:
<FlatList
data={this.state.globalPostsArray}
renderItem={renderItem}
keyExtractor={item => item.key}
contentContainerStyle={{ paddingBottom: 50 }}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
onRefresh={this._refresh}
refreshing={this.state.isLoading}
onEndReachedThreshold={0.5} <---------------------- Threshold
onEndReached={() => {this.getMore()}} <------------ Get more data
/>
Finally, once it is time to retrieve more data, I call this.getMore()
Here is the code to get the next 5 posts:
getMore = async() => {
const newPostsArray = [] <-------- new array for the next 5 posts
Firebase.firestore()
.collection('globalPosts')
.orderBy("date_created", "desc")
.startAfter(this.state.globalPostsArray[this.state.lastItemIndex].date_created) <--- note start after
.limit(5)
.onSnapshot(querySnapshot => {
querySnapshot.forEach((res) => {
const {
... same fields as getCollection()
} = res.data();
newPostsArray.push({
... same fields as getCollection()
});
});
this.setState({
globalPostsArray: this.state.globalPostsArray.concat(newPostsArray), <--- add to state array
lastItemIndex: this.state.globalPostsArray.length-1 <---- increment index
});
console.log(this.state.lastItemIndex) <------- I print out last item index
})
}
Some notes:
The code works fine in terms of fetching the data
The code works fine in terms of pagination, and only fetches 5 posts at a time
There is no discernible pattern I am seeing in which posts are being duplicated
I am ordering by date_created, descending when querying firestore in both getCollection() and getMore()
I console log "last item index" in my getMore(), and of course the index is higher than the number of posts
I keep getting the following warning/error, with different keys (post ID's in firestore), which shows me the duplication is happening at random, and not specific to one user. This warning/error doesn't break the application, but is telling me this weird behavior is happening:
Encountered two children with the same key, ZJu3FbhzOkXDM5mn6O6T. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
Can someone point me in the right direction, why my pagination is having such unusual behavior?
My issue was with lastItemIndex. Saving it in state was causing problems. I solved the problem by removing lastItemIndex from state, and making it a local variable in getMore():
getMore = async() => {
const newPostsArray = []
const lastItemIndex = this.state.globalPostsArray.length - 1 <---- added it here
await Firebase.firestore()
.collection('globalPosts')
.orderBy("date_created", "desc")
.startAfter(this.state.globalPostsArray[lastItemIndex].date_created)
.limit(5)
.onSnapshot(querySnapshot => {
querySnapshot.forEach((res) => {
const {
..fields
} = res.data();
newPostsArray.push({
key: res.id,
..fields
});
});
this.setState({
globalPostsArray: this.state.globalPostsArray.concat(newPostsArray)
});
})
}
You can only guarantee uniqueness on a document ID, not by the contents of a document's fields.
For example, as a workaround, you could concatenate two IDs into a single string and use it as the document ID. You would then use a transaction to ensure that a document does not already exist with that new composite ID before writing it.
In order to paginate through a list of these unique items you need to use .startAfter() and .limit(). You can then create a lastVisible variable that uses the last document in a batch to start a cursor for the next.
var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
The example below can be found here if you want to read more about it:
var first = db.collection("cities")
.orderBy("population")
.limit(25);
return first.get().then(function (documentSnapshots) {
// Get the last visible document
var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
console.log("last", lastVisible);
// Construct a new query starting at this document,
// get the next 25 cities.
var next = db.collection("cities")
.orderBy("population")
.startAfter(lastVisible) // <--- HERE
.limit(25);
});

Query into firebase realtime database through functions

I am using firebase functions and Admin SDK to implement a functions on a event trigger. I am trying to push a notification to user when a new update is available. So, when the update version changes from the current, I want to make some changes in the user object and set update_available key as true
Now, the location of event(update_version) that is being tracked and the data to be changed are in to completely different objects. The Object model is as follows:
|root
|- app_info
|- version
|- users
|- <uid>
|- checker
|- update_available: true
Not so far I have achieved this :
function setUpdateValueTrue() {
db.ref().orderByChild("checker/update_available").equalTo("false").once('value', (snapshot) => {
snapshot.forEach((childSnapshot) => {
console.log("got in here");
console.log(childSnapshot.val())
});
})
}
I guess this maybe completely wrong. At the moment i feel stuck. Any help is really appreciated. My major concern is how to I bypass the uid or query through it.
The following should work (not tested however).
Note the use of Promise.all(), since you need to update a variable number of users nodes in parallel.
exports.versionUpdate = functions.database.ref('/app_info').onUpdate((change, context) => {
const beforeData = change.before.val(); // data before the update
const afterData = change.after.val(); // data after the update
if (beforeData.version !== afterData.version) {
const promises = [];
const usersRef = admin.database().ref('/users');
return usersRef.once('value', function(snapshot) {
snapshot.forEach(childSnapshot => {
const uid = childSnapshot.key;
promises.push(usersRef.child(uid + '/checker/update_available').set(true));
});
return Promise.all(promises);
});
} else {
return null;
}
});

Size of child in firebase

I am using react with firebase realtime database
So i have this firebase data and i want to count total values in followers. However, what i know is that firebase do not have such a function to count and you need to manually do it.
From below i can see all the user data such as username and email. However, i still cannot figure out how to count the number of followers.
I hope someone can help. Thank you.
const ref = firebaseDB.ref();
ref
.child("Users")
.orderByChild("email")
.equalTo(this.state.currentUserEmail)
.once("value", snapshot => {
let userData = {};
for (var childSnapshot in snapshot.val()) {
userData.key = childSnapshot;
// then loop through the keys inside the child object and get the values like price (key) : 44.95 (value)
for (var value in snapshot.val()[childSnapshot]) {
console.log(value);
console.log(snapshot.val()[childSnapshot][value]);
userData[value] = snapshot.val()[childSnapshot][value];
}
}
console.log(userData);
});
Something like this should do the trick:
const ref = firebaseDB.ref();
ref
.child("Users")
.orderByChild("email")
.equalTo(this.state.currentUserEmail)
.once("value", snapshot => {
snapshot.forEach((childSnapshot) => {
console.log(childSnapshot.child("followers").numChildren());
}
console.log(userData);
});
The changes/additions:
Use DataSnapshot.forEach() for a cleaner way to loop over the children.
Use DataSnapshot.child() to get the snapshot for the followers node.
Use DataSnapshot.numChildren() to determine the number of followers.

Categories