Firebase: Checking for 'write or delete' for onWrite Events - javascript

I have the following functions that listens to an onWrite event for a database trigger like so:
exports.sendNotifications = functions.database.ref('/events/{eventId}/registered')
.onWrite(event => {
...
});
The above function is called regardless whether the node is deleted or added. How do I check if the onWrite event is a 'delete' event for that particular node, or a 'add' event, such that this function only gets called when it is a 'add' event.

If you want to only trigger this function for an add event, the onCreate() trigger would be the way to go.
However, you can also detect if it is an add event inside your onWrite() using the before and after properties of the Change object:
exports.sendNotifications = functions.database.ref('/events/{eventId}/registered')
.onWrite((change, context) => {
if (change.before.exists()) {
//update event
return null;
} else if (!change.after.exists()) {
//delete event
return null;
} else {
//add event
//do stuff
}
});
Find more details in the documentation:
https://firebase.google.com/docs/functions/database-events#handle_event_data

The event that is passed into the function contains both the previous data and the new data for the location that triggered the function. With these two pieces of information, you can deduct whether it was a new write, an update, or a delete.
From the Firebase documentation for database functions:
exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
.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;
}

An onWrite will trigger when a document is created, updated, or deleted. Here's how to check for each of those conditions with Firestore cloud functions.
if(event.after.data == undefined){
// Deleted: data before but no data after
console.log('Deleted');
}
if(event.before.data && event.after.data){
// Updated: data before and data after
console.log('Update');
}
if(!event.before.data && event.after.data){
// Created: no data before but data after
console.log('Created');
}

The API seems to have changed since most of the answers here. Posting version that currently (May 2021) works for me.
Whether it is Firestore or Realtime Database in both cases the onWrite hook accepts change object which is just an object holding two document snapshots: before and after.
Creation or Deletion can be detected using the exists (which is getter property, not a function).
Also if all you need to handle is creation, better use the onCreate event handler. It gets just the snapshot of created document in place of the change object.
// EITHER
exports.sendNotifications = functions.database.ref('/events/{eventId}/registered')
.onWrite((change, context) => {
// Change contains before and after documents snapshots
const { before, after } = change;
// Each snapshot knows if it exists.
if (!before.exists) {
const newEvent = after.data();
console.log('Event was created', newEvent);
return;
}
if (!after.exists) {
const oldEvent = before.data();
console.log('Event was deleted', oldEvent);
return;
}
console.log('Event changed', { from: before.data(), to: after.data() });
});
// OR
exports.sendNotifications = functions.database.ref('/events/{eventId}/registered')
.onCreate((snapshot, context) => {
const newEvent = snapshot.data();
console.log('Event was created', newEvent);
});

If your change to Cloud Firestore then it will be :
// 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;
}

After reading the following article, adding another check on the event element to the if-then statement has some benefits: https://firebase.googleblog.com/2017/07/cloud-functions-realtime-database.html
exports.sendNotification = functions.database
.ref("/messages/{messageId}").onWrite(event => {
if (event.data.exists() && !event.data.previous.exists()) {
// This is a new message, not a change or a delete.
// Put code here
}
})
If the read from the database failed (maybe a network error???), then event.data.previous.exists() will be false. The addition of event.data.exists() ensures the event element does in fact have data that you can use.

I do this quite a bit in my codebase so I have written a helper you could perhaps use :)
import { Change, firestore } from 'firebase-functions';
import { isNil } from 'lodash';
export enum FirestoreWriteEventType {
Delete,
Create,
Update,
Unknown
}
/**
* Returns what type of operation the onWrite was.
* #param change
*/
const onWriteHelper = (change: Change<firestore.DocumentSnapshot>): FirestoreWriteEventType => {
const oldData = change.before.data();
const newData = change.after.data();
if (isNil(newData)) {
return FirestoreWriteEventType.Delete;
}
if (oldData && newData) {
return FirestoreWriteEventType.Update;
}
if (!oldData && newData) {
return FirestoreWriteEventType.Create;
}
return FirestoreWriteEventType.Unknown;
};
export default onWriteHelper;

