transaction.js

"use strict";
/*!
 * Copyright 2014 Google LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
const promisify_1 = require("@google-cloud/promisify");
const arrify = require("arrify");
const entity_1 = require("./entity");
const request_1 = require("./request");
/**
 * A transaction is a set of Datastore operations on one or more entities. Each
 * transaction is guaranteed to be atomic, which means that transactions are
 * never partially applied. Either all of the operations in the transaction are
 * applied, or none of them are applied.
 *
 * @see [Transactions Reference]{@link https://cloud.google.com/datastore/docs/concepts/transactions}
 *
 * @class
 * @extends {Request}
 * @param {Datastore} datastore A Datastore instance.
 * @mixes module:datastore/request
 *
 * @example
 * const {Datastore} = require('@google-cloud/datastore');
 * const datastore = new Datastore();
 * const transaction = datastore.transaction();
 */
class Transaction extends request_1.DatastoreRequest {
    constructor(datastore, options) {
        super();
        /**
         * @name Transaction#datastore
         * @type {Datastore}
         */
        this.datastore = datastore;
        /**
         * @name Transaction#projectId
         * @type {string}
         */
        this.projectId = datastore.projectId;
        /**
         * @name Transaction#namespace
         * @type {string}
         */
        this.namespace = datastore.namespace;
        options = options || {};
        this.id = options.id;
        this.readOnly = options.readOnly === true;
        this.request = datastore.request_.bind(datastore);
        // A queue for entity modifications made during the transaction.
        this.modifiedEntities_ = [];
        // Queue the callbacks that process the API responses.
        this.requestCallbacks_ = [];
        // Queue the requests to make when we send the transactional commit.
        this.requests_ = [];
    }
    /**
     * Commit the remote transaction and finalize the current transaction
     * instance.
     *
     * If the commit request fails, we will automatically rollback the
     * transaction.
     *
     * @param {object} [gaxOptions] Request configuration options, outlined here:
     *     https://googleapis.github.io/gax-nodejs/global.html#CallOptions.
     * @param {function} callback The callback function.
     * @param {?error} callback.err An error returned while making this request.
     *   If the commit fails, we automatically try to rollback the transaction
     * (see {module:datastore/transaction#rollback}).
     * @param {object} callback.apiResponse The full API response.
     *
     * @example
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * const transaction = datastore.transaction();
     *
     * transaction.commit((err, apiResponse) => {
     *   if (err) {
     *     // Transaction could not be committed.
     *   }
     * });
     *
     * //-
     * // If the callback is omitted, we'll return a Promise.
     * //-
     * transaction.commit().then((data) => {
     *   const apiResponse = data[0];
     * });
     */
    commit(gaxOptionsOrCallback, cb) {
        const callback = typeof gaxOptionsOrCallback === 'function'
            ? gaxOptionsOrCallback
            : typeof cb === 'function'
                ? cb
                : () => { };
        const gaxOptions = typeof gaxOptionsOrCallback === 'object' ? gaxOptionsOrCallback : {};
        if (this.skipCommit) {
            setImmediate(callback);
            return;
        }
        const keys = {};
        this.modifiedEntities_
            // Reverse the order of the queue to respect the "last queued request
            // wins" behavior.
            .reverse()
            // Limit the operations we're going to send through to only the most
            // recently queued operations. E.g., if a user tries to save with the
            // same key they just asked to be deleted, the delete request will be
            // ignored, giving preference to the save operation.
            .filter((modifiedEntity) => {
            const key = modifiedEntity.entity.key;
            if (!entity_1.entity.isKeyComplete(key))
                return true;
            const stringifiedKey = JSON.stringify(modifiedEntity.entity.key);
            if (!keys[stringifiedKey]) {
                keys[stringifiedKey] = true;
                return true;
            }
            return false;
        })
            // Group entities together by method: `save` mutations, then `delete`.
            // Note: `save` mutations being first is required to maintain order when
            // assigning IDs to incomplete keys.
            .sort((a, b) => {
            return a.method < b.method ? 1 : a.method > b.method ? -1 : 0;
        })
            // Group arguments together so that we only make one call to each
            // method. This is important for `DatastoreRequest.save`, especially, as
            // that method handles assigning auto-generated IDs to the original keys
            // passed in. When we eventually execute the `save` method's API
            // callback, having all the keys together is necessary to maintain
            // order.
            .reduce((acc, entityObject) => {
            const lastEntityObject = acc[acc.length - 1];
            const sameMethod = lastEntityObject && entityObject.method === lastEntityObject.method;
            if (!lastEntityObject || !sameMethod) {
                acc.push(entityObject);
            }
            else {
                lastEntityObject.args = lastEntityObject.args.concat(entityObject.args);
            }
            return acc;
        }, [])
            // Call each of the mutational methods (DatastoreRequest[save,delete])
            // to build up a `req` array on this instance. This will also build up a
            // `callbacks` array, that is the same callback that would run if we
            // were using `save` and `delete` outside of a transaction, to process
            // the response from the API.
            .forEach((modifiedEntity) => {
            const method = modifiedEntity.method;
            const args = modifiedEntity.args.reverse();
            request_1.DatastoreRequest.prototype[method].call(this, args, () => { });
        });
        // Take the `req` array built previously, and merge them into one request to
        // send as the final transactional commit.
        const reqOpts = {
            mutations: this.requests_
                .map((x) => x.mutations)
                .reduce((a, b) => a.concat(b), []),
        };
        this.request_({
            client: 'DatastoreClient',
            method: 'commit',
            reqOpts,
            gaxOpts: gaxOptions || {},
        }, (err, resp) => {
            if (err) {
                // Rollback automatically for the user.
                this.rollback(() => {
                    // Provide the error & API response from the failed commit to the
                    // user. Even a failed rollback should be transparent. RE:
                    // https://github.com/GoogleCloudPlatform/google-cloud-node/pull/1369#discussion_r66833976
                    callback(err, resp);
                });
                return;
            }
            // The `callbacks` array was built previously. These are the callbacks
            // that handle the API response normally when using the
            // DatastoreRequest.save and .delete methods.
            this.requestCallbacks_.forEach((cb) => {
                cb(null, resp);
            });
            callback(null, resp);
        });
    }
    /**
     * Create a query for the specified kind. See {module:datastore/query} for all
     * of the available methods.
     *
     * @see [Datastore Queries]{@link https://cloud.google.com/datastore/docs/concepts/queries}
     *
     * @see {@link Query}
     *
     * @param {string} [namespace] Namespace.
     * @param {string} kind The kind to query.
     * @returns {Query}
     *
     * @example
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * const transaction = datastore.transaction();
     *
     * // Run the query inside the transaction.
     * transaction.run((err) => {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *   const ancestorKey = datastore.key(['ParentCompany', 'Alphabet']);
     *
     *   const query = transaction.createQuery('Company')
     *       .hasAncestor(ancestorKey);
     *
     *   query.run((err, entities) => {
     *     if (err) {
     *       // Error handling omitted.
     *     }
     *
     *     transaction.commit((err) => {
     *       if (!err) {
     *         // Transaction committed successfully.
     *       }
     *     });
     *   });
     * });
     *
     * // Run the query inside the transaction.with namespace
     * transaction.run((err) => {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *   const ancestorKey = datastore.key(['ParentCompany', 'Alphabet']);
     *
     *   const query = transaction.createQuery('CompanyNamespace', 'Company')
     *       .hasAncestor(ancestorKey);
     *
     *   query.run((err, entities) => {
     *     if (err) {
     *       // Error handling omitted.
     *     }
     *
     *     transaction.commit((err) => {
     *       if (!err) {
     *         // Transaction committed successfully.
     *       }
     *     });
     *   });
     * });
     */
    createQuery(namespaceOrKind, kind) {
        return this.datastore.createQuery.call(this, namespaceOrKind, kind);
    }
    /**
     * Delete all entities identified with the specified key(s) in the current
     * transaction.
     *
     * @param {Key|Key[]} key Datastore key object(s).
     *
     * @example
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * const transaction = datastore.transaction();
     *
     * transaction.run((err) => {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   // Delete a single entity.
     *   transaction.delete(datastore.key(['Company', 123]));
     *
     *   // Delete multiple entities at once.
     *   transaction.delete([
     *     datastore.key(['Company', 123]),
     *     datastore.key(['Product', 'Computer'])
     *   ]);
     *
     *   transaction.commit((err) => {
     *     if (!err) {
     *       // Transaction committed successfully.
     *     }
     *   });
     * });
     */
    // tslint:disable-next-line no-any
    delete(entities) {
        arrify(entities).forEach((ent) => {
            this.modifiedEntities_.push({
                entity: {
                    key: ent,
                },
                method: 'delete',
                args: [ent],
            });
        });
    }
    /**
     * Reverse a transaction remotely and finalize the current transaction
     * instance.
     *
     * @param {object} [gaxOptions] Request configuration options, outlined here:
     *     https://googleapis.github.io/gax-nodejs/global.html#CallOptions.
     * @param {function} callback The callback function.
     * @param {?error} callback.err An error returned while making this request.
     * @param {object} callback.apiResponse The full API response.
     *
     * @example
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * const transaction = datastore.transaction();
     *
     * transaction.run((err) => {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   transaction.rollback((err) => {
     *     if (!err) {
     *       // Transaction rolled back successfully.
     *     }
     *   });
     * });
     *
     * //-
     * // If the callback is omitted, we'll return a Promise.
     * //-
     * transaction.rollback().then((data) => {
     *   const apiResponse = data[0];
     * });
     */
    rollback(gaxOptionsOrCallback, cb) {
        const gaxOptions = typeof gaxOptionsOrCallback === 'object' ? gaxOptionsOrCallback : {};
        const callback = typeof gaxOptionsOrCallback === 'function' ? gaxOptionsOrCallback : cb;
        this.request_({
            client: 'DatastoreClient',
            method: 'rollback',
            gaxOpts: gaxOptions || {},
        }, (err, resp) => {
            this.skipCommit = true;
            callback(err || null, resp);
        });
    }
    /**
     * Begin a remote transaction. In the callback provided, run your
     * transactional commands.
     *
     * @param {object} [options] Configuration object.
     * @param {object} [options.gaxOptions] Request configuration options, outlined
     *     here: https://googleapis.github.io/gax-nodejs/global.html#CallOptions.
     * @param {boolean} [options.readOnly=false] A read-only transaction cannot
     *     modify entities.
     * @param {string} [options.transactionId] The ID of a previous transaction.
     * @param {function} callback The function to execute within the context of
     *     a transaction.
     * @param {?error} callback.err An error returned while making this request.
     * @param {Transaction} callback.transaction This transaction
     *     instance.
     * @param {object} callback.apiResponse The full API response.
     *
     * @example
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * const transaction = datastore.transaction();
     *
     * transaction.run((err, transaction) => {
     *   // Perform Datastore transactional operations.
     *   const key = datastore.key(['Company', 123]);
     *
     *   transaction.get(key, (err, entity) => {
     *     entity.name = 'Google';
     *
     *     transaction.save({
     *       key: key,
     *       data: entity
     *     });
     *
     *     transaction.commit((err) => {
     *       if (!err) {
     *         // Data saved successfully.
     *       }
     *     });
     *   });
     * });
     *
     * //-
     * // If the callback is omitted, we'll return a Promise.
     * //-
     * transaction.run().then((data) => {
     *   const transaction = data[0];
     *   const apiResponse = data[1];
     * });
     */
    run(optionsOrCallback, cb) {
        const options = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
        const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
        const reqOpts = {
            transactionOptions: {},
        };
        if (options.readOnly || this.readOnly) {
            reqOpts.transactionOptions.readOnly = {};
        }
        if (options.transactionId || this.id) {
            reqOpts.transactionOptions.readWrite = {
                previousTransaction: options.transactionId || this.id,
            };
        }
        if (options.transactionOptions) {
            reqOpts.transactionOptions = options.transactionOptions;
        }
        this.request_({
            client: 'DatastoreClient',
            method: 'beginTransaction',
            reqOpts,
            gaxOpts: options.gaxOptions,
        }, (err, resp) => {
            if (err) {
                callback(err, null, resp);
                return;
            }
            this.id = resp.transaction;
            callback(null, this, resp);
        });
    }
    /**
     * Insert or update the specified object(s) in the current transaction. If a
     * key is incomplete, its associated object is inserted and the original Key
     * object is updated to contain the generated ID.
     *
     * This method will determine the correct Datastore method to execute
     * (`upsert`, `insert`, or `update`) by using the key(s) provided. For
     * example, if you provide an incomplete key (one without an ID), the request
     * will create a new entity and have its ID automatically assigned. If you
     * provide a complete key, the entity will be updated with the data specified.
     *
     * By default, all properties are indexed. To prevent a property from being
     * included in *all* indexes, you must supply an `excludeFromIndexes` array.
     * See below for an example.
     *
     * @param {object|object[]} entities Datastore key object(s).
     * @param {Key} entities.key Datastore key object.
     * @param {string[]} [entities.excludeFromIndexes] Exclude properties from
     *     indexing using a simple JSON path notation. See the example below to
     * see how to target properties at different levels of nesting within your
     *     entity.
     * @param {object} entities.data Data to save with the provided key.
     *
     * @example
     * <caption>Save a single entity.</caption>
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * const transaction = datastore.transaction();
     *
     * // Notice that we are providing an incomplete key. After the transaction is
     * // committed, the Key object held by the `key` variable will be populated
     * // with a path containing its generated ID.
     * //-
     * const key = datastore.key('Company');
     *
     * transaction.run((err) => {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   transaction.save({
     *     key: key,
     *     data: {
     *       rating: '10'
     *     }
     *   });
     *
     *   transaction.commit((err) => {
     *     if (!err) {
     *       // Data saved successfully.
     *     }
     *   });
     * });
     *
     * @example
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * const transaction = datastore.transaction();
     *
     * // Use an array, `excludeFromIndexes`, to exclude properties from indexing.
     * // This will allow storing string values larger than 1500 bytes.
     *
     * transaction.run((err) => {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   transaction.save({
     *     key: key,
     *     excludeFromIndexes: [
     *       'description',
     *       'embeddedEntity.description',
     *       'arrayValue[].description'
     *     ],
     *     data: {
     *       description: 'Long string (...)',
     *       embeddedEntity: {
     *         description: 'Long string (...)'
     *       },
     *       arrayValue: [
     *         {
     *           description: 'Long string (...)'
     *         }
     *       ]
     *     }
     *   });
     *
     *   transaction.commit((err) => {
     *     if (!err) {
     *       // Data saved successfully.
     *     }
     *   });
     * });
     *
     * @example
     * <caption>Save multiple entities at once.</caption>
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * const transaction = datastore.transaction();
     * const companyKey = datastore.key(['Company', 123]);
     * const productKey = datastore.key(['Product', 'Computer']);
     *
     * transaction.run((err) => {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   transaction.save([
     *     {
     *       key: companyKey,
     *       data: {
     *         HQ: 'Dallas, TX'
     *       }
     *     },
     *     {
     *       key: productKey,
     *       data: {
     *         vendor: 'Dell'
     *       }
     *     }
     *   ]);
     *
     *   transaction.commit((err) => {
     *     if (!err) {
     *       // Data saved successfully.
     *     }
     *   });
     * });
     */
    // tslint:disable-next-line no-any
    save(entities) {
        arrify(entities).forEach((ent) => {
            this.modifiedEntities_.push({
                entity: {
                    key: ent.key,
                },
                method: 'save',
                args: [ent],
            });
        });
    }
}
exports.Transaction = Transaction;
/*! Developer Documentation
 *
 * All async methods (except for streams) will return a Promise in the event
 * that a callback is omitted.
 */
promisify_1.promisifyAll(Transaction, {
    exclude: ['createQuery', 'delete', 'save'],
});
//# sourceMappingURL=transaction.js.map