I'm trying to import a JSON into a collection. For testing purpose, I'm using emulators and document creation trigger.
The workflow is :
start emulators with firebase emulators:start
start functions shell in another terminal with firebase functions:shell (tells me that no emulator is running btw)
call my function with tempoCF()
it runs and add documents to collection but it seems that the function does not terminate. I cannot call another function and need to press CTRL+C to be able to write again in the shell.
Here is the function I use :
const functions = require("firebase-functions");
const admin = require('firebase-admin');
const fetch = require('node-fetch');
admin.initializeApp();
const db = admin.firestore();
exports.tempoCF = functions
.firestore.document('/tempo/{docId}')
.onCreate(async (snap, context) => {
console.log("onCreate");
let settings = { method: "Get" };
let url = "https://opendata.paris.fr/api/records/1.0/search/?dataset=sanisettesparis&q=&rows=-1";
try {
let response = await fetch(url, settings);
let json = await response.json();
// TODO for each json object, add new document
return Promise.all(json["records"].map(toiletJsonObject => {
console.log(toiletJsonObject);
return db.collection('toilets').doc(toiletJsonObject["recordid"]).set({});
}));
}
catch(error) {
console.log(error);
return null;
}
}
);
I know I can use the emulator UI to create a new document that trigger the tempoCF function and it works as well but I fear that my function isn't correct and could generate bugs in production.
Here is the screenshot of the terminal. It prints logs and at the end, there is no way to write anything on the last empty line in the screenshot. I run it in Android Studio but I don't think that it matters.
I'm not on Windows but on a Mac, and I can reproduce your problem in the Terminal by calling the function with tempoCF(). Somehow, by doing that, you are simulating the creation a Firestore document without data.
But if I pass some data when calling the Cloud Function, e.g. tempoCF({foo: "bar"}) (i.e. providing new test data for the onCreate operation) I'm able to write to the Terminal after the CF has completed. See the doc for more details.
Related
I have a variety of functions for Cloud Functions that use things derived from firebase-admin/app's initializeApp(). Since multiple functions use this, I had the idea of calling initializeApp() close to the beginning of the Typescript/Javascript. Like this:
import { initializeApp } from 'firebase-admin/app';
...
const app = initializeApp();
...
export const activeUser = https.onCall(
async (email: string, ctx: CallableContext) => {
...
const auth = getAuth(app);
const firestore = getFirestore(app);
try {
const user = await auth.getUserByEmail(email);
const snpsht = await firestore.collection('users').doc(user.uid).get();
...
export const userExists = https.onCall(
async (email: string, ctx: CallableContext) => {
if (!emulating() && forbiddenOrigin(ctx)) {
return `${ERRORFLAG}: forbidden`;
}
const auth = getAuth(app);
...
A testing headache showed me that I forgot about calling deleteApp(app).
Is deleteApp() necessary, or will the app be deleted as a side effect of Cloud tearing down the function?
Should initializeApp()/deleteApp() be called within each function where an app is needed, ie within activeUser and userExists in my above example?
Is there any way to tell Cloud Functions to run setup/teardown code before running a given function?
A call to deleteApp is not necessary. When a Cloud Functions instance terminates, all memory gets shut down with it (which is what deleteApp would do). The entire server instance is completely gone.
You only need to call deleteApp if you are controlling a process that needs to continue after the call to deleteApp with the Firebase resources freed from memory. Cloud Functions does not meet this criteria since you don't control the process startup or shutdown.
I tried to call my cloud function using the cloud_functions plugin from my Flutter project with the following code:
final HttpsCallable callable = new CloudFunctions(region: "europe-west3")
.getHttpsCallable(functionName: 'helloWorld');
dynamic resp = await callable.call(<String, dynamic>{'id': id, 'chatId': chat.chatId});
And get the following error:
ERROR: PlatformException(3840, The data couldn’t be read because it isn’t in the correct format., null)
By my research, I saw that the problem can appear when you forget to put the region on the server and client side, but the error persist.
Also I try to pass by http request who succeed:
var parameters = {'id': id, 'chatId': chat.chatId};
var url = "https://europe-west3-{MY_DOMAIN}.cloudfunctions.net/helloWorld";
await http.post(url, body: parameters).then((res) {...}
So I think the problem come from the plugin where I maybe may have forgotten something. Any ideas ?
Cloud function (test):
exports.helloWorld = functions
.region('europe-west3')
.https.onRequest((request, response) => {
try {
response.send('Hello from Firebase!');
} catch (e) {
console.log(e);
throw new functions.https.HttpsError('calc-error', e);
}
});
As you are using a callable on your Flutter app, try to convert your function to use onCall instead of onRequest:
Firebase function using onCall:
export const helloWorld = functions.https.onCall((data, context) => {
functions.logger.info("data:", data);
return {
message: "bye!"
};
});
Flutter app:
emulator setup:
// after: Firebase.initializeApp()
FirebaseFunctions.instance.useFunctionsEmulator(origin: 'http://localhost:5001');
calling the function:
FirebaseFunctions functions = FirebaseFunctions.instance;
HttpsCallable callable = functions
.httpsCallable('helloWorld');
final results = await callable.call({
'name': 'Erick M. Sprengel',
'email': 'erick#mail.com'
});
Be careful about the difference between onCall vs onRequest.
It's easy to convert, but I think it's better to check this question: Firebase Cloud Functions: Difference between onRequest and onCall
Extra tips:
I'm using the emulator, and I'm not setting the region.
Remember to re-build your function after each change. I'm using npm run build -- --watch
in my case, I have wrong callable function name
FirebaseFunctions.httpsCallable('wrong name in here')
You need to set region on both side.
In your function:
exports.foo = functions.region('europe-west3').https.onCall(async (data, context) => ...
In Flutter:
final functions = FirebaseFunctions.instanceFor(region: 'europe-west3');
functions.httpsCallable('foo');
so I make http trigger function to get all events from my firestore like the image above.
firestore.js
const functions = require('firebase-functions')
const admin = require("firebase-admin")
// initialize database
admin.initializeApp()
const db = admin.firestore();
const settings = {timestampsInSnapshots: true};
db.settings(settings)
const eventRef = db.collection('event')
module.getAllEventsFromFirestore = functions.https.onRequest(async (request,response) => {
try {
const events = await eventRef.get()
response.status(200).send(`number of event is ${event.size}`)
} catch (error) {
response.status(500).send(error)
}
})
and my index.js
const {getAllEventsFromFirestore} = require("./firestore")
after deploying the function, I expect will get the URL to access that http trigger function on my terminal, but I can't find it.
The Firebase CLI will only give you a URL the first time you deploy the function. If you update the function after the first deploy, it won't print the URL. You can get the URL of the function by going to the Firebase console and view your functions there. The URL will be available on the Functions dashboard page.
If you would like to see a change in behavior of the Firebase CLI, file a feature request with Firebase support.
I couldn't find a solution for this use case in Firebase official guides.
They are HTTPS callable functions
Want to run Functions locally using Cloud Functions shell to test
Functions save received data to Firestore
The 'auth' context information is also needed
My code as below. Thanks in advance.
Function :
exports.myFunction = functions.https.onCall((data, context) => {
const id = context.auth.uid;
const message = data.message;
admin.firestore()...
// Do something with Firestore //
});
Client call :
const message = { message: 'Hello.' };
firebase.functions().httpsCallable('myFunction')(message)
.then(result => {
// Do something //
})
.catch(error => {
// Error handler //
});
There is an api exactly for this use case, see here.
I used it in javascript(Client side) as follows -
button.addEventListener('click',()=>{
//use locally deployed function
firebase.functions().useFunctionsEmulator('http://localhost:5001');
//get function reference
const sayHello = firebase.functions().httpsCallable('sayHello');
sayHello().then(result=>{
console.log(result.data);
})
})
where sayHello() is the callable firebase function.
When the client is an android emulator/device. Use 10.0.2.2 in place of localhost.
Also the code for flutter would be like so -
CloudFunctions.instance.useFunctionsEmulator(origin: 'http://10.0.2.2:5000')
.getHttpsCallable(functionName: 'sayHello')
Cloud functions have emulators for that. Check this link it can suite your case. Its not functions shell, but for testing purposes i think it can still works for you
In newer versions of firebase, this is the way:
import firebaseApp from './firebaseConfig';
import { getFunctions, httpsCallable, connectFunctionsEmulator } from 'firebase/functions';
const functions = getFunctions(firebaseApp);
export async function post(funcName, params) {
connectFunctionsEmulator(functions, 'localhost', '5001'); // DEBUG
let func = httpsCallable(functions, funcName);
let result = await func(params);
return result.data;
}
Looking through the Firestore documentation, I see many examples of functions.firestore.document but I don't see any examples of functions.firestore.collection. Firestore syntax is
firebase.firestore().collection('...').doc('...')
I get an error message with
firebase.firestore().document('...')
Yet in Cloud Functions with this code:
exports.myFunction = functions.firestore.collection('...').doc('...').onUpdate(event => {
on deploy I get an error message:
TypeError: functions.firestore.collection is not a function
When I change the code to
exports.getWatsonTokenFirestore = functions.firestore.document('...').onUpdate(event => {
I don't get an error message on deploy.
Why does Cloud Functions appear to have a different data structure than Cloud Firestore?
Here's my full Cloud Function. My collection is User_Login_Event and my document is Toggle_Value:
exports.getWatsonTokenFS = functions.firestore.document('User_Login_Event/{Toggle_Value}').onUpdate(event => {
var username = 'TDK',
password = 'swordfish',
url = 'https://' + username + ':' + password + '#stream.watsonplatform.net/authorization/api/v1/token?url=https://stream.watsonplatform.net/speech-to-text/api';
request({url: url}, function (error, response, body) {
admin.firestore().collection('IBM_Watson_Token').document('Token_Value').update('token');
});
return 0; // prevents an error message "Function returned undefined, expected Promise or value"
});
The function deploys without error but when it executes I get this error message:
TypeError: firebase.firestore is not a function
I'm confused as firebase.firestore isn't in my Cloud Function. It's in my Angular front-end code in various places, without a problem. What is this error message referring to? I tried changing the line
admin.firestore().collection('IBM_Watson_Token').document('Token_Value').update('token');
to
firebase.firestore().collection('IBM_Watson_Token').document('Token_Value').update('token');
and to
console.log("getWatsonTokenFS response");
but I got the same error message.
Yes. You should format it as...
exports.getWatsonTokenFirestore = functions.firestore.document('myCollection/{documentId}').onUpdate(event => {
// code
});
collection and doc are methods within firebase.firestore. To access them via functions.firestore, you must use document.
You can see a full list of Classes for Cloud Firestore and the latest SDK for Cloud Functions for Firebase
Update
I've been working on your code. I've added in all of the dependencies and initialization, which I assume that you have in your code. I can't see where you're using any data from Firestore in your IBM Watson request and I can't see how you're writing any of the returned data back to Firestore. As I'm not familiar with your request method, I've commented it out, to give you what should be a working example of an update to Firestore and writes something back. I also edited some of your code to make it more readable and changed the Cloud Functions code to reflect v1.0.0, released today (I've been testing it for a while) :)
const admin = require('firebase-admin');
const functions = require('firebase-functions');
admin.initializeApp();
const firestore = admin.firestore();
exports.getWatsonTokenFS = functions.firestore
.document('User_Login_Event/{Toggle_Value}')
.onUpdate((snap, context) => {
let username = 'TDK';
let password = 'swordfish';
let url = `https://${username}:${password}#stream.watsonplatform.net/authorization/api/v1/token?url=https://stream.watsonplatform.net/speech-to-text/api`;
// request({url}, function (error, response, body) {
// firestore.doc(`${IBM_Watson_Token}/${Token_Value}`).update('token');
// });
return firestore.doc(`IBM_Watson_Token/Token_Value`).update('token')
.then(response => {
return Promise.resolve();
})
.catch(err => {
return Promise.reject(err);
});
});
Now that Firebase has updated firebase-admin to 5.12.0 and firebase-functions to 1.0.1 my test function is working. The function that Jason Berryman wrote is correct except for two lines. Jason wrote:
.onUpdate((snap, context) => {
That should be
.onUpdate((change, context) => {
Secondly, Jason wrote:
return firestore.doc(`IBM_Watson_Token/Token_Value`).update('token')
The corrected line is:
return firestore.collection('IBM_Watson_Token').doc('Token_Value').update({
token: 'newToken'
})
I made two changes in Jason's code. First, I changed the location syntax; more on this below. Second, update() requires an object as the argument.
To show the syntax for locations, I wrote a simple Cloud Function that triggers when a value at a location in Cloud Firestore changes, and then writes a new value to a different location in Cloud Firestore. I removed the line const firestore = admin.firestore(); to make the code more clear:
const admin = require('firebase-admin');
const functions = require('firebase-functions');
admin.initializeApp();
exports.testFunction = functions.firestore.document('triggerCollection/{documentID}').onUpdate((change, context) => {
return admin.firestore().collection('writeCollection').doc('documentID').update({
token: 'newValue'
})
.then(response => {
return Promise.resolve();
})
.catch(err => {
return Promise.reject(err);
});
});
Let's compare three syntaxes for locations in Cloud Firestore. First, in the browser I use this syntax:
firebase.firestore().collection('myCollection').doc('documentID')
Next, in a Cloud Function trigger I use this syntax:
functions.firestore.document('myCollection/{documentID}')
Third, in the Cloud Function return I use this syntax:
admin.firestore().collection('myCollection').doc('documentID')
The first and last lines are the same except that from the browser you call Firebase with firebase, when from the server you call Firebase using the firebase-admin Node package, here aliased to admin.
The middle line is different. It's calling Firebase using the firebase-functions Node package, here aliased to functions.
In other words, Firebase is called using different libraries, depending on whether you're calling from the browser or the server (e.g., a Cloud Function), and whether in a Cloud Function you're calling a trigger or a return.
Cloud functions is triggered based on events happening in Firebase example in realtime database, authentication.
Cloud firestore is triggered based on events happening in Firestore which uses the concept of documents and collections.
As explained here:
https://firebase.google.com/docs/functions/firestore-events
The cloud firestore triggers are used when there is a change in a document.
I had the same problem. I used following
const getReceiverDataPromise = admin.firestore().doc('users/' + receiverUID).get();
const getSenderDataPromise = admin.firestore().doc('users/' + senderUID).get();
return Promise.all([getReceiverDataPromise, getSenderDataPromise]).then(results => {
const receiver = results[0].data();
console.log("receiver: ", receiver);
const sender = results[1].data();
console.log("sender: ", sender);
});