How to compare two children of Firebase Realtime Database? - javascript

Based on the result of data.key === "high_temp_coil", I am printing the data into my webpage with data.val() as shown below:
var deviceRef = app.database().ref('/'+localStorage.getItem('machineid'));
deviceRef.on('child_added', function(data) {
if (data.key === 'high_temp_coil') {
$('#high_temp_coil .value').html(data.val())
}
if (data.key === 'low_temp_coil') {
$('#low_temp_coil .value').html(data.val());
}
}
In my code high_temp_coil represents the high temperature of the coil and low_temp_coil represents the low temperature of the coil, each with their own fields in my database. However, due to a manufacturing issue, sometimes the high temp and low temps are backwards and I need to figure this out before printing the data. This is how I was trying to do that but it doesn't work:
if (data.key === "high_temp_coil"){
let valueh= data.val();
if (data.key === "low_temp_coil"){
let valuel= data.val()
console.log(valueh);
console.log(valuel);
}
}
This is what the data looks like in my database:
{
"MachineNo": {
"water": "value"
"high_temp_coil": "value"
"low_temp_coil": "value"
}
}

When you use a child_added event listener, your callback will be invoked whenever one of the children under that database location changes. Using this, you would need to store high_temp_coil and low_temp_coil in variables outside of the function so that you can compare them properly. Because you store the result in an element, you could pull the current values from there.
Note: In the below snippets I follow the convention of naming the DataSnapshot object as snapshot, reserving data for the plain JavaScript object returned by snapshot.val(). This aids in preventing confusion later on, especially when not using TypeScript.
var deviceRef = app.database().ref('/'+localStorage.getItem('machineid'));
deviceRef.on('child_added', function(snapshot) {
if (snapshot.key === 'high_temp_coil') {
const newHighTempValue = snapshot.val();
const lowTempValue = $('#low_temp_coil .value').html();
if (Number(newHighTempValue) >= Number(lowTempValue)) { // <- assuming the values are numeric and not written as "52.1°C"
// new value is higher than current value
$('#high_temp_coil .value').html(newHighTempValue)
} else {
// new value is lower than current value, swap places
$('#high_temp_coil .value').html(lowTempValue)
$('#low_temp_coil .value').html(newHighTempValue)
}
}
if (snapshot.key === 'low_temp_coil') {
const newLowTempValue = snapshot.val();
const highTempValue = $('#high_temp_coil .value').html();
if (Number(newLowTempValue) < Number(highTempValue)) { // <- assuming the values are numeric and not written as "52.1°C"
// new value is lower than current value
$('#low_temp_coil .value').html(newLowTempValue)
} else {
// new value is higher than current value, swap places
$('#low_temp_coil .value').html(highTempValue)
$('#high_temp_coil .value').html(newLowTempValue)
}
}
}
The main issue with the above code is that the child_added events would fire once on page load, and not get live updates from any sensors updating the data because these changes would fire child_changed events.
However, for your data structure, you can greatly simplify your code by listening to value events instead. These listeners will be fired each time any of the data under that location is updated - including when a machine is created and any changes to the temperatures. Also, because the data is one level higher in the tree, you have access to the latest high_temp_coil and low_temp_coil values right in the snapshot. The trade-off for this listener is that you need to make sure to handle when the data does not exist (snapshot.exists()===false) because child_added listeners would only be invoked when data is created where it is guaranteed to exist.
var deviceRef = app.database().ref('/'+localStorage.getItem('machineid'));
deviceRef.on(
'value',
function(snapshot) {
if (!snapshot.exists()) {
// TODO: handle machine does not exist
console.error(`Machine ${localStorage.getItem('machineid')} does not exist in database`);
return;
}
const data = snapshot.val();
const highTempValue = data.high_temp_coil;
const lowTempValue = data.low_temp_coil;
if (Number(highTempValue) >= Number(lowTempValue)) { // <- assuming the values are numeric and not written as "52.1°C"
$('#high_temp_coil .value').html(highTempValue)
$('#low_temp_coil .value').html(lowTempValue)
} else {
$('#high_temp_coil .value').html(lowTempValue)
$('#low_temp_coil .value').html(highTempValue)
}
},
(error) => {
// TODO: implement better error handling
console.error("Listener was cancelled due to an error:", error);
}
);

Related

Bind event handler to document & have access to firebase api data via useEffect

Quick version:
My ultimate goal is to do something like the link below but with an async call to firebase per useEffect where the list data is composed of firebase object content.
https://codesandbox.io/s/usage-pxfy7
Problem
In the code below useEffect encapsulates code that pings firebase and gets some data back called "clients". The data is retrieved perfectly.
I then store that data using useState to two different instances of useState. The data is stored at clientList and clientListForRender.
So far so good.
Now the problem starts.
I have a third instance of useState that takes a number. I want to set a keypress event to the document so that I can use the up/down arrows to toggle the counter and access each value of the clientListForRender array.
When I set the eventListener I do not have access to the array (presumably due to the async calls not being in an order that allows for it).
I am not sure how to write my hooks in a way that gives me the result I want.
Thank you.
const clientsRef = firebase.database().ref('clients');
const [clientList,setClientListState] = useState([]);
const [clientListForRender,setClientListStateForRender] = useState([]);
const [selectedIndex, updateSelectedIndex] = useState(0);
useEffect(() => {
function handleKeyPress(event,arr){
console.log(arr)
if(event.key === "ArrowDown"){
updateSelectedIndex((prev)=>{
return prev += 1
});
}
}
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key; // __________________________1. get firebase data
setClientListState(function(prev){
setClientListStateForRender(()=>[client,...prev]); //_______2 store data
// document.addEventListener('keydown', handleKeyPress); <---I am not sure where to put this. I have experimented and
// I decided to omit my cluttered "experiments" to protect your eyes
return[client,...prev]
});
});
},[]);
Ok there are few issues with the code you posted:
1) You should definitely not add your keyboard listener in the child_ added listener ( this means that every time the child_added listener is called, you are going to create a new listener, leading to unexpected results and memory leak)
2) You are calling setState in a setState updater function (the callback function you provided for, setClientListState), which is an anti pattern and makes your code hard to follow and understand, and will cause unexpected effects once the component grows. If you want to update a state based on a previous state then use the useEffect callback
3) the useEffect function takes a second parameter, called array of dependencies. When you have provided it with an empty array, it means that you want your effect to run only once, which is problematic because we see that the function depends on clientsRef variable. ( from this actually comes your problem because the keyboard listener was having the old value of your clientsList which is the empty array, and so it was always returning 0, when keys where pressed, i explained more in the code sandbox)
4)You should return a callback function from the useEffect function to clean the effects you created, turning off the listeners you attached (or else you might have memory leaks depending on how much the component gets mounted/unmounted)
ok here is how the code should be to work:
const clientsRef = firebase.database().ref('clients');
const [clientList, setClientListState] = useState([]);
// I don't understand why you wanted another list, so for now i only use on list
// const [clientListForRender,setClientListStateForRender] = useState([]);
const [selectedIndex, updateSelectedIndex] = useState(0);
useEffect(() => {
function handleKeyPress(event, arr) {
if (event.key === 'ArrowDown') {
updateSelectedIndex(prev => {
if (prev >= clientList.length - 1) {
return (prev = 0);
} else {
return prev + 1;
}
});
}
}
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key; // __________________________1. get firebase data
setClientListState(function(prev) {
return [client, ...prev];
});
});
document.addEventListener('keydown', handleKeyPress);
// here you should return a callback to clear/clean your effects
return () => {
document.removeEventListener('keydown', handleKeyPress);
clientsRef.off();
};
// Its important to add these here, or else each time your keyboard listener runs it will have the initial value of
// clientsList ([]), and so clientsList.length = 0, and so you will always updateSelectedIndex(0)
}, [clientList, clientsRef]);
//here render based on selected list as you wish
Finally i have set up a working codesandbox that emulated data fetching based on the example you give https://codesandbox.io/s/usage-4sn92, i added some comments there to help explain what i said above.