I would instead use the .onCreate method which has slightly different syntax but accomplished what you are trying to do. (Note that onWrite and onCreate have both been modified in the jump from <= v0.9.1 to >1.0.0).
Try this:
exports.sendNotifications = functions.database.ref('/events/{eventId}/registered')
.onCreate((snap, context) => {
const data = snap.val();
// ^ the value of the newly inserted data
// context.params contains all of the wildcard fields used in the document reference.
});

Related

Uncaught DOMException: Failed to execute 'put' on 'IDBObjectStore': Evaluating the object store's key path did not yield a value at request.onsuccess

I'm trying to Store some application data using indexedDB
Here is my code
function _getLocalApplicationCache(_, payload) {
const indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.shimIndexedDB;
if (!indexedDB) {
if (__DEV__) {
console.error("IndexedDB could not found in this browser.");
}
}
const request = indexedDB.open("ApplicationCache", 1);
request.onerror = event => {
if (__DEV__) {
console.error("An error occurred with IndexedDB.");
console.error(event);
}
return;
};
request.onupgradeneeded = function () {
const db = request.result;
const store = db.createObjectStore("swimlane", {keyPath: "id", autoIncrement: true});
store.createIndex("keyData", ["name"], {unique: false});
};
request.onsuccess = () => {
// creating the transition
const db = request.result;
const transition = db.transaction("swimlane", "readwrite");
// Reference to our object store that holds the swimlane data;
const store = transition.objectStore("swimlane");
const swimlaneData = store.index("keyData");
payload = JSON.parse(JSON.stringify(payload));
store.put(payload);
const Query = swimlaneData.getAll(["keyData"]);
Query.onsuccess = () => {
if (__DEV__) {
console.log("Application Cache is loaded", Query.result);
}
};
transition.oncomplete = () => {
db.close();
};
};
}
If I do use different version then 1 here --> indexedDB.open("ApplicationCache", 1);
I'm getting a error like they keyPath is already exist. And other than than for version 1 I'm getting this error.
Can someone please help me where i'm doing wrong.
Review the introductory materials on using indexedDB.
If you did something like connect and create a database without a schema, or created an object store without an explicit key path, and then you stored some objects, and then you edited the upgradeneeded callback to specify the keypath, and then never triggered the upgradeneeded callback to run because you continue to use current version number instead of a newer version number, it would be one possible explanation for this error.
The upgradeneeded callback needs to have logic that checks for whether the object stores and indices already exist, and only create them if they do not exist. If the store does not exist, create it and its indices. If the store exists and the indices do not, add indices to the store. If the store exists and the indices exist, do nothing.
You need to trigger the upgradeneeded callback to run after changing your database schema by connecting with a higher version number. If you do not connect with a higher version number, the callback never runs, so you will end up connecting to the older version where your schema changes have not taken place.

runTransaction: Provided document reference is from a different Firestore instance error

I'm trying to implement easier to read documentIDs and through this strategy that Frank recommended but I've run into an error that I haven't been able to fix.
I can successfully read the value of highestCount from the COUNTER document but have an error when I try to update it.
The error states:
FirebaseError: Provided document reference is from a different Firestore instance
My Javascript code (using React):
const onSetCreation = async (e) => {
e.preventDefault();
var setNo = 0;
try {
await runTransaction(db, async (transaction) => {
const currentSetNo = await transaction.get(
doc(db, "users", userStatus.uid, "set", "COUNTER")
);
console.log(currentSetNo.data());
// if theres no counter, create one
if (!currentSetNo.exists()) {
transaction.update({ highestCount: 0 });
setNo = 0;
} else {
setNo = currentSetNo.data().highestCount + 1;
// update the COUNTER once iterated
transaction.update(currentSetNo, { highestCount: setNo });
}
});
// succesful update of COUNTER
console.log("Transaction successfully committed!");
} catch (e) {
console.log(e);
}
};
This function is triggered on a button click.
My Firestore structure
is users -> [userid] -> set -> COUNTER
Where counter is a document that holds highestCount which is to be updated everytime this function runs.
I'm able to console.log the value of COUNTER, just can't update it.
I haven't been able to find any similar problems.
I'm using the Web version 9 of Firestore, I've read this documentation for runTransaction.
Cheers
Okay, so I found the error. I was using the wrong reference in this line:
transaction.update(currentSetNo, { highestCount: setNo });
which should actually be
transaction.update(
doc(db, "users", userStatus.uid, "set", "COUNTER"),
{ highestCount: setNo }
);
Or whatever your db reference is, in replacement of currentSetNo in the first example.

