Firebase cloud function always getting null snapshot - node.js

I'm experiencing some problems while testing my firebase cloud function in the interactive shell (firebase experimental:functions:shell). Within the onWrite event, event.data.val() returns null when I first call it, instead of returning the information I expect. Also, after that, it's not automatically called again after I change some data through a mobile app.
Here is a sample code:
"use strict";
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.syncMySql = functions.database.ref('/something/{user_token}/{something_id/}')
.onWrite(event => {
const snapshot = event.data.val();
const user_token = event.params.user_token;
const mock_id = event.params.something_id;
console.log(functions.config());
console.log('# User: ' + user_token + ',\n# Something: ' + something_id + ',\n# Snapshot: ' + snapshot + '\n Evento: ' + JSON.stringify(event));
return;
});
And here is a sample of how I'm trying to call it from firebase experimental:functions:shell:
syncMySql('', {params: {user_token: 'sOmEUseRtoKen1234-i_', something_id: '456789'}})
Any ideas on what am I doing wrong?
Also, what is this first parameter called in syncMySql? What's it's purpose?

For invoking functions via the test environment like this, you need to actually provide the data that was supposedly written to the DB yourself. event.data.val() won't read the data from an exising entitity based on the params, but will only return the information you passed in the first parameter of the function call.
For invoking onWrite and onUpdate functions you need to provide the data in the form {before: 'old_data', after: 'new_data' }, where old_data and new_data can be primitives and/or objects, i.e. anything you would write to your DB.
So your full call should be something like this:
syncMySql(
{
before: null,
after: 'something'
},
{params: {user_token: 'sOmEUseRtoKen1234-i_', something_id: '456789'}}
);
In this case event.data.val() should return 'something'.

Related

get the number of documents before and after Firestore trigger - Cloud functions

I'm trying to get the number of documents in a collection before and after a document has been added using cloud functions, the code in nodeJs I wrote is this:
exports.onShowcaseCreated = functions.firestore
.document("Show/{document}")
.onCreate((snapshot, context) => {
const showcaseDict = snapshot.data();
const uid = showcaseDict.uid;
return db.collection("Showcases").where("uid", "==", uid).get()
.then((showsnap) => {
const numberOfShowcaseBefore = showsnap.size.before;
const numberOfShowcaseAfter = showsnap.size.after;
console.log( numberOfShowcaseBefore, numberOfShowcaseAfter);
if ( numberOfShowcaseBefore == 0 && numberOfShowcaseAfter == 1 ) {
return db.collection("Users").doc(uid).update({
timestamp: admin.firestore.Timestamp.now(),
});
});
});
});
but the console logs are undefined undefined it seems like this is not the right approach for taking the number of documents before and after a document has beed added
The before and after properties are only defined on the argument that is passed to onCreate. You call that snapshot in your code, but it's actually a Change object as defined here.
Reading data from the database gives you a QuerySnapshot object as defined here. As you can see, that size on that QuerySnapshot is just a number and does not have before or after properties.
So there's no way to determine the size before the event triggered with your approach. Any query you run in the code, runs after the event was triggered so will give you the size at that moment.
To implement this use-case I'd recommend storing a count of the number of relevant documents in the database itself, and then triggering a Cloud Function when that document changes. Inside the Cloud Function code you can then read the previous and new value of the size from the change document that is passed in.

Firestore query sometimes doesn't contain updated data

I'm using firestore's node sdk. In my code I call function1, which updates a table in firestore. When that function ends I call function2, which runs a query to get a reference of the table. About 80% of the time it works, but sometimes the data I need, which was added to the document in function1 doesn't come back in the snapshot so an error is thrown.
I added an await keyword before the update, but that doesn't seem to make the code wait for the firestore update to complete.
I suppose I could also return the data I was going to update in function1 and pass it into function2, but that feels kind of hacky, though I suppose I'd save a little money because I wouldn't have to get 1 document anymore. I could also make it one big function, but that would make it an 100 line function.
Here's an abbreviated version of my code:
const function1 = async (tableId) => {
const firestore = admin.firestore();
const tableSnapshot = await firestore.collection('tables').doc(tableId).get();
await tableSnapshot.ref.update({ smallBlind: {seat: 1, amount: 5000} }) // the seat number and amount number wont always be 1 and 5000. Otherwise I wouldn't need to look it up in function2
}
const function2 = async (tableId) => {
const firestore = admin.firestore();
const tableSnapshot = await firestore.collection('tables').doc(tableId).get();
const tableData = tableSnapshot.data();
const smallBlind = tableSnapshot.data().smallBlind; // the smallBlind data is not there. so smallBlind is undefined
}
const startStuff = async () => {
await function1(42); // example tableId is passed in
await function2(42);
}
startStuff()
The above code has no async issues. I had a different async issue in another portion of my code, which caused my problem.

Question about the correct use of transaction in a simple use case in a cloud function

