oztoygar
oztoygar

Reputation: 29

Function returned undefined, expected Promise or value in Cloud Functions

I have a Cloud Function that is triggered when a document is created in Firestore, and I keep getting Function returned undefined, expected Promise or value. The function does what it is supposed to do, but it sometimes takes around 25-30 seconds, so I thought it may have something to do with this error. I would really appreciate if someone could help me understand what to return here. My function is below:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();

const Iyzipay = require('iyzipay');

const iyzipay = new Iyzipay({
  apiKey: '...',
  secretKey: '...',
    uri: '...'
});

exports.pay = functions
.region('europe-west1')
.firestore
.document('requests/{docId}')
.onCreate((snap, context) => {
const newValue = snap.data();
       const request = {
        locale: Iyzipay.LOCALE.TR,
        conversationId: newValue.uid,
        price: newValue.price,
        paidPrice: newValue.price,
        currency: Iyzipay.CURRENCY.TRY,
        installment: '1',
        basketId: 'B67832',
        paymentChannel: Iyzipay.PAYMENT_CHANNEL.MOBILE_IOS,
        paymentGroup: Iyzipay.PAYMENT_GROUP.LISTING,
        paymentCard: {
            cardHolderName: newValue.cardHolderName,
            cardNumber: newValue.cardNumber,
            expireMonth: newValue.expireMonth,
            expireYear: newValue.expireYear,
            cvc: newValue.cvc
        },
        buyer: {
            id: newValue.uid,
            name: newValue.name,
            surname: newValue.surname,
            gsmNumber: newValue.gsmNumber,
            email: newValue.email,
            identityNumber: newValue.identityNumber,
            registrationAddress: newValue.registrationAddress,
            city: newValue.city,
            country: newValue.country,
            zipCode: newValue.zipCode
        },
        shippingAddress: {
            contactName: newValue.name,
            city: newValue.city,
            country: newValue.country,
            address: newValue.registrationAddress,
            zipCode: newValue.zipCode
        },
        billingAddress: {
          contactName: newValue.name,
          city: newValue.city,
          country: newValue.country,
          address: newValue.registrationAddress,
          zipCode: newValue.zipCode
        },
        basketItems: [
            {
                id: newValue.productid,
                name: newValue.productname,
                category1: newValue.category1,
                itemType: Iyzipay.BASKET_ITEM_TYPE.PHYSICAL,
                price: newValue.price
            },
        ]
       }
       iyzipay.payment.create(request, function (err, result) {
        console.log(err, result);
  
       const docRef1 = db.collection('results').doc().set(result);
    }) 
      })

Upvotes: 2

Views: 709

Answers (1)

Renaud Tarnec
Renaud Tarnec

Reputation: 83191

You need to terminate a Cloud Function when all the asynchronous work is completed, see the doc. In the case of a background triggered Cloud Function (e.g. Cloud Firestore function onCreate trigger, like your Cloud Function) you must return the chain of Promises returned by the asynchronous method calls.

I don't know the Iyzipay service nor the corresponding Node.js library, but it seems that there is no "promisified" version of the iyzipay.payment.create method. You should therefore wrap it in a Promise and chain this Promise with the Promise returned by the Firestore asynchronous set() method, as follows (untested).

exports.pay = functions
    .region('europe-west1')
    .firestore
    .document('requests/{docId}')
    .onCreate((snap, context) => {
        const newValue = snap.data();
        const request = { ... };

        return new Promise(function (resolve, reject) {
            iyzipay.payment.create(request, function (err, result) {
                if (err) {
                    reject(err)
                } else {
                    resolve(result)
                }
            })
        })
        .then(result => {
            return db.collection('results').doc().set(result);
        })
        .catch(error => {
            console.log(error);
            return null;
        });

    });

If you want to write something to the log when the operation is completed, do as follows:

    // ...
    .onCreate((snap, context) => {
        const newValue = snap.data();
        const request = { ... };

        return new Promise(function (resolve, reject) {
          // ...
        })
        .then(result => {
            return db.collection('results').doc().set(result);
        })
        .then(() => {
            console.log("Operation completed: " + result);
            return null;
        })
        .catch(error => {
            console.log(error);
            return null;
        });

Update following your comment:

How can I add another Firestore query after return db.collection('results').doc().set(result);? For example, I want to update a field in a document, so where can I add db.collection('listings').doc(newValue.productid).update({sold : true})?

You have two possibilities:

Approach #1

Since the update() method is an asynchronous method which returns a Promise (like all Firebase asynchronous methods), you need to add it to the chain of promises, as follows:

        return new Promise(function (resolve, reject) {
            iyzipay.payment.create(request, function (err, result) {
                if (err) {
                    reject(err)
                } else {
                    resolve(result)
                }
            })
        })
        .then(result => {
            return db.collection('results').doc().set(result);
        })
        .then(() => {
            return db.collection('listings').doc(newValue.productid).update({sold: true});
        })
        .catch(error => {
            console.log(error);
            return null;
        });

Approach #2

Since both the set() the update() methods write to a document, you could use a batched write, as follows:

        return new Promise(function (resolve, reject) {
            iyzipay.payment.create(request, function (err, result) {
                if (err) {
                    reject(err)
                } else {
                    resolve(result)
                }
            })
        })
        .then(result => {
           const batch = db.batch();

           var docRef1 = db.collection('results').doc();
           batch.set(docRef1, result);

           var docRef2 = db.collection('listings').doc(newValue.productid);
           batch.update(docRef2, {sold: true});

           return batch.commit();                
        })
        .catch(error => {
            console.log(error);
            return null;
        });

The difference with Approach #1 is that the two writes are done in one atomic action.


PS: Note that instead of doing db.collection('results').doc().set(result); you could do db.collection('results').add(result);

Upvotes: 4

Related Questions