Firebase Cloudstore: how to "update all"/"delete some" docs in a collection?

For context: I have a cron-job.org that fires an https function in my firebase project.
In this function, I have to go through all docs inside a collection and update a counter (each doc might have a different counter value). If the counter reaches a limit, I'll update another collection (independent from the first one), and delete the doc entry that reached the limit. If the counter is not beyond the limit, I simply update the doc entry with the updated counter value.
I tried adapting examples from the documentation, tried using transactions, batch, but I'm not sure how to proceed. According to transactions' description, that's the way to go, but examples only show how to edit a single doc.
This is what I have (tried adapting a realtime db sample):
function updateCounter() {
var ref = db.collection('my_collection_of_counters');
return ref.get().then(snapshot => {
const updates = {};
snapshot.forEach(child => {
var docData = child.data();
var newCounter = docData.counter+1;
if (newCounter == 10) {
// TO-DO: add to stock
updates[child.key] = null;
} else {
docData.counter = newCounter;
updates[child.key] = docData;
}
});
// execute all updates in one go and return the result to end the function
return ref.update(updates);
});
}
It doesn't work, collections don't have an update method. What is the best approach to updating each doc in a collection? One-by-one? Transaction? Is there an example?
PS: updateCounter is a function being called by the https trigger. Cron+trigger is working fine.
EDIT
When an item reaches the threshold, I want to update another collection, independent from the counter one. Is nested transactions a good solution?
Modified code:
function updateCounter() {
var ref = db.collection('my_collection_of_counters');
var transaction = db.runTransaction(t => {
return t.get(ref)
.then(snapshot => {
let docs = snapshot.docs;
for (let doc of docs) {
var item = doc.data();
var newCounter = item.counter + 1;
if (newCounter == 10) {
console.log("Update my_stock");
// ADD item.quantity to stock collection
}else{
t.update(doc.ref, {counter: newCounter});
}
}
});
})
.then(result => {
console.log('Transaction success');
})
.catch(err => {
console.log('Transaction failure:', err);
});
}
As you already noted yourself, you'll want to do this in a transaction to ensure that you can update the current counter value in a single operation. You can also create the new document, and delete the existing one, in that same transaction once your counter reaches its threshold. I don't see any benefit of doing this for all documents in a single transaction, since the operation on each doc seems unrelated to the others.
In a Firestore transaction, you perform the operations on a Transaction object as shown in the documentation. In your case you'd:
Get the current document with transaction.get().
Get the counter from the document.
Increment the counter.
If the new value is below your threshold:
Call transaction.update() to write the new counter value into the database
If the new value if above your threshold:
Call transaction.create on the new collection to create the document there.
Call transaction.delete on the existing document, to delete it.
For more, I recommend scanning the reference documentation for the Transaction class.

