UNPKG

25.2 kBJavaScriptView Raw
1"use strict";
2/*!
3 * Copyright 2014 Google LLC.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
18 if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
19 if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
20 return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
21};
22var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
23 if (kind === "m") throw new TypeError("Private method is not writable");
24 if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
25 if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
26 return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
27};
28var _Transaction_instances, _Transaction_mutex, _Transaction_state, _Transaction_runCommit, _Transaction_processBeginResults, _Transaction_parseRunSuccess, _Transaction_beginTransactionAsync, _Transaction_withBeginTransaction;
29Object.defineProperty(exports, "__esModule", { value: true });
30exports.Transaction = void 0;
31const promisify_1 = require("@google-cloud/promisify");
32const arrify = require("arrify");
33const _1 = require(".");
34const entity_1 = require("./entity");
35const request_1 = require("./request");
36const async_mutex_1 = require("async-mutex");
37var TransactionState;
38(function (TransactionState) {
39 TransactionState[TransactionState["NOT_STARTED"] = 0] = "NOT_STARTED";
40 TransactionState[TransactionState["IN_PROGRESS"] = 1] = "IN_PROGRESS";
41})(TransactionState || (TransactionState = {}));
42/**
43 * A transaction is a set of Datastore operations on one or more entities. Each
44 * transaction is guaranteed to be atomic, which means that transactions are
45 * never partially applied. Either all of the operations in the transaction are
46 * applied, or none of them are applied.
47 *
48 * @see {@link https://cloud.google.com/datastore/docs/concepts/transactions| Transactions Reference}
49 *
50 * @class
51 * @extends {Request}
52 * @param {Datastore} datastore A Datastore instance.
53 * @mixes module:datastore/request
54 *
55 * @example
56 * ```
57 * const {Datastore} = require('@google-cloud/datastore');
58 * const datastore = new Datastore();
59 * const transaction = datastore.transaction();
60 * ```
61 */
62class Transaction extends request_1.DatastoreRequest {
63 constructor(datastore, options) {
64 super();
65 _Transaction_instances.add(this);
66 _Transaction_mutex.set(this, new async_mutex_1.Mutex());
67 _Transaction_state.set(this, TransactionState.NOT_STARTED);
68 /**
69 * @name Transaction#datastore
70 * @type {Datastore}
71 */
72 this.datastore = datastore;
73 /**
74 * @name Transaction#namespace
75 * @type {string}
76 */
77 this.namespace = datastore.namespace;
78 options = options || {};
79 this.id = options.id;
80 this.readOnly = options.readOnly === true;
81 this.request = datastore.request_.bind(datastore);
82 // A queue for entity modifications made during the transaction.
83 this.modifiedEntities_ = [];
84 // Queue the callbacks that process the API responses.
85 this.requestCallbacks_ = [];
86 // Queue the requests to make when we send the transactional commit.
87 this.requests_ = [];
88 }
89 commit(gaxOptionsOrCallback, cb) {
90 const callback = typeof gaxOptionsOrCallback === 'function'
91 ? gaxOptionsOrCallback
92 : typeof cb === 'function'
93 ? cb
94 : () => { };
95 const gaxOptions = typeof gaxOptionsOrCallback === 'object' ? gaxOptionsOrCallback : {};
96 // This ensures that the transaction is started before calling runCommit
97 __classPrivateFieldGet(this, _Transaction_instances, "m", _Transaction_withBeginTransaction).call(this, gaxOptions, () => {
98 __classPrivateFieldGet(this, _Transaction_instances, "m", _Transaction_runCommit).call(this, gaxOptions, callback);
99 }, callback);
100 }
101 createQuery(namespaceOrKind, kind) {
102 return this.datastore.createQuery.call(this, namespaceOrKind, kind);
103 }
104 /**
105 * Create an aggregation query from the query specified. See {module:datastore/query} for all
106 * of the available methods.
107 *
108 */
109 createAggregationQuery(query) {
110 return this.datastore.createAggregationQuery.call(this, query);
111 }
112 /**
113 * Delete all entities identified with the specified key(s) in the current
114 * transaction.
115 *
116 * @param {Key|Key[]} key Datastore key object(s).
117 *
118 * @example
119 * ```
120 * const {Datastore} = require('@google-cloud/datastore');
121 * const datastore = new Datastore();
122 * const transaction = datastore.transaction();
123 *
124 * transaction.run((err) => {
125 * if (err) {
126 * // Error handling omitted.
127 * }
128 *
129 * // Delete a single entity.
130 * transaction.delete(datastore.key(['Company', 123]));
131 *
132 * // Delete multiple entities at once.
133 * transaction.delete([
134 * datastore.key(['Company', 123]),
135 * datastore.key(['Product', 'Computer'])
136 * ]);
137 *
138 * transaction.commit((err) => {
139 * if (!err) {
140 * // Transaction committed successfully.
141 * }
142 * });
143 * });
144 * ```
145 */
146 // eslint-disable-next-line @typescript-eslint/no-explicit-any
147 delete(entities) {
148 arrify(entities).forEach((ent) => {
149 this.modifiedEntities_.push({
150 entity: {
151 key: ent,
152 },
153 method: 'delete',
154 args: [ent],
155 });
156 });
157 }
158 get(keys, optionsOrCallback, cb) {
159 const options = typeof optionsOrCallback === 'object' && optionsOrCallback
160 ? optionsOrCallback
161 : {};
162 const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
163 // This ensures that the transaction is started before calling get
164 __classPrivateFieldGet(this, _Transaction_instances, "m", _Transaction_withBeginTransaction).call(this, options.gaxOptions, () => {
165 super.get(keys, options, callback);
166 }, callback);
167 }
168 /**
169 * 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`.
170 *
171 * @param {object|object[]} entities Datastore key object(s).
172 * @param {Key} entities.key Datastore key object.
173 * @param {string[]} [entities.excludeFromIndexes] Exclude properties from
174 * indexing using a simple JSON path notation. See the examples in
175 * {@link Datastore#save} to see how to target properties at different
176 * levels of nesting within your entity.
177 * @param {object} entities.data Data to save with the provided key.
178 */
179 insert(entities) {
180 entities = arrify(entities)
181 .map(request_1.DatastoreRequest.prepareEntityObject_)
182 .map((x) => {
183 x.method = 'insert';
184 return x;
185 });
186 this.save(entities);
187 }
188 rollback(gaxOptionsOrCallback, cb) {
189 const gaxOptions = typeof gaxOptionsOrCallback === 'object' ? gaxOptionsOrCallback : {};
190 const callback = typeof gaxOptionsOrCallback === 'function' ? gaxOptionsOrCallback : cb;
191 this.request_({
192 client: 'DatastoreClient',
193 method: 'rollback',
194 gaxOpts: gaxOptions || {},
195 }, (err, resp) => {
196 this.skipCommit = true;
197 callback(err || null, resp);
198 });
199 }
200 run(optionsOrCallback, cb) {
201 const options = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
202 const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
203 __classPrivateFieldGet(this, _Transaction_mutex, "f").runExclusive(async () => {
204 if (__classPrivateFieldGet(this, _Transaction_state, "f") === TransactionState.NOT_STARTED) {
205 const runResults = await __classPrivateFieldGet(this, _Transaction_instances, "m", _Transaction_beginTransactionAsync).call(this, options);
206 __classPrivateFieldGet(this, _Transaction_instances, "m", _Transaction_processBeginResults).call(this, runResults, callback);
207 }
208 else {
209 process.emitWarning('run has already been called and should not be called again.');
210 callback(null, this, { transaction: this.id });
211 }
212 });
213 }
214 runAggregationQuery(query, optionsOrCallback, cb) {
215 const options = typeof optionsOrCallback === 'object' && optionsOrCallback
216 ? optionsOrCallback
217 : {};
218 const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
219 // This ensures that the transaction is started before calling runAggregationQuery
220 __classPrivateFieldGet(this, _Transaction_instances, "m", _Transaction_withBeginTransaction).call(this, options.gaxOptions, () => {
221 super.runAggregationQuery(query, options, callback);
222 }, callback);
223 }
224 runQuery(query, optionsOrCallback, cb) {
225 const options = typeof optionsOrCallback === 'object' && optionsOrCallback
226 ? optionsOrCallback
227 : {};
228 const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
229 // This ensures that the transaction is started before calling runQuery
230 __classPrivateFieldGet(this, _Transaction_instances, "m", _Transaction_withBeginTransaction).call(this, options.gaxOptions, () => {
231 super.runQuery(query, options, callback);
232 }, callback);
233 }
234 /**
235 * Insert or update the specified object(s) in the current transaction. If a
236 * key is incomplete, its associated object is inserted and the original Key
237 * object is updated to contain the generated ID.
238 *
239 * This method will determine the correct Datastore method to execute
240 * (`upsert`, `insert`, or `update`) by using the key(s) provided. For
241 * example, if you provide an incomplete key (one without an ID), the request
242 * will create a new entity and have its ID automatically assigned. If you
243 * provide a complete key, the entity will be updated with the data specified.
244 *
245 * By default, all properties are indexed. To prevent a property from being
246 * included in *all* indexes, you must supply an `excludeFromIndexes` array.
247 * See below for an example.
248 *
249 * @param {object|object[]} entities Datastore key object(s).
250 * @param {Key} entities.key Datastore key object.
251 * @param {string[]} [entities.excludeFromIndexes] Exclude properties from
252 * indexing using a simple JSON path notation. See the example below to
253 * see how to target properties at different levels of nesting within your
254 * entity.
255 * @param {object} entities.data Data to save with the provided key.
256 *
257 * @example
258 * ```
259 * <caption>Save a single entity.</caption>
260 * const {Datastore} = require('@google-cloud/datastore');
261 * const datastore = new Datastore();
262 * const transaction = datastore.transaction();
263 *
264 * // Notice that we are providing an incomplete key. After the transaction is
265 * // committed, the Key object held by the `key` variable will be populated
266 * // with a path containing its generated ID.
267 * //-
268 * const key = datastore.key('Company');
269 *
270 * transaction.run((err) => {
271 * if (err) {
272 * // Error handling omitted.
273 * }
274 *
275 * transaction.save({
276 * key: key,
277 * data: {
278 * rating: '10'
279 * }
280 * });
281 *
282 * transaction.commit((err) => {
283 * if (!err) {
284 * // Data saved successfully.
285 * }
286 * });
287 * });
288 *
289 * ```
290 * @example
291 * ```
292 * const {Datastore} = require('@google-cloud/datastore');
293 * const datastore = new Datastore();
294 * const transaction = datastore.transaction();
295 *
296 * // Use an array, `excludeFromIndexes`, to exclude properties from indexing.
297 * // This will allow storing string values larger than 1500 bytes.
298 *
299 * transaction.run((err) => {
300 * if (err) {
301 * // Error handling omitted.
302 * }
303 *
304 * transaction.save({
305 * key: key,
306 * excludeFromIndexes: [
307 * 'description',
308 * 'embeddedEntity.description',
309 * 'arrayValue[].description'
310 * ],
311 * data: {
312 * description: 'Long string (...)',
313 * embeddedEntity: {
314 * description: 'Long string (...)'
315 * },
316 * arrayValue: [
317 * {
318 * description: 'Long string (...)'
319 * }
320 * ]
321 * }
322 * });
323 *
324 * transaction.commit((err) => {
325 * if (!err) {
326 * // Data saved successfully.
327 * }
328 * });
329 * });
330 *
331 * ```
332 * @example
333 * ```
334 * <caption>Save multiple entities at once.</caption>
335 * const {Datastore} = require('@google-cloud/datastore');
336 * const datastore = new Datastore();
337 * const transaction = datastore.transaction();
338 * const companyKey = datastore.key(['Company', 123]);
339 * const productKey = datastore.key(['Product', 'Computer']);
340 *
341 * transaction.run((err) => {
342 * if (err) {
343 * // Error handling omitted.
344 * }
345 *
346 * transaction.save([
347 * {
348 * key: companyKey,
349 * data: {
350 * HQ: 'Dallas, TX'
351 * }
352 * },
353 * {
354 * key: productKey,
355 * data: {
356 * vendor: 'Dell'
357 * }
358 * }
359 * ]);
360 *
361 * transaction.commit((err) => {
362 * if (!err) {
363 * // Data saved successfully.
364 * }
365 * });
366 * });
367 * ```
368 */
369 save(entities) {
370 arrify(entities).forEach((ent) => {
371 this.modifiedEntities_.push({
372 entity: {
373 key: ent.key,
374 },
375 method: 'save',
376 args: [ent],
377 });
378 });
379 }
380 /**
381 * 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`.
382 *
383 * @param {object|object[]} entities Datastore key object(s).
384 * @param {Key} entities.key Datastore key object.
385 * @param {string[]} [entities.excludeFromIndexes] Exclude properties from
386 * indexing using a simple JSON path notation. See the examples in
387 * {@link Datastore#save} to see how to target properties at different
388 * levels of nesting within your entity.
389 * @param {object} entities.data Data to save with the provided key.
390 */
391 update(entities) {
392 entities = arrify(entities)
393 .map(request_1.DatastoreRequest.prepareEntityObject_)
394 .map((x) => {
395 x.method = 'update';
396 return x;
397 });
398 this.save(entities);
399 }
400 /**
401 * 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`.
402 *
403 * @param {object|object[]} entities Datastore key object(s).
404 * @param {Key} entities.key Datastore key object.
405 * @param {string[]} [entities.excludeFromIndexes] Exclude properties from
406 * indexing using a simple JSON path notation. See the examples in
407 * {@link Datastore#save} to see how to target properties at different
408 * levels of nesting within your entity.
409 * @param {object} entities.data Data to save with the provided key.
410 */
411 upsert(entities) {
412 entities = arrify(entities)
413 .map(request_1.DatastoreRequest.prepareEntityObject_)
414 .map((x) => {
415 x.method = 'upsert';
416 return x;
417 });
418 this.save(entities);
419 }
420}
421exports.Transaction = Transaction;
422_Transaction_mutex = new WeakMap(), _Transaction_state = new WeakMap(), _Transaction_instances = new WeakSet(), _Transaction_runCommit = function _Transaction_runCommit(gaxOptions, callback) {
423 if (this.skipCommit) {
424 setImmediate(callback);
425 return;
426 }
427 const keys = {};
428 this.modifiedEntities_
429 // Reverse the order of the queue to respect the "last queued request
430 // wins" behavior.
431 .reverse()
432 // Limit the operations we're going to send through to only the most
433 // recently queued operations. E.g., if a user tries to save with the
434 // same key they just asked to be deleted, the delete request will be
435 // ignored, giving preference to the save operation.
436 .filter((modifiedEntity) => {
437 const key = modifiedEntity.entity.key;
438 if (!entity_1.entity.isKeyComplete(key))
439 return true;
440 const stringifiedKey = JSON.stringify(modifiedEntity.entity.key);
441 if (!keys[stringifiedKey]) {
442 keys[stringifiedKey] = true;
443 return true;
444 }
445 return false;
446 })
447 // Group entities together by method: `save` mutations, then `delete`.
448 // Note: `save` mutations being first is required to maintain order when
449 // assigning IDs to incomplete keys.
450 .sort((a, b) => {
451 return a.method < b.method ? 1 : a.method > b.method ? -1 : 0;
452 })
453 // Group arguments together so that we only make one call to each
454 // method. This is important for `DatastoreRequest.save`, especially, as
455 // that method handles assigning auto-generated IDs to the original keys
456 // passed in. When we eventually execute the `save` method's API
457 // callback, having all the keys together is necessary to maintain
458 // order.
459 .reduce((acc, entityObject) => {
460 const lastEntityObject = acc[acc.length - 1];
461 const sameMethod = lastEntityObject && entityObject.method === lastEntityObject.method;
462 if (!lastEntityObject || !sameMethod) {
463 acc.push(entityObject);
464 }
465 else {
466 lastEntityObject.args = lastEntityObject.args.concat(entityObject.args);
467 }
468 return acc;
469 }, [])
470 // Call each of the mutational methods (DatastoreRequest[save,delete])
471 // to build up a `req` array on this instance. This will also build up a
472 // `callbacks` array, that is the same callback that would run if we
473 // were using `save` and `delete` outside of a transaction, to process
474 // the response from the API.
475 .forEach((modifiedEntity) => {
476 const method = modifiedEntity.method;
477 const args = modifiedEntity.args.reverse();
478 _1.Datastore.prototype[method].call(this, args, () => { });
479 });
480 // Take the `req` array built previously, and merge them into one request to
481 // send as the final transactional commit.
482 const reqOpts = {
483 mutations: this.requests_
484 .map((x) => x.mutations)
485 .reduce((a, b) => a.concat(b), []),
486 };
487 this.request_({
488 client: 'DatastoreClient',
489 method: 'commit',
490 reqOpts,
491 gaxOpts: gaxOptions || {},
492 }, (err, resp) => {
493 if (err) {
494 // Rollback automatically for the user.
495 this.rollback(() => {
496 // Provide the error & API response from the failed commit to the
497 // user. Even a failed rollback should be transparent. RE:
498 // https://github.com/GoogleCloudPlatform/google-cloud-node/pull/1369#discussion_r66833976
499 callback(err, resp);
500 });
501 return;
502 }
503 // The `callbacks` array was built previously. These are the callbacks
504 // that handle the API response normally when using the
505 // DatastoreRequest.save and .delete methods.
506 this.requestCallbacks_.forEach((cb) => {
507 cb(null, resp);
508 });
509 callback(null, resp);
510 });
511}, _Transaction_processBeginResults = function _Transaction_processBeginResults(runResults, callback) {
512 const err = runResults.err;
513 const resp = runResults.resp;
514 if (err) {
515 callback(err, null, resp);
516 }
517 else {
518 __classPrivateFieldGet(this, _Transaction_instances, "m", _Transaction_parseRunSuccess).call(this, runResults);
519 callback(null, this, resp);
520 }
521}, _Transaction_parseRunSuccess = function _Transaction_parseRunSuccess(runResults) {
522 const resp = runResults.resp;
523 this.id = resp.transaction;
524 __classPrivateFieldSet(this, _Transaction_state, TransactionState.IN_PROGRESS, "f");
525}, _Transaction_beginTransactionAsync =
526/**
527 * This async function makes a beginTransaction call and returns a promise with
528 * the information returned from the call that was made.
529 *
530 * @param {RunOptions} options The options used for a beginTransaction call.
531 * @returns {Promise<RequestPromiseReturnType>}
532 *
533 *
534 **/
535async function _Transaction_beginTransactionAsync(options) {
536 const reqOpts = {
537 transactionOptions: {},
538 };
539 if (options.readOnly || this.readOnly) {
540 reqOpts.transactionOptions.readOnly = {};
541 }
542 if (options.transactionId || this.id) {
543 reqOpts.transactionOptions.readWrite = {
544 previousTransaction: options.transactionId || this.id,
545 };
546 }
547 if (options.transactionOptions) {
548 reqOpts.transactionOptions = options.transactionOptions;
549 }
550 return new Promise((resolve) => {
551 this.request_({
552 client: 'DatastoreClient',
553 method: 'beginTransaction',
554 reqOpts,
555 gaxOpts: options.gaxOptions,
556 },
557 // Always use resolve because then this function can return both the error and the response
558 (err, resp) => {
559 resolve({
560 err,
561 resp,
562 });
563 });
564 });
565}, _Transaction_withBeginTransaction = function _Transaction_withBeginTransaction(gaxOptions, fn, callback) {
566 (async () => {
567 if (__classPrivateFieldGet(this, _Transaction_state, "f") === TransactionState.NOT_STARTED) {
568 try {
569 await __classPrivateFieldGet(this, _Transaction_mutex, "f").runExclusive(async () => {
570 if (__classPrivateFieldGet(this, _Transaction_state, "f") === TransactionState.NOT_STARTED) {
571 // This sends an rpc call to get the transaction id
572 const runResults = await __classPrivateFieldGet(this, _Transaction_instances, "m", _Transaction_beginTransactionAsync).call(this, {
573 gaxOptions,
574 });
575 if (runResults.err) {
576 // The rpc getting the id was unsuccessful.
577 // Do not call the wrapped function.
578 throw runResults.err;
579 }
580 __classPrivateFieldGet(this, _Transaction_instances, "m", _Transaction_parseRunSuccess).call(this, runResults);
581 // The rpc saving the transaction id was successful.
582 // Now the wrapped function fn will be called.
583 }
584 });
585 }
586 catch (err) {
587 // Handle an error produced by the beginTransactionAsync call
588 return callback(err);
589 }
590 }
591 return fn();
592 })();
593};
594/*! Developer Documentation
595 *
596 * All async methods (except for streams) will return a Promise in the event
597 * that a callback is omitted.
598 */
599(0, promisify_1.promisifyAll)(Transaction, {
600 exclude: [
601 'createAggregationQuery',
602 'createQuery',
603 'delete',
604 'insert',
605 '#runAsync',
606 'save',
607 'update',
608 'upsert',
609 ],
610});
611//# sourceMappingURL=transaction.js.map
\No newline at end of file