FirestoreGoogleAppsScript icon indicating copy to clipboard operation
FirestoreGoogleAppsScript copied to clipboard

Feature suggestion: transaction

Open felpsio opened this issue 5 years ago • 6 comments

I didn't find this feature on the project Readme, but I think it would be great to have it in the library as well. For what I'm building I need this feature

felpsio avatar Jul 03 '19 08:07 felpsio

"For what I'm building I need this feature" => Did you manage to have this feature?

https://firebase.google.com/docs/firestore/reference/rest/v1/projects.databases.documents/beginTransaction https://firebase.google.com/docs/firestore/reference/rest/v1/projects.databases.documents/commit

I see that on the Firebase Rest API document there are 2 methods above, which might help you somehow.

If you did integrate those APIs, please create a PR

piavgh avatar Jul 31 '19 02:07 piavgh

Thanks @piavgh, actually seems that we need both methods to make the transaction work. The first one to create and the second to make it happens.

I'm working without transaction for now. While I have few data it's ok. But as the database gets larger it can become a problem

felpsio avatar Jul 31 '19 03:07 felpsio

Duplicate of #65

LaughDonor avatar May 04 '20 22:05 LaughDonor

This is not a duplicate, a batch write is not the same as transaction.

abkarino avatar Oct 05 '22 19:10 abkarino

Thanks, I must have missed the related text in batched write documentation:

The documents.batchWrite method does not apply the write operations atomically and can apply them out of order. Method does not allow more than one write per document. Each write succeeds or fails independently.

LaughDonor avatar Oct 05 '22 20:10 LaughDonor

Transactions are useful for atomic operations. In my case I need to update user creadits for the add-on. 

I've implemented this feature for my project. As this library's language is typesctipt, my code is not compatible. If someone wants to install transactions, here's how:

  1. Install the JS library code. I've manually copied it from here.
  2. Edit 2 classes, use the code shown below.

The code

Firestore.gs

    this.transformDocument_ = FirestoreWrite.prototype.transformDocument_;


    //
    // original class code...
    //


    /**
     * https://firebase.google.com/docs/firestore/reference/rest/v1beta1/Write#FieldTransform
     * 
     * @typedef {Object} FieldTransform
     * @property {String} fieldPath - path to field, use dots for nested fields: "parent.kid"
     * 
     * // Union field transform_type can be only one of the following:
     * @property {Number} [increment]
     * @property {Number} [maximum]
     * @property {Number} [minimum]
     * @property {Array} [appendMissingElements] - for arrays
     * @property {Array} [removeAllFromArray] - for arrays
     */
    /**
     * Transform document using transactions.
     * https://firebase.google.com/docs/firestore/manage-data/transactions
     * 
     * @param {String} path - path to the document in format: "collection/documentId"
     * @param {Array<FieldTransform>} fieldTransforms
     * @param {Request} request
     * 
     */
    transformDocument(path, fieldTransforms) {
        const baseUrl = this.baseUrl.slice(0, -1) + ':';
        const request = new Request(baseUrl, this.authToken);
        return this.transformDocument_(request, path, this.basePath, fieldTransforms);
    }

I've decided to add the code to "Write" class instead of creating a new class:

Write.gs

    /**
     * Transform document using transactions.
     * https://firebase.google.com/docs/firestore/manage-data/transactions
     * 
     * @param {Request} request
     * @param {String} path
     * * @param {Array<FieldTransform>} fieldTransforms
     * @param {String} basePath
     * 
     */
    transformDocument_(request, path, basePath, fieldTransforms) {
        // API documents
        // https://firebase.google.com/docs/firestore/reference/rest/v1beta1/projects.databases.documents/beginTransaction
        // https://firebase.google.com/docs/firestore/reference/rest/v1beta1/projects.databases.documents/commit
        const paypoadBeginTransaction = {
            "options": {
                "readWrite": {}
            }
        }
        const transactionData = request.post('beginTransaction', paypoadBeginTransaction);
        const transactionId = transactionData.transaction;


        const write = {
            "currentDocument": {
                "exists": true // the target document must exist
            },
            "transform": {
                "document": basePath + path,
                fieldTransforms
            },
        }

        const payloadCommit = {
            "writes": [write],
            "transaction": transactionId
        }
        const result = request.post('commit', payloadCommit);
        return result;
    }

Usage:

function test_transformDocument() {
    /** @type FieldTransform */
    var fieldTransform = {
        fieldPath: 'credits.gpt5',
        increment: {integer_value: -5}
    }
    var result = transformDocument_('test/max', [fieldTransform]);
    console.log(JSON.stringify(result));
}

/**
  * @param {String} path - path to the document in format: "collection/documentId"
  * @param {Array<FieldTransform>} fieldTransforms
 */
function transformDocument_(path, fieldTransforms) {
  /** @type Firestore */
  var app = getFirestoreApp_('v1beta1');
  return app.transformDocument(path, fieldTransforms)
}

/**
 * @param {String} [apiVersion] - v1
 */
function getFirestoreApp_(apiVersion) {
  var email = options.email;           // PUT YOUR SERVICE ACCOUNT EMAIL HERE
  var projectId = options.projectId;   // PUT YOUR PROJECT ID HERE
  var key = getFirestoreKey_();        // PUT YOUR KEY HERE
  var app = getFirestore(email, key, projectId, apiVersion);
  return app;
}

As result, "credits" for my user were refuced by 5, it was 500, and not it is 495. Here's what I see in logs:

{"writeResults":
 [{"updateTime":"2023-09-21T06:45:35.408298Z",
   "transformResults":[{"integerValue":"495"}]}],
   "commitTime":"2023-09-21T06:45:35.408298Z"}

With this code I'm sure if 2 operations will try to change user credits at the same time, no collision will happen with my data.

Usage notes

  • You need to give  fieldTransform object. Read docs here: https://firebase.google.com/docs/firestore/reference/rest/v1beta1/Write#FieldTransform
  • fieldPath option for map field is delimited with dots. If you have a nested map, like in my case: "credits" has "gpt5" field inside, the fieldPath is "credits.gpt5"
  • increment value is a bit tricky, see docs here: https://cloud.google.com/firestore/docs/reference/rpc/google.firestore.v1beta1#value
  • Note that this API uses version v1beta1
  • Look at the payload for a hidden treasure:  "writes": [write]. I use one read-write operation at a time to transform my field. But as you see, the API lets you to perform multiple operations. This is very poverful, but I think the library should stay simple, and I did not add this option.
  • API supports other operations, but in my case I needed to transform my field value.

Conclusion

I hope my solution will be helpful, and I hope one day a modified version of this code will be added to original library. Cheers to creator and maintaimers!

Max-Makhrov avatar Sep 21 '23 07:09 Max-Makhrov