request.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 projectify_1 = require("@google-cloud/projectify");
const promisify_1 = require("@google-cloud/promisify");
const arrify = require("arrify");
const concat = require('concat-stream');
const extend = require("extend");
const split_array_stream_1 = require("split-array-stream");
const streamEvents = require("stream-events");
const stream_1 = require("stream");
// Import the clients for each version supported by this package.
const gapic = Object.freeze({
    v1: require('./v1'),
});
const entity_1 = require("./entity");
const query_1 = require("./query");
/**
 * A map of read consistency values to proto codes.
 *
 * @type {object}
 * @private
 */
const CONSISTENCY_PROTO_CODE = {
    eventual: 2,
    strong: 1,
};
/**
 * Handle logic for Datastore API operations. Handles request logic for
 * Datastore.
 *
 * Creates requests to the Datastore endpoint. Designed to be inherited by
 * the {@link Datastore} and {@link Transaction} classes.
 *
 * @class
 */
class DatastoreRequest {
    /**
     * Format a user's input to mutation methods. This will create a deep clone of
     * the input, as well as allow users to pass an object in the format of an
     * entity.
     *
     * Both of the following formats can be supplied supported:
     *
     *     datastore.save({
     *       key: datastore.key('Kind'),
     *       data: { foo: 'bar' }
     *     }, (err) => {})
     *
     *     const entity = { foo: 'bar' }
     *     entity[datastore.KEY] = datastore.key('Kind')
     *     datastore.save(entity, (err) => {})
     *
     * @private
     *
     * @see [#1803]{@link https://github.com/GoogleCloudPlatform/google-cloud-node/issues/1803}
     *
     * @param {object} obj The user's input object.
     */
    static prepareEntityObject_(obj) {
        const entityObject = extend(true, {}, obj);
        // Entity objects are also supported.
        if (obj[entity_1.entity.KEY_SYMBOL]) {
            return {
                key: obj[entity_1.entity.KEY_SYMBOL],
                data: entityObject,
            };
        }
        return entityObject;
    }
    /**
     * Generate IDs without creating entities.
     *
     * @param {Key} key The key object to complete.
     * @param {number|object} options Either the number of IDs to allocate or an
     *     options object for further customization of the request.
     * @param {number} options.allocations How many IDs to allocate.
     * @param {object} [options.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 {array} callback.keys The generated IDs
     * @param {object} callback.apiResponse The full API response.
     *
     * @example
     * const incompleteKey = datastore.key(['Company']);
     *
     * //-
     * // The following call will create 100 new IDs from the Company kind, which
     * // exists under the default namespace.
     * //-
     * datastore.allocateIds(incompleteKey, 100, (err, keys) => {});
     *
     * //-
     * // Or, if you're using a transaction object.
     * //-
     * const transaction = datastore.transaction();
     *
     * transaction.run((err) => {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   transaction.allocateIds(incompleteKey, 100, (err, keys) => {
     *     if (err) {
     *       // Error handling omitted.
     *     }
     *
     *     transaction.commit((err) => {
     *       if (!err) {
     *         // Transaction committed successfully.
     *       }
     *     });
     *   });
     * });
     *
     * //-
     * // You may prefer to create IDs from a non-default namespace by providing
     * an
     * // incomplete key with a namespace. Similar to the previous example, the
     * call
     * // below will create 100 new IDs, but from the Company kind that exists
     * under
     * // the "ns-test" namespace.
     * //-
     * const incompleteKey = datastore.key({
     *   namespace: 'ns-test',
     *   path: ['Company']
     * });
     *
     * function callback(err, keys, apiResponse) {}
     *
     * datastore.allocateIds(incompleteKey, 100, callback);
     *
     * //-
     * // Returns a Promise if callback is omitted.
     * //-
     * datastore.allocateIds(incompleteKey, 100).then((data) => {
     *   const keys = data[0];
     *   const apiResponse = data[1];
     * });
     */
    allocateIds(key, options, callback) {
        if (entity_1.entity.isKeyComplete(key)) {
            throw new Error('An incomplete key should be provided.');
        }
        options = typeof options === 'number' ? { allocations: options } : options;
        this.request_({
            client: 'DatastoreClient',
            method: 'allocateIds',
            reqOpts: {
                keys: new Array(options.allocations).fill(entity_1.entity.keyToKeyProto(key)),
            },
            gaxOpts: options.gaxOptions,
        }, (err, resp) => {
            if (err) {
                callback(err, null, resp);
                return;
            }
            const keys = arrify(resp.keys).map(entity_1.entity.keyFromKeyProto);
            callback(null, keys, resp);
        });
    }
    /**
     * Retrieve the entities as a readable object stream.
     *
     * @throws {Error} If at least one Key object is not provided.
     *
     * @param {Key|Key[]} keys Datastore key object(s).
     * @param {object} [options] Optional configuration. See {@link Datastore#get}
     *     for a complete list of options.
     *
     * @example
     * const keys = [
     *   datastore.key(['Company', 123]),
     *   datastore.key(['Product', 'Computer'])
     * ];
     *
     * datastore.createReadStream(keys)
     *   .on('error', (err) =>  {})
     *   .on('data', (entity) => {
     *     // entity is an entity object.
     *   })
     *   .on('end', () => {
     *     // All entities retrieved.
     *   });
     */
    createReadStream(keys, options = {}) {
        keys = arrify(keys).map(entity_1.entity.keyToKeyProto);
        if (keys.length === 0) {
            throw new Error('At least one Key object is required.');
        }
        const makeRequest = (keys) => {
            const reqOpts = {
                keys,
            };
            if (options.consistency) {
                const code = CONSISTENCY_PROTO_CODE[options.consistency.toLowerCase()];
                reqOpts.readOptions = {
                    readConsistency: code,
                };
            }
            this.request_({
                client: 'DatastoreClient',
                method: 'lookup',
                reqOpts,
                gaxOpts: options.gaxOptions,
            }, (err, resp) => {
                if (err) {
                    stream.destroy(err);
                    return;
                }
                const entities = entity_1.entity.formatArray(resp.found, options.wrapNumbers);
                const nextKeys = (resp.deferred || [])
                    .map(entity_1.entity.keyFromKeyProto)
                    .map(entity_1.entity.keyToKeyProto);
                split_array_stream_1.split(entities, stream).then(streamEnded => {
                    if (streamEnded) {
                        return;
                    }
                    if (nextKeys.length > 0) {
                        makeRequest(nextKeys);
                        return;
                    }
                    stream.push(null);
                });
            });
        };
        const stream = streamEvents(new stream_1.Transform({ objectMode: true }));
        stream.once('reading', () => {
            makeRequest(keys);
        });
        return stream;
    }
    /**
     * Delete all entities identified with the specified key(s).
     *
     * @param {Key|Key[]} key Datastore key object(s).
     * @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 key = datastore.key(['Company', 123]);
     * datastore.delete(key, (err, apiResp) => {});
     *
     * //-
     * // Or, if you're using a transaction object.
     * //-
     * const transaction = datastore.transaction();
     *
     * transaction.run((err) => {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   transaction.delete(key);
     *
     *   transaction.commit((err) => {
     *     if (!err) {
     *       // Transaction committed successfully.
     *     }
     *   });
     * });
     *
     * //-
     * // Delete multiple entities at once.
     * //-
     * datastore.delete([
     *   datastore.key(['Company', 123]),
     *   datastore.key(['Product', 'Computer'])
     * ], (err, apiResponse) => {});
     *
     * //-
     * // Returns a Promise if callback is omitted.
     * //-
     * datastore.delete().then((data) => {
     *   const apiResponse = data[0];
     * });
     */
    delete(keys, gaxOptionsOrCallback, cb) {
        const gaxOptions = typeof gaxOptionsOrCallback === 'object' ? gaxOptionsOrCallback : {};
        const callback = typeof gaxOptionsOrCallback === 'function' ? gaxOptionsOrCallback : cb;
        const reqOpts = {
            mutations: arrify(keys).map(key => {
                return {
                    delete: entity_1.entity.keyToKeyProto(key),
                };
            }),
        };
        if (this.id) {
            this.requests_.push(reqOpts);
            return;
        }
        this.request_({
            client: 'DatastoreClient',
            method: 'commit',
            reqOpts,
            gaxOpts: gaxOptions,
        }, callback);
    }
    /**
     * Retrieve the entities identified with the specified key(s) in the current
     * transaction. Get operations require a valid key to retrieve the
     * key-identified entity from Datastore.
     *
     * @throws {Error} If at least one Key object is not provided.
     *
     * @param {Key|Key[]} keys Datastore key object(s).
     * @param {object} [options] Optional configuration.
     * @param {string} [options.consistency] Specify either `strong` or `eventual`.
     *     If not specified, default values are chosen by Datastore for the
     *     operation. Learn more about strong and eventual consistency
     *     [here](https://cloud.google.com/datastore/docs/articles/balancing-strong-and-eventual-consistency-with-google-cloud-datastore).
     * @param {object} [options.gaxOptions] Request configuration options, outlined
     *     here: https://googleapis.github.io/gax-nodejs/global.html#CallOptions.
     * @param {boolean | IntegerTypeCastOptions} [options.wrapNumbers=false]
     *     Wrap values of integerValue type in {@link Datastore#Int} objects.
     *     If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
     *     If an `object`, this will return a value returned by
     *     `wrapNumbers.integerTypeCastFunction`.
     *     Please see {@link IntegerTypeCastOptions} for options descriptions.
     * @param {function} callback The callback function.
     * @param {?error} callback.err An error returned while making this request
     * @param {object|object[]} callback.entity The entity object(s) which match
     *     the provided keys.
     *
     * @example
     * //-
     * // Get a single entity.
     * //-
     * const key = datastore.key(['Company', 123]);
     *
     * datastore.get(key, (err, entity) => {});
     *
     * //-
     * // Or, if you're using a transaction object.
     * //-
     * const transaction = datastore.transaction();
     *
     * transaction.run((err) => {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   transaction.get(key, (err, entity) => {
     *     if (err) {
     *       // Error handling omitted.
     *     }
     *
     *     transaction.commit((err) => {
     *       if (!err) {
     *         // Transaction committed successfully.
     *       }
     *     });
     *   });
     * });
     *
     * //-
     * // Get multiple entities at once with a callback.
     * //-
     * const keys = [
     *   datastore.key(['Company', 123]),
     *   datastore.key(['Product', 'Computer'])
     * ];
     *
     * datastore.get(keys, (err, entities) => {});
     *
     * //-
     * // Below is how to update the value of an entity with the help of the
     * // `save` method.
     * //-
     * datastore.get(key, (err, entity) => {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   entity.newValue = true;
     *
     *   datastore.save({
     *     key: key,
     *     data: entity
     *   }, (err) => {});
     * });
     *
     * //-
     * // Returns a Promise if callback is omitted.
     * //-
     * datastore.get(keys).then((data) => {
     *   const entities = data[0];
     * });
     */
    get(keys, optionsOrCallback, cb) {
        const options = typeof optionsOrCallback === 'object' && optionsOrCallback
            ? optionsOrCallback
            : {};
        const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
        this.createReadStream(keys, options)
            .on('error', callback)
            .pipe(concat((results) => {
            const isSingleLookup = !Array.isArray(keys);
            callback(null, isSingleLookup ? results[0] : results);
        }));
    }
    /**
     * Maps to {@link 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.
     * @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.
     */
    insert(entities, callback) {
        entities = arrify(entities)
            .map(DatastoreRequest.prepareEntityObject_)
            .map((x) => {
            x.method = 'insert';
            return x;
        });
        this.save(entities, callback);
    }
    /**
     * Datastore allows you to query entities by kind, filter them by property
     * filters, and sort them by a property name. Projection and pagination are
     * also supported.
     *
     * The query is run, and the results are returned as the second argument to
     * your callback. A third argument may also exist, which is a query object
     * that uses the end cursor from the previous query as the starting cursor for
     * the next query. You can pass that object back to this method to see if more
     * results exist.
     * @param {Query} query Query object.
     * @param {object} [options] Optional configuration.
     * @param {string} [options.consistency] Specify either `strong` or `eventual`.
     *     If not specified, default values are chosen by Datastore for the
     *     operation. Learn more about strong and eventual consistency
     *     [here](https://cloud.google.com/datastore/docs/articles/balancing-strong-and-eventual-consistency-with-google-cloud-datastore).
     * @param {object} [options.gaxOptions] Request configuration options, outlined
     *     here: https://googleapis.github.io/gax-nodejs/global.html#CallOptions.
     * @param {boolean | IntegerTypeCastOptions} [options.wrapNumbers=false]
     *     Wrap values of integerValue type in {@link Datastore#Int} objects.
     *     If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
     *     If an `object`, this will return a value returned by
     *     `wrapNumbers.integerTypeCastFunction`.
     *     Please see {@link IntegerTypeCastOptions} for options descriptions.
     * @param {function} [callback] The callback function. If omitted, a readable
     *     stream instance is returned.
     * @param {?error} callback.err An error returned while making this request
     * @param {object[]} callback.entities A list of entities.
     * @param {object} callback.info An object useful for pagination.
     * @param {?string} callback.info.endCursor Use this in a follow-up query to
     *     begin from where these results ended.
     * @param {string} callback.info.moreResults Datastore responds with one of:
     *
     *     - {@link Datastore#MORE_RESULTS_AFTER_LIMIT}: There *may* be more
     *       results after the specified limit.
     *     - {@link Datastore#MORE_RESULTS_AFTER_CURSOR}: There *may* be more
     *       results after the specified end cursor.
     *     - {@link Datastore#NO_MORE_RESULTS}: There are no more results.
     *
     * @example
     * //-
     * // Where you see `transaction`, assume this is the context that's relevant
     * to
     * // your use, whether that be a Datastore or a Transaction object.
     * //-
     * const query = datastore.createQuery('Lion');
     *
     * datastore.runQuery(query, (err, entities, info) => {
     *   // entities = An array of records.
     *
     *   // Access the Key object for an entity.
     *   const firstEntityKey = entities[0][datastore.KEY];
     * });
     *
     * //-
     * // Or, if you're using a transaction object.
     * //-
     * const transaction = datastore.transaction();
     *
     * transaction.run((err) => {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   transaction.runQuery(query, (err, entities) => {
     *     if (err) {
     *       // Error handling omitted.
     *     }
     *
     *     transaction.commit((err) => {
     *       if (!err) {
     *         // Transaction committed successfully.
     *       }
     *     });
     *   });
     * });
     *
     * //-
     * // A keys-only query returns just the keys of the result entities instead
     * of
     * // the entities themselves, at lower latency and cost.
     * //-
     * const keysOnlyQuery = datastore.createQuery('Lion').select('__key__');
     *
     * datastore.runQuery(keysOnlyQuery, (err, entities) => {
     *   const keys = entities.map((entity) => {
     *     return entity[datastore.KEY];
     *   });
     * });
     *
     * //-
     * // Returns a Promise if callback is omitted.
     * //-
     * datastore.runQuery(query).then((data) => {
     *   const entities = data[0];
     * });
     */
    runQuery(query, optionsOrCallback, cb) {
        const options = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
        const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
        let info;
        this.runQueryStream(query, options)
            .on('error', callback)
            .on('info', info_ => {
            info = info_;
        })
            .pipe(concat((results) => {
            callback(null, results, info);
        }));
    }
    /**
     * Get a list of entities as a readable object stream.
     *
     * See {@link Datastore#runQuery} for a list of all available options.
     *
     * @param {Query} query Query object.
     * @param {object} [options] Optional configuration.
     * @param {object} [options.gaxOptions] Request configuration options, outlined
     *     here: https://googleapis.github.io/gax-nodejs/global.html#CallOptions.
     *
     * @example
     * datastore.runQueryStream(query)
     *   .on('error', console.error)
     *   .on('data', (entity) => {
     *     // Access the Key object for this entity.
     *     const key = entity[datastore.KEY];
     *   })
     *   .on('info', (info) => {})
     *   .on('end', () => {
     *     // All entities retrieved.
     *   });
     *
     * //-
     * // If you anticipate many results, you can end a stream early to prevent
     * // unnecessary processing and API requests.
     * //-
     * datastore.runQueryStream(query)
     *   .on('data', (entity) => {
     *     this.end();
     *   });
     */
    runQueryStream(query, options = {}) {
        query = extend(true, new query_1.Query(), query);
        const makeRequest = (query) => {
            const reqOpts = {};
            try {
                reqOpts.query = entity_1.entity.queryToQueryProto(query);
            }
            catch (e) {
                // using setImmediate here to make sure this doesn't throw a
                // synchronous error
                setImmediate(onResultSet, e);
                return;
            }
            if (options.consistency) {
                const code = CONSISTENCY_PROTO_CODE[options.consistency.toLowerCase()];
                reqOpts.readOptions = {
                    readConsistency: code,
                };
            }
            if (query.namespace) {
                reqOpts.partitionId = {
                    namespaceId: query.namespace,
                };
            }
            this.request_({
                client: 'DatastoreClient',
                method: 'runQuery',
                reqOpts,
                gaxOpts: options.gaxOptions,
            }, onResultSet);
        };
        function onResultSet(err, resp) {
            if (err) {
                stream.destroy(err);
                return;
            }
            const info = {
                moreResults: resp.batch.moreResults,
            };
            if (resp.batch.endCursor) {
                info.endCursor = resp.batch.endCursor.toString('base64');
            }
            let entities = [];
            if (resp.batch.entityResults) {
                entities = entity_1.entity.formatArray(resp.batch.entityResults, options.wrapNumbers);
            }
            // Emit each result right away, then get the rest if necessary.
            split_array_stream_1.split(entities, stream).then(streamEnded => {
                if (streamEnded) {
                    return;
                }
                if (resp.batch.moreResults !== 'NOT_FINISHED') {
                    stream.emit('info', info);
                    stream.push(null);
                    return;
                }
                // The query is "NOT_FINISHED". Get the rest of the results.
                const offset = query.offsetVal === -1 ? 0 : query.offsetVal;
                query.start(info.endCursor).offset(offset - resp.batch.skippedResults);
                const limit = query.limitVal;
                if (limit && limit > -1) {
                    query.limit(limit - resp.batch.entityResults.length);
                }
                makeRequest(query);
            });
        }
        const stream = streamEvents(new stream_1.Transform({ objectMode: true }));
        stream.once('reading', () => {
            makeRequest(query);
        });
        return stream;
    }
    /**
     * Insert or update the specified object(s). 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.
     *
     * To prevent large properties from being included in *all* indexes, you must supply
     * `excludeLargeProperties: true`.
     *  See below for an example.
     *
     * @borrows {@link Transaction#save} as save
     *
     * @throws {Error} If an unrecognized method is provided.
     *
     * @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
     * @param {boolean} [entities.excludeLargeProperties] Automatically exclude
     *  large properties from indexing. It help in storing large values.
     * @param {string} [entities.method] Explicit method to use, either 'insert',
     *     'update', or 'upsert'.
     * @param {object} entities.data Data to save with the provided key.
     *     entity.
     * @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
     * //-
     * // Save a single entity.
     * //
     * // Notice that we are providing an incomplete key. After saving, the
     * original
     * // Key object used to save will be updated to contain the path with its
     * // generated ID.
     * //-
     * const key = datastore.key('Company');
     * const entity = {
     *   key: key,
     *   data: {
     *     rating: '10'
     *   }
     * };
     *
     * datastore.save(entity, (err) => {
     *   console.log(key.path); // [ 'Company', 5669468231434240 ]
     *   console.log(key.namespace); // undefined
     * });
     *
     * //-
     * // Save a single entity using a provided name instead of auto-generated ID.
     * //
     * // Here we are providing a key with name instead of an ID. After saving,
     * the
     * // original Key object used to save will be updated to contain the path
     * with
     * // the name instead of a generated ID.
     * //-
     * const key = datastore.key(['Company', 'donutshack']);
     * const entity = {
     *   key: key,
     *   data: {
     *     name: 'DonutShack',
     *     rating: 8
     *   }
     * };
     *
     * datastore.save(entity, (err) => {
     *   console.log(key.path); // ['Company', 'donutshack']
     *   console.log(key.namespace); // undefined
     * });
     *
     * //-
     * // Save a single entity with a provided namespace. Namespaces allow for
     * // multitenancy. To read more about this, see
     * // [the Datastore docs on key concepts](https://goo.gl/M1LUAu).
     * //
     * // Here we are providing a key with namespace.
     * //-
     * const key = datastore.key({
     *   namespace: 'my-namespace',
     *   path: ['Company', 'donutshack']
     * });
     *
     * const entity = {
     *   key: key,
     *   data: {
     *     name: 'DonutShack',
     *     rating: 8
     *   }
     * };
     *
     * datastore.save(entity, (err) => {
     *   console.log(key.path); // ['Company', 'donutshack']
     *   console.log(key.namespace); // 'my-namespace'
     * });
     *
     * //-
     * // Save different types of data, including ints, doubles, dates, booleans,
     * // blobs, and lists.
     * //
     * // Notice that we are providing an incomplete key. After saving, the
     * original
     * // Key object used to save will be updated to contain the path with its
     * // generated ID.
     * //-
     * const key = datastore.key('Company');
     * const entity = {
     *   key: key,
     *   data: {
     *     name: 'DonutShack',
     *     rating: datastore.int(10),
     *     worth: datastore.double(123456.78),
     *     location: datastore.geoPoint({
     *       latitude: 40.6894,
     *       longitude: -74.0447
     *     }),
     *     numDonutsServed: 45,
     *     founded: new Date('Tue May 12 2015 15:30:00 GMT-0400 (EDT)'),
     *     isStartup: true,
     *     donutEmoji: Buffer.from('\uD83C\uDF69'),
     *     keywords: [
     *       'donut',
     *       'coffee',
     *       'yum'
     *     ]
     *   }
     * };
     *
     * datastore.save(entity, (err, apiResponse) => {});
     *
     * //-
     * // Use an array, `excludeFromIndexes`, to exclude properties from indexing.
     * // This will allow storing string values larger than 1500 bytes.
     * //-
     * const entity = {
     *   key: datastore.key('Company'),
     *   excludeFromIndexes: [
     *     'description',
     *     'embeddedEntity.description',
     *     'arrayValue[]',
     *     'arrayValue[].description'
     *   ],
     *   data: {
     *     description: 'Long string (...)',
     *     embeddedEntity: {
     *       description: 'Long string (...)'
     *     },
     *     arrayValue: [
     *       'Long string (...)',
     *       {
     *         description: 'Long string (...)'
     *       }
     *     ]
     *   }
     * };
     *
     * datastore.save(entity, (err, apiResponse) => {});
     * //-
     * // Use boolean `excludeLargeProperties`, to auto exclude Large properties from indexing.
     * // This will allow storing string values larger than 1500 bytes.
     * //-
     * const entity = {
     *   key: datastore.key('Company'),
     *   data: {
     *     description: 'Long string (...)',
     *     embeddedEntity: {
     *       description: 'Long string (...)'
     *     },
     *     arrayValue: [
     *       'Long string (...)',
     *       {
     *         description: 'Long string (...)'
     *       }
     *     ]
     *   },
     *   excludeLargeProperties: true
     * };
     *
     * datastore.save(entity, (err, apiResponse) => {});
     *
     * //-
     * // Save multiple entities at once.
     * //-
     * const companyKey = datastore.key(['Company', 123]);
     * const productKey = datastore.key(['Product', 'Computer']);
     * const entities = [
     *   {
     *     key: companyKey,
     *     data: {
     *       HQ: 'Dallas, TX'
     *     }
     *   },
     *   {
     *     key: productKey,
     *     data: {
     *       vendor: 'Dell'
     *     }
     *   }
     * ];
     *
     * datastore.save(entities, (err, apiResponse) => {});
     *
     * //-
     * // Explicitly attempt to 'insert' a specific entity.
     * //-
     * const userKey = datastore.key(['User', 'chilts']);
     * const entity = {
     *   key: userKey,
     *   method: 'insert',
     *   data: {
     *     fullName: 'Andrew Chilton'
     *   }
     * };
     *
     * datastore.save(entity, (err, apiResponse) => {});
     *
     * //-
     * // Returns a Promise if callback is omitted.
     * //-
     * datastore.save(entity).then((data) => {
     *   const apiResponse = data[0];
     * });
     */
    save(entities, gaxOptionsOrCallback, cb) {
        entities = arrify(entities);
        const gaxOptions = typeof gaxOptionsOrCallback === 'object' ? gaxOptionsOrCallback : {};
        const callback = typeof gaxOptionsOrCallback === 'function' ? gaxOptionsOrCallback : cb;
        const insertIndexes = {};
        const mutations = [];
        const methods = {
            insert: true,
            update: true,
            upsert: true,
        };
        // Iterate over the entity objects, build a proto from all keys and values,
        // then place in the correct mutation array (insert, update, etc).
        entities
            .map(DatastoreRequest.prepareEntityObject_)
            .forEach((entityObject, index) => {
            const mutation = {};
            let entityProto = {};
            let method = 'upsert';
            if (entityObject.method) {
                if (methods[entityObject.method]) {
                    method = entityObject.method;
                }
                else {
                    throw new Error('Method ' + entityObject.method + ' not recognized.');
                }
            }
            if (entityObject.excludeLargeProperties) {
                entityObject.excludeFromIndexes = entity_1.entity.findLargeProperties_(entityObject.data, '', entityObject.excludeFromIndexes);
            }
            if (!entity_1.entity.isKeyComplete(entityObject.key)) {
                insertIndexes[index] = true;
            }
            // @TODO remove in @google-cloud/datastore@2.0.0
            // This was replaced with a more efficient mechanism in the top-level
            // `excludeFromIndexes` option.
            if (Array.isArray(entityObject.data)) {
                entityProto.properties = entityObject.data.reduce((acc, data) => {
                    const value = entity_1.entity.encodeValue(data.value);
                    if (typeof data.excludeFromIndexes === 'boolean') {
                        const excluded = data.excludeFromIndexes;
                        let values = value.arrayValue && value.arrayValue.values;
                        if (values) {
                            values = values.map((x) => {
                                x.excludeFromIndexes = excluded;
                                return x;
                            });
                        }
                        else {
                            value.excludeFromIndexes = data.excludeFromIndexes;
                        }
                    }
                    acc[data.name] = value;
                    return acc;
                }, {});
            }
            else {
                entityProto = entity_1.entity.entityToEntityProto(entityObject);
            }
            entityProto.key = entity_1.entity.keyToKeyProto(entityObject.key);
            mutation[method] = entityProto;
            mutations.push(mutation);
        });
        const reqOpts = {
            mutations,
        };
        function onCommit(err, resp) {
            if (err || !resp) {
                callback(err, resp);
                return;
            }
            arrify(resp.mutationResults).forEach((result, index) => {
                if (!result.key) {
                    return;
                }
                if (insertIndexes[index]) {
                    const id = entity_1.entity.keyFromKeyProto(result.key).id;
                    entities[index].key.id = id;
                }
            });
            callback(null, resp);
        }
        if (this.id) {
            this.requests_.push(reqOpts);
            this.requestCallbacks_.push(onCommit);
            return;
        }
        this.request_({
            client: 'DatastoreClient',
            method: 'commit',
            reqOpts,
            gaxOpts: gaxOptions,
        }, onCommit);
    }
    /**
     * Maps to {@link 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.
     * @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.
     */
    update(entities, callback) {
        entities = arrify(entities)
            .map(DatastoreRequest.prepareEntityObject_)
            .map((x) => {
            x.method = 'update';
            return x;
        });
        this.save(entities, callback);
    }
    /**
     * Maps to {@link 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.
     * @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.
     */
    upsert(entities, callback) {
        entities = arrify(entities)
            .map(DatastoreRequest.prepareEntityObject_)
            .map((x) => {
            x.method = 'upsert';
            return x;
        });
        this.save(entities, callback);
    }
    /**
     * Merge the specified object(s). If a key is incomplete, its associated object
     * is inserted and the original Key object is updated to contain the generated ID.
     * 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 get the data from datastore
     * and merge 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.
     *
     * Maps to {@link Datastore#save}, forcing the method to be `merge`.
     *
     * @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 merge to the same for provided key.
     * @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.
     */
    merge(entities, callback) {
        const transaction = this.datastore.transaction();
        transaction.run(async (err) => {
            if (err) {
                transaction.rollback();
                callback(err);
                return;
            }
            try {
                await Promise.all(arrify(entities).map(async (objEntity) => {
                    const obj = DatastoreRequest.prepareEntityObject_(objEntity);
                    const [data] = await transaction.get(obj.key);
                    obj.method = 'upsert';
                    obj.data = Object.assign({}, data, obj.data);
                    transaction.save(obj);
                }));
                const [response] = await transaction.commit();
                callback(null, response);
            }
            catch (err) {
                transaction.rollback();
                callback(err);
            }
        });
    }
    /**
     * Make a request to the API endpoint. Properties to indicate a transactional
     * or non-transactional operation are added automatically.
     *
     * @param {object} config Configuration object.
     * @param {object} config.gaxOpts GAX options.
     * @param {function} config.method The gax method to call.
     * @param {object} config.reqOpts Request options.
     * @param {function} callback The callback function.
     *
     * @private
     */
    request_(config, callback) {
        const datastore = this.datastore;
        const isTransaction = this.id ? true : false;
        const method = config.method;
        let reqOpts = extend(true, {}, config.reqOpts);
        reqOpts.projectId = datastore.projectId;
        // Set properties to indicate if we're in a transaction or not.
        if (method === 'commit') {
            if (isTransaction) {
                reqOpts.mode = 'TRANSACTIONAL';
                reqOpts.transaction = this.id;
            }
            else {
                reqOpts.mode = 'NON_TRANSACTIONAL';
            }
        }
        if (method === 'rollback') {
            reqOpts.transaction = this.id;
        }
        if (isTransaction && (method === 'lookup' || method === 'runQuery')) {
            if (reqOpts.readOptions && reqOpts.readOptions.readConsistency) {
                throw new Error('Read consistency cannot be specified in a transaction.');
            }
            reqOpts.readOptions = {
                transaction: this.id,
            };
        }
        datastore.auth.getProjectId((err, projectId) => {
            if (err) {
                callback(err);
                return;
            }
            const clientName = config.client;
            if (!datastore.clients_.has(clientName)) {
                datastore.clients_.set(clientName, new gapic.v1[clientName](datastore.options));
            }
            const gaxClient = datastore.clients_.get(clientName);
            reqOpts = projectify_1.replaceProjectIdToken(reqOpts, projectId);
            const gaxOpts = extend(true, {}, config.gaxOpts, {
                headers: {
                    'google-cloud-resource-prefix': `projects/${projectId}`,
                },
            });
            gaxClient[method](reqOpts, gaxOpts, callback);
        });
    }
}
exports.DatastoreRequest = DatastoreRequest;
/*! Developer Documentation
 *
 * All async methods (except for streams) will return a Promise in the event
 * that a callback is omitted.
 */
promisify_1.promisifyAll(DatastoreRequest);
//# sourceMappingURL=request.js.map