diff --git a/packages/common-gax/package.json b/packages/common-gax/package.json new file mode 100644 index 00000000000..99618c1948e --- /dev/null +++ b/packages/common-gax/package.json @@ -0,0 +1,38 @@ +{ + "name": "@google-cloud/common-gax", + "version": "0.0.0", + "author": "Google Inc.", + "description": "Common components for Cloud APIs Node.js Client Libraries that require google-gax", + "contributors": [ + { + "name": "Stephen Sawchuk", + "email": "sawchuk@gmail.com" + } + ], + "main": "./src/index.js", + "files": [ + "src", + "AUTHORS", + "CONTRIBUTORS", + "COPYING" + ], + "repository": "googlecloudplatform/google-cloud-node", + "dependencies": { + "@google-cloud/common": "^0.13.1", + "extend": "^3.0.0", + "stream-events": "^1.0.1", + "through2": "^2.0.3" + }, + "devDependencies": { + "mocha": "^3.2.0", + "proxyquire": "^1.7.10" + }, + "scripts": { + "publish-module": "node ../../scripts/publish.js common-gax", + "test": "mocha test/*.js" + }, + "license": "Apache-2.0", + "engines": { + "node": ">=4.0.0" + } +} diff --git a/packages/common-gax/src/index.js b/packages/common-gax/src/index.js new file mode 100644 index 00000000000..a6539ef151f --- /dev/null +++ b/packages/common-gax/src/index.js @@ -0,0 +1,26 @@ +/*! + * Copyright 2017 Google Inc. All Rights Reserved. + * + * 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. + */ + +/*! + * @module common-gax + */ + +'use strict'; + +/** + * @type {module:common-gax/service} + */ +exports.Service = require('./service.js'); diff --git a/packages/common-gax/src/service.js b/packages/common-gax/src/service.js new file mode 100644 index 00000000000..eb7e6ce82b1 --- /dev/null +++ b/packages/common-gax/src/service.js @@ -0,0 +1,168 @@ +/*! + * Copyright 2017 Google Inc. All Rights Reserved. + * + * 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. + */ + +/*! + * @module common-gax/service + */ + +'use strict'; + +var common = require('@google-cloud/common'); +var extend = require('extend'); +var streamEvents = require('stream-events'); +var through = require('through2'); +var util = require('util'); + +/** + * GaxService is a base class, meant to be inherited from by a "service" that + * uses GAX. + * + * @constructor + * @alias module:common-gax/service + * + * @param {object} config - Configuration object. + * @param {*} config.module - The generated module. + * @param {object} options - [Configuration object](#/docs/?method=gcloud). + */ +function GaxService(config, options) { + config = extend({ + scopes: config.module.ALL_SCOPES + }, config); + + this.api = {}; + this.module = config.module; + this.options = options; + + common.Service.call(this, config, options); +} + +util.inherits(GaxService, common.Service); + +/** + * Funnel all API requests through this method, to be sure we have a project ID. + * + * @param {object} config - Configuration object. + * @param {string} config.client - The gax client name. + * @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. + */ +GaxService.prototype.request = function(config, callback) { + if (global.GCLOUD_SANDBOX_ENV) { + return; + } + + this.prepareRequest_(config, function(err, requestFn) { + if (err) { + callback(err); + return; + } + + requestFn(callback); + }); +}; + +/** + * Make a request as a stream. + * + * @param {object} config - Configuration object. + * @param {string} config.client - The gax client name. + * @param {object} config.gaxOpts - GAX options. + * @param {function} config.method - The gax method to call. + * @param {object} config.reqOpts - Request options. + * @return {stream} + */ +GaxService.prototype.requestStream = function(config) { + if (global.GCLOUD_SANDBOX_ENV) { + return through.obj(); + } + + var self = this; + + var gaxStream; + var stream = streamEvents(through.obj()); + + stream.abort = function() { + if (gaxStream && gaxStream.cancel) { + gaxStream.cancel(); + } + }; + + stream.once('reading', function() { + self.prepareRequest_(config, function(err, requestFn) { + if (err) { + stream.destroy(err); + return; + } + + gaxStream = requestFn(); + + gaxStream + .on('error', function(err) { + stream.destroy(err); + }) + .pipe(stream); + }); + }); + + return stream; +}; + +/** + * Prepare a request by instantiating and caching the GAX client. Project ID + * placeholder tokens will be replaced in the request options. + * + * @private + * + * @param {object} config - Configuration object. + * @param {string} config.client - The gax client name. + * @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. + */ +GaxService.prototype.prepareRequest_ = function(config, callback) { + var self = this; + + this.getProjectId(function(err, projectId) { + if (err) { + callback(err); + return; + } + + var gaxClient = self.api[config.client]; + + if (!gaxClient) { + // Lazily instantiate client. + gaxClient = self.module(self.options)[config.client](self.options); + self.api[config.client] = gaxClient; + } + + var reqOpts = extend(true, {}, config.reqOpts); + reqOpts = common.util.replaceProjectIdToken(reqOpts, projectId); + + var requestFn = gaxClient[config.method].bind( + gaxClient, + reqOpts, + config.gaxOpts + ); + + callback(null, requestFn); + }); +}; + +module.exports = GaxService; \ No newline at end of file diff --git a/packages/common-gax/test/index.js b/packages/common-gax/test/index.js new file mode 100644 index 00000000000..7f82ed4f190 --- /dev/null +++ b/packages/common-gax/test/index.js @@ -0,0 +1,38 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * 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. + */ + +'use strict'; + +var assert = require('assert'); +var proxyquire = require('proxyquire'); + +var fakeService = {}; + +describe('common-gax', function() { + var commonGax; + + before(function() { + commonGax = proxyquire('../src/index.js', { + './service.js': fakeService + }); + }); + + it('should correctly export the commonGax modules', function() { + assert.deepEqual(commonGax, { + Service: fakeService + }); + }); +}); diff --git a/packages/common-gax/test/service.js b/packages/common-gax/test/service.js new file mode 100644 index 00000000000..12e9d724b7d --- /dev/null +++ b/packages/common-gax/test/service.js @@ -0,0 +1,319 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * 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. + */ + +'use strict'; + +var assert = require('assert'); +var extend = require('extend'); +var proxyquire = require('proxyquire'); +var through = require('through2'); +var util = require('@google-cloud/common').util; + +var replaceProjectIdTokenOverride; +var fakeUtil = extend({}, util, { + replaceProjectIdToken: function(reqOpts) { + if (replaceProjectIdTokenOverride) { + return replaceProjectIdTokenOverride.apply(null, arguments); + } + + return reqOpts; + } +}); + +function FakeService() { + this.calledWith_ = arguments; +} + +describe('GaxService', function() { + var GaxService; + var gaxService; + + var MODULE = { + ALL_SCOPES: ['scope1, scope2'] + }; + var CONFIG = { + module: MODULE + }; + var OPTIONS = {}; + + before(function() { + GaxService = proxyquire('../src/service.js', { + '@google-cloud/common': { + Service: FakeService, + util: fakeUtil + } + }); + }); + + beforeEach(function() { + replaceProjectIdTokenOverride = null; + + gaxService = new GaxService(CONFIG, OPTIONS); + }); + + describe('instantiation', function() { + it('should initialize the API object', function() { + assert.deepEqual(gaxService.api, {}); + }); + + it('should localize the GAX modules', function() { + assert.strictEqual(gaxService.module, MODULE); + }); + + it('should localize the options', function() { + assert.strictEqual(gaxService.options, OPTIONS); + }); + + it('should inherit from Service', function() { + assert(gaxService instanceof FakeService); + + assert.deepEqual(gaxService.calledWith_[0], extend({ + scopes: MODULE.ALL_SCOPES + }, CONFIG)); + assert.strictEqual(gaxService.calledWith_[1], OPTIONS); + }); + }); + + describe('request', function() { + var REQ_CONFIG = {}; + + it('should return if in snippet sandbox', function(done) { + gaxService.prepareRequest_ = function() { + done(new Error('Should not have gotten project ID.')); + }; + + global.GCLOUD_SANDBOX_ENV = true; + var returnValue = gaxService.request(REQ_CONFIG, assert.ifError); + delete global.GCLOUD_SANDBOX_ENV; + + assert.strictEqual(returnValue, undefined); + done(); + }); + + it('should prepare the request', function(done) { + gaxService.prepareRequest_ = function(config) { + assert.strictEqual(config, REQ_CONFIG); + done(); + }; + + gaxService.request(REQ_CONFIG, assert.ifError); + }); + + it('should execute callback with error', function(done) { + var error = new Error('Error.'); + + gaxService.prepareRequest_ = function(config, callback) { + callback(error); + }; + + gaxService.request(REQ_CONFIG, function(err) { + assert.strictEqual(err, error); + done(); + }); + }); + + it('should execute the returned request function', function(done) { + gaxService.prepareRequest_ = function(config, callback) { + function preparedRequestFn(cb) { + cb(); // done() + } + + callback(null, preparedRequestFn); + }; + + gaxService.request(REQ_CONFIG, done); + }); + }); + + describe('requestStream', function() { + var REQ_CONFIG = {}; + var GAX_STREAM; + + beforeEach(function() { + GAX_STREAM = through(); + + gaxService.prepareRequest_ = function(config, callback) { + function preparedRequestFn() { + return GAX_STREAM; + } + + callback(null, preparedRequestFn); + }; + }); + + it('should return if in snippet sandbox', function(done) { + gaxService.prepareRequest_ = function() { + done(new Error('Should not have gotten project ID.')); + }; + + global.GCLOUD_SANDBOX_ENV = true; + var returnValue = gaxService.requestStream(REQ_CONFIG); + returnValue.emit('reading'); + delete global.GCLOUD_SANDBOX_ENV; + + assert(returnValue instanceof require('stream')); + done(); + }); + + it('should expose an abort function', function(done) { + GAX_STREAM.cancel = done; + + var requestStream = gaxService.requestStream(REQ_CONFIG); + requestStream.emit('reading'); + requestStream.abort(); + }); + + it('should prepare the request once reading', function(done) { + gaxService.prepareRequest_ = function(config) { + assert.strictEqual(config, REQ_CONFIG); + done(); + }; + + var requestStream = gaxService.requestStream(REQ_CONFIG); + requestStream.emit('reading'); + }); + + it('should destroy the stream with prepare error', function(done) { + var error = new Error('Error.'); + + gaxService.prepareRequest_ = function(config, callback) { + callback(error); + }; + + var requestStream = gaxService.requestStream(CONFIG); + requestStream.emit('reading'); + + requestStream.on('error', function(err) { + assert.strictEqual(err, error); + done(); + }); + }); + + it('should destroy the stream with GAX error', function(done) { + var error = new Error('Error.'); + + var requestStream = gaxService.requestStream(CONFIG); + requestStream.emit('reading'); + + requestStream.on('error', function(err) { + assert.strictEqual(err, error); + done(); + }); + + GAX_STREAM.emit('error', error); + }); + }); + + describe('prepareRequest_', function() { + var CONFIG = { + client: 'client', + method: 'method', + reqOpts: { + a: 'b', + c: 'd' + }, + gaxOpts: {} + }; + + var PROJECT_ID = 'project-id'; + + beforeEach(function() { + gaxService.getProjectId = function(callback) { + callback(null, PROJECT_ID); + }; + + gaxService.api[CONFIG.client] = { + [CONFIG.method]: util.noop + }; + }); + + it('should get the project ID', function(done) { + gaxService.getProjectId = function() { + done(); + }; + + gaxService.prepareRequest_(CONFIG, assert.ifError); + }); + + it('should return error if getting project ID failed', function(done) { + var error = new Error('Error.'); + + gaxService.getProjectId = function(callback) { + callback(error); + }; + + gaxService.prepareRequest_(CONFIG, function(err) { + assert.strictEqual(err, error); + done(); + }); + }); + + it('should initiate and cache the client', function() { + var fakeClient = { + [CONFIG.method]: util.noop + }; + + gaxService.module = function(options) { + assert.strictEqual(options, gaxService.options); + + return { + [CONFIG.client]: function(options) { + assert.strictEqual(options, gaxService.options); + return fakeClient; + } + }; + }; + + gaxService.api = {}; + + gaxService.prepareRequest_(CONFIG, assert.ifError); + + assert.strictEqual(gaxService.api[CONFIG.client], fakeClient); + }); + + it('should use the cached client', function() { + gaxService.module = function() { + throw new Error('Should not re-instantiate a GAX client.'); + }; + + gaxService.prepareRequest_(CONFIG, assert.ifError); + }); + + it('should replace the project ID token', function(done) { + var replacedReqOpts = {}; + + replaceProjectIdTokenOverride = function(reqOpts, projectId) { + assert.notStrictEqual(reqOpts, CONFIG.reqOpts); + assert.deepEqual(reqOpts, CONFIG.reqOpts); + assert.strictEqual(projectId, PROJECT_ID); + + return replacedReqOpts; + }; + + gaxService.api[CONFIG.client][CONFIG.method] = { + bind: function(gaxClient, reqOpts) { + assert.strictEqual(reqOpts, replacedReqOpts); + + setImmediate(done); + + return util.noop; + } + }; + + gaxService.prepareRequest_(CONFIG, assert.ifError); + }); + }); +}); diff --git a/scripts/docs/config.js b/scripts/docs/config.js index 8857ef3ede1..563617d0c56 100644 --- a/scripts/docs/config.js +++ b/scripts/docs/config.js @@ -23,6 +23,7 @@ module.exports = { TOC: 'toc.json', IGNORE: [ 'common', + 'common-gax', 'common-grpc', 'bigtable/src/mutation.js', 'datastore/src/entity.js', diff --git a/scripts/helpers.js b/scripts/helpers.js index 184a9754d68..e9b9bf22971 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -238,6 +238,7 @@ Module.prototype.runSnippetTests = function() { Module.prototype.runSystemTests = function() { var modulesExcludedFromSystemTests = [ 'common', + 'common-gax', 'common-grpc', 'error-reporting', 'google-cloud', diff --git a/scripts/link-common.js b/scripts/link-common.js index 814b9a8969e..2b2f2a2d9ce 100644 --- a/scripts/link-common.js +++ b/scripts/link-common.js @@ -29,14 +29,17 @@ var Module = require('./helpers').Module; var common = new Module('common'); +var commonGax = new Module('common-gax'); var commonGrpc = new Module('common-grpc'); var packages = Module.getAll(); common.link(); +commonGax.link(); commonGrpc.link(); packages.forEach(function(pkg) { pkg.link(common); + pkg.link(commonGax); pkg.link(commonGrpc); });