UNPKG

@google-cloud/datastore

Version:
807 lines 30.3 kB
"use strict"; /*! * Copyright 2014 Google LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.DatastoreRequest = exports.TransactionState = exports.transactionExpiredError = void 0; exports.getTransactionRequest = getTransactionRequest; const promisify_1 = require("@google-cloud/promisify"); const arrify = require("arrify"); // eslint-disable-next-line @typescript-eslint/no-var-requires const concat = require('concat-stream'); const extend = require("extend"); const split_array_stream_1 = require("split-array-stream"); const stream_1 = require("stream"); // eslint-disable-next-line @typescript-eslint/no-var-requires const streamEvents = require('stream-events'); exports.transactionExpiredError = 'This transaction has already expired.'; // 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"); const _1 = require("."); const google_gax_1 = require("google-gax"); const gax = require("google-gax"); const root = gax.protobuf.loadSync('google/protobuf/struct.proto'); const Struct = root.lookupType('Struct'); // This function decodes Struct proto values function decodeStruct(structValue) { return google_gax_1.serializer.toProto3JSON(Struct.fromObject(structValue)); } // This function gets a RunQueryInfo object that contains explain metrics that // were returned from the server. function getInfoFromStats(resp) { // Decode struct values stored in planSummary and executionStats const explainMetrics = {}; if (resp && resp.explainMetrics && resp.explainMetrics.planSummary && resp.explainMetrics.planSummary.indexesUsed) { Object.assign(explainMetrics, { planSummary: { indexesUsed: resp.explainMetrics.planSummary.indexesUsed.map((index) => decodeStruct(index)), }, }); } if (resp && resp.explainMetrics && resp.explainMetrics.executionStats) { const executionStats = {}; { const resultsReturned = resp.explainMetrics.executionStats.resultsReturned; if (resultsReturned) { Object.assign(executionStats, { resultsReturned: typeof resultsReturned === 'string' ? parseInt(resultsReturned) : resultsReturned, }); } } { const executionDuration = resp.explainMetrics.executionStats.executionDuration; if (executionDuration) { Object.assign(executionStats, { executionDuration: typeof executionDuration === 'string' ? parseInt(executionDuration) : executionDuration, }); } } { const readOperations = resp.explainMetrics.executionStats.readOperations; if (readOperations) { Object.assign(executionStats, { readOperations: typeof readOperations === 'string' ? parseInt(readOperations) : readOperations, }); } } { const debugStats = resp.explainMetrics.executionStats.debugStats; if (debugStats) { Object.assign(executionStats, { debugStats: decodeStruct(debugStats) }); } } Object.assign(explainMetrics, { executionStats }); } if (explainMetrics.planSummary || explainMetrics.executionStats) { return { explainMetrics }; } return {}; } const readTimeAndConsistencyError = 'Read time and read consistency cannot both be specified.'; // Write function to check for readTime and readConsistency. function throwOnReadTimeAndConsistency(options) { if (options.readTime && options.consistency) { throw new Error(readTimeAndConsistencyError); } } /** * A map of read consistency values to proto codes. * * @type {object} * @private */ const CONSISTENCY_PROTO_CODE = { eventual: 2, strong: 1, }; /** * By default a DatastoreRequest is in the NOT_TRANSACTION state. If the * DatastoreRequest is a Transaction object, then initially it will be in * the NOT_STARTED state, but then the state will become IN_PROGRESS after the * transaction has started. */ var TransactionState; (function (TransactionState) { TransactionState[TransactionState["NOT_TRANSACTION"] = 0] = "NOT_TRANSACTION"; TransactionState[TransactionState["NOT_STARTED"] = 1] = "NOT_STARTED"; TransactionState[TransactionState["IN_PROGRESS"] = 2] = "IN_PROGRESS"; TransactionState[TransactionState["EXPIRED"] = 3] = "EXPIRED"; })(TransactionState || (exports.TransactionState = TransactionState = {})); /** * Handles request logic for Datastore API operations. * * Creates requests to the Datastore endpoint. Designed to be inherited by * the {@link Datastore} and {@link Transaction} classes. * * @class */ class DatastoreRequest { id; requests_; requestCallbacks_; datastore; state = TransactionState.NOT_TRANSACTION; /** * 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) => {}) * * @internal * * @see {@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; } 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); }); } /* This throws an error if the transaction has already expired. * */ checkExpired() { if (this.state === TransactionState.EXPIRED) { throw Error(exports.transactionExpiredError); } } /** * Retrieve the entities as a readable object stream. * * @throws {Error} If at least one Key object is not provided. * @throws {Error} If read time and read consistency cannot both be specified. * * @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.'); } this.checkExpired(); throwOnReadTimeAndConsistency(options); const reqOpts = this.getRequestOptions(options); throwOnTransactionErrors(this, reqOpts); const makeRequest = (keys) => { Object.assign(reqOpts, { keys }); this.request_({ client: 'DatastoreClient', method: 'lookup', reqOpts, gaxOpts: options.gaxOptions, }, (err, resp) => { this.parseTransactionResponse(resp); if (err) { stream.destroy(err); return; } let entities = []; try { entities = entity_1.entity.formatArray(resp.found, options.wrapNumbers); } catch (err) { stream.destroy(err); return; } const nextKeys = (resp.deferred || []) .map(entity_1.entity.keyFromKeyProto) .map(entity_1.entity.keyToKeyProto); (0, split_array_stream_1.split)(entities, stream) .then(streamEnded => { if (streamEnded) { return; } if (nextKeys.length > 0) { makeRequest(nextKeys); return; } stream.push(null); }) .catch(err => { throw err; }); }); }; const stream = streamEvents(new stream_1.Transform({ objectMode: true })); stream.once('reading', () => { makeRequest(keys); }); return stream; } 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), }; }), // eslint-disable-next-line @typescript-eslint/no-explicit-any }; if (this.id) { this.requests_.push(reqOpts); return; } this.request_({ client: 'DatastoreClient', method: 'commit', reqOpts, gaxOpts: gaxOptions, }, callback); } get(keys, optionsOrCallback, cb) { const options = typeof optionsOrCallback === 'object' && optionsOrCallback ? optionsOrCallback : {}; const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb; try { this.createReadStream(keys, options) .on('error', callback) .pipe(concat((results) => { const isSingleLookup = !Array.isArray(keys); callback(null, isSingleLookup ? results[0] : results); })); } catch (err) { callback(err); } } /** * This function saves results from a successful beginTransaction call. * * @param {object} [response] The response from a call to * begin a transaction that completed successfully. * **/ parseTransactionResponse(resp) { if (resp && resp.transaction && Buffer.byteLength(resp.transaction) > 0) { this.id = resp.transaction; this.state = TransactionState.IN_PROGRESS; } } runAggregationQuery(query, optionsOrCallback, cb) { const options = typeof optionsOrCallback === 'object' ? optionsOrCallback : {}; const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb; if (this.state === TransactionState.EXPIRED) { callback(new Error(exports.transactionExpiredError)); return; } if (options.readTime && options.consistency) { callback(new Error(readTimeAndConsistencyError)); return; } query.query = extend(true, new query_1.Query(), query.query); let queryProto; try { queryProto = entity_1.entity.queryToQueryProto(query.query); } catch (e) { // using setImmediate here to make sure this doesn't throw a // synchronous error setImmediate(callback, e); return; } let sharedQueryOpts; try { sharedQueryOpts = this.getQueryOptions(query.query, options); throwOnTransactionErrors(this, sharedQueryOpts); } catch (error) { callback(error); return; } const aggregationQueryOptions = { nestedQuery: queryProto, aggregations: query.toProto(), }; const reqOpts = Object.assign(sharedQueryOpts, { aggregationQuery: aggregationQueryOptions, }); this.request_({ client: 'DatastoreClient', method: 'runAggregationQuery', reqOpts, gaxOpts: options.gaxOptions, }, (err, res) => { const info = getInfoFromStats(res); this.parseTransactionResponse(res); if (res && res.batch) { const results = res.batch.aggregationResults; const finalResults = results .map((aggregationResult) => aggregationResult.aggregateProperties) .map((aggregateProperties) => Object.fromEntries(new Map(Object.keys(aggregateProperties).map(key => [ key, entity_1.entity.decodeValueProto(aggregateProperties[key]), ])))); callback(err, finalResults, info); } else { callback(err, [], info); } }); } runQuery(query, optionsOrCallback, cb) { const options = typeof optionsOrCallback === 'object' ? optionsOrCallback : {}; const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb; let info; try { this.runQueryStream(query, options) .on('error', callback) .on('info', info_ => { info = info_; }) .pipe(concat((results) => { callback(null, results, info); })); } catch (err) { callback(err); } } /** * Get a list of entities as a readable object stream. * * See {@link Datastore#runQuery} for a list of all available options. * * @param {Query} query A 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. * * @throws {Error} If read time and read consistency cannot both be specified. * * @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 = {}) { this.checkExpired(); throwOnReadTimeAndConsistency(options); query = extend(true, new query_1.Query(), query); const sharedQueryOpts = this.getQueryOptions(query, options); throwOnTransactionErrors(this, sharedQueryOpts); const makeRequest = (query) => { let queryProto; try { queryProto = entity_1.entity.queryToQueryProto(query); } catch (e) { // using setImmediate here to make sure this doesn't throw a // synchronous error setImmediate(onResultSet, e); return; } const reqOpts = sharedQueryOpts; reqOpts.query = queryProto; this.request_({ client: 'DatastoreClient', method: 'runQuery', reqOpts, gaxOpts: options.gaxOptions, }, onResultSet); }; const onResultSet = (err, resp) => { this.parseTransactionResponse(resp); if (err) { stream.destroy(err); return; } if (!resp.batch) { // If there are no results then send any stats back and end the stream. stream.emit('info', getInfoFromStats(resp)); stream.push(null); return; } const info = Object.assign(getInfoFromStats(resp), { moreResults: resp.batch.moreResults, }); if (resp.batch.endCursor) { info.endCursor = resp.batch.endCursor.toString('base64'); } let entities = []; if (resp.batch.entityResults) { try { entities = entity_1.entity.formatArray(resp.batch.entityResults, options.wrapNumbers); } catch (err) { stream.destroy(err); return; } } // Emit each result right away, then get the rest if necessary. (0, 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); }) .catch(err => { throw err; }); }; const stream = streamEvents(new stream_1.Transform({ objectMode: true })); stream.once('reading', () => { makeRequest(query); }); return stream; } /** * Gets request options from a RunQueryStream options configuration * * @param {RunQueryStreamOptions} [options] The RunQueryStream options configuration */ getRequestOptions(options) { const sharedQueryOpts = {}; if (isTransaction(this)) { if (this.state === TransactionState.NOT_STARTED) { if (sharedQueryOpts.readOptions === undefined) { sharedQueryOpts.readOptions = {}; } sharedQueryOpts.readOptions.newTransaction = getTransactionRequest(this, {}); sharedQueryOpts.readOptions.consistencyType = 'newTransaction'; } } if (options.consistency) { const code = CONSISTENCY_PROTO_CODE[options.consistency.toLowerCase()]; if (sharedQueryOpts.readOptions === undefined) { sharedQueryOpts.readOptions = {}; } sharedQueryOpts.readOptions.readConsistency = code; } if (options.readTime) { if (sharedQueryOpts.readOptions === undefined) { sharedQueryOpts.readOptions = {}; } const readTime = options.readTime; const seconds = readTime / 1000; sharedQueryOpts.readOptions.readTime = { seconds: Math.floor(seconds), }; } return sharedQueryOpts; } /** * Gets request options from a RunQueryStream options configuration * * @param {Query} [query] A Query object * @param {RunQueryStreamOptions} [options] The RunQueryStream options configuration */ getQueryOptions(query, options = {}) { const sharedQueryOpts = this.getRequestOptions(options); if (options.explainOptions) { sharedQueryOpts.explainOptions = options.explainOptions; } if (query.namespace) { sharedQueryOpts.partitionId = { namespaceId: query.namespace, }; } return sharedQueryOpts; } merge(entities, callback) { const transaction = this.datastore.transaction(); transaction.run(async (err) => { if (err) { try { await transaction.rollback(); } catch (error) { // Provide the error & API response from the failed run to the user. // Even a failed rollback should be transparent. // RE: https://github.com/GoogleCloudPlatform/gcloud-node/pull/1369#discussion_r66833976 } 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) { try { await transaction.rollback(); } catch (error) { // Provide the error & API response from the failed commit to the user. // Even a failed rollback should be transparent. // RE: https://github.com/GoogleCloudPlatform/gcloud-node/pull/1369#discussion_r66833976 } callback(err); } }); } /** * Builds a request and sends it to the Gapic Layer. * * @param {object} config Configuration object. * @param {function} callback The callback function. * * @private */ prepareGaxRequest_(config, callback) { const datastore = this.datastore; const isTransaction = this.id ? true : false; const method = config.method; const reqOpts = extend(true, {}, config.reqOpts); // 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 (datastore.options && datastore.options.databaseId) { reqOpts.databaseId = datastore.options.databaseId; } if (method === 'rollback') { reqOpts.transaction = this.id; } throwOnTransactionErrors(this, reqOpts); if (isTransaction && (method === 'lookup' || method === 'runQuery' || method === 'runAggregationQuery')) { if (reqOpts.readOptions) { Object.assign(reqOpts.readOptions, { transaction: this.id }); } else { 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.projectId = projectId; const gaxOpts = extend(true, {}, config.gaxOpts, { headers: { 'google-cloud-resource-prefix': `projects/${projectId}`, }, }); const requestFn = gaxClient[method].bind(gaxClient, reqOpts, gaxOpts); callback(null, requestFn); }); } request_(config, callback) { this.prepareGaxRequest_(config, (err, requestFn) => { if (err) { callback(err); return; } requestFn(callback); }); } /** * Make a request as a stream. * * @param {object} config Configuration object. * @param {object} config.gaxOpts GAX options. * @param {string} config.client The name of the gax client. * @param {string} config.method The gax method to call. * @param {object} config.reqOpts Request options. */ requestStream_(config) { let gaxStream; const stream = streamEvents(new stream_1.PassThrough({ objectMode: true })); stream.abort = () => { if (gaxStream && gaxStream.cancel) { gaxStream.cancel(); } }; stream.once('reading', () => { this.prepareGaxRequest_(config, (err, requestFn) => { if (err) { stream.destroy(err); return; } gaxStream = requestFn(); gaxStream .on('error', stream.destroy.bind(stream)) .on('response', stream.emit.bind(stream, 'response')) .pipe(stream); }); }); return stream; } } exports.DatastoreRequest = DatastoreRequest; /** * Check to see if a request is a Transaction * * @param {DatastoreRequest} request The Datastore request object * */ function isTransaction(request) { return request instanceof _1.Transaction; } /** * Throw an error if read options are not properly specified. * * @param {DatastoreRequest} request The Datastore request object * @param {SharedQueryOptions} options The Query options * */ function throwOnTransactionErrors(request, options) { const isTransaction = request.id ? true : false; if (isTransaction || (options.readOptions && options.readOptions.newTransaction)) { if (options.readOptions && options.readOptions.readConsistency) { throw new Error('Read consistency cannot be specified in a transaction.'); } if (options.readOptions && options.readOptions.readTime) { throw new Error('Read time cannot be specified in a transaction.'); } } } /** * This function gets transaction request options used for defining a * request to create a new transaction on the server. * * @param {Transaction} transaction The transaction for which the request will be made. * @param {RunOptions} options Custom options that will be used to create the request. */ function getTransactionRequest(transaction, options) { // If transactionOptions are provide then they will be used. // Otherwise, options passed into this function are used and when absent // options that exist on Transaction are used. return options.transactionOptions // If transactionOptions is specified: ? options.transactionOptions.readOnly // Use readOnly on transactionOptions ? { readOnly: {} } : options.transactionOptions.id // Use retry transaction if specified: ? { readWrite: { previousTransaction: options.transactionOptions.id } } : {} : options.readOnly || transaction.readOnly // If transactionOptions not set: ? { readOnly: {} } // Create a readOnly transaction if readOnly option set : options.transactionId || transaction.id ? { // Create readWrite transaction with a retry transaction set readWrite: { previousTransaction: options.transactionId || transaction.id, }, } : {}; // Request will be readWrite with no retry transaction set; } /*! Developer Documentation * * All async methods (except for streams) will return a Promise in the event * that a callback is omitted. */ (0, promisify_1.promisifyAll)(DatastoreRequest, { exclude: ['checkExpired', 'getQueryOptions', 'getRequestOptions'], }); //# sourceMappingURL=request.js.map