Firestore listener removes a message from pagination when adding a new message in React Native

I am trying to do Firestore reactive pagination. I know there are posts, comments, and articles saying that it's not possible but anyways...
When I add a new message, it kicks off or "removes" the previous message
Here's the main code. I'm paginating 4 messages at a time
async getPaginatedRTLData(queryParams: TQueryParams, onChange: Function){
let collectionReference = collection(firestore, queryParams.pathToDataInCollection);
let collectionReferenceQuery = this.modifyQueryByOperations(collectionReference, queryParams);
//Turn query into snapshot to track changes
const unsubscribe = onSnapshot(collectionReferenceQuery, (snapshot: QuerySnapshot) => {
snapshot.docChanges().forEach((change: DocumentChange<DocumentData>) => {
//Now save data to format later
let formattedData = this.storeData(change, queryParams)
onChange(formattedData);
})
})
this.unsubscriptions.push(unsubscribe)
}
For completeness this is how Im building my query
let queryParams: TQueryParams = {
limitResultCount: 4,
uniqueKey: '_id',
pathToDataInCollection: messagePath,
orderBy: {
docField: orderByKey,
direction: orderBy
}
}
modifyQueryByOperations(
collectionReference: CollectionReference<DocumentData> = this.collectionReference,
queryParams: TQueryParams) {
//Extract query params
let { orderBy, where: where_param, limitResultCount = PAGINATE} = queryParams;
let queryCall: Query<DocumentData> = collectionReference;
if(where_param) {
let {searchByField, whereFilterOp, valueToMatch} = where_param;
//collectionReferenceQuery = collectionReference.where(searchByField, whereFilterOp, valueToMatch)
queryCall = query(queryCall, where(searchByField, whereFilterOp, valueToMatch) )
}
if(orderBy) {
let { docField, direction} = orderBy;
//collectionReferenceQuery = collectionReference.orderBy(docField, direction)
queryCall = query(queryCall, fs_orderBy(docField, direction) )
}
if(limitResultCount) {
//collectionReferenceQuery = collectionReference.limit(limitResultCount)
queryCall = query(queryCall, limit(limitResultCount) );
}
if(this.lastDocInSortedOrder) {
//collectionReferenceQuery = collectionReference.startAt(this.lastDocInSortedOrder)
queryCall = query(queryCall, startAt(this.lastDocInSortedOrder) )
}
return queryCall
}
See the last line removed is removed when I add a new message to the collection. Whats worse is it's not consistent. I debugged this and Firestore is removing the message.
I almost feel like this is a bug in Firestore's handling of listeners
As mentioned in the comments and confirmed by you the problem you are facing is occuring due to the fact that some values of the fields that your are searching in your query changed while the listener was still active and this makes the listener think of this document as a removed one.
This is proven by the fact that the records are not being deleted from Firestore itself, but are just being excluded from the listener.
This can be fixed by creating a better querying structure, separating the old data from new data incoming from the listener, which you mentioned you've already done in the comments as well.

Firebase real time database access data array from deleted node

