cloud functions firebase v9 runTransaction - node.js

this is my cloud function:
const { getFirestore, runTransaction, FieldValue } = require('firebase-admin/firestore')
exports.purchasesStatistics = functions.firestore
.document('transactions/{purchaseId}')
.onUpdate((snap, context ) => {
if (snap.before.data().status === 'RECEIVED') {
return '0'
}
let purchasePaid = snap.after.data().status === 'RECEIVED' ? true : false
if (purchasePaid === false) {
return '0'
}
let allPurchase = snap.after.data()
functions.logger.log('allPurchase', allPurchase)
let ref = getFirestore().collection('statistics').doc('checkout')
return runTransaction(ref, (transaction) => {
return transaction.get(ref).then((doc) => {
functions.logger.log('documento atualizado:', doc.data())
return '0'
})
})
})
Buy, it's returning "runTransaction is not a function". What i'm doing wrong ? Didn't find proper way to use runTransaction on firebase v9

It appears that you are using the admin SDK for NodeJS. This SDK is initialized differently from the Web SDK V9 and the way you use the functions is also different. Here is a detailed guide on how to initialize the admin SDK from scratch. After trying to add or import the runTransaction() function as your sample code, I also received the same error message. I followed the get started guide in addition to the documentation example to properly use this function, by using it from the firestore object that is created:
const { initializeApp, applicationDefault, cert } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');
const serviceAccount = require('/path-to-service-account'); //Path to your service account, depends on implementation
initializeApp({
credential: cert(serviceAccount)
});
const fireDB = getFirestore();
fireDB.runTransaction(); //Use the relevant args
This additional page contains a different example for using transactions.

Related

Is Firestore Local persistence available for Windows/

I am running the following code:
const { initializeApp } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');
const {firestore} = require("firebase-admin");
const QuerySnapshot = firestore.QuerySnapshot;
initializeApp()
const db = getFirestore();
const initializeListener = (collectionName) => {
console.log('called function');
const query = db.collection(collectionName);
query.onSnapshot((querySnapshot) => {
querySnapshot.docs().
console.log('snapshot received');
querySnapshot.docChanges().forEach((change) => {
console.log('doc change found');
if (change.type === "added") {
console.log("New " + collectionName, change.doc.data());
}
});
}, (erry) => {
console.log(`Encountered error: ${err}`);
});
}
initializeListener('my_collection');
If running whilst offline I don't see the 'snapshot received' message until I go online. If offline persistence should be available here, how do I access it?
You are using the Firebase Admin SDK (a wrapper around the Google Cloud backend SDK), which does not have any sort of persistence on any platform. Offline persistence is only available for the web and client SDKs provided by Firebase. As you can see from the linked documentation:
Note: Offline persistence is supported only in Android, Apple, and web apps.

Why this callable cloud function is failing with "app":"MISSING"?

I am calling a cloud function which runs a transaction, however it is returning an error to console which says:
Callable request verification passed {"verifications":{"auth":"VALID","app":"MISSING"}}
Googling it led me to App Check which is a new thing in Firebase. I am using React-Native firebase npm packages and following its documentation about App Check is extremely difficult due to lack of proper explanation and examples.
Below I have the code which I am trying to execute in the function:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const firestore_ = admin.firestore();
// const { CustomProvider } = require("#react-native-firebase/app-check");
const appCheckForDefaultApp = admin.appCheck();
const GeoPoint = admin.firestore.GeoPoint;
const FieldValue = admin.firestore.FieldValue;
const _geofirestore_ = require("geofirestore");
const GeoFirestore = _geofirestore_.initializeApp(firestore_);
exports.createNew = functions.runWith({
allowInvalidAppCheckToken: true // Opt-out: Requests with invalid App
// Check tokens continue to your code.
}).region('europe-west6').https.onCall(async (data, context) => {
try {
//Checking that the user calling the Cloud Function is authenticated
if (!context.auth) {
return "The user is not authenticated: " + context.auth;
// throw new UnauthenticatedError('The user is not authenticated. Only authenticated Admin users can create new users.');
}
const longitude = data.longitude;
const latitude = data.latitude;
const thirty_mins_old = data.thirty_mins_old;
const currenttime = data.currenttime;
const GeoFirestore_ = new _geofirestore_.GeoFirestore(firestore_);
const sfDocRef = GeoFirestore_.collection('mycollection')
.limit(1)
.near({ center: new GeoPoint(latitude, longitude), radius: 0.05 });
GeoFirestore.runTransaction((transaction) => {
const geotransaction = new _geofirestore_.GeoTransaction(transaction, new GeoPoint(latitude, longitude));
return geotransaction.get(sfDocRef._collectionPath).then((sfDoc) => {
...
});
});
} catch (error) {
if (error.type === 'UnauthenticatedError') {
throw new functions.https.HttpsError('unauthenticated', error.message);
} else if (error.type === 'NotAnAdminError' || error.type === 'InvalidRoleError') {
throw new functions.https.HttpsError('failed-precondition', error.message);
} else {
throw new functions.https.HttpsError('internal', error.message);
}
}
});
EDIT:
I am debugging the app so I am not working on production. Does debugging still requires this to be configured?
The log message you are seeing is not an error - it's informational.
On each request, your callable functions will verify any auth or appcheck token included in the request. When these tokens are not present, the execution is passed to your handler - it's your responsibility to handle requests with missing tokens if necessary. It looks like you are already handling the case for missing auth token.
When executing functions in your auth emulator, auth/appcheck tokens are minimally verified - i.e. they should be valid JWT token but we don't actually verify the signature to ensure that it's signed by Firebase Auth/AppCheck backend.
If your function is erroring in your development environment, I suspect that the error is elsewhere.

