How to Sort/Filter Firebase Realtime Database with Query in Javascript - javascript

I'm having a hard time understanding why this simple query is not giving me the expected results.
In my Firebase realtime database my structure looks like this:
Pretty simple, right? Next I have a cloud function that runs whenever I manually change one of the orbs values under any of the users. This is my function:
exports.testTopPlayers2 = functions.database.ref('/TestUsers/{user}/Statistics').onUpdate(_ => {
const allPlayersRef = admin.database().ref('/TestUsers');
const query = allPlayersRef.orderByChild('orbs').limitToLast(2)
return query.on('value', (snap, context) => {
return console.log(snap.val());
})
Also very straightforward I would think. In regular english, what I'm expecting that function to do is: "When any of the orbs values are changed, run this function to sort all of the users by their orbs value (lowest to highest) and take a new DataSnapshot of only the final 2 users' data (The users with the highest number of orbs)".
When I print to the console, I'm expecting to see the data from User3 & User6 because they have the highest number of orbs... but instead it seems to be sorting by the username (User5 & User6) or simply not sorting at all. Here is the log output:
Which clearly does not sort by what I'm defining in my query.
Maybe I'm missing something obvious but I've stared at it long enough... hoping someone can spot a mistake in my function regarding how it's sorting.
I appreciate the help!

You're looking for:
allPlayersRef.orderByChild('Statistics/orbs').limitToLast(2)
Note that you'll also want to use once() instead of on():
return query.once('value').then((snap) => {
return snap.val();
})
You'll also want to remove that console.log around snap.val(), since console.log() doesn't return anything.
Finally: when you call .val() on a snapshot, you're converting it to key-value pairs and lose the ordering information. If you want to maintain the ordering, you'll want to use forEach:
return query.once('value').then((snap) => {
let results = [];
snap.forEach((player) => {
results.push( {key: snap.key, ...snap.val() }
});
return results;
})

Thanks to Frank's thorough answer, I was able to understand what needs to happen and got it working perfectly with minor tweaks.
With the exact modifications suggested by Frank, my output in the Firebase console looked like this:
Which is a lot better than what I was able to produce until now, but it still wasn't showing the exact data I wanted to use.
I wasn't interested in the key of the snap (which is the top-level TestUsers node), but rather I wanted the key of the player which provides the username responsible for the high score.
Next, Instead of getting an [object] for the orb values, I needed the actual value of the orbs, so I had to dive into the children of each player object to get that value.
This is my tweaked cloud function in all it's glory lol:
exports.testTopPlayers2 = functions.database.ref('/TestUsers/{user}/Statistics').onUpdate(_ => {
const allPlayersRef = admin.database().ref('/TestUsers');
const query = allPlayersRef.orderByChild('/Statistics/orbs').limitToLast(2)
return query.once('value').then((snap) => {
let results = [];
snap.forEach((player) => {
let username = player.key
let orbsValue = player.child("Statistics").child("orbs").val()
results.push( {Player: username, Score: orbsValue } )
});
console.log(results)
return results;
})
})
And the result of the Firebase log is this:
This is exactly what I was after and can now proceed :D

Related

variable inside Observable subscription gets empty value

I know that Observables take some time to get data while javascript keeps running the others codes and that is troubling me a lot.
I have used ngrx in my angular project. Here, I am trying to fetch some data from the store which is working fine. Then, I convert this data stream into string[] which is also working fine.
To use this string[] me subscribeto this observable. And inside subscription I try to assign the value to other values named filterSizeValues.
Here, the problem comes. If I console.logthis filterSizeValuesinitially I got and empty array. When the observable finishes his job filterSizeValues variable is filled with data.
But I can not effort filterSizeValues variable to be empty array initially. What can I do?
I have already searched the solution in the internet but nothing is working out.
Help me out please. And Many Many Thanks in advance.
Here is my code;
this.sizeTargetingStore$.dispatch(SizeTargetingActions.getSizeTargeting({
campaignId: this.campaignId,
lineItemId: this.lineItemId
}));
Here I am accessing the store to get data.
this.sizeTargeting$
.pipe(switchMap(sizes=>{
let temporary:string[] = [];
sizes.forEach(eachSize=>{
temporary.push(eachSize.name);
})
this.filterSizeValues$ = of(temporary);
return this.filterSizeValues$;
}))
.subscribe(size_name=>{
this.filters.set('size_name', size_name);
})
Here, I am trying to set the filter values.
I also tried this way also.
this.sizeTargeting$
.pipe(switchMap(sizes=>{
let temporary:string[] = [];
sizes.forEach(eachSize=>{
temporary.push(eachSize.name);
})
this.filterSizeValues$ = of(temporary);
return this.filterSizeValues$;
}))
.subscribe(size_name=>{
this.filterSizeValues = size_name
})
this.filters.set('size_name', this.filterSizeValues);
But all ways filters set to an empty array.
Anyone can help me out please?
From my understanding, you have 2 possibilities, either filter out the empty values or skip the first value. You can do so with the filter and skip rxjs operator respectively.
Also I believe that you are misusing the switchMap operator, since you are not using asynchronous operations within your switchMap we can use the map operator instead, so below I have a simplified version of your code with your 2 options to fix your problem.
Option 1:
this.sizeTargeting$.pipe(
filter(sizes => sizes.length > 0), // filter out empty array values
map(sizes => sizes.map(size => size.name)) // perform your remap
).subscribe(sizes => {
this.filterSizeValues = size_name; // Only arrays with values will reach this step
});
Option 2:
this.sizeTargeting$.pipe(
skip(1), // skip the first value
map(sizes => sizes.map(size => size.name)) // perform your remap
).subscribe(sizes => {
this.filterSizeValues = size_name; // Only arrays with values will reach this step
});
Normally when I subscribe to something that I am waiting on to return what I do is I set up a Subject:
private componentDestroyed$ = new Subject<void>();
then in the Observable piping and subscription I do it as:
this.sizeTargeting$
.pipe(takeUntil(this.componentDestroyed$))
.subscribe((sizes: YourTypeHere[]) => {
if(sizes) {
//Do what I need to do with my sizes here, populate what I need,
//dispatch any other actions needed.
}
})

How to pass a function to React in State to sort your data based on multiple queries?

I am new to React and have a question regarding passing down functions in a state. I have a couple of 'sorting functions' in my util folder, which look like this:
export const sortColdestSummerCountries = (filteredCountries) => {
return filteredCountries.sort(
(a, b) => a.avsummertemp20802099 - b.avsummertemp20802099
);
};
and a few others:
sortHighestSummerTemp, sortLargestIncreaseHotDays, sortColdestSummerCountries, sortMostColdDays, sortWorstAffectedCountries which pretty much look similar. I use them to sort my data by users' request, and if I wrap sortHighestSummerTemp(filteredCountries) around my data, it works as a charm.
Now the issue: because I will have eventually 10+ filters, it makes sense to me to create a global state like this:
const onChangeQueryFilter = (e) => {
setQueryFilter(e.target.value);
};
Yet, upon trying queryFilter (filteredCountries) the terminal shows me "queryFilter is not a function" when I filter it? It's still the same sortHighestSummerTemp function right or what am I missing here? Do I summarize this problem correctly?
Hopefully it was clear and you understand what I am trying to do.
UPDATE:
I tried the following as well:
function sortBy(filteredCountries, sortKey) {
return [...filteredCountries].sort((a, b) => a[sortKey] - b[sortKey]);
}
// the queryFilter responds to the key, "filteredCountries.avsummertemp20802099" for example which passes to [sortKey].
const [queryFilter, setQueryFilter] = useState(filteredCountries.avsummertemp20802099);
const filteredData = sortBy(filteredCountries, queryFilter);
This does not work either, it still shows the old data (filteredCountries) but the filtering doesn't happen, the data isn't sorted. Does someone have a clue what I am doing wrong? Is it something in the sorting method? Or does someone have a better practice to pass a function to state?
Thank you a lot for being out here. :)

How to execute a sort after loading in data into an array in UseEffect - React Native

I'm trying to create a chat app and there is a small issue. Whenever I load in my messages from firebase, they appear in the chat app in unsorted order, so I'm attempting to sort the messages by timestamp so they appear in order. I can do this if I move the sort and setMessages within onReceive of useEffect, but I feel like this will be pretty inefficient because it sorts and setsMessages a separate time for each message that's retrieved from firebase. I want to just do it all at the end after all the messages are loaded into the array.
Right now with my logs, I get this:
[REDACTED TIME] LOG []
[REDACTED TIME] LOG pushing into loadedMessages
[REDACTED TIME] LOG pushing into loadedMessages
So it's printing the (empty) array first, then loading in messages. How can I make sure this is done in the correct order?
useEffect(() => {
// Gets User ID
fetchUserId(getUserId());
const messagesRef = firebase.database().ref(`${companySymbol}Messages`);
messagesRef.off();
messagesRef.off();
const onReceive = async (data) => {
const message = data.val();
const iMessage = {
_id: message._id,
text: message.text,
createdAt: new Date(message.createdAt),
user: {
_id: message.user._id,
name: message.user.name,
},
};
loadedMessages.push(iMessage);
console.log('pushing into loadedMessages');
};
messagesRef.on('child_added', onReceive);
loadedMessages.sort(
(message1, message2) => message2.createdAt - message1.createdAt,
);
console.log(loadedMessages);
return () => {
console.log('useEffect Return:');
messagesRef.off();
};
}, []);
I think that the perspective is a bit off.
The right way to do so will be to fetch the firebase data sorted.
Firebase has a built-in sort, although it does come with its limitations.
In my opinion, you sould try something like:
const messagesRef = firebase.database().ref(`${companySymbol}Messages`);
messagesRef.orderByChild("createdAt").on("child_added", function(snapshot) {
// the callback function once a new message has been created.
console.log(snapshot.val());
});
And if I may add one more thing, to bring every single message from the down of time can be a bit harry once you've got over a thousand or so, so I would recommend limiting it. that can be achieved using the built-in limit function limitToLast(1000) for example.
Good luck!
Well, the name of the database is "Realtime Database". You are using the "child_added" listener which is going to be triggered every time a new object gets added to the Messages collection. The onReceive callback should do the sorting - otherwise the messages won't be in the correct order. Yes, that is inefficient for the first load as your "child_added" will most probably be triggered for every item returned from the collection and you'll be repeating sorting.
What you could explore as alternative is to have a .once listener: https://firebase.google.com/docs/database/web/read-and-write#read_data_once the first time you populate the data in your app. This will return all the data you need. After that is complete you can create your "child_added" listener and only listen for new objects. This way onReceive shouldn't be called that often the first time and afterwards it already makes sense to sort on every new item that comes in.
Also have a look at sorting: https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data
You might be able to return the messages in the correct order.
And also - if you need queries - look at firestore...

Firebase child_added for new items only

I am using Firebase and Node with Redux. I am loading all objects from a key as follows.
firebaseDb.child('invites').on('child_added', snapshot => {
})
The idea behind this method is that we get a payload from the database and only use one action to updated my local data stores via the Reducers.
Next, I need to listen for any NEW or UPDATED children of the key invite.
The problem now, however, is that the child_added event triggers for all existing keys, as well as newly added ones. I do not want this behaviour, I only require new keys, as I have the existing data retrieved.
I am aware that child_added is typically used for this type of operation, however, i wish to reduce the number of actions fired, and renders triggered as a result.
What would be the best pattern to achieve this goal?
Thanks,
Although the limit method is pretty good and efficient, but you still need to add a check to the child_added for the last item that will be grabbed. Also I don't know if it's still the case, but you might get "old" events from previously deleted items, so you might need to watch at for this too.
Other solutions would be to either:
Use a boolean that will prevent old added objects to call the callback
let newItems = false
firebaseDb.child('invites').on('child_added', snapshot => {
if (!newItems) { return }
// do
})
firebaseDb.child('invites').once('value', () => {
newItems = true
})
The disadvantage of this method is that it would imply getting events that will do nothing but still if you have a big initial list might be problematic.
Or if you have a timestamp on your invites, do something like
firebaseDb.child('invites')
.orderByChild('timestamp')
.startAt(Date.now())
.on('child_added', snapshot => {
// do
})
I have solved the problem using the following method.
firebaseDb.child('invites').limitToLast(1).on('child_added', cb)
firebaseDb.child('invites').on('child_changed', cb)
limitToLast(1) gets the last child object of invites, and then listens for any new ones, passing a snapshot object to the cb callback.
child_changed listens for any child update to invites, passing a snapshot to the cb
I solved this by ignoring child_added all together, and using just child_changed. The way I did this was to perform an update() on any items i needed to handle after pushing them to the database. This solution will depend on your needs, but one example is to update a timestamp key whenever you want the event triggered. For example:
var newObj = { ... }
// push the new item with no events
fb.push(newObj)
// update a timestamp key on the item to trigger child_changed
fb.update({ updated: yourTimeStamp })
there was also another solution:
get the number of children and extract that value:
and it's working.
var ref = firebaseDb.child('invites')
ref.once('value').then((dataSnapshot) => {
return dataSnapshot.numChildren()
}).then((count) =>{
ref .on('child_added', (child) => {
if(count>0){
count--
return
}
console.log("child really added")
});
});
If your document keys are time based (unix epoch, ISO8601 or the firebase 'push' keys), this approach, similar to the second approach #balthazar proposed, worked well for us:
const maxDataPoints = 100;
const ref = firebase.database().ref("someKey").orderByKey();
// load the initial data, up to whatever max rows we want
const initialData = await ref.limitToLast(maxDataPoints).once("value")
// get the last key of the data we retrieved
const lastDataPoint = initialDataTimebasedKeys.length > 0 ? initialDataTimebasedKeys[initialDataTimebasedKeys.length - 1].toString() : "0"
// start listening for additions past this point...
// this works because we're fetching ordered by key
// and the key is timebased
const subscriptionRef = ref.startAt(lastDataPoint + "0");
const listener = subscriptionRef.on("child_added", async (snapshot) => {
// do something here
});

Angular2 update value from http request

I have a simple service does a few get requests:
getDevices() {
return this.http.get('https://api.particle.io/v1/devices')
.map((response: Response) => response.json());
}
getVariable(deviceId: string) {
return this.http.get('https://api.particle.io/v1/devices/' + deviceId + '/running')
.map((response: Response) => response.json());
}
In my component I have the following function which fetches devices and adds the variable from getVariable to each device in this.devices.
getDevicesAndRunning() {
function callback(array, data, res) {
array.push(Object.assign(data, {"running": res.result}))
}
this.photonService.getDevices()
.subscribe(
data => {
const myArray = [];
for (var i=0; i<data.length; i++) {
if (data[i].connected) {
console.log(data, i);
this.photonService.getVariable(data[i].id)
.subscribe(
callback.bind(this, myArray, data[i])
);
};
}
this.devices = myArray;
}
);
}
I can then call getDevicesAndRunning() to update the list of devices and iterate over them in the view. Currently I'm doing this in OnInit and whenever the user clicks a button. However, since this.devices gets repopulated each time the function is called the user sees a blank list for however long the request takes to respond. All I want is for the res.result to be updated, not the entire array.
I do this sort of thing by not using the originally returned list. I implemented something like this for an app that's in production now and it works great.
When you get the original list, stash it off to the side. Create a copy and use that for the display.
When you need to update it, get the new list, copy it, and now you have a lot of options; old vs. new, replace old, combine old and new, display delta, etc. There's some nice ES6 functions, not native but simple enough, out there to do things like unions, diffs, etc, and of course lodash will serve.
True, this involves a copy of the list, but in the app I built I dealt with thousands of objects and at least six lists simultaneously and never had any problems with memory etc. You just have to remember to clean it up if necessary.
Since you are bulk updating the list you are blanking it out while the refresh is happening. You will need to preserve the array and alter it on the fly. Splice out the removed items and push in the new ones. Let the sort function angular provides do the rest of the work.

Categories

Resources