Get event for just newly added child node on firebase database

I am trying to get a trigger for newly added child node on reference path and get value for just the new data. But event.data.val returns all the value in that reference path.
Following is the cloud function code I am using:
exports.sendOfferNotification = functions.database.ref('/Users/{uid}/offers/New').onWrite(event => {
// Only edit data when it is first created.
// if (event.data.previous.exists()) {
// return;
// }
// Exit when the data is deleted.
if (!event.data.exists()) {
return;
}
// Grab the current value of what was written to the Realtime Database.
const original = event.data.val();
console.log('New offer', event.params.uid, original);
});
I tried using onUpdate instead of onWrite but still same result.
you could try this:
exports.sendOfferNotification =
functions.database.ref('/Users/{uid}/offers/New/{values}') //notice this change
.onWrite(event => {
if (!event.data.exists()){return};
const original = event.params.values; //notice this change
console.log('New offer', event.params.uid, original);
});
Solved it using onCreate() on the nested level of New/
exports.sendNotificationForImportantOffers = functions.database.ref('/Users/{uid}/offers/New/Important/{values}').onCreate(event => {
// Only edit data when it is first created.
// if (event.data.previous.exists()) {
// return;
// }
// Exit when the data is deleted.
if (!event.data.exists()) {
return;
}
// Grab the current value of what was written to the Realtime Database.
const original = event.data.val();
console.log('New offer', event.params.uid, original);
});
Initially, I was trying to have one single function to listen to changes on all the nested level under New/ path but now I will have to monitor trigger for each sub-level separately with onCreate() which returns me only the newly added child.

Publishing changes to a single field of a subdocument

