1 | ;
|
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 | */
|
17 | Object.defineProperty(exports, "__esModule", { value: true });
|
18 | exports.Transaction = void 0;
|
19 | const promisify_1 = require("@google-cloud/promisify");
|
20 | const arrify = require("arrify");
|
21 | const _1 = require(".");
|
22 | const entity_1 = require("./entity");
|
23 | const 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 | */
|
44 | class 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 | }
|
471 | exports.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 |