Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature suggestion: transaction #59

Open
felpsio opened this issue Jul 3, 2019 · 6 comments
Open

Feature suggestion: transaction #59

felpsio opened this issue Jul 3, 2019 · 6 comments

Comments

@felpsio
Copy link

felpsio commented Jul 3, 2019

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
Copy link

piavgh commented Jul 31, 2019

"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
Copy link
Author

felpsio commented Jul 31, 2019

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
Copy link
Collaborator

Duplicate of #65

@LaughDonor LaughDonor marked this as a duplicate of #65 May 4, 2020
@abkarino
Copy link

abkarino commented Oct 5, 2022

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

@LaughDonor
Copy link
Collaborator

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 LaughDonor reopened this Oct 5, 2022
@Max-Makhrov
Copy link

Max-Makhrov commented Sep 21, 2023

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants