Skip to content
Advertisement

Batch write with Firebase Cloud Functions

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();
});

Advertisement

Answer

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
User contributions licensed under: CC BY-SA
3 People found this is helpful
Advertisement