Firebase cloud functions update document with storage public URL

A frontend application is creating documents in firestorm with following model
fileRef is string : "gs://bucket-location/folder/fileName.extention"
Now after creation I want to get the public URL of the file and update the document with the URL
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
const firebase = admin.initializeApp();
interface DocumentDataType {
fileRef: string;
fileType: "image" | "video";
fileUrl: string;
timestamp: FirebaseFirestore.Timestamp;
location: FirebaseFirestore.GeoPoint;
}
exports.onDocumentCreated = functions.firestore
.document("db/{docId}")
.onCreate((snapshot, context) => {
const bucket = firebase.storage().bucket();
const { fileRef } = <DocumentDataType>snapshot.data();
const file = bucket.file(fileRef);
const fileUrl = file.publicUrl();
const batch = admin.firestore().batch();
batch.update(snapshot.ref, { ...snapshot.data(), fileUrl });
});
The functions get triggered but the file URL does not update.
is it the right approach for getting the file in cloud storage? -
and also does SDK v9 update is with batch? I really got confused reading the documentation and could not find a proper solution.
Batched writes are useful when you are trying to add/update/delete multiple documents and want to ensure all the operations either pass or fail. In the provided code you are not commiting the batch. Using commit() should update the document:
batch.commit().then(() => console.log("Document updated"));
However if you just want to update a single document then I would prefer update() instead:
exports.onDocumentCreated = functions.firestore
.document("db/{docId}")
.onCreate(async (snapshot, context) => {
const bucket = firebase.storage().bucket();
const { fileRef } = <DocumentDataType>snapshot.data();
const file = bucket.file(fileRef);
const fileUrl = file.publicUrl();
return snapshot.ref.update({ ...snapshot.data(), fileUrl });
});

Verify if a phone number exist in firebase app using firebase cloud function

