I am trying to query my database such that it retrieves an ordered list based on a child key. I do it as follows (see below), but nothing happens, meaning that it returns an object ordered exactly in the same way as it is stored in the Firebase database. What is going on?
self.getAllProfiles = function () {
var qProfile = $q.defer();
var ref = new Firebase(FBURL);
ref.child("users").orderByChild('last_update').on("value", function (snapshot) {
console.log(snapshot.val()) // HERE IS WHERE IT SHOULD BE ORDERED
qProfile.resolve(snapshot.val());
}, function (errorObject) {
qProfile.reject(errorObject);
});
return qProfile.promise;
};
To add, my users node looks as follows:
users
/$username
/last_update
/id
/data
/profile_image
/display_name
Here is a snapshot:
Tester: Object
github: Object
last_update: 1447732462170
userId: "github:12345"
When you call snapshot.val(), you are getting back a JSON object. The order of keys in a JSON object is determined by your browser and not by Firebase.
To get the children in order use the built-in forEach method of the snapshot:
self.getAllProfiles = function () {
var qProfile = $q.defer();
var ref = new Firebase(FBURL);
ref.child("users").orderByChild('last_update').on("value", function (snapshot) {
snapshot.forEach(function(child) {
console.log(child.val()) // NOW THE CHILDREN PRINT IN ORDER
});
qProfile.resolve(snapshot.val());
}, function (errorObject) {
qProfile.reject(errorObject);
});
return qProfile.promise;
};
You can leave the q.resolve() call where it is: snapshot.forEach() is not an asynchronous call.
I know this question has been answered and is more than 1 year old, but since there are still some confusion in the comment section, I would like to add some information.
The problem
The original problem is that the OP want to retrieve an ordered list based on a child key from Firebase realtime database, but the .orderByChild('arg') does not work as expected.
But what didn't work as expected is not .orderByChild('arg'), but .on("value", callback). Because .on("value", callback) works a bit of different from other eventTypes like .on("child_added", callback).
Example
Say we have a firebase realtime database as below:
{
myData: {
-KYNMmYHrzLcL-OVGiTU: {
NO: 1,
image: ...
},
-KYNMwNIz4ObdKJ7AGAL: {
NO: 2,
image: ...
},
-KYNNEDkLpuwqQHSEGhw: {
NO: 3,
image: ...
},
}
}
--
If we use .on("value", callback), the callback() will be called 1 time, and return an Object Array of 3 objects.
ref.on("value", function(snapshot) {
console.log(snapshot.val());
// Please see Frank van Puffelen's answer
}
--
If we use .on("child_added", callback), the callback() will be called 3 times, each time returns an Object, and they are returned in order.
ref.once("child_added", function(snapshot) {
console.log(snapshot.val());
// The objects are returned in order, do whatever you like
}
Conclusion
If you only need to fetch ordered data from firebase (e.g. to initialize UI.) Then ref.orderByChild('arg').once("child_added", callback) suits you well, it is simple and easy to use.
However, if for some reason you need to use ref.orderByChild('arg').on("value", callback), then please see Frank van Puffelen's answer.
Reference
Please read Firebase Document for more information about on(eventType, callback, cancelCallbackOrContext, context), their arguments and their return values.
Another useful document: Work with Lists of Data on the Web
For ordering using the value event listener:
firebase.database().ref('/invoices').orderByChild('name').on('value', snapshot => {
snapshot.forEach(child => {
console.log(child.key, child.val());
});
}
If you want to reverse the order, try:
function reverseSnapshotOrder (snapshot) {
let reversed = [];
snapshot.forEach(child => {
reversed.unshift(child);
});
return reversed;
}
reverseSnapshotOrder(snapshot).forEach(child => {
console.log(child.key, child.val());
});
Please see: https://firebase.google.com/docs/database/web/lists-of-data#listen_for_value_events
I struggled with the same problem in iOS. If you convert snapshot to NSDictionary object, it converts to unordered list. Objective-c version in case of need:
[[self.refChild queryOrderedByChild:#"date_created"]
observeEventType:FIRDataEventTypeValue
withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
for (FIRDataSnapshot *child in snapshot.children) {
NSDictionary *anObject = child.value;
NSString *aKey = child.key;
}
}];
Related
I am trying to query my database such that it retrieves an ordered list based on a child key. I do it as follows (see below), but nothing happens, meaning that it returns an object ordered exactly in the same way as it is stored in the Firebase database. What is going on?
self.getAllProfiles = function () {
var qProfile = $q.defer();
var ref = new Firebase(FBURL);
ref.child("users").orderByChild('last_update').on("value", function (snapshot) {
console.log(snapshot.val()) // HERE IS WHERE IT SHOULD BE ORDERED
qProfile.resolve(snapshot.val());
}, function (errorObject) {
qProfile.reject(errorObject);
});
return qProfile.promise;
};
To add, my users node looks as follows:
users
/$username
/last_update
/id
/data
/profile_image
/display_name
Here is a snapshot:
Tester: Object
github: Object
last_update: 1447732462170
userId: "github:12345"
When you call snapshot.val(), you are getting back a JSON object. The order of keys in a JSON object is determined by your browser and not by Firebase.
To get the children in order use the built-in forEach method of the snapshot:
self.getAllProfiles = function () {
var qProfile = $q.defer();
var ref = new Firebase(FBURL);
ref.child("users").orderByChild('last_update').on("value", function (snapshot) {
snapshot.forEach(function(child) {
console.log(child.val()) // NOW THE CHILDREN PRINT IN ORDER
});
qProfile.resolve(snapshot.val());
}, function (errorObject) {
qProfile.reject(errorObject);
});
return qProfile.promise;
};
You can leave the q.resolve() call where it is: snapshot.forEach() is not an asynchronous call.
I know this question has been answered and is more than 1 year old, but since there are still some confusion in the comment section, I would like to add some information.
The problem
The original problem is that the OP want to retrieve an ordered list based on a child key from Firebase realtime database, but the .orderByChild('arg') does not work as expected.
But what didn't work as expected is not .orderByChild('arg'), but .on("value", callback). Because .on("value", callback) works a bit of different from other eventTypes like .on("child_added", callback).
Example
Say we have a firebase realtime database as below:
{
myData: {
-KYNMmYHrzLcL-OVGiTU: {
NO: 1,
image: ...
},
-KYNMwNIz4ObdKJ7AGAL: {
NO: 2,
image: ...
},
-KYNNEDkLpuwqQHSEGhw: {
NO: 3,
image: ...
},
}
}
--
If we use .on("value", callback), the callback() will be called 1 time, and return an Object Array of 3 objects.
ref.on("value", function(snapshot) {
console.log(snapshot.val());
// Please see Frank van Puffelen's answer
}
--
If we use .on("child_added", callback), the callback() will be called 3 times, each time returns an Object, and they are returned in order.
ref.once("child_added", function(snapshot) {
console.log(snapshot.val());
// The objects are returned in order, do whatever you like
}
Conclusion
If you only need to fetch ordered data from firebase (e.g. to initialize UI.) Then ref.orderByChild('arg').once("child_added", callback) suits you well, it is simple and easy to use.
However, if for some reason you need to use ref.orderByChild('arg').on("value", callback), then please see Frank van Puffelen's answer.
Reference
Please read Firebase Document for more information about on(eventType, callback, cancelCallbackOrContext, context), their arguments and their return values.
Another useful document: Work with Lists of Data on the Web
For ordering using the value event listener:
firebase.database().ref('/invoices').orderByChild('name').on('value', snapshot => {
snapshot.forEach(child => {
console.log(child.key, child.val());
});
}
If you want to reverse the order, try:
function reverseSnapshotOrder (snapshot) {
let reversed = [];
snapshot.forEach(child => {
reversed.unshift(child);
});
return reversed;
}
reverseSnapshotOrder(snapshot).forEach(child => {
console.log(child.key, child.val());
});
Please see: https://firebase.google.com/docs/database/web/lists-of-data#listen_for_value_events
I struggled with the same problem in iOS. If you convert snapshot to NSDictionary object, it converts to unordered list. Objective-c version in case of need:
[[self.refChild queryOrderedByChild:#"date_created"]
observeEventType:FIRDataEventTypeValue
withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
for (FIRDataSnapshot *child in snapshot.children) {
NSDictionary *anObject = child.value;
NSString *aKey = child.key;
}
}];
So the document contains an array of objects, each object containing it's own array. So how would I go about updating one of the elements in the array that's inside the object which is inside another array. I've read some things with $. But I don't understand completely how to use it to call a position. I know the position of the element. But I can't just say $[] because the position is defined in a variable and not a string...
I've tried doing a simple
db.collection.findOne({...}, (err, data) => {...});
and then changing the arrays in the objects in the array in there with a simple:
data.arr[x].type[y] = z; data.save().catch(err => {console.log(err)});
But it doesn't save the new values I set for for the element of the array.
Sample structure after proposed solution from #Tom Slabbaert:
Data.findOne({
userID: 'CMA'
}, (err, doc) => {
if(err) {console.log(err)}
if(doc) {
for(var i = 0; i<CMA.stockMarket.length; i++) {
if(CMA.stockMarket[i].name == data.userID) {
for(var z = 0; z<CMA.stockMarket[i].userStock.length; z++) {
if(z == company) {
var updateAmount = CMA.stockMarket[i].userStock[z]+args[1]
var updateKey = `stockMarket.${i}.userStock.${z}`
Data.updateOne({userID: 'CMA'}, {'$set': {[updateKey]: updateAmount}})
}
}
}
}
}
});
-------------------------EDIT-------------------------
So I tried changing some things around in the data base to see if that would fix the problem I was having. I modified the updated code that was provided by #Tom Slabbaert. But nothing seems to work for some reason :/ Here's what I have so far, at this point I hope it's just a syntax error somewhere. Cause this is really frustrating at this point. Note that I'm still using the for loops here to find if the info exists. And if not, push that info into the database. This might only be temporary until I find a better way / if there is a better way.
for(var i = 0; i<CMA.userStocks.length; i++) {
if(CMA.userStocks[i].name == data.userID) {
for(var z = 0; z<CMA.userStocks[i].shares.length; z++) {
//console.log(CMA.userStocks[i].shares[z].companyName)
if(CMA.userStocks[i].shares[z].companyName == args[0]) {
var updateKey = `CMA.userStocks.$[elem1].shares.$[elem2].amount`
Data.updateOne(
{userID: 'CMA'},
{
"$inc": {
[updateKey]: args[1]
}
},
{
arrayFilters: [
{
"elem1.name": data.userID,
"elem2.companyName": args[0]
}
]
}
)
purchaseComplete(); return;
}
}
CMA.userStocks[i].shares.push({companyName: args[0], amount: parseInt(args[1])})
CMA.save().catch(err => {console.log(err)});
purchaseComplete(); return;
}
}
CMA.userStocks.push({name: data.userID, shares: [{companyName: args[0], amount: parseInt(args[1])}]});
CMA.save().catch(err => {console.log(err)});
purchaseComplete(); return;
The data I'm trying to find and change is structured like the following:
And what I'm trying to change in the end is the 'amount' (which is an integer)
_id: (Not relavent in this question)
userID: 'CMA'
stockMarket: [...] (Not relavent in this question)
userStocks: [
Object: (position 0 in userStocks array)
name: 'string' (equal to data.userID in the code)
shares: [
Object: (position 0 in shares array)
companyName: 'string' (this is args[0] in the code)
amount: integer
]
]
You can just prepare the "key" ahead of time. like so:
const updateKey = `arr.${x}.type.${y}`
db.collection.updateOne(
{...},
{
"$set": {
[updateKey]: z
}
})
Mongo Playground
Using Mongo's positional operators ($ and $[]) are usually required when you don't know the position in the array and want to use a condition to update the element.
------ EDIT-----
After given your sample code you just have a minor syntax error:
var updateKey = `stockMarket.${i}.userStock.${z}`
Should just be:
var updateKey = `CMA.stockMarket.${i}.userStock.${z}`
However After seeing your code I recommend you execute the following solution which uses a single update with arrayFilters, it just cleans up the code quite a bit:
const updateKey = `CMA.stockMarket.$[elem1].userStock.${company}`;
db.collection.update(
{userID: 'CMA'},
{
"$inc": {
[updateKey]: args[1]
}
},
{
arrayFilters: [
{
"elem1.name": data.userID
}
]
})
Mongo Playground
Well I found something that worked. Apparently it didn't save the db.collection.updateMany unless I made a .then() function on the end? I have no idea why, but it's the same with an aggregate I made. (It basically does the same as a Data.findOne and save it too, but it isn't limited by the parallel save error)
Solution I found with aggregation:
<collection field> = <new data for collection field>
Data.aggregate([
{
$match: { //This is used to create a filter
['<insert field>']: <insert filter>
}
}, {
$addFields: { //This is used to update existing data, or create a new field containing the data if the field isn't found
['<collection field>']: <new data for collection field>
}
}, {
$merge: { //This is used to merge the new data / document with the rest of the collection. Thus having the same effect as a standard save
into: {
db: '<insert database name>',
coll: '<insert collection name>'
}
}
}
]).then(() => {
//After it's done, do something here. Or do nothing at all it doesn't matter as long as the .then() statement remains. I found that not having this part will break the code and make it not save / work for some reason.
}); return;
Solution I found with db.collection.updateMany
db.collection.updateMany(
{<insert field>: filter}, {$set: {'<insert field>': <new data>}}
).then(() => {
//This .then() statment in my case was needed for the updateMany function to work correctly. It wouldn't save data without it for some reason, it does not need to contain any actual info in this part. As long as it's here.
});
With this new info I could simply access and change the data that I was trying to before using the previous instructions provided by #Tom Slabbaert and my new method of actually making it save the changes made into the document.
exports.editData = functions.database.ref('/AllData/hello/A').onWrite((change, context) => {
const after = change.after;
if (after.exists()) {
const data = after.val();
var value = data;
// set of data to multiply by turns ratio
var actualEIn = (value.ein)*200;
console.log('Data Edited');
}
return admin.database().ref('/editedData/hello/A').push({
ein: actualEIn,
});
});
Edit: made some edits to the code as suggested! However, when I deploy it there are literally no logs.
Change this:
exports.editValues = functions.database.ref('/AllData/hello/A').onWrite((snapshot) => {
const data = snapshot.val();
if (data.exists()) {
into this:
exports.editValues = functions.database.ref('/AllData/hello/A').onWrite((change,context) => {
const data = change.after.val();
if (data.exists()) {
more info here:
https://firebase.google.com/docs/functions/beta-v1-diff#realtime-database
exports.editData = functions.database.ref('/AllData/hello/A/{id}').onWrite((change, context) => {
const afterData = change.after;
if (afterData.exists()) {
console.log('hey');
const data = afterData.val();
// set of data to multiply by turns ratio
var actualEIn = (data.ein)*200;
}
return admin.database().ref('/editedData/hello/A').push({
ein: actualEIn,
});
});
Hi guys thank you for all your help! :) I managed to solve this by adding a /{id} at the back!
You've got two things wrong here.
First, newer versions of the firebase-functions SDK since version 1.0 deliver a Change object to onWrite handlers instead of a snapshot, as it appears you are expecting. The Change object has properties for before and after with DataSnapshot objects of the contents of the database before and after the change that triggered the function. Please read the documentation for database triggers to get all the information.
Second, exists() is a method on DataSnapshot, but you're using it on the raw JavaScript object value of the contents of the database the location of change. JavaScript objects coming from val() will not have any methods to call.
You should probably update your code to:
Use the latest version of the firebase-functions module
Alter your function to accept the Change object instead of a snapshot
Use the exists() method on a snapshot in the change, rather than a raw JavaScript object.
Starter code:
exports.editValues = functions.database.ref('/AllData/hello/A').onWrite((change) => {
const after = change.after; // the DataSnapshot of the data after it was changed
if (after.exists()) {
const data = after.val() // the raw JavaScript value of the location
// use data here
}
})
At a loss on this one.
I'm using Ember and Ember data. I've got this extra implementation of ic-ajax to make GET, POST and PUT calls. Anyway, i'm trying to make a GET call then turn those results into model instances.
return this.GET('/editor')
.then((data) => {
return data.drafts.map((draftData) => {
let draft = this.store.find('draft',draftData.id);
console.log(draft.get('type'));
return draft;
});
});
My API returns proper data as data.drafts. This map is supposed to return an array of promises that resolve to draft models. It does not. It resolves to a draft model that has id, date, and title. But that's it. I have 25 others attributions.
In another part of the application i'm getting drafts using findAll on the model. And those models look fine. But when I try store.findRecord('draft',id) i get these fake objects.
-- edit
This is what my ReOpenClass method looks like for getting an array of objects from the server and turning them into ember objects
search(critera) {
let query = { search: critera };
let adapter = this.store.adapterFor('application');
let url = adapter.buildURL('article','search');
return adapter.ajax(url,'GET', { data: query }).then(response => {
let articleRecords = response.articles.map((article) => {
let record;
try {
record = this.store.createRecord('article', article);
} catch(e) {
record = this.store.peekRecord('article', article.id);
}
return record;
});
return articleRecords;
});
},
So far I can't find a better way to pull this off.
The ultimate goal is to detect changes between an existing Parse object and the incoming update using the beforeSave function in Cloud Code.
From the Cloud Code log available through parse.com, one can see the input to beforeSave contains a field called original and another one called update.
Cloud Code log:
Input: {"original": { ... }, "update":{...}
I wonder if, and how, we can access the original field in order to detect changing fields before saving.
Note that I've already tried several approaches for solving this without success:
using (object).changedAttributes()
using (object).previousAttributes()
fetching the existing object, before updating it with the new data
Note on request.object.changedAttributes():
returns false when using in beforeSave and afterSave -- see below for more details:
Log for before_save -- summarised for readability:
Input: { original: {units: '10'}, update: {units: '11'} }
Result: Update changed to { units: '11' }
[timestamp] false <--- console.log(request.object.changedAttributes())
Log for corresponding after_save:
[timestamp] false <--- console.log(request.object.changedAttributes())
There is a problem with changedAttributes(). It seems to answer false all the time -- or at least in beforeSave, where it would reasonably be needed. (See here, as well as other similar posts)
Here's a general purpose work-around to do what changedAttributes ought to do.
// use underscore for _.map() since its great to have underscore anyway
// or use JS map if you prefer...
var _ = require('underscore');
function changesOn(object, klass) {
var query = new Parse.Query(klass);
return query.get(object.id).then(function(savedObject) {
return _.map(object.dirtyKeys(), function(key) {
return { oldValue: savedObject.get(key), newValue: object.get(key) }
});
});
}
// my mre beforeSave looks like this
Parse.Cloud.beforeSave("Dummy", function(request, response) {
var object = request.object;
var changedAttributes = object.changedAttributes();
console.log("changed attributes = " + JSON.stringify(changedAttributes)); // null indeed!
changesOn(object, "Dummy").then(function(changes) {
console.log("DIY changed attributes = " + JSON.stringify(changes));
response.success();
}, function(error) {
response.error(error);
});
});
When I change someAttribute (a number column on a Dummy instance) from 32 to 1222 via client code or data browser, the log shows this:
I2015-06-30T20:22:39.886Z]changed attributes = false
I2015-06-30T20:22:39.988Z]DIY changed attributes =
[{"oldValue":32,"newValue":1222}]