I have a complicated data structure being built by queries on multiple collections and published.
It is working great for the initial creation, and on my local machine all the changes observed are reflected in the client as expected. However, in my staging environment I get the following error from mini-mongo when a change is observed
Uncaught Error: When replacing document, field name may not contain '.'(…)
The publishing code looks like this, where pub is the this from a Meteor.publish and rootObj is a reference to an Object in memory which gets properties modified but never has it's reference destoryed.
function _republish(pub, rootId, rootObj, handles, startup) {
// cleanup handles
if (handles.foo) {
handles.foo.stop();
}
// some query which could depend on rootObj/other calculated values
let cursor = SubColl.find({_id: {$in: bar}});
handles.foo = cursor.observeChanges({
removed(_id) {
rootObj.bar = rootObj.bar.filter(o => o._id !== _id);
pub.changed('foobarbaz', rootId, {bar: rootObj.bar})
},
changed(_id, fields) {
const index = rootObj.bar.findIndex(line => line._id === _id);
const changed = {};
_.each(fields, (value, field) => {
rootObj.bar[index][field] = value;
changed[`bar.${index}.${field}`] = value;
});
pub.changed('foobarbaz', rootId, changed);
},
added(_id, fields) {
rootObj.bar.push(_.extend({}, fields, {_id}));
if (!startup) {
// deeper children stuff
pub.changed('foobarbaz', rootId, {bar: rootObj.bar});
}
}
});
// deeper children stuff
startup = false;
// if startup was true, expect caller to publish this
}
As we can see, the publish works fine when I'm pub.changeding on just bar, but attempting to update a specific subdocument field (e.g. bar.0.prop) results in the inconsistent behaviour
If possible I want to avoid re-publishing the whole of bar as it is huge compared to updating a simple property.
How can I publish the change to a single field of a subdocument?

How to check if key is not found in IndexedDB?

On the indexeddb i want to look if there is a key permanent and do some actions. But if not, i want to make some other actions. I can do the actions if the permanent is there, however when it is not I can get the onerror to work. Is the onerror suppose to do this thing? How can I check if there is not value in it?
var hashtype = 'permanent';
var getPermanent = store.get(hashtype);
getPermanent.onsuccess = function() {
var ivrame = getPermanent.result.value;
};
getPermanent.onerror = function() {
console.log('onerror')
};
See the note under https://w3c.github.io/IndexedDB/#dom-idbobjectstore-get - the get method yields success with undefined if there is no matching record.
So you have a few options:
Use get(key) and test the result for undefined. This works unless undefined is a value you expect to store (it's a valid value)
Use count(key) - the result will be 1 if present, 0 if absent. Easy if you're just testing for existence, but doesn't get you the record.
Use openCursor(key) and test to see if the request's result is a cursor (record present as request.result.value) or undefined (no record in range)
For your code:
var hashtype='permanent';
// #1: Use get
var getPermanent = store.get(hashtype);
getPermanent.onsuccess = function() {
if (getPermanent.result === undefined) {
// no record with that key
} else {
var value = getPermanent.result;
}
};
// #2: Use count
var getPermanent = store.count(hashtype);
getPermanent.onsuccess = function() {
if (getPermanent.result === 0) {
// no record with that key
} else {
...
}
};
// #3: Use cursor
var getPermanent = store.openCursor(hashtype);
getPermanent.onsuccess = function() {
var cursor = getPermanent.result;
if (!cursor) {
// no record with that key
} else {
var value = cursor.value;
}
};
The function assigned to request.onsuccess is a callback function that is always called, regardless of whether the value is present in the store. When there is no corresponding object in the store, the result object will be undefined. When there is a corresponding object in the store, the result object will be defined. So you simply need to check if the object is defined from within the onsuccess callback function.
request.onerror is a separate callback from request.onsuccess. onerror gets called when there is some type of failure in indexedDB (e.g. something like you tried to get a value from a store that doesn't exist, or you tried to put a duplicate object into a store that doesn't permit duplicates). request.onerror does not get called when no value is found as a result of calling store.get, because that is not considered an 'error' in the failure sense.
So, what you want to do is something like this:
var hashtype='permanent';
var getPermanent = store.get(hashtype);
getPermanent.onsuccess = function(event) {
//var ivrame=getPermanent.result.value;
var result = getPermanent.result;
if(result) {
console.log('Got a result!', result);
var ivrame = result;
} else {
console.log('Result was undefined! No matching object found');
}
};
getPermanent.onerror = function() {
console.log('Something went wrong trying to perform the get request');
};
Do not try and access request.result.value. There is no such thing in the case of a get request. When using store.get, request.result contains the matching object you want, or is undefined. When using store.openCursor, request.result contains the cursor, which is defined if there is at least one matching object and you have not already iterated past it. To get the matching object at the cursor's current position, you would use cursor.value. Here, cursor.value will always be defined, because cursor would otherwise be undefined, and you would obviously check for that beforehand.
Instead of using getPermanent.result to access data provided by 'get' request it is better to use event.target.result. It also can be compared with undefined to check absence of requested key:
db = this.result;
var tr = db.transaction("data");
var objstore = tr.objectStore("data");
var getres = objstore.get(0);
getres.onsuccess = function(event)
{
if(event.target.result.data === undefined)
console.log("key not found");
}

Categories