I am new to the firebase (and all its features) space. I have read the documentation, and I have been able to use the web sdk properly. I have created a file where all my current firebase code is written as seen in firebaseApi.js below. Also, below is an example of how I have used the functions under registration.js (Kindly correct if I am doing it wrong), the sample works. I was trying to implement
admin.auth().getUserByPhoneNumber(phoneNumber),
which I want to use to check if a currently inputted phone number already exists in the App. But I have read the Admin SDKs cannot be used in client-side environments and should only be used in privileged server environments owned or managed by the developers of a Firebase app. I am kinda lost on how to go around this.
is it possible to connect firebase cloud functions to the client-side like
I am doing with the firebaseApi?
I have cleaned up the code and kept only the relevant parts
firebaseApi.js
import firebase from 'firebase/app';
import 'firebase/firestore';
import 'firebase/auth';
import 'firebase/database';
import 'firebase/storage';
const config = {config};
firebase.initializeApp(config);
class Firebase {
register = ({ fullname, email, phone }) => {
const user = Firebase.auth.currentUser.uid;
const firestoreRef = Firebase.firestore.collection('Users').doc(user);
const settings = {
fullname,
email,
phone,
};
firestoreRef
.set(settings);
};
static init() {
Firebase.auth = firebase.auth();
Firebase.firestore = firebase.firestore();
Firebase.database = firebase.database();
Firebase.storage = firebase.storage();
Firebase.email = firebase.auth.EmailAuthProvider;
Firebase.google = firebase.auth.GoogleAuthProvider;
Firebase.phoneVerify = new firebase.auth.PhoneAuthProvider();
Firebase.phone = firebase.auth.PhoneAuthProvider;
}
}
Firebase.shared = new Firebase();
export default Firebase;
registration.js
import Firebase from './firebaseApi';
onCompleteReg() {
const { fullname, email, email } = this.state;
const settings = {
fullname,
email,
email
};
Firebase.shared
.registerSettings(settings)
.then(() => {
console.log('Successful');
}).catch((e) => {
console.log(e);
})
}
As a matter of privacy and best practices, unless the current user is an administrator, I would not be exposing the ability to check if any given phone number is used by any individual and/or is tied to your application.
Wrapped in Cloud Function
As the Admin SDK is to be used only from secure environments, you can only expose it's functionality by way of some API. It is beneficial in this case to handle user authentication and CORS automatically, so I'll use a Callable Function. Based on the sensitive nature of such an API, it would also be advised to rate-limit access to it which can be easily achieved using the firebase-functions-rate-limiter package. In the below code, we limit the API calls to 2 uses per user and 10 uses across all users, per 15 second period to prevent abuse.
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
import { FirebaseFunctionsRateLimiter } from 'firebase-functions-rate-limiter';
admin.initializeApp();
const realtimeDb = admin.database();
const perUserLimiter = FirebaseFunctionsRateLimiter.withRealtimeDbBackend(
{
name: 'rate-limit-phone-check',
maxCalls: 2,
periodSeconds: 15,
},
realtimeDb
);
const globalLimiter = FirebaseFunctionsRateLimiter.withRealtimeDbBackend(
{
name: 'rate-limit-phone-check',
maxCalls: 10,
periodSeconds: 15,
},
realtimeDb
);
exports.phoneNumber = functions.https.onCall(async (data, context) => {
// assert required params
if (!data.phoneNumber) {
throw new functions.https.HttpsError(
'invalid-argument',
'Value for "phoneNumber" is required.'
);
} else if (!context.auth || !context.auth.uid) {
throw new functions.https.HttpsError(
'failed-precondition',
'The function must be called while authenticated.'
);
}
// rate limiter
const [userLimitExceeded, globalLimitExceeded] = await Promise.all(
perUserLimiter.isQuotaExceededOrRecordUsage('u_' + context.auth.uid),
globalLimiter.isQuotaExceededOrRecordUsage('global'));
if (userLimitExceeded || globalLimitExceeded) {
throw new functions.https.HttpsError(
'resource-exhausted',
'Call quota exceeded. Try again later',
);
}
let userRecord = await admin.auth.getUserByPhoneNumber(phoneNumber);
return userRecord.uid;
}
To call the check, you would use the following code on the client:
let checkPhoneNumber = firebase.functions().httpsCallable('phoneNumber');
checkPhoneNumber({phoneNumber: "61123456789"})
.then(function (result) {
let userId = result.data;
// do something with userId
})
.catch(function (error) {
console.error('Failed to check phone number: ', error)
});
Attempt by Login
Rather than allow users to find out if a phone number exists or specifically exists on your service, it is best to follow the Phone Number authentication flow and allow them to prove that they own a given phone number. As the user can't verify more than one number en-masse, this is the safest approach.
From the Firebase Phone Auth Reference, the following code is used to verify a phone number:
// 'recaptcha-container' is the ID of an element in the DOM.
var applicationVerifier = new firebase.auth.RecaptchaVerifier(
'recaptcha-container');
var provider = new firebase.auth.PhoneAuthProvider();
provider.verifyPhoneNumber('+16505550101', applicationVerifier)
.then(function(verificationId) {
var verificationCode = window.prompt('Please enter the verification ' +
'code that was sent to your mobile device.');
return firebase.auth.PhoneAuthProvider.credential(verificationId,
verificationCode);
})
.then(function(phoneCredential) {
return firebase.auth().signInWithCredential(phoneCredential);
});
Privileged Phone Search
If you want an appropriately privileged user (whether they have an administrator or management role) to be able to query users by a phone number, you can use the following scaffolding. In these code samples, I limit access to those who have the isAdmin claim on their authentication token.
Database structure: (see this answer for more info)
"phoneNumbers": {
"c011234567890": { // with CC for US
"userId1": true
},
"c611234567890": { // with CC for AU
"userId3": true
},
...
}
Database rules:
{
"rules": {
...,
"phoneNumbers": {
"$phoneNumber": {
"$userId": {
".write": "auth.uid === $userId && (!newData.exists() || root.child('users').child(auth.uid).child('phoneNumber').val() == ($phoneNumber).replace('c', ''))" // only this user can edit their own record and only if it is their phone number or they are deleting this record
}
},
".read": "auth != null && auth.token.isAdmin == true", // admins may read/write everything under /phoneNumbers
".write": "auth != null && auth.token.isAdmin == true"
}
}
}
Helper functions:
function doesPhoneNumberExist(phoneNumber) {
return firebase.database.ref("phoneNumbers").child("c" + phoneNumber).once('value')
.then((snapshot) => snapshot.exists());
}
// usage: let exists = await doesPhoneNumberExist("611234567890")
function getUsersByPhoneNumber(phoneNumber) {
return firebase.database.ref("phoneNumbers").child("c" + phoneNumber).once('value')
.then((snapshot) => snapshot.exists() ? Object.keys(snapshot.val()) : []);
}
// usage: let usersArray = await getUsersByPhoneNumber("611234567890") - normally only one user
function searchPhoneNumbersThatStartWith(str) {
if (!str || str.length < 5) return Promise.reject(new Error('Search string is too short'));
return firebase.database.ref("phoneNumbers").startAt("c" + str).endAt("c" + str + "\uf8ff").once('value')
.then((snapshot) => {
let phoneNumbers = [];
snapshot.forEach((phoneEntrySnapshot) => phoneNumbers.push(phoneEntrySnapshot.key));
return phoneNumbers;
});
}
// usage: let matches = await searchPhoneNumbersThatStartWith("61455")
// best handled by Cloud Function not client
function linkPhoneNumberWithUser(phoneNumber, userId) {
return firebase.database.ref("phoneNumbers").child("c" + phoneNumber).child(userId).set(true);
}
// usage: linkPhoneNumberWithUser("611234567890", firebase.auth().currentUser.uid)
// best handled by Cloud Function not client
function unlinkPhoneNumberWithUser(phoneNumber, userId) {
return firebase.database.ref("phoneNumbers").child("c" + phoneNumber).child(userId).remove();
}
// usage: unlinkPhoneNumberWithUser("611234567890", firebase.auth().currentUser.uid)

How to read a value from Firebase realtime database with a Google cloud functions?

I'm starting to use firebase cloud functions, and I have trouble reading an entry "Hello" from my database tree :
I'm trying to read the "Hello" value inside HANDLE/suj1/part1 from my tree. I am using a firebase cloud function that is triggered when I create an other entry with an IOS Application in the database inside "INTENT". The functions gets called well but every time I try to read the "Hello" value , it returns a null value in my firebase console where I expect it to return "Hello".
Here is the code I use :
const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp()
exports.match = functions.database.ref('INTENT/{userid}').onCreate(snapshot => {
const user = snapshot.key
return admin.database().ref('HANDLE/suj1/part1').once('value', (snap) => {
const hello = snap.val()
console.log(hello) // Null
});
Can someone tell what am I doing wrong ?
I found out with this line and Frank's help:
admin.database().ref().once('value', (snap) => { console.log(JSON.stringify(snap.val())); });
That I had added a whitespace at the end of "HANDLE " in my path, which does not appear in the Firebase console. I had to delete the branch and create an other one.
Try this:
const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp()
exports.match =
functions.database.ref('OTHERLOCATION/{userid}').onCreate((snapshot) => {
const user = snapshot.key
return admin.database().ref().child('HANDLE/suj1').once('value', function(snap) => {
const hello = snap.val().part1
console.log(hello) // "Hello"
});
please try this
const userName = admin.database().ref('/HANDLE/suj1/part1').once('value',function(snapshot) {
const hello = snapshot.val();
console.log(hello);
});

Resources