I deployed a function with the following query:
admin.firestore().collection("fcm").where("devices",'array-contains', mobile).get().then((snapshots)=> {...});
This returns the following error from the Cloud Function Log:
msgTrigger: Function execution started
msgTrigger: Function returned undefined, expected Promise or value
msgTrigger: Function execution took 8429 ms, finished with status: 'ok'
msgTrigger: Unhandled rejection
msgTrigger: TypeError: Cannot read property 'Symbol(Symbol.iterator)' of undefined at admin.firestore.collection.where.get.then (/user_code/index.js:23:65) at process._tickDomainCallback (internal/process/next_tick.js:135:7)
Anyone please?
Fighting for days with the editor here. decided to post my function code in chunks:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
var msgData;
var mobile;
Second part:
exports.msgTrigger = functions.firestore
.document('Messages/{MessageID}')
.onCreate((snapshot, context) => {
msgData = snapshot.data();
mobile = msgData.mobile;
admin.firestore().collection("fcm").where("devices", 'array-contains', mobile).get().then((snapshots) => {
Third part:
var tokens = [];
if (snapshots.empty) {
console.log('No devices');
} else {
for (var token of snapshot.docs) {
tokens.push(token.data().token);
}
var payLoad = {
"notification": {
"title": "de " + msgData.name,
"body": "Alerta de Emergência!",
"sound": "default",
"icon": msgData.icon
},
"data": {
"remetente": +msgData.name,
"mensagem": "Alerta de Emergência!"
}
}
Fourth part:
return admin.messaging().sendToDevice(tokens, payLoad).then((response) => {
console.log("mensagens enviadas");
}).catch((err) => {
console.log("erro: " + err);
});
}
});
});
Firestore 0.8 is quite an old version, see https://cloud.google.com/nodejs/docs/reference/firestore/0.8.x/. It is only from version 0.16 that you can use the array-contains query operator , see https://github.com/googleapis/nodejs-firestore/releases/tag/v0.16.0. So you should update to the latest version.
I've also adapted your function code by first (and very important, see below) returning the promise returned by the first asynchronous task, then re-organising your promises chaining in the if/then/else.
Does it execute correctly now??
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(); // Here changed, see https://firebase.google.com/docs/functions/beta-v1-diff#new_initialization_syntax_for_firebase-admin
exports.msgTrigger = functions.firestore
.document('Messages/{MessageID}')
.onCreate((snapshot, context) => {
const msgData = snapshot.data(); //No need to declare this outside of the Cloud Function, see https://www.youtube.com/watch?v=2mjfI0FYP7Y
const mobile = msgData.mobile;
return admin. // <- HERE return
.firestore()
.collection('fcm')
.where('devices', 'array-contains', mobile)
.get()
.then(snapshots => {
let tokens = [];
if (snapshots.empty) {
console.log('No devices');
return null;
} else {
for (var token of snapshot.docs) {
tokens.push(token.data().token);
}
var payLoad = {
notification: {
title: 'de ' + msgData.name,
body: 'Alerta de Emergência!',
sound: 'default',
icon: msgData.icon
},
data: {
remetente: +msgData.name,
mensagem: 'Alerta de Emergência!'
}
};
return admin.messaging().sendToDevice(tokens, payLoad);
}
})
.catch(err => {
console.log('erro: ' + err);
return null;
});
});
Why is it important to return the promises returned by asynchronous tasks? Watch the 3 videos about "JavaScript Promises" from the official Firebase video series (https://firebase.google.com/docs/functions/video-series/) for the answer!!
Removing ".where('devices', 'array-contains', mobile)" gives the same error. I added msgData.name and msgData.mobile to console.log and they get printed, so the first part is fine:
1: 31: 05.140 AM
msgTrigger
Function execution started
1: 31: 11.289 AM
msgTrigger
Nome: Alan
1: 31: 11.291 AM
msgTrigger
mobile: 91983372845
1: 31: 11.291 AM
msgTrigger
erro: TypeError: Cannot read property 'Symbol(Symbol.iterator)' of undefined
1: 31: 11.297 AM
msgTrigger
Function execution took 6158 ms, finished with status: 'ok'
Ok, for what it's worth I finally found the culprit:
for (var token of snapshot.docs) {
snapshot should be snapshots. Yes it's emabarrasing and it took nothing less than the excellent Firebase Support Team to point it out to me. Wished Android Studio could pick up this kind of silly typos in js code.
Will still mark Renaud's answer, since he helped optimize my code and gave me some usefull tips along the way.
Related
I am using the source code from a security rules tutorial to attempt to do integration testing with Jest for my Javascript async function async_create_post, used for my firebase HTTP function create_post The files involved has a directory structure of the following:
Testing file: root/tests/handlers/posts.test.js
File to be tested: root/functions/handlers/posts.js
Helper code from the tutorial: root/tests/rules/helpers.js
And here is the source code that is involved:
posts.test.js
const { setup, teardown} = require("../rules/helpers");
const {
async_get_all_undeleted_posts,
async_get_post,
async_delete_post,
async_create_post
} = require("../../functions/handlers/posts");
describe("Post Creation", () => {
afterEach(async () => {
await teardown();
});
test("should create a post", async () => {
const db = await setup();
const malloryUID = "non-existent uid";
const firstPost = {
body: "First post from Mallory",
author_id: malloryUID,
images: ["url1", "url2"]
}
const before_post_snapshot = await db.collection("posts").get();
expect(before_post_snapshot.docs.length).toBe(0);
await async_create_post(firstPost); //fails at this point, expected to create a new post, but instead threw an error
const after_post_snapshot = await db.collection("posts").get();
expect(after_post_snapshot.docs.length).toBe(1);
});
});
posts.js
const {admin, db } = require('../util/admin');
//admin.initializeApp(config); //my credentials
//const db = admin.firestore();
const { uuid } = require("uuidv4");
const {
success_response,
error_response
} = require("../util/validators");
exports.async_create_post = async (data, context) => {
try {
const images = [];
data.images.forEach((url) => {
images.push({
uid: uuid(),
url: url
});
})
const postRecord = {
body: data.body,
images: images,
last_updated: admin.firestore.FieldValue.serverTimestamp(),
like_count: 0,
comment_count: 0,
deleted: false,
author_id: data.author_id
};
const generatedToken = uuid();
await db
.collection("posts")
.doc(generatedToken)
.set(postRecord);
// return success_response();
return success_response(generatedToken);
} catch (error) {
console.log("Error in creation of post", error);
return error_response(error);
}
}
When I run the test in Webstorm IDE, with 1 terminal running Firebase emulators:start , I get the following error message.
console.log
Error in creation of post TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received an instance of Object
at validateString (internal/validators.js:120:11)
at Object.basename (path.js:1156:5)
at GrpcClient.loadProto (/Users/isaac/Desktop/project/functions/node_modules/google-gax/src/grpc.ts:166:23)
at new FirestoreClient (/Users/isaac/Desktop/project/functions/node_modules/#google-cloud/firestore/build/src/v1/firestore_client.js:118:38)
at ClientPool.clientFactory (/Users/isaac/Desktop/project/functions/node_modules/#google-cloud/firestore/build/src/index.js:330:26)
at ClientPool.acquire (/Users/isaac/Desktop/project/functions/node_modules/#google-cloud/firestore/build/src/pool.js:87:35)
at ClientPool.run (/Users/isaac/Desktop/project/functions/node_modules/#google-cloud/firestore/build/src/pool.js:164:29)
at Firestore.request (/Users/isaac/Desktop/project/functions/node_modules/#google-cloud/firestore/build/src/index.js:961:33)
at WriteBatch.commit_ (/Users/isaac/Desktop/project/functions/node_modules/#google-cloud/firestore/build/src/write-batch.js:485:48)
at exports.async_create_post (/Users/isaac/Desktop/project/functions/handlers/posts.js:36:5) {
code: 'ERR_INVALID_ARG_TYPE'
}
at exports.async_create_post (/Users/isaac/Desktop/project/functions/handlers/posts.js:44:13)
Error: expect(received).toBe(expected) // Object.is equality
Expected: 1
Received: 0
<Click to see difference>
at Object.<anonymous> (/Users/isaac/Desktop/project/tests/handlers/posts.test.js:59:45)
Error in creation of post comes from the console.log("Error in creation of post", error); in posts.js, so the error is shown in the title of this post.
I want to know why calling the async_create_post from posts.test.js will cause this error and does not populate my database with an additional record as expected behaviour. Do inform me if more information is required to solve the problem.
Here are some code snippets that may give more context.
helpers.js [Copied from the repository]
const firebase = require("#firebase/testing");
const fs = require("fs");
module.exports.setup = async (auth, data) => {
const projectId = `rules-spec-${Date.now()}`;
const app = firebase.initializeTestApp({
projectId,
auth
});
const db = app.firestore();
// Apply the test rules so we can write documents
await firebase.loadFirestoreRules({
projectId,
rules: fs.readFileSync("firestore-test.rules", "utf8")
});
// write mock documents if any
if (data) {
for (const key in data) {
const ref = db.doc(key); // This means the key should point directly to a document
await ref.set(data[key]);
}
}
// Apply the actual rules for the project
await firebase.loadFirestoreRules({
projectId,
rules: fs.readFileSync("firestore.rules", "utf8")
});
return db;
// return firebase;
};
module.exports.teardown = async () => {
// Delete all apps currently running in the firebase simulated environment
Promise.all(firebase.apps().map(app => app.delete()));
};
// Add extensions onto the expect method
expect.extend({
async toAllow(testPromise) {
let pass = false;
try {
await firebase.assertSucceeds(testPromise);
pass = true;
} catch (error) {
// log error to see which rules caused the test to fail
console.log(error);
}
return {
pass,
message: () =>
"Expected Firebase operation to be allowed, but it was denied"
};
}
});
expect.extend({
async toDeny(testPromise) {
let pass = false;
try {
await firebase.assertFails(testPromise);
pass = true;
} catch (error) {
// log error to see which rules caused the test to fail
console.log(error);
}
return {
pass,
message: () =>
"Expected Firebase operation to be denied, but it was allowed"
};
}
});
index.js
const functions = require('firebase-functions');
const {
async_get_all_undeleted_posts,
async_get_post,
async_delete_post,
async_create_post
} = require('./handlers/posts');
exports.create_post = functions.https.onCall(async_create_post);
The error message means that a method of the path module (like path.join) expects one of its arguments to be a string but got something else.
I found the offending line by binary search commenting the program until the error was gone.
Maybe one of your modules uses path and you supply the wrong arguments.
I have written a small contact form, that calls a firebase cloud function to store the request to cloud firestore. Everything works fine, except that after 60seconds the website throws the following error:
Error: deadline-exceeded
I used this reference:https://github.com/firebase/quickstart-js/blob/7d514fb4700d3a1681c47bf3e0ff0fa3d7c91910/functions/functions/index.js
This is my cloud function:
exports.newRequest = functions.https.onCall((data, context) => {
return admin
.firestore()
.collection("requests")
.add(data)
.then(ref => {
console.log(`New request written. ${ref}`)
return ref.id
})
.catch(err => {
throw new functions.https.HttpsError("unknown", error.message, error)
})
})
This is the function call:
const functions = firebase.functions()
const addMessage = functions.httpsCallable(`newRequest`)
addMessage({
name: name,
contact: contact,
message: message,
timestamp: new Date(Date.now()).toLocaleString(),
})
.then(result => {
console.log(`Cloud function called successfully. Ref: ${result.data}`)
})
.catch(error => {
// Getting the Error details.
var code = error.code
var message = error.message
var details = error.details
console.log(code, message, details)
})
Does anybody know how to fix this?
Edit:
Here is the cloud function log:
7:19:33.751 PM newRequest Function execution started
7:19:33.751 PM newRequest Billing account not configured. External network is not accessible and quotas are severely limited. Configure billing account to remove these restrictions
7:19:33.755 PM newRequest Function execution took 5 ms, finished with status code: 204
7:19:33.896 PM newRequest Function execution started
7:19:33.896 PM newRequest Billing account not configured. External network is not accessible and quotas are severely limited. Configure billing account to remove these restrictions
7:19:34.744 PM newRequest New request written. [object Object]
7:19:34.746 PM newRequest Function execution took 851 ms, finished with status code: 200
My setup in detail:
I got a gatsby page, where I init firebase.
import * as firebase from "firebase/app"
const firebaseConfig = {}
useEffect(() => {
if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig)
} else {
console.log(firebase.apps)
}
})
I got a contact form react component with the following handleSubmit method.
import * as firebase from "firebase/app"
import "firebase/functions"
const handleSubmit = evt => {
evt.preventDefault()
const addMessage = firebase.functions().httpsCallable(`newRequest`)
addMessage({
name: name,
contact: contact,
message: message,
timestamp: new Date(Date.now()).toLocaleString(),
})
.then(result => {
console.log(`Cloud function called successfully. Ref: ${result.data}`)
})
.catch(error => {
// Getting the Error details.
var code = error.code
var message = error.message
var details = error.details
console.log(code, message, details)
})
resetName()
resetContact()
resetMessage()
}
This is what the chrome dev tools say:
Exception: Error: deadline-exceeded at new HttpsErrorImpl (http://localhost:8000/commons.js:4409:28) at http://localhost:8000/commons.js:4715:20
code: "deadline-exceeded"
details: undefined
message: "deadline-exceeded"
stack: "Error: deadline-exceeded↵ at new HttpsErrorImpl (http://localhost:8000/commons.js:4409:28)↵ at http://localhost:8000/commons.js:4715:20"
__proto__: Error
And this is the promise creator:
/**
* Returns a Promise that will be rejected after the given duration.
* The error will be of type HttpsErrorImpl.
*
* #param millis Number of milliseconds to wait before rejecting.
*/
function failAfter(millis) {
return new Promise(function (_, reject) {
setTimeout(function () {
reject(new HttpsErrorImpl('deadline-exceeded', 'deadline-exceeded'));
}, millis);
});
}
I'm experiencing this problem since several days now and I don't know where it is comming from :(
3 years later! I got this error also on a long running (e.g. 2 mins) firebase function, also using HttpsCallable like the OP
FirebaseError functions/deadline-exceeded
I fixed it by making 2 changes:
(1) increase the firebase function timeout on the server as follows:
exports.myFunction = functions.runWith({
timeoutSeconds: 540
}).https.onCall(async (data: any) => {
return await myFunction(data)
})
OR
alternatively you can manually set the firebase function timeout via the GCP console at https://console.cloud.google.com/functions/list?project=YOUR-PROJECT
click the function name and click edit and you can increase the timeout there
(2) set the HttpsCallableOptions timeout option:
const functions: Functions = getFunctions()
const options: HttpsCallableOptions = { timeout: 530 * 1000 } // slightly less than the 540 seconds BE timeout
const callable: HttpsCallable = httpsCallable(functions, 'flowPlayerExists', options)
const promise: Promise<HttpsCallableResult> = callable({ id })
I had to make BOTH these changes to get it to work!
Is it "useEffect" from React Hooks ?
If yes, your firebase init (in useEffect) is done every render.
If you want that useEffect apply only once (like componentDidMount) you should pass an empty array in second param.
useEffect(() => {
if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig)
} else {
console.log(firebase.apps)
}
}, []);
Sorry if this seems like a really basic question, the concept of cloud functions is extremely new to me and i'm still highly in the learning process.
However, whilst trying to execute this cloud function i get the following error
TypeError: Cannot read property 'data' of undefined
Full log can be seen here
For reference as well, I didnt make this function, im just trying to get it working, i used this video.
The actual cloud function:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
const firestore = admin.firestore();
const settings = { timestampInSnapshots: true };
firestore.settings(settings);
const stripe = require('stripe')(functions.config().stripe.token);
exports.addStripeSource =
functions.firestore.document('cards/{userid}/tokens/{tokenid}')
.onCreate(async (tokenSnap, context) => {
var customer;
const data = tokenSnap.after.data();
if (data === null) {
return null
}
const token = data.tokenId;
const snapchat = await
firestore.collection('cards').doc(context.params.userId).get();
const customerId = snapshot.data().custId;
const customerEmail = snpashot.data().email;
if (customerId === 'new') {
customer = await stripe.customers.create({
email: customerEmail,
source: token
});
firestore.collection('cards').doc(context.params.userId).update({
custId: customer.id
});
}
else {
customer = await stripe.customers.retrieve(customerId)
}
const customerSource = customer.sources.data[0];
return firestore.collection('cards').doc(context.params.userId).collection('sources').doc(customerSource.card.fingerprint).set(customersource, { merge: true });})
The dart code im using for writing a payment service:
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class PaymentService {
addCard(token) {
FirebaseAuth.instance.currentUser().then((user) {
print("Found User");
Firestore.instance
.collection('cards')
.document(user.uid)
.collection('tokens')
.add({'tokenId': token}).then((val) {
print('saved');
});
});
}
}
And finally, what executes when i push the button:
StripeSource.addSource().then((String token) {
print("Stripe!");
PaymentService().addCard(token);
});
As you can see the code is clearly being triggered, but i guess there is some sort of error with the data var, JavaScript is brand new to me so im sure its some sort of very dumb syntax issue.
From the log image attached the error is context is not defined
functions.firestore.document('cards/{userid}/tokens/{tokenid}')
.onCreate(async (tokenSnap, conetxt) => {
In the above function, you have passed parameter as conetxt and later in the function context is used, because of which it is giving undefined error.
Change the parameter name conetxt to context.
As your provided log output explains : you need to define a reference for your firestore.document function :
functions.firestore.document('cards/{userid}/tokens/{tokenid}')
modify it to :
functions.firestore.documentReference(){
}
I am trying to do TWO things here.
1)Send a notification to all employees. 2)Copy a Specific ref to the
Employees id ref. if no Special ref exists i will copy General ref.
The program runs without errors. Infact its perfect. But sometimes i get a Timed out error with the Notifications code part.
Error: fcm.googleapis.com network timeout. Please try again.
The code that copys one reference to another, always works, never ever received an error there.
I feel the error is due to not correctly handling promises with forEach. Could you help me get this code to excecute flawlessly, with correctly placed Promises?
exports.myFunc = functions.https.onRequest( (request, response) => {
admin.database().ref('/Employees').once('value').then(function(snap) {
snap.forEach(function (snapshot) {
var obj = snapshot.val();
if(obj.department){//only go ahead if dept is present
console.log(' : ' + obj.department);
var id, tkid, dept;
id = obj.empId; tkid = obj.tokenId; dept = obj.department;
var welcomeStr="hello! Welcom to our Department";
//================================================================notifications
var payload = {
data: {
greeting: welcomeStr,
to_who: id
}
};
admin.messaging().sendToDevice(tkid,payload)
.then(function(response){
console.log("Successfully sent message: ", response);
})
.catch(function(error){
console.log("Error sending message: ", error);
})
//===================================================Ref copying
var destinationRef = admin.database().ref('/Employees/' + id);//final destination
var option2Ref = admin.database().ref('/Company/General');//when special doesnt exist
var option1Ref = admin.database().ref('/Company/Special');//if special exists
option1.once('value', function(snapshot1){
if (snapshot1.exists()){//copy from straing from option11 to Employees/id
option1.once('value', function(snap) {
destinationRef.set( snap.val(), function(error) {
if( error && typeof(console) !== 'undefined' && console.error ) { console.error(error); }
console.log('DONE .... ' + id);
});
});
}
else{//we need to copy from option2 to Employees/id
option2Ref.once('value', function(snap) {
newRef.set( snap.val(), function(error) {
if( error && typeof(console) !== 'undefined' && console.error ) { console.error(error); }
console.log('DONE .... ' + id);
});
});
}
});
}
else{
console.log('No Department: ' + obj.dept);
return;
}
});
});
response.send("WOKAY!");
});
here i've rewritten your code in hopes to clean up the complicated promise chains
dropped promises are one of the most common and difficult problems to debug, i've seen my fair share
important changes to your code:
modern async syntax
so that the promises are cleaner to organize
use Promise.all instead of forEach
this way every promise is awaited without being forgotten
(hopefully) all of the promises are returned properly
all snapshot operations are run concurrently, and the onRequest handler should wait until they're all finished before terminating
using promises for once and set instead of callbacks
i'm not really sure what libraries these are
it looks like they accept promise-based usage
so i eliminated callback usage in favor of promises
please review the TODO mark
not really sure what's intended for that else block, so be sure to patch that up
async function handleSnapshot(snapshot) {
const {empId, tokenId, department} = snapshot.val()
// only go ahead if dept is present
if (!department) throw new Error("no department")
console.log("department:", department)
//================================================================notifications
const payload = {
data: {
greeting: "Hello! Welcome to our Department",
to_who: empId
}
}
const response = await admin.messaging().sendToDevice(tokenId, payload)
console.log("successfully sent message", response)
//===================================================Ref copying
const destinationRef = admin.database().ref('/Employees/' + empId) // final destination
const option2Ref = admin.database().ref('/Company/General') // when special doesnt exist
const option1Ref = admin.database().ref('/Company/Special') // if special exists
const snapshot1 = await option1Ref.once("value")
// copy from string from option1 to Employees/id
if (snapshot1.exists()) {
await destinationRef.set(snapshot1.val())
console.log("DONE1...", empId)
}
// TODO review this block
// we need to copy from option2 to Employees/id
else {
const snapshot2 = await option2Ref.once("value")
await destinationRef.set(snapshot2.val())
console.log("DONE2...", empId)
}
}
exports.myFunc = functions.https.onRequest(async(request, response) => {
const snapshots = await admin.database().ref('/Employees').once('value')
await Promise.all(snapshots.map(handleSnapshot))
response.send("WOKAY!")
})
To add one very important step to #ChaseMoskal answer.
For those using TypeScript with Firebase, since firebase server is not running v8+ in NodeJs, theres a great chance you might get this error:
"TypeError: snapshots.map is not a function"... on the line: await Promise.all(snapshots.map(handleSnapshot)).
Thats cause in your tsconfig.json its possibly "lib": ["es6"]. In that case just add this small snippet to the accepted answer, to get the Firebase Datasnapshot into an array that could be used with .map(...)
Longer Version:
exports.myFunc = functions.https.onRequest(async(request, response) => {
const snapshots = await admin.database().ref('/Employees').once('value')
var data_snap_arr = [];
snapshots.forEach(function(child_Snapshot) {
var stuff = child_Snapshot.val();
stuff.key = child_Snapshot.key;
data_snap_arr.push(stuff);
await Promise.all(data_snap_arr.map(handleSnapshot))
response.send("WOKAY!")
})
Shorter Version:
exports.myFunc = functions.https.onRequest(async(request, response) => {
const snapshots = await admin.database().ref('/Employees').once('value')
let data_snap_arr = Object.keys(snapshots.val() || {}) .map(k => snapshots.val()[k]);
await Promise.all(data_snap_arr.map(handleSnapshot))
response.send("WOKAY!")
})
I'm using tmi.js to get the data of a chat message on www.twitch.tv.
This is my code:
const tmi = require('tmi.js');
require('dotenv').config();
const options = {
options: {
debug: true
},
identity: {
username: process.env.OAUTH_USERNAME,
password: process.env.OAUTH_PASSWORD
},
connection: {
reconnect: true
},
channels: [`instak`]
};
const client = new tmi.client(options);
client.on(`chat`, (channel, userstate, message/*, self */) => {
console.log(userstate.username); //This logs my userstate, I can see the username which is passed on correctly.
switch (message) {
case `!kluiten`:
fetch(`http://localhost:8000/api/users/${userstate.username}`) // fetch from Express.js server
.then(response => response.json())
.then(result => {
console.log(`USERSTATE IS`, userstate);
client.action(channel, `${userstate[`display-name`]}, you've got ${result.instakluiten} instakluiten.`);
});
break;
default:
break;
}
});
// Connect the client to the server..
client.connect();
As I said in the commented part after the console.log(userstate), I get all the correct information I was expecting, so outside of the switch case and the fetch. However, when I log my userstate.username inside the fetch, userstate is undefined... I don't see why that happens, because when I did it in the front-end I had no problems... Now I'm doing it in node and it's undefined...
This looks like a common "you need to wait for your response"-problem, but I don't see a logical explanation for that being the problem because in the client.on(chat) userstate is defined... I'm troubled.