I am deleting a FRTDB node, I want to access deleted data from that node. the functions looks as follow:
exports.events = functions.database.ref('/events/{eventId}').onWrite(async (change, context) => {
const eventId = context.params.eventId
if (!change.after.exists() && change.before.exists()) {
//data removed
return Promise.all([admin.database().ref(`/events/${eventId}/dayofweek`).once('value')]).then(n => {
const pms = []
const days = n[0]
days.forEach(x => {
pms.push(admin.database().ref(`${change.before.val().active ? 'active' : 'inactive'}/${x.key}/${eventId}`).set(null))
})
return Promise.all(pms)
});
else {
return null;
}
})
The probem I am having is that
admin.database().ref(`/events/${eventId}/dayofweek
do not loop the data because it seems data is no longer there so the forEach is not working. How can I get access to this data and get to loop the deleted data?
Of course you won't be able to read data that was just deleted. The function runs after the delete is complete. If you want to get the data that was just deleted, you're supposed to use change.before as described in the documentation:
The Change object has a before property that lets you inspect what was
saved to Realtime Database before the event. The before property
returns a DataSnapshot where all methods (for example, val() and
exists()) refer to the previous value. You can read the new value
again by either using the original DataSnapshot or reading the after
property. This property on any Change is another DataSnapshot
representing the state of the data after the event happened.
The data that was deleted from the database is actually included in the call to your Cloud Function. You can get if from change.before.
exports.events = functions.database.ref('/events/{eventId}').onWrite(async (change, context) => {
const eventId = context.params.eventId
if (!change.after.exists() && change.before.exists()) {
//data removed
days = change.before.val().dayofweek;
...
})

Is there an event in the Excel JavaScript API that triggers when the name of a table is changed?

For an Excel add-in I am developing, I need to know when the name of a table is changed. The “Events” section for Table does not have an onNameChanged or something similar. I’ve tried whether perhaps the name change is also covered by onChanged, but in the snippet below, it is not triggered by the name change of the table. The other handlers in the snippet are not triggered either. I’ve tested the snippet in Office Online and Excel for Mac (v16.39). Is there another event that is triggered when the name of a table is changed?
async function run() {
await Excel.run(async (context) => {
const table = context.workbook.tables.add("A1:B2", true);
table.load('worksheet');
await context.sync();
const createHandler = (name) => (async () => {
console.log(`Received event: ${name}`);
});
table.onChanged.add(createHandler('Table.onChanged'));
context.workbook.tables.onChanged.add(createHandler('TableCollection.onChanged'));
context.workbook.tables.onAdded.add(createHandler('TableCollection.onAdded'));
context.workbook.tables.onDeleted.add(createHandler('TableCollection.onDeleted'));
table.worksheet.onChanged.add(createHandler('Worksheet.onChanged'));
await context.sync();
table.name = 'TestTable';
await context.sync();
});
}
Unfortunately, Office JS Excel API currently doesn't support table.onNameChanged event. In your gist, you try to use table.onChanged, however, table.onChanged only occurs when data in cells changes on this table.
Therefore, I would suggest that you could submit the request and upvote this request at uservoice, kindly also share your scenario in this request.
Given you would like an event fired when renaming a table made in Excel (not javascript), and that the presumed onChanged event on a Table doesn't fire when renaming, my hacky workaround is as follows:
Listen for the onChanged event on the Worksheet (which holds [Table]) so that you listen for all tables data changes. When the event triggers, check Table in worksheet.tables) for a change in name (compare with a Map of old names):
const map = new Map();
worksheet.tables.forEach((table, i) => {
const newName = table.name;
const key = `${worksheet.id}-${i}`;
map.set(key, newName);
}
worksheet.onChanged.add(() => {
worksheet.tables.forEach((table, i) => {
const newName = table.name;
const key = `${worksheet.id}-${i}`;
let oldName;
if (map.has(key)) {
oldName = map.get(key);
} else {
map.set(key, newName);
}
if (oldName !== newName) {
console.log(`Name changed from ${oldName} to ${newName}`);
console.log(`For table ${key}`);
}
});
});
On a side note: You can always listen for changes in Javascript by wrapping the object into a Proxy. Here is an example:
let obj = {
name: 'harry potter',
age: 27,
};
// we want to listen for changes on name
// so wrap object in Proxy
obj = new Proxy(obj, {
set(obj, prop, newval) {
if (prop === 'name') {
// put your changing logic here
// or fire an event listener
console.log("Name has changed");
}
// default behavior to store value
obj[prop] = newval;
return true;
}
})
obj.name = 'john smith';
// outputs 'Name has changed'
Read more about proxies here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

Categories

Resources