grahamearley / FirestoreGoogleAppsScript

A Google Apps Script library for accessing Google Cloud Firestore.
http://grahamearley.website/blog/2017/10/18/firestore-in-google-apps-script.html
MIT License
644 stars 108 forks source link

Feature suggestion: transaction #59

Open felpsio opened 5 years ago

felpsio commented 5 years ago

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

piavgh commented 5 years ago

"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

felpsio commented 5 years ago

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

LaughDonor commented 4 years ago

Duplicate of #65

abkarino commented 2 years ago

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

LaughDonor commented 2 years ago

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.

Max-Makhrov commented 1 year ago

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

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!