I am trying to add +1 to a specific field in the realtime database. My function:
exports.dbWriteOnNewPost = functions.database.ref('/posts/{postid}').onWrite((change, context) => {
const postUUID = context.params.postid;
const postData = change.after.val();
const communityUUID = postData.community;
const authorUUID = postData.author;
const postDate = postData.date;
const promisePostByCommunity = admin.database().ref('/posts_by_community/' + communityUUID + '/' + postUUID).set(postDate);
const promisePostByUser = admin.database().ref('/posts_by_user/' + authorUUID + '/' + postUUID).set(postDate);
const promiseCommunityPostsCount = admin.database().ref('/communities/' + communityUUID + '/posts_count').transaction(
(posts_value) => {
return posts_value + 1;
}
);
return Promise.all([promisePostByCommunity, promisePostByUser, promiseCommunityPostsCount]);
});
I am simply asking if this transaction will prevent assigning wrong value, if for example 10 users are creating posts in the exact same time, which is going to happen if I use typical .once value => .set ?
EDIT: Finally managed to test it without breaking anything and the code above works perfectly fine.
I'm not too familiar with Firebase, but it looks like Transactions will do exactly what you want out of the box. The description for this seems to directly answer your question
The update function takes the current state of the data as an argument and returns the new desired state you would like to write. If another client writes to the location before your new value is successfully written, your update function is called again with the new current value, and the write is retried.
documentation: https://firebase.google.com/docs/database/web/read-and-write#save_data_as_transactions

Cannot get FieldValue.increment working in Firestore emulator with Admin SDK

I am trying to get a simple atomic increase/decrease working in Firebase Cloud Functions with Firestore triggers.
To test these, I want to execute local tests with the Firestore emulator.
But I always get the error
FirebaseError: Function DocumentReference.update() called with invalid data. Unsupported field value: a custom object (found in field myfield)
My code is as simple as following
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp();
export const mytest = functions.firestore.document('my/{doc}').onUpdate(async (change, context) => {
return perform();
});
export async function perform(): Promise<FirebaseFirestore.WriteResult> {
const myDocRef = .... // this is a FirebaseFirestore.DocumentReference
const decrement = admin.firestore.FieldValue.increment(1);
return myDocRef.update({
myfield: decrement
});
}
In my tests I just call the perform function and then I receive the above error. It's definitely the FieldValue.increment that causes the error because if I just put a hardcoded number instead of the decrement in the update call, it works (updates to that hardcoded number).
I am on Firebase CLI 7.2.1 and emulator 1.6.2 which - according to Most efficient way to increment a value of everything in Firebase and https://github.com/firebase/firebase-js-sdk/issues/1799 should support FieldValue.increment in the emulator.
What am I doing wrong?
What version of firebase-admin are you using?
From your description it sounds like you're calling perform() directly from a Node.js script. I would recommend re-working your setup to more closely match what will actually happen in production: a client will connect directly to Firestore, create a document, and your function will be triggered.
Here's an example you can run that does this:
const admin = require('firebase-admin');
const functions = require('firebase-functions');
admin.initializeApp();
exports.createItem = functions.https.onRequest(async (request, response) => {
const item = await admin.firestore().collection("items").add({ addedAt: Date.now() });
response.send(`added ${item.id}\n`);
});
const counter = admin.firestore().collection("counters").doc("items");
exports.increment = functions.firestore.document('items/{itemId}').onCreate(async (change, context) => {
await counter.set({ total: admin.firestore.FieldValue.increment(1) }, { merge: true });
const current = await counter.get();
console.log(`incremented counter to ${current.data().total}`);
});
The first function, createItem, is an HTTP trigger that adds a document to the items/ collection. The second function is a Firestore trigger that increments a field in the counters/items document whenever a document is created.
When I run
curl -X POST http://localhost:5001/ryanpbrewster-test/us-central1/createItem
it creates a document and sends a response like
added LGOCapHSQtlXKIMEA8Do
This triggers the other function, which I can see in the emulator logs:
I function: Beginning execution of "increment"
> incremented counter to 6
I function: Finished "increment" in ~1s

Update value once write completes in Cloud Function

I'm trying to update one value after a write completes (in a Cloud Function) but it just wont work (I'm sure this is a stupidly simple problem). Code below:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const firebase = require('firebase');
admin.initializeApp(functions.config().firebase);
exports.createMessage = functions.https.onRequest((request, response) => {
const json = JSON.parse(request.query.json); // == "{'start':0, 'end':0}"
json.start = firebase.database.ServerValue.TIMESTAMP;
admin.database().ref('/messages/').push(json).then(snapshot => {
//Here is the problem. Whatever I try here it won't work to retrieve the value.
//So, how to I get the "start" value, which has been written to the DB (TIMESTAMP value)?
var startValue = snapshot.ref.child('start').val();
snapshot.ref.update({ end: (startValue + 85800000) }).then(snapshot2=>{
response.redirect(303, snapshot.ref);
});
});
});
Is the problem that I'm using admin.database()?
This code:
var startValue = snapshot.ref.child('start').val();
doesn't actually retrieve any values. Take a look at the docs for DataSnapshot. Reach into that snapshot directly with child() - you don't need the ref. Maybe this is what you meant?
var startValue = snapshot.child('start').val();
I'm not sure if there's a bug in Firebase or if I'm using it wrong, but if I try to call any method on the snapshot-reference I will only get an error saying: TypeError: snapshot.xxx is not a function where xxx is the function name i try to use (for example: child(...), forEach(...), etc).
However, the following seems to fix the issue with the snapshot:
admin.database().ref('/messages/').push(json).once('value').then(snapshot => {
instead of:
admin.database().ref('/messages/').push(json).then(snapshot => {
My uneducated guess is that the then-promise, for the push-function returns some faulty snapshot since the only thing that seems to work is snapshot.key.
Also, if I'm not mistaken, doesn't my solution make two reads now, instead of one? Since push will write and then (supposedly) read and return the written value and then I read it once more with once(value).
Does anyone has any further insights into this problem?

Resources