I am new to firebase. I'm trying to retreive data from a real time database using a node.js server sending associated credentials to firebase, but something gets broken after once('value') is called: its returned promise never gets resolved and server stops itself logging this message: "Process exited with code 3221226505".
I wrote the following code:
async function testFirebase1(firebaseCredentialsObj, path) {
let firebase = require('firebase')
firebase.initializeApp(firebaseCredentialsObj);
var database = firebase.database();
var ref = database.ref(path);
console.log(ref.toString());
try {
// Attempt 1
var prom = await ref.once('value');
const data = prom.;
console.log('data ' + data)
// Attempt 2
prom.then((snapshot) => {
console.log('snapshot ' + snapshot)
}).catch((error) => { console.log(error)} )
} catch (error) {
console.log(error)
}
}
No error ever gets catched.
I also tried to get data as an admin, but i got the same failing result
async function testFirebase3(firebaseCredentials, serviceAccountKey, databaseURL, path) {
const admin=require('firebase-admin');
const serviceAccount = serviceAccountKey;
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: databaseURL
});
var db=admin.database();
var userRef=db.ref(path);
const prom = await userRef.once('value');
console.log(prom)
}
Promise returned from once() method keep beeing pendent. This is its log:
[[PromiseStatus]]:'pending'
[[PromiseValue]]:undefined
Server is supposed to get databases'data in json format and send it to the client.
Why is this happening?
Based on your code, you are mixing traditional Promise chaining and async/await syntax together which is leading to your confusion.
Note: In the below snippets, I use the database query coding style I describe at the end of this answer.
SDK Initialization
To start with, in both testFirebase1 and testFirebase3, you initialize the default Firebase app instance in the function. If you call either function only once, you won't experience any problems, but any time you call them another time, they will always throw an error stating that the app has already been initialized. To solve this, you can lazily load these libraries using the following functions:
function lazyFirebase(options, name = undefined) {
const firebase = require('firebase');
// alternatively use the Promise-based version in an async function:
// const firebase = await import('firebase');
try {
firebase.app(name);
} catch (err) {
firebase.initializeApp(options, name);
}
return firebase;
}
function lazyFirebaseAdmin(options, name = undefined) {
const admin = require('firebase-admin');
// alternatively use the Promise-based version in an async function:
// const admin = await import('firebase-admin');
try {
admin.app(name);
} catch (err) {
const cred = options.credential;
if (typeof cred === "string") {
options.credential = admin.credential.cert(cred)
}
admin.initializeApp(options, name);
}
return admin;
}
Important Note: Neither of the above functions checks whether they use the same options object to initialize them. It just assumes they are the same configuration object.
Correcting testFirebase1
In testFirebase1, you are initializing the default Firebase app instance and then starting the process of the getting the data from the database. Because you haven't returned the promise from the ref.once('value') in the function, the caller will get a Promise<undefined> that resolves before the database call completes.
async function testFirebase1(firebaseCredentialsObj, path) {
let firebase = require('firebase')
// bug: throws error if initializeApp called more than once
firebase.initializeApp(firebaseCredentialsObj);
// bug: using `var` - use `const` or `let`
var database = firebase.database();
var ref = database.ref(path);
console.log(ref.toString());
try {
// Attempt 1
// bug: using `await` here, makes this a DataSnapshot not a Promise<DataSnapshot>
// hence `prom` should be `snapshot`
// bug: using `var` - use `const` or `let`
var prom = await ref.once('value');
// bug: syntax error, assuming this was meant to be `prom.val()`
const data = prom.;
console.log('data ' + data)
// Attempt 2
// bug: a `DataSnapshot` doesn't have a `then` or `catch` method
// bug: if `prom` was a `Promise`, you should return it here
prom
.then((snapshot) => {
console.log('snapshot ' + snapshot)
})
.catch((error) => {
console.log(error)
})
} catch (error) {
console.log(error)
}
}
Correcting these problems (and making use of my coding style when dealing with RTDB queries) gives:
async function testFirebase1(firebaseCredentialsObj, path) {
const firebase = lazyFirebase(firebaseCredentialsObj);
const snapshot = await firebase.database()
.ref(path)
.once('value');
// returns data at this location
return snapshot.val();
}
Correcting testFirebase3
In testFirebase3, you are initializing the default Firebase Admin app instance and correctly waiting for the data from the database. Because you haven't returned the data from the database, the caller will get a Promise<undefined> that resolves when the database call completes but without the containing data.
async function testFirebase3(firebaseCredentials, serviceAccountKey, databaseURL, path) {
const admin = require('firebase-admin');
// note: unnecessary line, either call `serviceAccountKey` `serviceAccount` or use `serviceAccountKey` as-is
const serviceAccount = serviceAccountKey;
// bug: throws error if initializeApp called more than once
// bug: `firebaseCredentials` is unused
// note: when initializing the *default* app's configuration, you
// should specify all options to prevent bugs when using
// `admin.messaging()`, `admin.auth()`, `admin.storage()`, etc
// as they all share the default app instance
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: databaseURL
});
// bug: using `var` - use `const` or `let`
var db=admin.database();
var userRef=db.ref(path);
// bug: using `await` here, makes this a DataSnapshot not a Promise<DataSnapshot>
// hence `prom` should be `snapshot`
const prom = await userRef.once('value');
// bug: logging a `DataSnapshot` object isn't useful because it
// doesn't serialize properly (it doesn't define `toString()`,
// so it will be logged as "[object Object]")
console.log(prom)
}
Correcting these problems (and making use of my coding style when dealing with RTDB queries) gives:
async function testFirebase3(firebaseCredentials, serviceAccountKey, databaseURL, path) {
const admin = lazyFirebaseAdmin({
...firebaseCredentials, // note: assuming `firebaseCredentials` is the complete app configuration,
credential: serviceAccountKey,
databaseURL: databaseURL
});
const snapshot = await admin.database()
.ref(path)
.once('value');
return snapshot.val();
}
Related
I am trying to interact with firebase firestore from my cloud function. The cloud functions looks like the following:
const admin = require("firebase-admin");
const functions = require("firebase-functions");
admin.initializeApp();
const db = admin.firestore();
exports.addVote = functions.https.onCall((data, context) => {
return db
.doc("sdd-enheter/enhet/votes/voteID")
.set({ user: "user", vote: 0 });
});
When calling the function from the client side I get a firebase internal error, indicating that the function has been called but throws an error. What should I do to fix this?
Your function needs to return a promise or otherwise terminate by throwing an https error. Throwing the https error will give the client back a relevant error that it can handle, so consider making that a habit. The function below covers both of those bases. If you still get an error then share that error with us as it appears in the console's function log.
exports.addVote = functions.https.onCall((_data, _context) => {
const db = admin.firestore();
try {
return db.doc("sdd-enheter/enhet/votes/voteID").set({user: "user", vote: 0});
} catch (error) {
throw new functions.https.HttpsError("unknown", "Failed to add vote.", error);
}
});
I have an array of docs ids that I want to delete in using a cloud function, my code looks like the following :
//If the user decieds on deleting his account forever we need to make sure we wont have any thing left inside of db after this !!
// incoming payload array of 3 docs
data = {array : ['a302-5e9c8ae97b3b','92c8d309-090d','a302-5e932c8ae97b3b']}
export const deleteClients = functions.https.onCall(async (data, context) => {
try {
// declare batch
const batch = db.batch();
// set
data.arr.forEach((doc: string) => {
batch.delete(db.collection('Data'), doc);
});
// commit
await batch.commit();
} catch (e) {
console.log(e);
}
return null;
});
I am getting a syntax error on batch.delete how to pass the right arguments in to the batch delete to reference that doc I want to submit for deletion before commit ?
Delete takes a single param, the doc ref of the doc to be deleted.
data.arr.forEach((docId: string) => {
batch.delete(doc(db, "Data", docId));
});
There are several errors in your code:
data.arr.forEach() cannot work wince your data object contains one element with the key array and not the key arr.
You are mixing up the syntax of the JS SDK v9 and the Admin SDK. See the write batch Admin SDK syntax here.
You need to send back some data to the client when all the asynchronous work is complete, to correctly terminate your CF.
You do return null; AFTER the try/catch block: this means that, in most of the cases, your Cloud Function will be terminated before asynchronous work is complete (see the link above)
So the following should do the trick (untested):
const db = admin.firestore();
const data = {array : ['a302-5e9c8ae97b3b','92c8d309-090d','a302-5e932c8ae97b3b']};
export const deleteClients = functions.https.onCall(async (data, context) => {
try {
const batch = db.batch();
const parentCollection = db.collection('Data')
data.array.forEach((docId) => {
batch.delete(parentCollection.doc(docId));
});
// commit
await batch.commit();
return {result: 'success'} // IMPORTANT, see explanations above
} catch (e) {
console.log(e);
// IMPORTANT See https://firebase.google.com/docs/functions/callable#handle_errors
}
});
I am trying to switch my environment variables to Google Secrets Manager, but I am encountering a problem. I am running an expressjs api trying to establish a database connection. But whatever I try, it only returns Promise { <pending> } instead of waiting for the async function to finish. Any help is highly appreciated!
gcloud.js
const { SecretManagerServiceClient } = require('#google-cloud/secret-manager');
const client = new SecretManagerServiceClient();
async function getSecret(name) {
const name = `projects/PROJECT-ID/secrets/${name}/versions/latest`;
const [version] = await client.accessSecretVersion({
name: name,
});
const payload = version.payload.data.toString();
return payload;
}
module.exports.getSecret = getSecret;
config.js
const gcloud = require('./config/gcloud.js')
const config = {
authSecret: gcloud.getSecret(SECRET_NAME)
}
module.exports = config
You need to await the result of gcloud.getSecret:
// Not actually valid
const config = {
authSecret: await gcloud.getSecret(SECRET_NAME)
}
However, top-level await isn't widely supported yet, so you'll need to build the config inside a function: NodeJS Async / Await - Build configuration file with API call
I have a Firebase cloud function that does this:
const admin = require('firebase-admin')
admin.initializeApp()
exports.setSessionState = functions.https.onCall(async (data, context) => {
const stateId = data.stateId
const details = data.details
const stateRef = admin.database().ref(`state/${stateId}`)
stateRef.transaction((state) => {
if (state) {
state.details = details
}
return state
})
})
The code works well in the actual application and the state is updated but I run into problems when running this in a test, where the state is always null. (In other words, the details are never set.) The test uses the Mocha framework and is run against a real Firebase project and I can't see how anything would differ here but the behaviour is consistently different when calling this cloud function from the client application and when calling it from the test suite:
const chai = require('chai')
const assert = chai.assert
const test = require('firebase-functions-test')({
databaseURL: '<redacted>',
storageBucket: '<redacted>',
projectId: '<redacted>',
}, '../service-account-credentials.json')
describe('CFsetSessionState', () => {
let wrapped
let cloudFunctions
let admin
before(() => {
cloudFunctions = require('../index')
admin = require('firebase-admin')
wrapped = test.wrap(cloudFunctions.CFsetSessionState)
})
it('Test setting state', async () => {
const stateId = 'abc'
const data = {
stateId: stateId,
details: {
name: 'New name'
}
}
const context = {
auth: {
uid: 123
}
}
const stateRef = admin.database().ref(`state/${stateId}`)
await stateRef.set({
name: 'Previous name',
active: true
})
await wrapped(data, context)
const snapshot = await stateRef.once('value')
const state = snapshot.val()
// Details do not exist here, why?
assert.equal(state.details.name, 'New name')
})
})
I leave the database state as is after the tests so I can see there is indeed data in the state object but the details have not been set. Experimenting a bit with setting and fetching data (using .once()) before and after the call to the cloud function can change the behaviour, making me think it might be some cache issue, but this experimenting does not give me any particular stable end state. I have no idea how the equivalent of a local cache works in cloud functions, it currently shows random behaviour.
What could cause this?
I haven't tried running your Cloud Function with your test, but most probably the problem comes from the fact that you incorrectly manage the life cycle of your Cloud Function. Since it is Callable one, you need to terminate it by returning a Promise (or a value when all the asynchronous work is completed). More details here and here in the doc.
You should therefore adapt your CF as follows:
exports.setSessionState = functions.https.onCall(async (data, context) => {
const stateId = data.stateId
const details = data.details
const stateRef = admin.database().ref(`state/${stateId}`)
return stateRef.transaction((state) => { // Note the return here
if (state) {
state.details = details
}
return state
})
})
We are returning the Promise returned by the Transaction.
I am currently trying to sync my firestore documents with algolia upon a new document creation or the update of a document. The path to the collection in firestore is videos/video. The function seems to be triggering fine, however after triggering, the firebase function does not seem to relay any of the information to algolia (no records are being created). I am not getting any errors in the log. (I also double checked the rules and made sure the node could be read by default, and yes I am on the blaze plan). Does anyone know how to sync a firestore node and algolia? Thanks for all your help!
"use strict";
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const algoliasearch_1 = require("algoliasearch");
// Set up Firestore.
admin.initializeApp();
const env = functions.config();
// Set up Algolia.
// The app id and API key are coming from the cloud functions environment, as we set up in Part 1,
const algoliaClient = algoliasearch_1.default(env.algolia.appid, env.algolia.apikey);
// Since I'm using develop and production environments, I'm automatically defining
// the index name according to which environment is running. functions.config().projectId is a default property set by Cloud Functions.
const collectionindexvideo = algoliaClient.initIndex('videos');
exports.collectionvideoOnCreate = functions.firestore.document('videos/{uid}').onCreate(async(snapshot, context) => {
await savevideo(snapshot);
});
exports.collectionvideoOnUpdate = functions.firestore.document('videos/{uid}').onUpdate(async(change, context) => {
await updatevideo(change);
});
exports.collectionvideoOnDelete = functions.firestore.document('videos/{uid}').onDelete(async(snapshot, context) => {
await deletevideo(snapshot);
});
async function savevideo(snapshot) {
if (snapshot.exists) {
const document = snapshot.data();
// Essentially, you want your records to contain any information that facilitates search,
// display, filtering, or relevance. Otherwise, you can leave it out.
const record = {
objectID: snapshot.id,
uid: document.uid,
title: document.title,
thumbnailurl: document.thumbnailurl,
date: document.date,
description: document.description,
genre: document.genre,
recipe: document.recipe
};
if (record) { // Removes the possibility of snapshot.data() being undefined.
if (document.isIncomplete === false) {
// In this example, we are including all properties of the Firestore document
// in the Algolia record, but do remember to evaluate if they are all necessary.
// More on that in Part 2, Step 2 above.
await collectionindexvideo.saveObject(record); // Adds or replaces a specific object.
}
}
}
}
async function updatevideo(change) {
const docBeforeChange = change.before.data();
const docAfterChange = change.after.data();
if (docBeforeChange && docAfterChange) {
if (docAfterChange.isIncomplete && !docBeforeChange.isIncomplete) {
// If the doc was COMPLETE and is now INCOMPLETE, it was
// previously indexed in algolia and must now be removed.
await deletevideo(change.after);
} else if (docAfterChange.isIncomplete === false) {
await savevideo(change.after);
}
}
}
async function deletevideo(snapshot) {
if (snapshot.exists) {
const objectID = snapshot.id;
await collectionindexvideo.deleteObject(objectID);
}
}
Still don't know what I did wrong, however if anyone else is stuck in this situation, this repository is a great resource: https://github.com/nayfin/algolia-firestore-sync. I used it and was able to properly sync firebase and algolia. Cheers!
// Takes an the Algolia index and key of document to be deleted
const removeObject = (index, key) => {
// then it deletes the document
return index.deleteObject(key, (err) => {
if (err) throw err
console.log('Key Removed from Algolia Index', key)
})
}
// Takes an the Algolia index and data to be added or updated to
const upsertObject = (index, data) => {
// then it adds or updates it
return index.saveObject(data, (err, content) => {
if (err) throw err
console.log(`Document ${data.objectID} Updated in Algolia Index `)
})
}
exports.syncAlgoliaWithFirestore = (index, change, context) => {
const data = change.after.exists ? change.after.data() : null;
const key = context.params.id; // gets the id of the document changed
// If no data then it was a delete event
if (!data) {
// so delete the document from Algolia index
return removeObject(index, key);
}
data['objectID'] = key;
// upsert the data to the Algolia index
return upsertObject(index, data);
};