UNPKG

18.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 */
17Object.defineProperty(exports, "__esModule", { value: true });
18exports.Transaction = void 0;
19const promisify_1 = require("@google-cloud/promisify");
20const arrify = require("arrify");
21const _1 = require(".");
22const entity_1 = require("./entity");
23const request_1 = require("./request");
24/**
25 * A transaction is a set of Datastore operations on one or more entities. Each
26 * transaction is guaranteed to be atomic, which means that transactions are
27 * never partially applied. Either all of the operations in the transaction are
28 * applied, or none of them are applied.
29 *
30 * @see {@link https://cloud.google.com/datastore/docs/concepts/transactions| Transactions Reference}
31 *
32 * @class
33 * @extends {Request}
34 * @param {Datastore} datastore A Datastore instance.
35 * @mixes module:datastore/request
36 *
37 * @example
38 * ```
39 * const {Datastore} = require('@google-cloud/datastore');
40 * const datastore = new Datastore();
41 * const transaction = datastore.transaction();
42 * ```
43 */
44class Transaction extends request_1.DatastoreRequest {
45 constructor(datastore, options) {
46 super();
47 /**
48 * @name Transaction#datastore
49 * @type {Datastore}
50 */
51 this.datastore = datastore;
52 /**
53 * @name Transaction#namespace
54 * @type {string}
55 */
56 this.namespace = datastore.namespace;
57 options = options || {};
58 this.id = options.id;
59 this.readOnly = options.readOnly === true;
60 this.request = datastore.request_.bind(datastore);
61 // A queue for entity modifications made during the transaction.
62 this.modifiedEntities_ = [];
63 // Queue the callbacks that process the API responses.
64 this.requestCallbacks_ = [];
65 // Queue the requests to make when we send the transactional commit.
66 this.requests_ = [];
67 }
68 commit(gaxOptionsOrCallback, cb) {
69 const callback = typeof gaxOptionsOrCallback === 'function'
70 ? gaxOptionsOrCallback
71 : typeof cb === 'function'
72 ? cb
73 : () => { };
74 const gaxOptions = typeof gaxOptionsOrCallback === 'object' ? gaxOptionsOrCallback : {};
75 if (this.skipCommit) {
76 setImmediate(callback);
77 return;
78 }
79 const keys = {};
80 this.modifiedEntities_
81 // Reverse the order of the queue to respect the "last queued request
82 // wins" behavior.
83 .reverse()
84 // Limit the operations we're going to send through to only the most
85 // recently queued operations. E.g., if a user tries to save with the
86 // same key they just asked to be deleted, the delete request will be
87 // ignored, giving preference to the save operation.
88 .filter((modifiedEntity) => {
89 const key = modifiedEntity.entity.key;
90 if (!entity_1.entity.isKeyComplete(key))
91 return true;
92 const stringifiedKey = JSON.stringify(modifiedEntity.entity.key);
93 if (!keys[stringifiedKey]) {
94 keys[stringifiedKey] = true;
95 return true;
96 }
97 return false;
98 })
99 // Group entities together by method: `save` mutations, then `delete`.
100 // Note: `save` mutations being first is required to maintain order when
101 // assigning IDs to incomplete keys.
102 .sort((a, b) => {
103 return a.method < b.method ? 1 : a.method > b.method ? -1 : 0;
104 })
105 // Group arguments together so that we only make one call to each
106 // method. This is important for `DatastoreRequest.save`, especially, as
107 // that method handles assigning auto-generated IDs to the original keys
108 // passed in. When we eventually execute the `save` method's API
109 // callback, having all the keys together is necessary to maintain
110 // order.
111 .reduce((acc, entityObject) => {
112 const lastEntityObject = acc[acc.length - 1];
113 const sameMethod = lastEntityObject && entityObject.method === lastEntityObject.method;
114 if (!lastEntityObject || !sameMethod) {
115 acc.push(entityObject);
116 }
117 else {
118 lastEntityObject.args = lastEntityObject.args.concat(entityObject.args);
119 }
120 return acc;
121 }, [])
122 // Call each of the mutational methods (DatastoreRequest[save,delete])
123 // to build up a `req` array on this instance. This will also build up a
124 // `callbacks` array, that is the same callback that would run if we
125 // were using `save` and `delete` outside of a transaction, to process
126 // the response from the API.
127 .forEach((modifiedEntity) => {
128 const method = modifiedEntity.method;
129 const args = modifiedEntity.args.reverse();
130 _1.Datastore.prototype[method].call(this, args, () => { });
131 });
132 // Take the `req` array built previously, and merge them into one request to
133 // send as the final transactional commit.
134 const reqOpts = {
135 mutations: this.requests_
136 .map((x) => x.mutations)
137 .reduce((a, b) => a.concat(b), []),
138 };
139 this.request_({
140 client: 'DatastoreClient',
141 method: 'commit',
142 reqOpts,
143 gaxOpts: gaxOptions || {},
144 }, (err, resp) => {
145 if (err) {
146 // Rollback automatically for the user.
147 this.rollback(() => {
148 // Provide the error & API response from the failed commit to the
149 // user. Even a failed rollback should be transparent. RE:
150 // https://github.com/GoogleCloudPlatform/google-cloud-node/pull/1369#discussion_r66833976
151 callback(err, resp);
152 });
153 return;
154 }
155 // The `callbacks` array was built previously. These are the callbacks
156 // that handle the API response normally when using the
157 // DatastoreRequest.save and .delete methods.
158 this.requestCallbacks_.forEach((cb) => {
159 cb(null, resp);
160 });
161 callback(null, resp);
162 });
163 }
164 createQuery(namespaceOrKind, kind) {
165 return this.datastore.createQuery.call(this, namespaceOrKind, kind);
166 }
167 /**
168 * Create an aggregation query from the query specified. See {module:datastore/query} for all
169 * of the available methods.
170 *
171 */
172 createAggregationQuery(query) {
173 return this.datastore.createAggregationQuery.call(this, query);
174 }
175 /**
176 * Delete all entities identified with the specified key(s) in the current
177 * transaction.
178 *
179 * @param {Key|Key[]} key Datastore key object(s).
180 *
181 * @example
182 * ```
183 * const {Datastore} = require('@google-cloud/datastore');
184 * const datastore = new Datastore();
185 * const transaction = datastore.transaction();
186 *
187 * transaction.run((err) => {
188 * if (err) {
189 * // Error handling omitted.
190 * }
191 *
192 * // Delete a single entity.
193 * transaction.delete(datastore.key(['Company', 123]));
194 *
195 * // Delete multiple entities at once.
196 * transaction.delete([
197 * datastore.key(['Company', 123]),
198 * datastore.key(['Product', 'Computer'])
199 * ]);
200 *
201 * transaction.commit((err) => {
202 * if (!err) {
203 * // Transaction committed successfully.
204 * }
205 * });
206 * });
207 * ```
208 */
209 // eslint-disable-next-line @typescript-eslint/no-explicit-any
210 delete(entities) {
211 arrify(entities).forEach((ent) => {
212 this.modifiedEntities_.push({
213 entity: {
214 key: ent,
215 },
216 method: 'delete',
217 args: [ent],
218 });
219 });
220 }
221 /**
222 * 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`.
223 *
224 * @param {object|object[]} entities Datastore key object(s).
225 * @param {Key} entities.key Datastore key object.
226 * @param {string[]} [entities.excludeFromIndexes] Exclude properties from
227 * indexing using a simple JSON path notation. See the examples in
228 * {@link Datastore#save} to see how to target properties at different
229 * levels of nesting within your entity.
230 * @param {object} entities.data Data to save with the provided key.
231 */
232 insert(entities) {
233 entities = arrify(entities)
234 .map(request_1.DatastoreRequest.prepareEntityObject_)
235 .map((x) => {
236 x.method = 'insert';
237 return x;
238 });
239 this.save(entities);
240 }
241 rollback(gaxOptionsOrCallback, cb) {
242 const gaxOptions = typeof gaxOptionsOrCallback === 'object' ? gaxOptionsOrCallback : {};
243 const callback = typeof gaxOptionsOrCallback === 'function' ? gaxOptionsOrCallback : cb;
244 this.request_({
245 client: 'DatastoreClient',
246 method: 'rollback',
247 gaxOpts: gaxOptions || {},
248 }, (err, resp) => {
249 this.skipCommit = true;
250 callback(err || null, resp);
251 });
252 }
253 run(optionsOrCallback, cb) {
254 const options = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
255 const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
256 const reqOpts = {
257 transactionOptions: {},
258 };
259 if (options.readOnly || this.readOnly) {
260 reqOpts.transactionOptions.readOnly = {};
261 }
262 if (options.transactionId || this.id) {
263 reqOpts.transactionOptions.readWrite = {
264 previousTransaction: options.transactionId || this.id,
265 };
266 }
267 if (options.transactionOptions) {
268 reqOpts.transactionOptions = options.transactionOptions;
269 }
270 this.request_({
271 client: 'DatastoreClient',
272 method: 'beginTransaction',
273 reqOpts,
274 gaxOpts: options.gaxOptions,
275 }, (err, resp) => {
276 if (err) {
277 callback(err, null, resp);
278 return;
279 }
280 this.id = resp.transaction;
281 callback(null, this, resp);
282 });
283 }
284 /**
285 * Insert or update the specified object(s) in the current transaction. If a
286 * key is incomplete, its associated object is inserted and the original Key
287 * object is updated to contain the generated ID.
288 *
289 * This method will determine the correct Datastore method to execute
290 * (`upsert`, `insert`, or `update`) by using the key(s) provided. For
291 * example, if you provide an incomplete key (one without an ID), the request
292 * will create a new entity and have its ID automatically assigned. If you
293 * provide a complete key, the entity will be updated with the data specified.
294 *
295 * By default, all properties are indexed. To prevent a property from being
296 * included in *all* indexes, you must supply an `excludeFromIndexes` array.
297 * See below for an example.
298 *
299 * @param {object|object[]} entities Datastore key object(s).
300 * @param {Key} entities.key Datastore key object.
301 * @param {string[]} [entities.excludeFromIndexes] Exclude properties from
302 * indexing using a simple JSON path notation. See the example below to
303 * see how to target properties at different levels of nesting within your
304 * entity.
305 * @param {object} entities.data Data to save with the provided key.
306 *
307 * @example
308 * ```
309 * <caption>Save a single entity.</caption>
310 * const {Datastore} = require('@google-cloud/datastore');
311 * const datastore = new Datastore();
312 * const transaction = datastore.transaction();
313 *
314 * // Notice that we are providing an incomplete key. After the transaction is
315 * // committed, the Key object held by the `key` variable will be populated
316 * // with a path containing its generated ID.
317 * //-
318 * const key = datastore.key('Company');
319 *
320 * transaction.run((err) => {
321 * if (err) {
322 * // Error handling omitted.
323 * }
324 *
325 * transaction.save({
326 * key: key,
327 * data: {
328 * rating: '10'
329 * }
330 * });
331 *
332 * transaction.commit((err) => {
333 * if (!err) {
334 * // Data saved successfully.
335 * }
336 * });
337 * });
338 *
339 * ```
340 * @example
341 * ```
342 * const {Datastore} = require('@google-cloud/datastore');
343 * const datastore = new Datastore();
344 * const transaction = datastore.transaction();
345 *
346 * // Use an array, `excludeFromIndexes`, to exclude properties from indexing.
347 * // This will allow storing string values larger than 1500 bytes.
348 *
349 * transaction.run((err) => {
350 * if (err) {
351 * // Error handling omitted.
352 * }
353 *
354 * transaction.save({
355 * key: key,
356 * excludeFromIndexes: [
357 * 'description',
358 * 'embeddedEntity.description',
359 * 'arrayValue[].description'
360 * ],
361 * data: {
362 * description: 'Long string (...)',
363 * embeddedEntity: {
364 * description: 'Long string (...)'
365 * },
366 * arrayValue: [
367 * {
368 * description: 'Long string (...)'
369 * }
370 * ]
371 * }
372 * });
373 *
374 * transaction.commit((err) => {
375 * if (!err) {
376 * // Data saved successfully.
377 * }
378 * });
379 * });
380 *
381 * ```
382 * @example
383 * ```
384 * <caption>Save multiple entities at once.</caption>
385 * const {Datastore} = require('@google-cloud/datastore');
386 * const datastore = new Datastore();
387 * const transaction = datastore.transaction();
388 * const companyKey = datastore.key(['Company', 123]);
389 * const productKey = datastore.key(['Product', 'Computer']);
390 *
391 * transaction.run((err) => {
392 * if (err) {
393 * // Error handling omitted.
394 * }
395 *
396 * transaction.save([
397 * {
398 * key: companyKey,
399 * data: {
400 * HQ: 'Dallas, TX'
401 * }
402 * },
403 * {
404 * key: productKey,
405 * data: {
406 * vendor: 'Dell'
407 * }
408 * }
409 * ]);
410 *
411 * transaction.commit((err) => {
412 * if (!err) {
413 * // Data saved successfully.
414 * }
415 * });
416 * });
417 * ```
418 */
419 save(entities) {
420 arrify(entities).forEach((ent) => {
421 this.modifiedEntities_.push({
422 entity: {
423 key: ent.key,
424 },
425 method: 'save',
426 args: [ent],
427 });
428 });
429 }
430 /**
431 * 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`.
432 *
433 * @param {object|object[]} entities Datastore key object(s).
434 * @param {Key} entities.key Datastore key object.
435 * @param {string[]} [entities.excludeFromIndexes] Exclude properties from
436 * indexing using a simple JSON path notation. See the examples in
437 * {@link Datastore#save} to see how to target properties at different
438 * levels of nesting within your entity.
439 * @param {object} entities.data Data to save with the provided key.
440 */
441 update(entities) {
442 entities = arrify(entities)
443 .map(request_1.DatastoreRequest.prepareEntityObject_)
444 .map((x) => {
445 x.method = 'update';
446 return x;
447 });
448 this.save(entities);
449 }
450 /**
451 * 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`.
452 *
453 * @param {object|object[]} entities Datastore key object(s).
454 * @param {Key} entities.key Datastore key object.
455 * @param {string[]} [entities.excludeFromIndexes] Exclude properties from
456 * indexing using a simple JSON path notation. See the examples in
457 * {@link Datastore#save} to see how to target properties at different
458 * levels of nesting within your entity.
459 * @param {object} entities.data Data to save with the provided key.
460 */
461 upsert(entities) {
462 entities = arrify(entities)
463 .map(request_1.DatastoreRequest.prepareEntityObject_)
464 .map((x) => {
465 x.method = 'upsert';
466 return x;
467 });
468 this.save(entities);
469 }
470}
471exports.Transaction = Transaction;
472/*! Developer Documentation
473 *
474 * All async methods (except for streams) will return a Promise in the event
475 * that a callback is omitted.
476 */
477(0, promisify_1.promisifyAll)(Transaction, {
478 exclude: [
479 'createAggregationQuery',
480 'createQuery',
481 'delete',
482 'insert',
483 'save',
484 'update',
485 'upsert',
486 ],
487});
488//# sourceMappingURL=transaction.js.map
\No newline at end of file