diff --git a/package.json b/package.json index 72dd5f704..6a2ef22b8 100644 --- a/package.json +++ b/package.json @@ -53,20 +53,22 @@ "prettier": "repo-tools exec -- prettier --write src/*.js src/*/*.js samples/*.js samples/*/*.js test/*.js test/*/*.js system-test/*.js system-test/*/*.js" }, "dependencies": { - "@google-cloud/common": "^0.13.0", - "@google-cloud/common-grpc": "^0.4.0", + "@google-cloud/common": "^0.14.0", "arrify": "^1.0.0", "concat-stream": "^1.5.0", "create-error-class": "^3.0.2", "extend": "^3.0.0", + "google-auto-auth": "^0.7.2", "google-gax": "^0.14.2", "google-proto-files": "^0.13.1", "is": "^3.0.1", "lodash.flatten": "^4.2.0", - "modelo": "^4.2.0", "prop-assign": "^1.0.0", "propprop": "^0.3.0", - "split-array-stream": "^1.0.0" + "safe-buffer": "^5.1.1", + "split-array-stream": "^1.0.0", + "stream-events": "^1.0.2", + "through2": "^2.0.3" }, "devDependencies": { "@google-cloud/nodejs-repo-tools": "^2.1.1", @@ -78,10 +80,9 @@ "eslint-plugin-prettier": "^2.3.1", "ink-docstrap": "^1.3.0", "jsdoc": "^3.5.5", - "mocha": "^3.0.1", + "mocha": "^4.0.1", "prettier": "^1.7.4", "proxyquire": "^1.7.10", - "sinon": "^1.17.6", - "through2": "^2.0.0" + "sinon": "^4.1.1" } } diff --git a/src/entity.js b/src/entity.js index 297aec47f..91202d23e 100644 --- a/src/entity.js +++ b/src/entity.js @@ -22,6 +22,7 @@ 'use strict'; var arrify = require('arrify'); +var Buffer = require('safe-buffer').Buffer; var createErrorClass = require('create-error-class'); var extend = require('extend'); var is = require('is'); @@ -215,7 +216,7 @@ entity.isDsKey = isDsKey; * // */ function decodeValueProto(valueProto) { - var valueType = valueProto.value_type; + var valueType = valueProto.valueType; var value = valueProto[valueType]; switch (valueType) { @@ -378,7 +379,7 @@ entity.encodeValue = encodeValue; * map: { * name: { * value: { - * value_type: 'stringValue', + * valueType: 'stringValue', * stringValue: 'Stephen' * } * } @@ -482,7 +483,7 @@ function entityToEntityProto(entityObject) { var delimiter = firstPathPartIsArray ? '[]' : '.'; var splitPath = path.split(delimiter); var firstPathPart = splitPath.shift(); - var remainderPath = splitPath.join(delimiter).replace(/^(\.|[])/, ''); + var remainderPath = splitPath.join(delimiter).replace(/^(\.|\[\])/, ''); if (!entity.properties[firstPathPart]) { return; @@ -581,9 +582,9 @@ function keyFromKeyProto(keyProto) { keyProto.path.forEach(function(path, index) { keyOptions.path.push(path.kind); - var id = path[path.id_type]; + var id = path[path.idType]; - if (path.id_type === 'id') { + if (path.idType === 'id') { id = new entity.Int(id); } diff --git a/src/index.js b/src/index.js index 39bd6ad98..e31826ddf 100644 --- a/src/index.js +++ b/src/index.js @@ -22,10 +22,11 @@ var arrify = require('arrify'); var common = require('@google-cloud/common'); -var commonGrpc = require('@google-cloud/common-grpc'); +var extend = require('extend'); +var grpc = require('google-gax').grpc; +var googleAuth = require('google-auto-auth'); var is = require('is'); -var modelo = require('modelo'); -var path = require('path'); +var util = require('util'); /** * @type {module:datastore/request} @@ -51,6 +52,11 @@ var Query = require('./query.js'); */ var Transaction = require('./transaction.js'); +// Import the clients for each version supported by this package. +const gapic = Object.freeze({ + v1: require('./v1'), +}); + /** * @constructor * @alias module:datastore @@ -301,34 +307,36 @@ function Datastore(options) { return new Datastore(options); } + options = options || {}; + + this.clients_ = new Map(); + this.datastore = this; + this.namespace = options.namespace; + this.projectId = + process.env.DATASTORE_PROJECT_ID || options.projectId || '{{projectId}}'; + this.defaultBaseUrl_ = 'datastore.googleapis.com'; this.determineBaseUrl_(options.apiEndpoint); - this.namespace = options.namespace; - this.projectId = process.env.DATASTORE_PROJECT_ID || options.projectId; - - var config = { - projectIdRequired: false, - baseUrl: this.baseUrl_, - customEndpoint: this.customEndpoint_, - protosDir: path.resolve(__dirname, '../protos'), - protoServices: { - Datastore: { - path: 'google/datastore/v1/datastore.proto', - service: 'datastore.v1', - }, + this.options = extend( + { + libName: 'gccl', + libVersion: require('../package.json').version, + scopes: gapic.v1.DatastoreClient.scopes, + servicePath: this.baseUrl_, + port: is.number(this.port_) ? this.port_ : 443, }, - scopes: ['https://www.googleapis.com/auth/datastore'], - packageJson: require('../package.json'), - grpcMetadata: { - 'google-cloud-resource-prefix': 'projects/' + this.projectId, - }, - }; + options + ); + + if (this.customEndpoint_) { + this.options.sslCreds = grpc.credentials.createInsecure(); + } - commonGrpc.Service.call(this, config, options); + this.auth = googleAuth(this.options); } -modelo.inherits(Datastore, DatastoreRequest, commonGrpc.Service); +util.inherits(Datastore, DatastoreRequest); /** * Helper function to get a Datastore Double object. @@ -595,6 +603,7 @@ Datastore.prototype.determineBaseUrl_ = function(customApiEndpoint) { var baseUrl = this.defaultBaseUrl_; var leadingProtocol = new RegExp('^https*://'); var trailingSlashes = new RegExp('/*$'); + var port = new RegExp(':(\\d+)'); if (customApiEndpoint) { baseUrl = customApiEndpoint; @@ -604,8 +613,13 @@ Datastore.prototype.determineBaseUrl_ = function(customApiEndpoint) { this.customEndpoint_ = true; } + if (port.test(baseUrl)) { + this.port_ = baseUrl.match(port)[1]; + } + this.baseUrl_ = baseUrl .replace(leadingProtocol, '') + .replace(port, '') .replace(trailingSlashes, ''); }; @@ -614,4 +628,4 @@ Datastore.Query = Query; Datastore.Transaction = Transaction; module.exports = Datastore; -module.exports.v1 = require('./v1'); +module.exports.v1 = gapic.v1; diff --git a/src/request.js b/src/request.js index 4c45e546a..a2284b6b7 100644 --- a/src/request.js +++ b/src/request.js @@ -27,6 +27,13 @@ var extend = require('extend'); var is = require('is'); var propAssign = require('prop-assign'); var split = require('split-array-stream'); +var streamEvents = require('stream-events'); +var through = require('through2'); + +// Import the clients for each version supported by this package. +const gapic = Object.freeze({ + v1: require('./v1'), +}); /** * @type {module:datastore/entity} @@ -106,8 +113,12 @@ DatastoreRequest.prepareEntityObject_ = function(obj) { /** * Generate IDs without creating entities. * - * @param {Key} incompleteKey - The key object to complete. - * @param {number} n - How many IDs to generate. + * @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 @@ -168,35 +179,37 @@ DatastoreRequest.prepareEntityObject_ = function(obj) { * var apiResponse = data[1]; * }); */ -DatastoreRequest.prototype.allocateIds = function(incompleteKey, n, callback) { - if (entity.isKeyComplete(incompleteKey)) { +DatastoreRequest.prototype.allocateIds = function(key, options, callback) { + if (entity.isKeyComplete(key)) { throw new Error('An incomplete key should be provided.'); } - var incompleteKeys = []; - for (var i = 0; i < n; i++) { - incompleteKeys.push(entity.keyToKeyProto(incompleteKey)); + if (is.number(options)) { + options = { + allocations: options, + }; } - var protoOpts = { - service: 'Datastore', - method: 'allocateIds', - }; + this.request_( + { + client: 'DatastoreClient', + method: 'allocateIds', + reqOpts: { + keys: new Array(options.allocations).fill(entity.keyToKeyProto(key)), + }, + gaxOpts: options.gaxOptions, + }, + function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } - var reqOpts = { - keys: incompleteKeys, - }; + var keys = arrify(resp.keys).map(entity.keyFromKeyProto); - this.request_(protoOpts, reqOpts, function(err, resp) { - if (err) { - callback(err, null, resp); - return; + callback(null, keys, resp); } - - var keys = (resp.keys || []).map(entity.keyFromKeyProto); - - callback(null, keys, resp); - }); + ); }; /** @@ -234,19 +247,13 @@ DatastoreRequest.prototype.createReadStream = function(keys, options) { throw new Error('At least one Key object is required.'); } - var limiter = common.util.createLimiter(makeRequest, options); - var stream = limiter.stream; + var stream = streamEvents(through.obj()); stream.once('reading', function() { - limiter.makeRequest(keys); + makeRequest(keys); }); function makeRequest(keys) { - var protoOpts = { - service: 'Datastore', - method: 'lookup', - }; - var reqOpts = { keys: keys, }; @@ -259,30 +266,38 @@ DatastoreRequest.prototype.createReadStream = function(keys, options) { }; } - self.request_(protoOpts, reqOpts, function(err, resp) { - if (err) { - stream.destroy(err); - return; - } - - var entities = entity.formatArray(resp.found); - var nextKeys = (resp.deferred || []) - .map(entity.keyFromKeyProto) - .map(entity.keyToKeyProto); - - split(entities, stream, function(streamEnded) { - if (streamEnded) { + self.request_( + { + client: 'DatastoreClient', + method: 'lookup', + reqOpts: reqOpts, + gaxOpts: options.gaxOptions, + }, + function(err, resp) { + if (err) { + stream.destroy(err); return; } - if (nextKeys.length > 0) { - limiter.makeRequest(nextKeys); - return; - } + var entities = entity.formatArray(resp.found); + var nextKeys = (resp.deferred || []) + .map(entity.keyFromKeyProto) + .map(entity.keyToKeyProto); - stream.push(null); - }); - }); + split(entities, stream, function(streamEnded) { + if (streamEnded) { + return; + } + + if (nextKeys.length > 0) { + makeRequest(nextKeys); + return; + } + + stream.push(null); + }); + } + ); } return stream; @@ -292,6 +307,8 @@ DatastoreRequest.prototype.createReadStream = function(keys, options) { * 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. @@ -334,13 +351,13 @@ DatastoreRequest.prototype.createReadStream = function(keys, options) { * var apiResponse = data[0]; * }); */ -DatastoreRequest.prototype.delete = function(keys, callback) { - callback = callback || common.util.noop; +DatastoreRequest.prototype.delete = function(keys, gaxOptions, callback) { + if (is.fn(gaxOptions)) { + callback = gaxOptions; + gaxOptions = {}; + } - var protoOpts = { - service: 'Datastore', - method: 'commit', - }; + callback = callback || common.util.noop; var reqOpts = { mutations: arrify(keys).map(function(key) { @@ -355,7 +372,15 @@ DatastoreRequest.prototype.delete = function(keys, callback) { return; } - this.request_(protoOpts, reqOpts, callback); + this.request_( + { + client: 'DatastoreClient', + method: 'commit', + reqOpts: reqOpts, + gaxOpts: gaxOptions, + }, + callback + ); }; /** @@ -371,7 +396,8 @@ DatastoreRequest.prototype.delete = function(keys, callback) { * 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 {number} options.maxApiCalls - Maximum API calls to make. + * @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 {object|object[]} callback.entity - The entity object(s) which match @@ -499,7 +525,8 @@ DatastoreRequest.prototype.insert = function(entities, callback) { * 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 {number} options.maxApiCalls - Maximum API calls to make. + * @param {object} options.gaxOptions - Request configuration options, outlined + * here: https://googleapis.github.io/gax-nodejs/global.html#CallOptions. * @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 @@ -600,6 +627,8 @@ DatastoreRequest.prototype.runQuery = function(query, options, callback) { * * @param {module:datastore/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) @@ -629,19 +658,13 @@ DatastoreRequest.prototype.runQueryStream = function(query, options) { query = extend(true, new Query(), query); - var limiter = common.util.createLimiter(makeRequest, options); - var stream = limiter.stream; + var stream = streamEvents(through.obj()); stream.once('reading', function() { - limiter.makeRequest(query); + makeRequest(query); }); function makeRequest(query) { - var protoOpts = { - service: 'Datastore', - method: 'runQuery', - }; - var reqOpts = { query: entity.queryToQueryProto(query), }; @@ -659,7 +682,15 @@ DatastoreRequest.prototype.runQueryStream = function(query, options) { }; } - self.request_(protoOpts, reqOpts, onResultSet); + self.request_( + { + client: 'DatastoreClient', + method: 'runQuery', + reqOpts: reqOpts, + gaxOpts: options.gaxOptions, + }, + onResultSet + ); } function onResultSet(err, resp) { @@ -704,7 +735,7 @@ DatastoreRequest.prototype.runQueryStream = function(query, options) { query.limit(limit - resp.batch.entityResults.length); } - limiter.makeRequest(query); + makeRequest(query); }); } @@ -736,6 +767,8 @@ DatastoreRequest.prototype.runQueryStream = function(query, options) { * indexing using a simple JSON path notation. See the example below to see * how to target properties at different levels of nesting within your * entity. + * @param {object=} gaxOptions - Request configuration options, outlined here: + * https://googleapis.github.io/gax-nodejs/global.html#CallOptions. * @param {string=} entities.method - Explicit method to use, either 'insert', * 'update', or 'upsert'. * @param {object} entities.data - Data to save with the provided key. @@ -912,9 +945,14 @@ DatastoreRequest.prototype.runQueryStream = function(query, options) { * var apiResponse = data[0]; * }); */ -DatastoreRequest.prototype.save = function(entities, callback) { +DatastoreRequest.prototype.save = function(entities, gaxOptions, callback) { entities = arrify(entities); + if (is.fn(gaxOptions)) { + callback = gaxOptions; + gaxOptions = {}; + } + var insertIndexes = {}; var mutations = []; var methods = { @@ -976,11 +1014,6 @@ DatastoreRequest.prototype.save = function(entities, callback) { mutations.push(mutation); }); - var protoOpts = { - service: 'Datastore', - method: 'commit', - }; - var reqOpts = { mutations: mutations, }; @@ -1011,7 +1044,15 @@ DatastoreRequest.prototype.save = function(entities, callback) { return; } - this.request_(protoOpts, reqOpts, onCommit); + this.request_( + { + client: 'DatastoreClient', + method: 'commit', + reqOpts: reqOpts, + gaxOpts: gaxOptions, + }, + onCommit + ); }; /** @@ -1062,24 +1103,24 @@ DatastoreRequest.prototype.upsert = function(entities, callback) { * Make a request to the API endpoint. Properties to indicate a transactional or * non-transactional operation are added automatically. * - * @param {string} method - Datastore action (allocateIds, commit, etc.). - * @param {object=} body - Request configuration object. + * @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 */ -DatastoreRequest.prototype.request_ = function(protoOpts, reqOpts, callback) { - if (!callback) { - callback = reqOpts; - reqOpts = {}; - } +DatastoreRequest.prototype.request_ = function(config, callback) { + var datastore = this.datastore; callback = callback || common.util.noop; var isTransaction = is.defined(this.id); - var method = protoOpts.method; + var method = config.method; + var reqOpts = extend(true, {}, config.reqOpts); - reqOpts.projectId = this.projectId; + reqOpts.projectId = datastore.projectId; // Set properties to indicate if we're in a transaction or not. if (method === 'commit') { @@ -1105,7 +1146,33 @@ DatastoreRequest.prototype.request_ = function(protoOpts, reqOpts, callback) { }; } - this.request(protoOpts, reqOpts, callback); + datastore.auth.getProjectId(function(err, projectId) { + if (err) { + callback(err); + return; + } + + var clientName = config.client; + + if (!datastore.clients_.has(clientName)) { + datastore.clients_.set( + clientName, + new gapic.v1[clientName](datastore.options) + ); + } + + var gaxClient = datastore.clients_.get(clientName); + + reqOpts = common.util.replaceProjectIdToken(reqOpts, projectId); + + var gaxOpts = extend(true, {}, config.gaxOpts, { + headers: { + 'google-cloud-resource-prefix': `projects/${projectId}`, + }, + }); + + gaxClient[method](reqOpts, gaxOpts, callback); + }); }; /*! Developer Documentation diff --git a/src/transaction.js b/src/transaction.js index 8c00bda5d..0d6a9e356 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -23,6 +23,7 @@ var arrify = require('arrify'); var common = require('@google-cloud/common'); var flatten = require('lodash.flatten'); +var is = require('is'); var prop = require('propprop'); var util = require('util'); @@ -63,7 +64,7 @@ function Transaction(datastore) { this.projectId = datastore.projectId; this.namespace = datastore.namespace; - this.request = datastore.request.bind(datastore); + this.request = datastore.request_.bind(datastore); // A queue for entity modifications made during the transaction. this.modifiedEntities_ = []; @@ -92,6 +93,8 @@ util.inherits(Transaction, Request); * * If the commit request fails, we will automatically rollback the transaction. * + * @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. * If the commit fails, we automatically try to rollback the transaction (see @@ -112,9 +115,14 @@ util.inherits(Transaction, Request); * var apiResponse = data[0]; * }); */ -Transaction.prototype.commit = function(callback) { +Transaction.prototype.commit = function(gaxOptions, callback) { var self = this; + if (is.fn(gaxOptions)) { + callback = gaxOptions; + gaxOptions = {}; + } + callback = callback || common.util.noop; if (this.skipCommit) { @@ -182,38 +190,41 @@ Transaction.prototype.commit = function(callback) { Request.prototype[method].call(self, args, common.util.noop); }); - var protoOpts = { - service: 'Datastore', - method: 'commit', - }; - // Take the `req` array built previously, and merge them into one request to // send as the final transactional commit. var reqOpts = { mutations: flatten(this.requests_.map(prop('mutations'))), }; - this.request_(protoOpts, reqOpts, function(err, resp) { - if (err) { - // Rollback automatically for the user. - self.rollback(function() { - // Provide the error & API response from the failed commit to the user. - // Even a failed rollback should be transparent. - // RE: https://github.com/GoogleCloudPlatform/google-cloud-node/pull/1369#discussion_r66833976 - callback(err, resp); - }); - return; - } + this.request_( + { + client: 'DatastoreClient', + method: 'commit', + reqOpts: reqOpts, + gaxOpts: gaxOptions, + }, + function(err, resp) { + if (err) { + // Rollback automatically for the user. + self.rollback(function() { + // Provide the error & API response from the failed commit to the user. + // Even a failed rollback should be transparent. + // RE: https://github.com/GoogleCloudPlatform/google-cloud-node/pull/1369#discussion_r66833976 + callback(err, resp); + }); + return; + } - // The `callbacks` array was built previously. These are the callbacks that - // handle the API response normally when using the DatastoreRequest.save and - // .delete methods. - self.requestCallbacks_.forEach(function(cb) { - cb(null, resp); - }); + // The `callbacks` array was built previously. These are the callbacks that + // handle the API response normally when using the DatastoreRequest.save and + // .delete methods. + self.requestCallbacks_.forEach(function(cb) { + cb(null, resp); + }); - callback(null, resp); - }); + callback(null, resp); + } + ); }; /** @@ -299,6 +310,8 @@ Transaction.prototype.delete = function(entities) { /** * Reverse a transaction remotely and finalize the current transaction instance. * + * @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. @@ -323,27 +336,36 @@ Transaction.prototype.delete = function(entities) { * var apiResponse = data[0]; * }); */ -Transaction.prototype.rollback = function(callback) { +Transaction.prototype.rollback = function(gaxOptions, callback) { var self = this; - callback = callback || common.util.noop; + if (is.fn(gaxOptions)) { + callback = gaxOptions; + gaxOptions = {}; + } - var protoOpts = { - service: 'Datastore', - method: 'rollback', - }; + callback = callback || common.util.noop; - this.request_(protoOpts, function(err, resp) { - self.skipCommit = true; + this.request_( + { + client: 'DatastoreClient', + method: 'rollback', + gaxOpts: gaxOptions, + }, + function(err, resp) { + self.skipCommit = true; - callback(err || null, resp); - }); + callback(err || null, resp); + } + ); }; /** * Begin a remote transaction. In the callback provided, run your transactional * commands. * + * @param {object=} gaxOptions - Request configuration options, outlined here: + * https://googleapis.github.io/gax-nodejs/global.html#CallOptions. * @param {function} callback - The function to execute within the context of * a transaction. * @param {?error} callback.err - An error returned while making this request. @@ -380,26 +402,33 @@ Transaction.prototype.rollback = function(callback) { * var apiResponse = data[1]; * }); */ -Transaction.prototype.run = function(callback) { +Transaction.prototype.run = function(gaxOptions, callback) { var self = this; - callback = callback || common.util.noop; + if (is.fn(gaxOptions)) { + callback = gaxOptions; + gaxOptions = {}; + } - var protoOpts = { - service: 'Datastore', - method: 'beginTransaction', - }; + callback = callback || common.util.noop; - this.request_(protoOpts, function(err, resp) { - if (err) { - callback(err, null, resp); - return; - } + this.request_( + { + client: 'DatastoreClient', + method: 'beginTransaction', + gaxOpts: gaxOptions, + }, + function(err, resp) { + if (err) { + callback(err, null, resp); + return; + } - self.id = resp.transaction; + self.id = resp.transaction; - callback(null, self, resp); - }); + callback(null, self, resp); + } + ); }; /** diff --git a/system-test/datastore.js b/system-test/datastore.js index 4a62cd681..b2a9b2075 100644 --- a/system-test/datastore.js +++ b/system-test/datastore.js @@ -22,11 +22,10 @@ var Buffer = require('safe-buffer').Buffer; var Datastore = require('../'); var entity = require('../src/entity.js'); -var env = require('../../../system-test/env.js'); describe('Datastore', function() { var testKinds = []; - var datastore = new Datastore(env); + var datastore = new Datastore({}); // Override the Key method so we can track what keys are created during the // tests. They are then deleted in the `after` hook. var key = datastore.key; diff --git a/test/entity.js b/test/entity.js index f46da572b..3fbecce8c 100644 --- a/test/entity.js +++ b/test/entity.js @@ -17,6 +17,7 @@ 'use strict'; var assert = require('assert'); +var Buffer = require('safe-buffer').Buffer; var deepStrictEqual = require('deep-strict-equal'); assert.deepStrictEqual = assert.deepStrictEqual || @@ -182,7 +183,7 @@ describe('entity', function() { var expectedValue = [{}]; var valueProto = { - value_type: 'arrayValue', + valueType: 'arrayValue', arrayValue: { values: expectedValue, }, @@ -208,7 +209,7 @@ describe('entity', function() { var expectedValue = new Buffer('Hi'); var valueProto = { - value_type: 'blobValue', + valueType: 'blobValue', blobValue: expectedValue.toString('base64'), }; @@ -219,7 +220,7 @@ describe('entity', function() { var expectedValue = null; var valueProto = { - value_type: 'nullValue', + valueType: 'nullValue', nullValue: 0, }; @@ -231,7 +232,7 @@ describe('entity', function() { var expectedValue = 8.3; var valueProto = { - value_type: 'doubleValue', + valueType: 'doubleValue', doubleValue: expectedValue, }; @@ -242,7 +243,7 @@ describe('entity', function() { var expectedValue = 8; var valueProto = { - value_type: 'integerValue', + valueType: 'integerValue', integerValue: expectedValue, }; @@ -253,7 +254,7 @@ describe('entity', function() { var expectedValue = {}; var valueProto = { - value_type: 'entityValue', + valueType: 'entityValue', entityValue: expectedValue, }; @@ -269,7 +270,7 @@ describe('entity', function() { var expectedValue = {}; var valueProto = { - value_type: 'keyValue', + valueType: 'keyValue', keyValue: expectedValue, }; @@ -290,7 +291,7 @@ describe('entity', function() { var expectedValue = new Date(seconds * 1000 + ms); var valueProto = { - value_type: 'timestampValue', + valueType: 'timestampValue', timestampValue: { seconds: seconds, nanos: ms * 1e6, @@ -304,7 +305,7 @@ describe('entity', function() { var expectedValue = false; var valueProto = { - value_type: 'booleanValue', + valueType: 'booleanValue', booleanValue: expectedValue, }; @@ -544,7 +545,7 @@ describe('entity', function() { var entityProto = { properties: { name: { - value_type: 'stringValue', + valueType: 'stringValue', stringValue: expectedEntity.name, }, }, @@ -816,12 +817,12 @@ describe('entity', function() { }, path: [ { - id_type: 'id', + idType: 'id', kind: 'Kind', id: '111', }, { - id_type: 'name', + idType: 'name', kind: 'Kind2', name: 'name', }, diff --git a/test/index.js b/test/index.js index acef949c4..1444469a0 100644 --- a/test/index.js +++ b/test/index.js @@ -18,44 +18,45 @@ var assert = require('assert'); var extend = require('extend'); -var path = require('path'); var proxyquire = require('proxyquire'); var util = require('@google-cloud/common').util; +var v1 = require('../src/v1/index.js'); + var fakeEntity = { KEY_SYMBOL: Symbol('fake key symbol'), Int: function(value) { this.value = value; }, - isDsInt: function() { - this.calledWith_ = arguments; - }, Double: function(value) { this.value = value; }, - isDsDouble: function() { - this.calledWith_ = arguments; - }, GeoPoint: function(value) { this.value = value; }, - isDsGeoPoint: function() { - this.calledWith_ = arguments; - }, Key: function() { this.calledWith_ = arguments; }, - isDsKey: function() { - this.calledWith_ = arguments; - }, }; var fakeUtil = extend({}, util); -function FakeGrpcService() { - this.calledWith_ = arguments; +var googleAutoAuthOverride; +function fakeGoogleAutoAuth() { + return (googleAutoAuthOverride || util.noop).apply(null, arguments); } +var createInsecureOverride; +var fakeGoogleGax = { + grpc: { + credentials: { + createInsecure: function() { + return (createInsecureOverride || util.noop).apply(null, arguments); + }, + }, + }, +}; + function FakeQuery() { this.calledWith_ = arguments; } @@ -87,23 +88,30 @@ describe('Datastore', function() { '@google-cloud/common': { util: fakeUtil, }, - '@google-cloud/common-grpc': { - Service: FakeGrpcService, - }, './entity.js': fakeEntity, './query.js': FakeQuery, './transaction.js': FakeTransaction, './v1': FakeV1, + 'google-auto-auth': fakeGoogleAutoAuth, + 'google-gax': fakeGoogleGax, }); }); beforeEach(function() { + createInsecureOverride = null; + googleAutoAuthOverride = null; + datastore = new Datastore({ projectId: PROJECT_ID, namespace: NAMESPACE, }); }); + after(function() { + createInsecureOverride = null; + googleAutoAuthOverride = null; + }); + it('should export GAX client', function() { assert.strictEqual(Datastore.v1, FakeV1); }); @@ -128,6 +136,40 @@ describe('Datastore', function() { fakeUtil.normalizeArguments = normalizeArguments; }); + it('should initialize an empty Client map', function() { + assert(datastore.clients_ instanceof Map); + assert.strictEqual(datastore.clients_.size, 0); + }); + + it('should alias itself to the datastore property', function() { + assert.strictEqual(datastore.datastore, datastore); + }); + + it('should localize the namespace', function() { + assert.strictEqual(datastore.namespace, NAMESPACE); + }); + + it('should localize the projectId', function() { + assert.strictEqual(datastore.projectId, PROJECT_ID); + }); + + it('should default project ID to placeholder', function() { + var datastore = new Datastore({}); + assert.strictEqual(datastore.projectId, '{{projectId}}'); + }); + + it('should use DATASTORE_PROJECT_ID', function() { + var datastoreProjectIdCached = process.env.DATASTORE_PROJECT_ID; + var projectId = 'overridden-project-id'; + + process.env.DATASTORE_PROJECT_ID = projectId; + + var datastore = new Datastore(OPTIONS); + process.env.DATASTORE_PROJECT_ID = datastoreProjectIdCached; + + assert.strictEqual(datastore.projectId, projectId); + }); + it('should set the default base URL', function() { assert.strictEqual(datastore.defaultBaseUrl_, 'datastore.googleapis.com'); }); @@ -145,52 +187,72 @@ describe('Datastore', function() { new Datastore(OPTIONS); }); - it('should localize the namespace', function() { - assert.strictEqual(datastore.namespace, NAMESPACE); - }); + it('should localize the options', function() { + var options = { + a: 'b', + c: 'd', + }; - it('should localize the projectId', function() { - assert.strictEqual(datastore.projectId, PROJECT_ID); + var datastore = new Datastore(options); + + assert.notStrictEqual(datastore.options, options); + + assert.deepEqual( + datastore.options, + extend( + { + libName: 'gccl', + libVersion: require('../package.json').version, + scopes: v1.DatastoreClient.scopes, + servicePath: datastore.baseUrl_, + port: 443, + }, + options + ) + ); }); - it('should use DATASTORE_PROJECT_ID', function() { - var datastoreProjectIdCached = process.env.DATASTORE_PROJECT_ID; - var projectId = 'overridden-project-id'; + it('should set port if detected', function() { + var determineBaseUrl_ = Datastore.prototype.determineBaseUrl_; - process.env.DATASTORE_PROJECT_ID = projectId; + var port = 99; + Datastore.prototype.determineBaseUrl_ = function() { + Datastore.prototype.determineBaseUrl_ = determineBaseUrl_; + this.port_ = port; + }; var datastore = new Datastore(OPTIONS); - process.env.DATASTORE_PROJECT_ID = datastoreProjectIdCached; - assert.strictEqual(datastore.projectId, projectId); + assert.strictEqual(datastore.options.port, port); }); - it('should inherit from GrpcService', function() { - var datastore = new Datastore(OPTIONS); + it('should set grpc ssl credentials if custom endpoint', function() { + var determineBaseUrl_ = Datastore.prototype.determineBaseUrl_; - var calledWith = datastore.calledWith_[0]; + Datastore.prototype.determineBaseUrl_ = function() { + Datastore.prototype.determineBaseUrl_ = determineBaseUrl_; + this.customEndpoint_ = true; + }; - assert.strictEqual(calledWith.projectIdRequired, false); - assert.strictEqual(calledWith.baseUrl, datastore.baseUrl_); - assert.strictEqual(calledWith.customEndpoint, datastore.customEndpoint_); + var fakeInsecureCreds = {}; + createInsecureOverride = function() { + return fakeInsecureCreds; + }; - var protosDir = path.resolve(__dirname, '../protos'); - assert.strictEqual(calledWith.protosDir, protosDir); + var datastore = new Datastore(OPTIONS); - assert.deepStrictEqual(calledWith.protoServices, { - Datastore: { - path: 'google/datastore/v1/datastore.proto', - service: 'datastore.v1', - }, - }); + assert.strictEqual(datastore.options.sslCreds, fakeInsecureCreds); + }); - assert.deepEqual(calledWith.scopes, [ - 'https://www.googleapis.com/auth/datastore', - ]); - assert.deepEqual(calledWith.packageJson, require('../package.json')); - assert.deepEqual(calledWith.grpcMetadata, { - 'google-cloud-resource-prefix': 'projects/' + datastore.projectId, - }); + it('should cache a local google-auto-auth instance', function() { + var fakeGoogleAutoAuthInstance = {}; + + googleAutoAuthOverride = function() { + return fakeGoogleAutoAuthInstance; + }; + + var datastore = new Datastore({}); + assert.strictEqual(datastore.auth, fakeGoogleAutoAuthInstance); }); }); @@ -206,18 +268,6 @@ describe('Datastore', function() { }); }); - describe('isDouble', function() { - it('should expose Double identifier', function() { - var something = {}; - Datastore.isDouble(something); - assert.strictEqual(fakeEntity.calledWith_[0], something); - }); - - it('should also be on the prototype', function() { - assert.strictEqual(datastore.isDouble, Datastore.isDouble); - }); - }); - describe('geoPoint', function() { it('should expose GeoPoint builder', function() { var aGeoPoint = {latitude: 24, longitude: 88}; @@ -230,18 +280,6 @@ describe('Datastore', function() { }); }); - describe('isGeoPoint', function() { - it('should expose GeoPoint identifier', function() { - var something = {}; - Datastore.isGeoPoint(something); - assert.strictEqual(fakeEntity.calledWith_[0], something); - }); - - it('should also be on the prototype', function() { - assert.strictEqual(datastore.isGeoPoint, Datastore.isGeoPoint); - }); - }); - describe('int', function() { it('should expose Int builder', function() { var anInt = 7; @@ -254,18 +292,6 @@ describe('Datastore', function() { }); }); - describe('isInt', function() { - it('should expose Int identifier', function() { - var something = {}; - Datastore.isInt(something); - assert.strictEqual(fakeEntity.calledWith_[0], something); - }); - - it('should also be on the prototype', function() { - assert.strictEqual(datastore.isInt, Datastore.isInt); - }); - }); - describe('KEY', function() { it('should expose the KEY symbol', function() { assert.strictEqual(Datastore.KEY, fakeEntity.KEY_SYMBOL); @@ -366,18 +392,6 @@ describe('Datastore', function() { }); }); - describe('isKey', function() { - it('should expose Key identifier', function() { - var something = {}; - datastore.isKey(something); - assert.strictEqual(fakeEntity.calledWith_[0], something); - }); - - it('should also be on the namespace', function() { - assert.strictEqual(datastore.isKey, Datastore.isKey); - }); - }); - describe('transaction', function() { it('should return a Transaction object', function() { var transaction = datastore.transaction(); @@ -403,25 +417,31 @@ describe('Datastore', function() { }); it('should remove slashes from the baseUrl', function() { - var expectedBaseUrl = 'localhost:8080'; + var expectedBaseUrl = 'localhost'; - setHost('localhost:8080/'); + setHost('localhost/'); datastore.determineBaseUrl_(); assert.strictEqual(datastore.baseUrl_, expectedBaseUrl); - setHost('localhost:8080//'); + setHost('localhost//'); datastore.determineBaseUrl_(); assert.strictEqual(datastore.baseUrl_, expectedBaseUrl); }); it('should remove the protocol if specified', function() { - setHost('http://localhost:8080'); + setHost('http://localhost'); datastore.determineBaseUrl_(); - assert.strictEqual(datastore.baseUrl_, 'localhost:8080'); + assert.strictEqual(datastore.baseUrl_, 'localhost'); + + setHost('https://localhost'); + datastore.determineBaseUrl_(); + assert.strictEqual(datastore.baseUrl_, 'localhost'); + }); - setHost('https://localhost:8080'); + it('should set port if one was found', function() { + setHost('http://localhost:9090'); datastore.determineBaseUrl_(); - assert.strictEqual(datastore.baseUrl_, 'localhost:8080'); + assert.strictEqual(datastore.port_, '9090'); }); it('should not set customEndpoint_ when using default baseurl', function() { @@ -442,6 +462,8 @@ describe('Datastore', function() { describe('with DATASTORE_EMULATOR_HOST environment variable', function() { var DATASTORE_EMULATOR_HOST = 'localhost:9090'; + var EXPECTED_BASE_URL = 'localhost'; + var EXPECTED_PORT = '9090'; beforeEach(function() { setHost(DATASTORE_EMULATOR_HOST); @@ -453,7 +475,8 @@ describe('Datastore', function() { it('should use the DATASTORE_EMULATOR_HOST env var', function() { datastore.determineBaseUrl_(); - assert.strictEqual(datastore.baseUrl_, DATASTORE_EMULATOR_HOST); + assert.strictEqual(datastore.baseUrl_, EXPECTED_BASE_URL); + assert.strictEqual(datastore.port_, EXPECTED_PORT); }); it('should set customEndpoint_', function() { diff --git a/test/request.js b/test/request.js index 7bff78c4e..f1bcace57 100644 --- a/test/request.js +++ b/test/request.js @@ -17,11 +17,11 @@ 'use strict'; var assert = require('assert'); +var Buffer = require('safe-buffer').Buffer; var extend = require('extend'); var is = require('is'); var proxyquire = require('proxyquire'); var sinon = require('sinon').sandbox.create(); -var stream = require('stream'); var through = require('through2'); var util = require('@google-cloud/common').util; @@ -37,6 +37,13 @@ var fakeUtil = extend({}, util, { }, }); +var v1FakeClientOverride; +var fakeV1 = { + FakeClient: function() { + return (v1FakeClientOverride || util.noop).apply(null, arguments); + }, +}; + var overrides = {}; function override(name, object) { @@ -87,12 +94,14 @@ describe('Request', function() { }, './entity.js': entity, './query.js': FakeQuery, + './v1': fakeV1, }); override('Request', Request); }); after(function() { + v1FakeClientOverride = null; resetOverrides(); }); @@ -102,6 +111,7 @@ describe('Request', function() { path: ['Company', 123], }); FakeQuery.prototype = new Query(); + v1FakeClientOverride = null; resetOverrides(); request = new Request(); }); @@ -146,84 +156,130 @@ describe('Request', function() { }); describe('allocateIds', function() { - var incompleteKey; - var apiResponse = { - keys: [{path: [{id_type: 'id', kind: 'Kind', id: 123}]}], + var INCOMPLETE_KEY = {}; + + var ALLOCATIONS = 2; + var OPTIONS = { + allocations: ALLOCATIONS, }; beforeEach(function() { - incompleteKey = new entity.Key({namespace: null, path: ['Kind']}); + overrides.entity.isKeyComplete = util.noop; + overrides.entity.keyToKeyProto = util.noop; }); - it('should produce proper allocate IDs req protos', function(done) { - request.request_ = function(protoOpts, reqOpts, callback) { - assert.strictEqual(protoOpts.service, 'Datastore'); - assert.strictEqual(protoOpts.method, 'allocateIds'); + it('should throw if the key is complete', function() { + overrides.entity.isKeyComplete = function(key) { + assert.strictEqual(key, INCOMPLETE_KEY); + return true; + }; + + assert.throws(function() { + request.allocateIds(INCOMPLETE_KEY, OPTIONS, assert.ifError); + }, new RegExp('An incomplete key should be provided.')); + }); - assert.equal(reqOpts.keys.length, 1); + it('should make the correct request', function(done) { + var keyProto = {}; - callback(null, apiResponse); + overrides.entity.keyToKeyProto = function(key) { + assert.strictEqual(key, INCOMPLETE_KEY); + return keyProto; }; - request.allocateIds(incompleteKey, 1, function(err, keys) { - assert.ifError(err); - var generatedKey = keys[0]; - assert.strictEqual(generatedKey.path.pop(), '123'); + request.request_ = function(config) { + assert.strictEqual(config.client, 'DatastoreClient'); + assert.strictEqual(config.method, 'allocateIds'); + + var expectedKeys = []; + expectedKeys.length = ALLOCATIONS; + expectedKeys.fill(keyProto); + + assert.deepStrictEqual(config.reqOpts.keys, expectedKeys); + + assert.strictEqual(config.gaxOpts, undefined); + done(); - }); - }); + }; - it('should exec callback with error & API response', function(done) { - var error = new Error('Error.'); + request.allocateIds(INCOMPLETE_KEY, OPTIONS, assert.ifError); + }); - request.request_ = function(protoOpts, reqOpts, callback) { - callback(error, apiResponse); + it('should allow a numeric shorthand for allocations', function(done) { + request.request_ = function(config) { + assert.strictEqual(config.reqOpts.keys.length, ALLOCATIONS); + done(); }; - request.allocateIds(incompleteKey, 1, function(err, keys, apiResponse_) { - assert.strictEqual(err, error); - assert.strictEqual(keys, null); - assert.strictEqual(apiResponse_, apiResponse); + request.allocateIds(INCOMPLETE_KEY, ALLOCATIONS, assert.ifError); + }); + + it('should allow customization of GAX options', function(done) { + var options = extend({}, OPTIONS, { + gaxOptions: {}, + }); + + request.request_ = function(config) { + assert.strictEqual(config.gaxOpts, options.gaxOptions); done(); + }; + + request.allocateIds(INCOMPLETE_KEY, options, assert.ifError); + }); + + describe('error', function() { + var ERROR = new Error('Error.'); + var API_RESPONSE = {}; + + beforeEach(function() { + request.request_ = function(config, callback) { + callback(ERROR, API_RESPONSE); + }; + }); + + it('should exec callback with error & API response', function(done) { + request.allocateIds(INCOMPLETE_KEY, OPTIONS, function(err, keys, resp) { + assert.strictEqual(err, ERROR); + assert.strictEqual(keys, null); + assert.strictEqual(resp, API_RESPONSE); + done(); + }); }); }); - it('should return apiResponse in callback', function(done) { - request.request_ = function(protoOpts, reqOpts, callback) { - callback(null, apiResponse); + describe('success', function() { + var KEY = {}; + var API_RESPONSE = { + keys: [KEY], }; - request.allocateIds(incompleteKey, 1, function(err, keys, apiResponse_) { - assert.ifError(err); - assert.strictEqual(apiResponse_, apiResponse); - done(); + beforeEach(function() { + request.request_ = function(config, callback) { + callback(null, API_RESPONSE); + }; }); - }); - it('should throw if trying to allocate IDs with complete keys', function() { - assert.throws(function() { - request.allocateIds(key); + it('should create and return Keys & API response', function(done) { + var key = {}; + + overrides.entity.keyFromKeyProto = function(keyProto) { + assert.strictEqual(keyProto, API_RESPONSE.keys[0]); + return key; + }; + + request.allocateIds(INCOMPLETE_KEY, OPTIONS, function(err, keys, resp) { + assert.ifError(err); + assert.deepStrictEqual(keys, [key]); + assert.strictEqual(resp, API_RESPONSE); + done(); + }); }); }); }); describe('createReadStream', function() { beforeEach(function() { - request.request_ = function() {}; - - overrides.util.createLimiter = function(makeRequest) { - var transformStream = new stream.Transform({objectMode: true}); - transformStream.destroy = through.obj().destroy.bind(transformStream); - - setImmediate(function() { - transformStream.emit('reading'); - }); - - return { - makeRequest: makeRequest, - stream: transformStream, - }; - }; + request.request_ = util.noop; }); it('should throw if no keys are provided', function() { @@ -241,54 +297,59 @@ describe('Request', function() { request.createReadStream(key).on('error', done); }); - it('should create a limiter', function(done) { - var options = {}; + it('should make correct request when stream is ready', function(done) { + request.request_ = function(config) { + assert.strictEqual(config.client, 'DatastoreClient'); + assert.strictEqual(config.method, 'lookup'); - overrides.util.createLimiter = function(makeRequest, options_) { - assert.strictEqual(options_, options); + assert.deepEqual(config.reqOpts.keys[0], entity.keyToKeyProto(key)); - setImmediate(done); - - return { - makeRequest: makeRequest, - stream: through(), - }; + done(); }; - request.createReadStream(key, options).on('error', done); - }); + var stream = request.createReadStream(key); - it('should make correct request', function(done) { - request.request_ = function(protoOpts, reqOpts) { - assert.strictEqual(protoOpts.service, 'Datastore'); - assert.strictEqual(protoOpts.method, 'lookup'); + stream.emit('reading'); + }); - assert.deepEqual(reqOpts.keys[0], entity.keyToKeyProto(key)); + it('should allow customization of GAX options', function(done) { + var options = { + gaxOptions: {}, + }; + request.request_ = function(config) { + assert.strictEqual(config.gaxOpts, options.gaxOptions); done(); }; - request.createReadStream(key).on('error', done); + request + .createReadStream(key, options) + .on('error', done) + .emit('reading'); }); it('should allow setting strong read consistency', function(done) { - request.request_ = function(protoOpts, reqOpts) { - assert.strictEqual(reqOpts.readOptions.readConsistency, 1); + request.request_ = function(config) { + assert.strictEqual(config.reqOpts.readOptions.readConsistency, 1); done(); }; - request.createReadStream(key, {consistency: 'strong'}).on('error', done); + request + .createReadStream(key, {consistency: 'strong'}) + .on('error', done) + .emit('reading'); }); it('should allow setting strong eventual consistency', function(done) { - request.request_ = function(protoOpts, reqOpts) { - assert.strictEqual(reqOpts.readOptions.readConsistency, 2); + request.request_ = function(config) { + assert.strictEqual(config.reqOpts.readOptions.readConsistency, 2); done(); }; request .createReadStream(key, {consistency: 'eventual'}) - .on('error', done); + .on('error', done) + .emit('reading'); }); describe('error', function() { @@ -296,7 +357,7 @@ describe('Request', function() { var apiResponse = {a: 'b', c: 'd'}; beforeEach(function() { - request.request_ = function(protoOpts, reqOpts, callback) { + request.request_ = function(config, callback) { setImmediate(function() { callback(error, apiResponse); }); @@ -387,16 +448,9 @@ describe('Request', function() { ]; beforeEach(function() { - request.request_ = function(protoOpts, reqOpts, callback) { + request.request_ = function(config, callback) { callback(null, apiResponse); }; - - overrides.util.createLimiter = function(makeRequest) { - return { - makeRequest: makeRequest, - stream: new stream.Transform({objectMode: true}), - }; - }; }); it('should format the results', function(done) { @@ -415,7 +469,7 @@ describe('Request', function() { it('should continue looking for deferred results', function(done) { var numTimesCalled = 0; - request.request_ = function(protoOpts, reqOpts, callback) { + request.request_ = function(config, callback) { numTimesCalled++; if (numTimesCalled === 1) { @@ -427,7 +481,7 @@ describe('Request', function() { .map(entity.keyFromKeyProto) .map(entity.keyToKeyProto); - assert.deepEqual(reqOpts.keys, expectedKeys); + assert.deepEqual(config.reqOpts.keys, expectedKeys); done(); }; @@ -451,7 +505,7 @@ describe('Request', function() { it('should not push more results if stream was ended', function(done) { var entitiesEmitted = 0; - request.request_ = function(protoOpts, reqOpts, callback) { + request.request_ = function(config, callback) { setImmediate(function() { callback(null, apiResponseWithMultiEntities); }); @@ -473,7 +527,7 @@ describe('Request', function() { it('should not get more results if stream was ended', function(done) { var lookupCount = 0; - request.request_ = function(protoOpts, reqOpts, callback) { + request.request_ = function(config, callback) { lookupCount++; setImmediate(function() { callback(null, apiResponseWithDeferred); @@ -497,10 +551,10 @@ describe('Request', function() { describe('delete', function() { it('should delete by key', function(done) { - request.request_ = function(protoOpts, reqOpts, callback) { - assert.strictEqual(protoOpts.service, 'Datastore'); - assert.strictEqual(protoOpts.method, 'commit'); - assert(is.object(reqOpts.mutations[0].delete)); + request.request_ = function(config, callback) { + assert.strictEqual(config.client, 'DatastoreClient'); + assert.strictEqual(config.method, 'commit'); + assert(is.object(config.reqOpts.mutations[0].delete)); callback(); }; request.delete(key, done); @@ -508,7 +562,7 @@ describe('Request', function() { it('should return apiResponse in callback', function(done) { var resp = {success: true}; - request.request_ = function(protoOpts, reqOpts, callback) { + request.request_ = function(config, callback) { callback(null, resp); }; request.delete(key, function(err, apiResponse) { @@ -519,13 +573,24 @@ describe('Request', function() { }); it('should multi delete by keys', function(done) { - request.request_ = function(protoOpts, reqOpts, callback) { - assert.equal(reqOpts.mutations.length, 2); + request.request_ = function(config, callback) { + assert.equal(config.reqOpts.mutations.length, 2); callback(); }; request.delete([key, key], done); }); + it('should allow customization of GAX options', function(done) { + var gaxOptions = {}; + + request.request_ = function(config) { + assert.strictEqual(config.gaxOpts, gaxOptions); + done(); + }; + + request.delete(key, gaxOptions, assert.ifError); + }); + describe('transactions', function() { beforeEach(function() { // Trigger transaction mode. @@ -673,40 +738,6 @@ describe('Request', function() { beforeEach(function() { overrides.entity.queryToQueryProto = util.noop; request.request_ = util.noop; - - overrides.util.createLimiter = function(makeRequest) { - var transformStream = new stream.Transform({objectMode: true}); - transformStream.destroy = through.obj().destroy.bind(transformStream); - - setImmediate(function() { - transformStream.emit('reading'); - }); - - return { - makeRequest: makeRequest, - stream: transformStream, - }; - }; - }); - - it('should create a limiter', function(done) { - var options = {}; - - overrides.util.createLimiter = function(makeRequest, options_) { - assert.strictEqual(options_, options); - - setImmediate(done); - - return { - makeRequest: makeRequest, - stream: through(), - }; - }; - - request - .runQueryStream({}, options) - .on('error', done) - .emit('reading'); }); it('should clone the query', function(done) { @@ -726,7 +757,7 @@ describe('Request', function() { .emit('reading'); }); - it('should make correct request', function(done) { + it('should make correct request when the stream is ready', function(done) { var query = {namespace: 'namespace'}; var queryProto = {}; @@ -734,12 +765,16 @@ describe('Request', function() { return queryProto; }; - request.request_ = function(protoOpts, reqOpts) { - assert.strictEqual(protoOpts.service, 'Datastore'); - assert.strictEqual(protoOpts.method, 'runQuery'); - assert(is.empty(reqOpts.readOptions)); - assert.strictEqual(reqOpts.query, queryProto); - assert.strictEqual(reqOpts.partitionId.namespaceId, query.namespace); + request.request_ = function(config) { + assert.strictEqual(config.client, 'DatastoreClient'); + assert.strictEqual(config.method, 'runQuery'); + assert(is.empty(config.reqOpts.readOptions)); + assert.strictEqual(config.reqOpts.query, queryProto); + assert.strictEqual( + config.reqOpts.partitionId.namespaceId, + query.namespace + ); + assert.strictEqual(config.gaxOpts, undefined); done(); }; @@ -750,9 +785,25 @@ describe('Request', function() { .emit('reading'); }); + it('should allow customization of GAX options', function(done) { + var options = { + gaxOptions: {}, + }; + + request.request_ = function(config) { + assert.strictEqual(config.gaxOpts, options.gaxOptions); + done(); + }; + + request + .runQueryStream({}, options) + .on('error', done) + .emit('reading'); + }); + it('should allow setting strong read consistency', function(done) { - request.request_ = function(protoOpts, reqOpts) { - assert.strictEqual(reqOpts.readOptions.readConsistency, 1); + request.request_ = function(config) { + assert.strictEqual(config.reqOpts.readOptions.readConsistency, 1); done(); }; @@ -763,8 +814,8 @@ describe('Request', function() { }); it('should allow setting strong eventual consistency', function(done) { - request.request_ = function(protoOpts, reqOpts) { - assert.strictEqual(reqOpts.readOptions.readConsistency, 2); + request.request_ = function(config) { + assert.strictEqual(config.reqOpts.readOptions.readConsistency, 2); done(); }; @@ -778,16 +829,19 @@ describe('Request', function() { var error = new Error('Error.'); beforeEach(function() { - request.request_ = function(protoOpts, reqOpts, callback) { + request.request_ = function(config, callback) { callback(error); }; }); it('should emit error on a stream', function(done) { - request.runQueryStream({}).on('error', function(err) { - assert.strictEqual(err, error); - done(); - }); + request + .runQueryStream({}) + .on('error', function(err) { + assert.strictEqual(err, error); + done(); + }) + .emit('reading'); }); }); @@ -807,7 +861,7 @@ describe('Request', function() { }; beforeEach(function() { - request.request_ = function(protoOpts, reqOpts, callback) { + request.request_ = function(config, callback) { callback(null, apiResponse); }; @@ -859,7 +913,7 @@ describe('Request', function() { return entityResultsPerApiCall[timesRequestCalled]; }; - request.request_ = function(protoOpts, reqOpts, callback) { + request.request_ = function(config, callback) { timesRequestCalled++; var resp = extend(true, {}, apiResponse); @@ -867,8 +921,8 @@ describe('Request', function() { entityResultsPerApiCall[timesRequestCalled]; if (timesRequestCalled === 1) { - assert.strictEqual(protoOpts.service, 'Datastore'); - assert.strictEqual(protoOpts.method, 'runQuery'); + assert.strictEqual(config.client, 'DatastoreClient'); + assert.strictEqual(config.method, 'runQuery'); resp.batch.moreResults = 'NOT_FINISHED'; @@ -876,7 +930,7 @@ describe('Request', function() { } else { assert.strictEqual(startCalled, true); assert.strictEqual(offsetCalled, true); - assert.strictEqual(reqOpts.query, queryProto); + assert.strictEqual(config.reqOpts.query, queryProto); resp.batch.moreResults = 'MORE_RESULTS_AFTER_LIMIT'; @@ -953,7 +1007,7 @@ describe('Request', function() { limitVal: -1, }; - request.request_ = function(protoOpts, reqOpts, callback) { + request.request_ = function(config, callback) { var batch; if (++timesRequestCalled === 2) { @@ -992,7 +1046,7 @@ describe('Request', function() { var timesRequestCalled = 0; var entitiesEmitted = 0; - request.request_ = function(protoOpts, reqOpts, callback) { + request.request_ = function(config, callback) { timesRequestCalled++; var resp = extend(true, {}, apiResponse); @@ -1023,7 +1077,7 @@ describe('Request', function() { it('should not get more results if stream was ended', function(done) { var timesRequestCalled = 0; - request.request_ = function(protoOpts, reqOpts, callback) { + request.request_ = function(config, callback) { timesRequestCalled++; callback(null, apiResponse); }; @@ -1171,11 +1225,12 @@ describe('Request', function() { ], }; - request.request_ = function(protoOpts, reqOpts, callback) { - assert.strictEqual(protoOpts.service, 'Datastore'); - assert.strictEqual(protoOpts.method, 'commit'); + request.request_ = function(config, callback) { + assert.strictEqual(config.client, 'DatastoreClient'); + assert.strictEqual(config.method, 'commit'); - assert.deepEqual(reqOpts, expectedReq); + assert.deepEqual(config.reqOpts, expectedReq); + assert.deepEqual(config.gaxOpts, {}); callback(); }; @@ -1185,6 +1240,24 @@ describe('Request', function() { ); }); + it('should allow customization of GAX options', function(done) { + var gaxOptions = {}; + + request.request_ = function(config) { + assert.strictEqual(config.gaxOpts, gaxOptions); + done(); + }; + + request.save( + { + key: key, + data: {}, + }, + gaxOptions, + assert.ifError + ); + }); + it('should prepare entity objects', function(done) { var entityObject = {}; var prepared = false; @@ -1208,19 +1281,19 @@ describe('Request', function() { }); it('should save with specific method', function(done) { - request.request_ = function(protoOpts, reqOpts, callback) { - assert.equal(reqOpts.mutations.length, 3); - assert(is.object(reqOpts.mutations[0].insert)); - assert(is.object(reqOpts.mutations[1].update)); - assert(is.object(reqOpts.mutations[2].upsert)); + request.request_ = function(config, callback) { + assert.equal(config.reqOpts.mutations.length, 3); + assert(is.object(config.reqOpts.mutations[0].insert)); + assert(is.object(config.reqOpts.mutations[1].update)); + assert(is.object(config.reqOpts.mutations[2].upsert)); - var insert = reqOpts.mutations[0].insert; + var insert = config.reqOpts.mutations[0].insert; assert.deepEqual(insert.properties.k, {stringValue: 'v'}); - var update = reqOpts.mutations[1].update; + var update = config.reqOpts.mutations[1].update; assert.deepEqual(update.properties.k2, {stringValue: 'v2'}); - var upsert = reqOpts.mutations[2].upsert; + var upsert = config.reqOpts.mutations[2].upsert; assert.deepEqual(upsert.properties.k3, {stringValue: 'v3'}); callback(); @@ -1280,7 +1353,7 @@ describe('Request', function() { it('should return apiResponse in callback', function(done) { var key = new entity.Key({namespace: 'ns', path: ['Company']}); var mockCommitResponse = {}; - request.request_ = function(protoOpts, reqOpts, callback) { + request.request_ = function(config, callback) { callback(null, mockCommitResponse); }; request.save({key: key, data: {}}, function(err, apiResponse) { @@ -1291,8 +1364,8 @@ describe('Request', function() { }); it('should allow setting the indexed value of a property', function(done) { - request.request_ = function(protoOpts, reqOpts) { - var property = reqOpts.mutations[0].upsert.properties.name; + request.request_ = function(config) { + var property = config.reqOpts.mutations[0].upsert.properties.name; assert.strictEqual(property.stringValue, 'value'); assert.strictEqual(property.excludeFromIndexes, true); done(); @@ -1314,8 +1387,8 @@ describe('Request', function() { }); it('should allow setting the indexed value on arrays', function(done) { - request.request_ = function(protoOpts, reqOpts) { - var property = reqOpts.mutations[0].upsert.properties.name; + request.request_ = function(config) { + var property = config.reqOpts.mutations[0].upsert.properties.name; property.arrayValue.values.forEach(function(value) { assert.strictEqual(value.excludeFromIndexes, true); @@ -1359,7 +1432,7 @@ describe('Request', function() { ], }; - request.request_ = function(protoOpts, reqOpts, callback) { + request.request_ = function(config, callback) { callback(null, response); }; @@ -1498,45 +1571,154 @@ describe('Request', function() { }); describe('request_', function() { + var CONFIG = { + client: 'FakeClient', // name set at top of file + method: 'method', + reqOpts: { + a: 'b', + c: 'd', + }, + gaxOpts: { + a: 'b', + c: 'd', + }, + }; + var PROJECT_ID = 'project-id'; - var PROTO_OPTS = {}; beforeEach(function() { - request.projectId = PROJECT_ID; + var clients_ = new Map(); + clients_.set(CONFIG.client, { + [CONFIG.method]: util.noop, + }); + + request.datastore = { + clients_: clients_, + + auth: { + getProjectId: function(callback) { + callback(null, PROJECT_ID); + }, + }, + }; }); - it('should not require reqOpts', function(done) { - request.request = function(protoOpts, reqOpts, callback) { - callback(); // done() + it('should get the project ID', function(done) { + request.datastore.auth.getProjectId = function() { + done(); }; - request.request_(PROTO_OPTS, done); + request.request_(CONFIG, assert.ifError); }); - it('should make the correct request', function(done) { - var reqOpts = {}; + it('should return error if getting project ID failed', function(done) { + var error = new Error('Error.'); + + request.datastore.auth.getProjectId = function(callback) { + callback(error); + }; - request.request = function(protoOpts, reqOpts_) { - assert.strictEqual(protoOpts, PROTO_OPTS); - assert.strictEqual(reqOpts_, reqOpts); - assert.strictEqual(reqOpts_.projectId, PROJECT_ID); + request.request_(CONFIG, function(err) { + assert.strictEqual(err, error); done(); + }); + }); + + it('should initiate and cache the client', function() { + var fakeClient = { + [CONFIG.method]: util.noop, }; - request.request_(PROTO_OPTS, reqOpts, assert.ifError); + v1FakeClientOverride = function(options) { + assert.deepStrictEqual(options, request.datastore.options); + return fakeClient; + }; + + request.datastore.clients_ = new Map(); + + request.request_(CONFIG, assert.ifError); + + var client = request.datastore.clients_.get(CONFIG.client); + + assert.strictEqual(client, fakeClient); + }); + + it('should use the cached client', function(done) { + v1FakeClientOverride = function() { + done(new Error('Should not re-instantiate a GAX client.')); + }; + + request.request_(CONFIG); + done(); + }); + + it('should replace the project ID token', function(done) { + var replacedReqOpts = {}; + + var expectedReqOpts = extend({}, CONFIG.reqOpts); + expectedReqOpts.projectId = request.projectId; + + overrides.util.replaceProjectIdToken = function(reqOpts, projectId) { + assert.notStrictEqual(reqOpts, CONFIG.reqOpts); + assert.deepEqual(reqOpts, expectedReqOpts); + assert.strictEqual(projectId, PROJECT_ID); + + return replacedReqOpts; + }; + + request.datastore.clients_ = new Map(); + request.datastore.clients_.set(CONFIG.client, { + [CONFIG.method]: function(reqOpts) { + assert.strictEqual(reqOpts, replacedReqOpts); + done(); + }, + }); + + request.request_(CONFIG, assert.ifError); + }); + + it('should send gaxOpts', function(done) { + request.datastore.clients_ = new Map(); + request.datastore.clients_.set(CONFIG.client, { + [CONFIG.method]: function(_, gaxO) { + delete gaxO.headers; + assert.deepStrictEqual(gaxO, CONFIG.gaxOpts); + done(); + }, + }); + + request.request_(CONFIG, assert.ifError); + }); + + it('should send google-cloud-resource-prefix', function(done) { + request.datastore.clients_ = new Map(); + request.datastore.clients_.set(CONFIG.client, { + [CONFIG.method]: function(_, gaxO) { + assert.deepStrictEqual(gaxO.headers, { + 'google-cloud-resource-prefix': 'projects/' + PROJECT_ID, + }); + done(); + }, + }); + + request.request_(CONFIG, assert.ifError); }); describe('commit', function() { it('should set the mode', function(done) { - var reqOpts = {}; + request.datastore.clients_ = new Map(); + request.datastore.clients_.set(CONFIG.client, { + commit: function(reqOpts) { + assert.strictEqual(reqOpts.mode, 'NON_TRANSACTIONAL'); + done(); + }, + }); - request.request = function(protoOpts, reqOpts_) { - assert.strictEqual(reqOpts_, reqOpts); - assert.strictEqual(reqOpts_.mode, 'NON_TRANSACTIONAL'); - done(); - }; + var config = extend({}, CONFIG, { + method: 'commit', + }); - request.request_({method: 'commit'}, reqOpts, assert.ifError); + request.request_(config, assert.ifError); }); }); @@ -1548,75 +1730,82 @@ describe('Request', function() { }); it('should set the commit transaction info', function(done) { - var reqOpts = {}; + request.datastore.clients_ = new Map(); + request.datastore.clients_.set(CONFIG.client, { + commit: function(reqOpts) { + assert.strictEqual(reqOpts.mode, 'TRANSACTIONAL'); + assert.strictEqual(reqOpts.transaction, TRANSACTION_ID); + done(); + }, + }); - request.request = function(protoOpts, reqOpts_) { - assert.strictEqual(reqOpts_, reqOpts); - assert.strictEqual(reqOpts_.mode, 'TRANSACTIONAL'); - assert.strictEqual(reqOpts_.transaction, request.id); - done(); - }; + var config = extend({}, CONFIG, { + method: 'commit', + }); - request.id = 'transaction-id'; - request.request_({method: 'commit'}, reqOpts, assert.ifError); + request.request_(config, assert.ifError); }); it('should set the rollback transaction info', function(done) { - var reqOpts = {}; + request.datastore.clients_ = new Map(); + request.datastore.clients_.set(CONFIG.client, { + rollback: function(reqOpts) { + assert.strictEqual(reqOpts.transaction, TRANSACTION_ID); + done(); + }, + }); - request.request = function(protoOpts, reqOpts_) { - assert.strictEqual(reqOpts_, reqOpts); - assert.strictEqual(reqOpts_.transaction, request.id); - done(); - }; + var config = extend({}, CONFIG, { + method: 'rollback', + }); - request.id = 'transaction-id'; - request.request_({method: 'rollback'}, reqOpts, assert.ifError); + request.request_(config, assert.ifError); }); it('should set the lookup transaction info', function(done) { - var reqOpts = { - readOptions: {}, - }; + var config = extend(true, {}, CONFIG, { + method: 'lookup', + }); - request.request = function(protoOpts, reqOpts_) { - assert.strictEqual(reqOpts_, reqOpts); - assert.strictEqual(reqOpts_.readOptions, reqOpts.readOptions); - assert.strictEqual(reqOpts_.readOptions.transaction, request.id); - done(); - }; + request.datastore.clients_ = new Map(); + request.datastore.clients_.set(CONFIG.client, { + lookup: function(reqOpts) { + assert.strictEqual(reqOpts.readOptions.transaction, TRANSACTION_ID); + done(); + }, + }); - request.id = 'transaction-id'; - request.request_({method: 'lookup'}, reqOpts, assert.ifError); + request.request_(config, assert.ifError); }); - it('should set the lookup transaction info', function(done) { - var reqOpts = { - readOptions: {}, - }; + it('should set the runQuery transaction info', function(done) { + var config = extend(true, {}, CONFIG, { + method: 'runQuery', + }); - request.request = function(protoOpts, reqOpts_) { - assert.strictEqual(reqOpts_, reqOpts); - assert.strictEqual(reqOpts_.readOptions, reqOpts.readOptions); - assert.strictEqual(reqOpts_.readOptions.transaction, request.id); - done(); - }; + request.datastore.clients_ = new Map(); + request.datastore.clients_.set(CONFIG.client, { + runQuery: function(reqOpts) { + assert.strictEqual(reqOpts.readOptions.transaction, TRANSACTION_ID); + done(); + }, + }); - request.id = 'transaction-id'; - request.request_({method: 'runQuery'}, reqOpts, assert.ifError); + request.request_(config, assert.ifError); }); it('should throw if read consistency is specified', function() { - var reqOpts = { - readOptions: { - readConsistency: 1, + var config = extend(true, {}, CONFIG, { + method: 'runQuery', + reqOpts: { + readOptions: { + readConsistency: 1, + }, }, - }; - - request.id = 'transaction-id'; + }); assert.throws(function() { - request.request_({method: 'runQuery'}, reqOpts, assert.ifError); + request.request_(config, assert.ifError); }, /Read consistency cannot be specified in a transaction\./); }); }); diff --git a/test/transaction.js b/test/transaction.js index eea2029d5..7aeda9c2b 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -66,7 +66,7 @@ describe('Transaction', function() { var NAMESPACE = 'a-namespace'; var DATASTORE = { - request: function() {}, + request_: function() {}, projectId: PROJECT_ID, namespace: NAMESPACE, }; @@ -109,7 +109,7 @@ describe('Transaction', function() { var transaction; var fakeDataset = { - request: { + request_: { bind: function(context) { assert.strictEqual(context, fakeDataset); @@ -139,14 +139,26 @@ describe('Transaction', function() { }); it('should commit', function(done) { - transaction.request_ = function(protoOpts) { - assert.equal(protoOpts.service, 'Datastore'); - assert.equal(protoOpts.method, 'commit'); + transaction.request_ = function(config) { + assert.equal(config.client, 'DatastoreClient'); + assert.equal(config.method, 'commit'); + assert.strictEqual(config.gaxOptions, undefined); done(); }; transaction.commit(); }); + it('should accept gaxOptions', function(done) { + var gaxOptions = {}; + + transaction.request_ = function(config) { + assert.strictEqual(config.gaxOpts, gaxOptions); + done(); + }; + + transaction.commit(gaxOptions); + }); + it('should skip the commit', function(done) { transaction.skipCommit = true; @@ -168,8 +180,7 @@ describe('Transaction', function() { callback(rollbackError, rollbackApiResponse); }; - transaction.request_ = function(protoOpts, reqOpts, callback) { - callback = callback || reqOpts; + transaction.request_ = function(config, callback) { callback(error, apiResponse); }; }); @@ -185,8 +196,7 @@ describe('Transaction', function() { it('should pass apiResponse to callback', function(done) { var resp = {success: true}; - transaction.request_ = function(protoOpts, reqOpts, callback) { - callback = callback || reqOpts; + transaction.request_ = function(config, callback) { callback(null, resp); }; transaction.commit(function(err, apiResponse) { @@ -282,8 +292,8 @@ describe('Transaction', function() { }, ]; - transaction.request_ = function(protoOpts, reqOpts) { - assert.deepEqual(reqOpts, { + transaction.request_ = function(config) { + assert.deepEqual(config.reqOpts, { mutations: [{a: 'b'}, {c: 'd'}, {e: 'f'}, {g: 'h'}], }); done(); @@ -305,7 +315,7 @@ describe('Transaction', function() { }, ]; - transaction.request_ = function(protoOpts, reqOpts, cb) { + transaction.request_ = function(config, cb) { cb(); }; @@ -361,18 +371,29 @@ describe('Transaction', function() { }); it('should rollback', function(done) { - transaction.request_ = function(protoOpts) { - assert.strictEqual(protoOpts.service, 'Datastore'); - assert.equal(protoOpts.method, 'rollback'); + transaction.request_ = function(config) { + assert.strictEqual(config.client, 'DatastoreClient'); + assert.equal(config.method, 'rollback'); + assert.strictEqual(config.gaxOptions, undefined); done(); }; transaction.rollback(); }); + it('should allow setting gaxOptions', function(done) { + var gaxOptions = {}; + + transaction.request_ = function(config) { + assert.strictEqual(config.gaxOpts, gaxOptions); + done(); + }; + + transaction.rollback(gaxOptions); + }); + it('should pass error to callback', function(done) { var error = new Error('Error.'); - transaction.request_ = function(protoOpts, reqOpts, callback) { - callback = callback || reqOpts; + transaction.request_ = function(config, callback) { callback(error); }; transaction.rollback(function(err) { @@ -383,8 +404,7 @@ describe('Transaction', function() { it('should pass apiResponse to callback', function(done) { var resp = {success: true}; - transaction.request_ = function(protoOpts, reqOpts, callback) { - callback = callback || reqOpts; + transaction.request_ = function(config, callback) { callback(null, resp); }; transaction.rollback(function(err, apiResponse) { @@ -395,8 +415,7 @@ describe('Transaction', function() { }); it('should set skipCommit', function(done) { - transaction.request_ = function(protoOpts, reqOpts, callback) { - callback = callback || reqOpts; + transaction.request_ = function(config, callback) { callback(); }; transaction.rollback(function() { @@ -406,8 +425,7 @@ describe('Transaction', function() { }); it('should set skipCommit when rollback errors', function(done) { - transaction.request_ = function(protoOpts, reqOpts, callback) { - callback = callback || reqOpts; + transaction.request_ = function(config, callback) { callback(new Error('Error.')); }; transaction.rollback(function() { @@ -419,24 +437,33 @@ describe('Transaction', function() { describe('run', function() { it('should make the correct API request', function(done) { - transaction.request_ = function(protoOpts) { - assert.deepEqual(protoOpts, { - service: 'Datastore', - method: 'beginTransaction', - }); - + transaction.request_ = function(config) { + assert.strictEqual(config.client, 'DatastoreClient'); + assert.strictEqual(config.method, 'beginTransaction'); + assert.deepEqual(config.gaxOpts, {}); done(); }; transaction.run(assert.ifError); }); + it('should allow setting gaxOptions', function(done) { + var gaxOptions = {}; + + transaction.request_ = function(config) { + assert.strictEqual(config.gaxOpts, gaxOptions); + done(); + }; + + transaction.run(gaxOptions); + }); + describe('error', function() { var error = new Error('Error.'); var apiResponse = {}; beforeEach(function() { - transaction.request_ = function(protoOpts, callback) { + transaction.request_ = function(config, callback) { callback(error, apiResponse); }; }); @@ -457,7 +484,7 @@ describe('Transaction', function() { }; beforeEach(function() { - transaction.request_ = function(protoOpts, callback) { + transaction.request_ = function(config, callback) { callback(null, apiResponse); }; });