UNPKG

@google-cloud/datastore

Version:
488 lines 18.2 kB
"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 }); exports.Transaction = void 0; const promisify_1 = require("@google-cloud/promisify"); const arrify = require("arrify"); const _1 = require("."); 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 {@link https://cloud.google.com/datastore/docs/concepts/transactions| Transactions Reference} * * @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#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(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(); _1.Datastore.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); }); } createQuery(namespaceOrKind, kind) { return this.datastore.createQuery.call(this, namespaceOrKind, kind); } /** * Create an aggregation query from the query specified. See {module:datastore/query} for all * of the available methods. * */ createAggregationQuery(query) { return this.datastore.createAggregationQuery.call(this, query); } /** * 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. * } * }); * }); * ``` */ // eslint-disable-next-line @typescript-eslint/no-explicit-any delete(entities) { arrify(entities).forEach((ent) => { this.modifiedEntities_.push({ entity: { key: ent, }, method: 'delete', args: [ent], }); }); } /** * Maps to {@link https://cloud.google.com/nodejs/docs/reference/datastore/latest/datastore/transaction#_google_cloud_datastore_Transaction_save_member_1_|Datastore#save}, forcing the method to be `insert`. * * @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 examples in * {@link Datastore#save} 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. */ insert(entities) { entities = arrify(entities) .map(request_1.DatastoreRequest.prepareEntityObject_) .map((x) => { x.method = 'insert'; return x; }); this.save(entities); } 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); }); } 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. * } * }); * }); * ``` */ save(entities) { arrify(entities).forEach((ent) => { this.modifiedEntities_.push({ entity: { key: ent.key, }, method: 'save', args: [ent], }); }); } /** * Maps to {@link https://cloud.google.com/nodejs/docs/reference/datastore/latest/datastore/transaction#_google_cloud_datastore_Transaction_save_member_1_|Datastore#save}, forcing the method to be `update`. * * @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 examples in * {@link Datastore#save} 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. */ update(entities) { entities = arrify(entities) .map(request_1.DatastoreRequest.prepareEntityObject_) .map((x) => { x.method = 'update'; return x; }); this.save(entities); } /** * Maps to {@link https://cloud.google.com/nodejs/docs/reference/datastore/latest/datastore/transaction#_google_cloud_datastore_Transaction_save_member_1_|Datastore#save}, forcing the method to be `upsert`. * * @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 examples in * {@link Datastore#save} 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. */ upsert(entities) { entities = arrify(entities) .map(request_1.DatastoreRequest.prepareEntityObject_) .map((x) => { x.method = 'upsert'; return x; }); this.save(entities); } } exports.Transaction = Transaction; /*! Developer Documentation * * All async methods (except for streams) will return a Promise in the event * that a callback is omitted. */ (0, promisify_1.promisifyAll)(Transaction, { exclude: [ 'createAggregationQuery', 'createQuery', 'delete', 'insert', 'save', 'update', 'upsert', ], }); //# sourceMappingURL=transaction.js.map