I writed this method which should console.log the trigered node data, but i get error.
This is what I tried"
exports.makeUppercase = functions.database
.ref('/users/{userId}/matches')
.onWrite((snapshot, context) => {
// Grab the current value of what was written to the Realtime Database.
//const original = snapshot.val();
console.log('OnWrite works' + snapshot.after.val());
// const uppercase = original.toUpperCase();
// You must return a Promise when performing asynchronous tasks inside a Functions such as
// writing to the Firebase Realtime Database.
// Setting an "uppercase" sibling in the Realtime Database returns a Promise.
return null;
});
This is the error:
makeUppercase
TypeError: snapshot.val is not a function at exports.makeUppercase.functions.database.ref.onWrite (/srv/index.js:49:44) at cloudFunction (/srv/node_modules/firebase-functions/lib/cloud-functions.js:131:23) at /worker/worker.js:825:24 at at process._tickDomainCallback (internal/process/next_tick.js:229:7)
Did I made something wrong?
From the docs:
Event data now a DataSnapshot.
In earlier releases, event.data was a DeltaSnapshot; from v 1.0 onward it is a DataSnapshot.
For onWrite and onUpdate events, the data parameter has before and after fields. Each of these is a DataSnapshot with the same methods available in admin.database.DataSnapshot.
For example:
exports.dbWrite = functions.database.ref('/path').onWrite((change, context) => {
const beforeData = change.before.val(); // data before the write
const afterData = change.after.val(); // data after the write
});
Therefore in your code, you need to either use the after property to retrieve the after the write or before property:
const original = snapshot.after.val();
Related
I am writing a cloud function that will move expired events from a collection to another. It is not working as expected and I am very novice at Javascript. Please save me.
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const db = admin.firestore();
exports.expireEvents = functions.region("us-west2").pubsub.schedule('* * * * *').onRun(async (context) => {
await db.collection("Events").where('endDate', '<=', admin.firestore.Timestamp.now().millisecondsSinceEpoch).get().then((snapshot) => {
snapshot.forEach(async (doc) => {
// For all expired events, we will perform operations on some of their fields
const event = doc.data();
// Write document to collection of "expired events"
await db.collection("ArchivedEvents").doc(event.eid).set(event);
// For all the guests in that expired event, do stuff; guests should be a list of strings.
event.guests.forEach(async (uid) => {
// Create a new write batch
const batch = db.batch();
// Get user and update some attributes
const guest = await db.collection("Users").doc(uid);
// Add all operations to be performed for given user to this batch
batch.update(guest, {eventsAttended: admin.firestore.FieldValue.arrayUnion(event.eid)});
batch.update(guest, {eventsAttending: admin.firestore.FieldValue.arrayRemove(event.eid)});
// Execute batch of operations
await batch.commit();
});
// Delete doc from "not expired" collection
await db.collection("Events").doc(event.eid).delete();
});
console.log(`Successfully expired events ending on ${admin.firestore.Timestamp.now()}.`);
return true;
})
.catch((err) => {
console.error(`Could not get or update documents. Error ${err}.`);
return false;
});
});
This is the next part of the error. I tried with a collection with no documents and a few documents, but I am starting to think that because none of them have expired yet, that's why I am getting this error?
Rest of error log
If you want to ignore undefined values, enable `ignoreUndefinedProperties`.
at Object.validateUserInput (/workspace/node_modules/#google-cloud/firestore/build/src/serializer.js:277:19)
at validateQueryValue (/workspace/node_modules/#google-cloud/firestore/build/src/reference.js:2230:18)
at CollectionReference.where (/workspace/node_modules/#google-cloud/firestore/build/src/reference.js:1061:9)
at /workspace/index.js:139:33
at cloudFunction (/workspace/node_modules/firebase-functions/lib/cloud-functions.js:131:23)
at /layers/google.nodejs.functions-framework/functions-framework/node_modules/#google-cloud/functions-framework/build/src/function_wrappers.js:144:25
at processTicksAndRejections (node:internal/process/task_queues:96:5)
You've at least two problems:
I think you want a JavaScript Date object (not Firebase Timestamp object) in your query, i.e. where('endDate', '<=', new Date())
Firebase Timestamp doesn't have a millisecondsSinceEpoch property which is -- I think -- causing the "undefined" error that you're encountering.
I'm using Firebase as backend to my iOS app and can't figure out how to construct a batch write through their Cloud Functions.
I have two collections in my Firestore, drinks and customers. Each new drink and each new customer is assigned a userId property that corresponds to the uid of the currently logged in user. This userId is used with a query to the Firestore to fetch only the drinks and customers connected to the logged in user, like so: Firestore.firestore().collection("customers").whereField("userId", isEqualTo: Auth.auth().currentUser.uid)
Users are able to log in anonymously and also subscribe while anonymous. The problem is if they log out there's no way to log back in to the same anonymous uid. The uid is also stored as an appUserID with the RevenueCat SDK so I can still access it, but since I can't log the user back in to their anonymous account using the uid the only way to help a user access their data in case of a restoring of purchases is to update the userId field of their data from the old uid to the new uid. This is where the need for a batch write comes in.
I'm relatively new to programming in general but I'm super fresh when it comes to Cloud Functions, JavaScript and Node.js. I dove around the web though and thought I found a solution where I make a callable Cloud Function and send both old and new userID with the data object, query the collections for documents with the old userID and update their userId fields to the new. Unfortunately it's not working and I can't figure out why.
Here's what my code looks like:
// Cloud Function
exports.transferData = functions.https.onCall((data, context) => {
const firestore = admin.firestore();
const customerQuery = firestore.collection('customers').where('userId', '==', `${data.oldUser}`);
const drinkQuery = firestore.collection('drinks').where('userId', '==', `${data.oldUser}`);
const customerSnapshot = customerQuery.get();
const drinkSnapshot = drinkQuery.get();
const batch = firestore.batch();
for (const documentSnapshot of customerSnapshot.docs) {
batch.update(documentSnapshot.ref, { 'userId': `${data.newUser}` });
};
for (const documentSnapshot of drinkSnapshot.docs) {
batch.update(documentSnapshot.ref, { 'userId': `${data.newUser}` });
};
return batch.commit();
});
// Call from app
func transferData(from oldUser: String, to newUser: String) {
let functions = Functions.functions()
functions.httpsCallable("transferData").call(["oldUser": oldUser, "newUser": newUser]) { _, error in
if let error = error as NSError? {
if error.domain == FunctionsErrorDomain {
let code = FunctionsErrorCode(rawValue: error.code)
let message = error.localizedDescription
let details = error.userInfo[FunctionsErrorDetailsKey]
print(code)
print(message)
print(details)
}
}
}
}
This is the error message from the Cloud Functions log:
Unhandled error TypeError: customerSnapshot.docs is not iterable
at /workspace/index.js:22:51
at fixedLen (/workspace/node_modules/firebase-functions/lib/providers/https.js:66:41)
at /workspace/node_modules/firebase-functions/lib/common/providers/https.js:385:32
at processTicksAndRejections (internal/process/task_queues.js:95:5)
From what I understand customerSnapshot is something called a Promise which I'm guessing is why I can't iterate over it. By now I'm in way too deep for my sparse knowledge and don't know how to handle these Promises returned by the queries.
I guess I could just force users to create a login before they subscribe but that feels like a cowards way out now that I've come this far. I'd rather have both options available and make a decision instead of going down a forced path. Plus, I'll learn some more JavaScript if I figure this out!
Any and all help is greatly appreciated!
EDIT:
Solution:
// Cloud Function
exports.transferData = functions.https.onCall(async(data, context) => {
const firestore = admin.firestore();
const customerQuery = firestore.collection('customers').where('userId', '==', `${data.oldUser}`);
const drinkQuery = firestore.collection('drinks').where('userId', '==', `${data.oldUser}`);
const customerSnapshot = await customerQuery.get();
const drinkSnapshot = await drinkQuery.get();
const batch = firestore.batch();
for (const documentSnapshot of customerSnapshot.docs.concat(drinkSnapshot.docs)) {
batch.update(documentSnapshot.ref, { 'userId': `${data.newUser}` });
};
return batch.commit();
});
As you already guessed, the call customerQuery.get() returns a promise.
In order to understand what you need, you should first get familiar with the concept of promises here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
For your use case, you will probably end up with either using the then callback:
customerQuery.get().then((result) => {
// now you can access the result
}
or by making the method call synchronous, by using the await statement:
const result = await customerQuery.get()
// now you can access the result
When trying to deal with an array of references I get the error "FirebaseError: Function Firestore.doc() requires its first argument to be of type non-empty string, but it was: a custom t object"
My user doc has an array of references called reviews and I am trying to get the data of each of the references.
const handleFetch = async () => {
let db = firebase.firestore();
let userRef = await db
.collection("users")
.doc(props.user.id.uid) //<- doc works and returns correctly
.get();
userRef.data().reviews.forEach((ref, indx) => {
let hold = db.doc(ref).get(); //<- error occurs here
});
}
In firestore, a Reference object is constructed into a document or collection reference and you can invoke any of the document methods on it as needed.
const ref = snapshot.data().reviews[0];
ref.get();
ref.remove();
The error in particular is saying that the item is not a string, which if it is a firestore reference, is an object and thus, not compatible.
A great video to watch: https://www.youtube.com/watch?v=Elg2zDVIcLo&t=276s
exports.editData = functions.database.ref('/AllData/hello/A').onWrite((change, context) => {
const after = change.after;
if (after.exists()) {
const data = after.val();
var value = data;
// set of data to multiply by turns ratio
var actualEIn = (value.ein)*200;
console.log('Data Edited');
}
return admin.database().ref('/editedData/hello/A').push({
ein: actualEIn,
});
});
Edit: made some edits to the code as suggested! However, when I deploy it there are literally no logs.
Change this:
exports.editValues = functions.database.ref('/AllData/hello/A').onWrite((snapshot) => {
const data = snapshot.val();
if (data.exists()) {
into this:
exports.editValues = functions.database.ref('/AllData/hello/A').onWrite((change,context) => {
const data = change.after.val();
if (data.exists()) {
more info here:
https://firebase.google.com/docs/functions/beta-v1-diff#realtime-database
exports.editData = functions.database.ref('/AllData/hello/A/{id}').onWrite((change, context) => {
const afterData = change.after;
if (afterData.exists()) {
console.log('hey');
const data = afterData.val();
// set of data to multiply by turns ratio
var actualEIn = (data.ein)*200;
}
return admin.database().ref('/editedData/hello/A').push({
ein: actualEIn,
});
});
Hi guys thank you for all your help! :) I managed to solve this by adding a /{id} at the back!
You've got two things wrong here.
First, newer versions of the firebase-functions SDK since version 1.0 deliver a Change object to onWrite handlers instead of a snapshot, as it appears you are expecting. The Change object has properties for before and after with DataSnapshot objects of the contents of the database before and after the change that triggered the function. Please read the documentation for database triggers to get all the information.
Second, exists() is a method on DataSnapshot, but you're using it on the raw JavaScript object value of the contents of the database the location of change. JavaScript objects coming from val() will not have any methods to call.
You should probably update your code to:
Use the latest version of the firebase-functions module
Alter your function to accept the Change object instead of a snapshot
Use the exists() method on a snapshot in the change, rather than a raw JavaScript object.
Starter code:
exports.editValues = functions.database.ref('/AllData/hello/A').onWrite((change) => {
const after = change.after; // the DataSnapshot of the data after it was changed
if (after.exists()) {
const data = after.val() // the raw JavaScript value of the location
// use data here
}
})
I am trying to set some variables to send to pass into a function if a Firebase node is set to true. I am trying to use the .parent and .val() function to set a customer_id, based on the documentation here: https://firebase.google.com/docs/functions/database-events
exports.newCloudFunction = functions.database.ref('/user/{userId}/sources/saveSource').onWrite(event => {
// Retrieve true/false value to verify whether card should be kept on file
const saveSource = event.data.val();
if (saveSource) {
let snap = event.data;
let customer_id = snap.ref.parent.child('customer_id').val();
console.log(customer_id);
// pass customer_id into function
}
I was expecting snap.ref.parent to reference /sources and .child('customer_id').val() to access the value from the customer_id key.
However, when I try to run this function I get the following error:
TypeError: snap.ref.parent.child(...).val is not a function
at exports.linkCardToSquareAccount.functions.database.ref.onWrite.event (/user_code/index.js:79:56)
at /user_code/node_modules/firebase-functions/lib/cloud-functions.js:35:20
at process._tickDomainCallback (internal/process/next_tick.js:129:7)
How can I reference a node outside the scope of the original onWrite location?
You can't just call .val() on a database reference and expect to get the data at that location. You need to add a value listener in order to get new data.
Fortunately, this is fully supported inside of Cloud Functions:
exports.newCloudFunction = functions.database.ref('/user/{userId}/sources/saveSource').onWrite(event => {
// Retrieve true/false value to verify whether card should be kept on file
const saveSource = event.data.val();
if (saveSource) {
const customerIdRef = event.data.adminRef.parent.child('customer_id')
// attach a 'once' value listener to get the data at this location only once
// this returns a promise, so we know the function won't terminate before we have retrieved the customer_id
return customerIdRef.once('value').then(snap => {
const customer_id = snap.val();
console.log(customer_id);
// use customer_id here
});
}
});
You can learn more here.