Search code examples
javascriptfirebasegoogle-cloud-firestoretransactionsgoogle-cloud-functions

How to move transaction code inside another function


I am trying to rewrite my firebase-functions using transactions. The question I now come upon is, should I return a Promise of Transcations? For example, I want to use a function which has a transcation parameter. But this function should not instantly perform the transaction, but instead return the Promise of it.

Here is my current function:

export const dbUserDocsOnCreated = functions
    .region(constants.region)
    .firestore
    .document("users/{id}")
    .onCreate((snapshot, context) => doOnCreatedUserDocs(db, stripe, snapshot, context));


export async function doOnCreatedUserDocs(
    db: FirebaseFirestore.Firestore, 
    stripe: Stripe, 
    snapshot: functions.firestore.QueryDocumentSnapshot, 
    context: functions.EventContext
) {
    try {
        const transactionPromise = db.runTransaction(async (t) => {
            const snapshotData: FirebaseFirestore.DocumentData = (await t.get(snapshot.ref)).data() as FirebaseFirestore.DocumentData
            
            // IF onCreate() has already been called once.
            if (!isDataNullOrEmpty(snapshotData.eventID)) {
                return Promise.all([null])
            }

            const createdUserNr: string = await getIdTransactionally(t, db, constants.collectionUsers)
            const createdStripeUser: Stripe.Response<Stripe.Customer> = await createStripeUser(stripe, snapshotData, firebaseUserEmail, currentUserNr, snapshot.id, idempotencyKey)
            
            const updateUserNrPromise: Promise<FirebaseFirestore.Transaction> = incrementIdTransactionally(t, db,  constants.collectionUsers)
            const updateUserDocsPromise: Promise<FirebaseFirestore.Transaction> = updateUserDocsTransactionally(t, snapshot.ref, createdUserNr, idempotencyKey)

            return Promise.all([
                updateUserDocsPromise,
                updateUserNrPromise,
            ])
        })

        return Promise.all([
            transactionPromise
        ])
    } catch (err) {
        // CATCHING
    }
}

Used Functions that work with transactions

export async function incrementIdTransactionally(
    transaction: FirebaseFirestore.Transaction, 
    db: FirebaseFirestore.Firestore, 
    doc: string
): Promise<FirebaseFirestore.Transaction> {
    const incrementor: FirebaseFirestore.FieldValue = FirebaseFirestore.FieldValue.increment(1);
    const collectionReference: FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData> = db.collection(constants.collectionAutoIds).doc(doc)

    return transaction.update(collectionReference, { id: incrementor })
}

async function updateUserDocsTransactionally(
    transaction: FirebaseFirestore.Transaction, 
    ref: FirebaseFirestore.DocumentReference, 
    generatedUserNr: String,
    eventId: string,
): Promise<FirebaseFirestore.Transaction> {
    return transaction.set(ref, { 
        userNr: generatedUserNr, 
        eventId: eventId,
        optionalAddress: FirebaseFirestore.FieldValue.delete() 
    }, { merge: true })
}

function createStripeUser(
    stripe: Stripe, 
    data: FirebaseFirestore.DocumentData, 
    eMail: string, 
    usernr: string, 
    uid: string,
    idempotencKey: string,
): Promise<Stripe.Response<Stripe.Customer>> {
    return stripe.customers.create({
        name: data.fullName,
        email: eMail,
        description: "Created by Firebase Cloud-Functions",
        metadata: { uuid: uid, userNr: usernr },
        preferred_locales: ["de-DE"],
    }, { idempotencyKey: idempotencKey })
}

Solution

  • After reviewing the previous thread and the SDK reference, it looks like the set(), update(), and delete() operations of transactions do not return promises, but they just return the same Transaction instance to be used in transaction chaining:

    Returns Transaction: This Transaction instance. Used for chaining method calls.

    This is not the same for the get() method, which does return a Promise<DocumentSnapshot>. I tested a simple transaction broken up into two functions, and found no apparent issues:

    
    function fireTransaction(){
        return fireDB.runTransaction((tr) => {
            const myDoc = fireDB.collection("users").doc("testUser1");
            return tr.get(myDoc).then(tDoc => {
                setUserAgeTR(tr, tDoc.ref)
            })
        })
        .then(res => console.log("Completed"))
        .catch(err => console.log("Failed Transaction: ", err))
    }
    
    function setUserAgeTR(tr, docRef){
        return tr.update(docRef, {userAge: 9999}) //Requires DocumentReference, can be obtained from 'ref'
    }
    
    

    Since you are using TypeScript, the return type for your helper functions should be Transaction, according to the SDK doc.