tra-analysis/website/functions/node_modules/@google-cloud/common/src/util.js
2019-01-06 13:02:35 -06:00

831 lines
23 KiB
JavaScript

/**
* Copyright 2014 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/util
*/
'use strict';
const createErrorClass = require('create-error-class');
const duplexify = require('duplexify');
const ent = require('ent');
const extend = require('extend');
const googleAuth = require('google-auto-auth');
const is = require('is');
const request = require('request').defaults({
timeout: 60000,
gzip: true,
forever: true,
pool: {
maxSockets: Infinity,
},
});
const retryRequest = require('retry-request');
const streamEvents = require('stream-events');
const through = require('through2');
const uniq = require('array-uniq');
const util = module.exports;
/**
* Custom error type for missing project ID errors.
*/
util.MissingProjectIdError = createErrorClass(
'MissingProjectIdError',
function() {
this.message = `Sorry, we cannot connect to Cloud Services without a project
ID. You may specify one with an environment variable named
"GOOGLE_CLOUD_PROJECT".`.replace(/ +/g, ' ');
}
);
/**
* No op.
*
* @example
* function doSomething(callback) {
* callback = callback || noop;
* }
*/
function noop() {}
util.noop = noop;
/**
* Custom error type for API errors.
*
* @param {object} errorBody - Error object.
*/
util.ApiError = createErrorClass('ApiError', function(errorBody) {
this.code = errorBody.code;
this.errors = errorBody.errors;
this.response = errorBody.response;
try {
this.errors = JSON.parse(this.response.body).error.errors;
} catch (e) {
this.errors = errorBody.errors;
}
const messages = [];
if (errorBody.message) {
messages.push(errorBody.message);
}
if (this.errors && this.errors.length === 1) {
messages.push(this.errors[0].message);
} else if (this.response && this.response.body) {
messages.push(ent.decode(errorBody.response.body.toString()));
} else if (!errorBody.message) {
messages.push('Error during request.');
}
this.message = uniq(messages).join(' - ');
});
/**
* Custom error type for partial errors returned from the API.
*
* @param {object} b - Error object.
*/
util.PartialFailureError = createErrorClass('PartialFailureError', function(b) {
const errorObject = b;
this.errors = errorObject.errors;
this.response = errorObject.response;
const defaultErrorMessage = 'A failure occurred during this request.';
this.message = errorObject.message || defaultErrorMessage;
});
/**
* Uniformly process an API response.
*
* @param {*} err - Error value.
* @param {*} resp - Response value.
* @param {*} body - Body value.
* @param {function} callback - The callback function.
*/
function handleResp(err, resp, body, callback) {
callback = callback || util.noop;
const parsedResp = extend(
true,
{err: err || null},
resp && util.parseHttpRespMessage(resp),
body && util.parseHttpRespBody(body)
);
callback(parsedResp.err, parsedResp.body, parsedResp.resp);
}
util.handleResp = handleResp;
/**
* Sniff an incoming HTTP response message for errors.
*
* @param {object} httpRespMessage - An incoming HTTP response message from
* `request`.
* @return {object} parsedHttpRespMessage - The parsed response.
* @param {?error} parsedHttpRespMessage.err - An error detected.
* @param {object} parsedHttpRespMessage.resp - The original response object.
*/
function parseHttpRespMessage(httpRespMessage) {
const parsedHttpRespMessage = {
resp: httpRespMessage,
};
if (httpRespMessage.statusCode < 200 || httpRespMessage.statusCode > 299) {
// Unknown error. Format according to ApiError standard.
parsedHttpRespMessage.err = new util.ApiError({
errors: [],
code: httpRespMessage.statusCode,
message: httpRespMessage.statusMessage,
response: httpRespMessage,
});
}
return parsedHttpRespMessage;
}
util.parseHttpRespMessage = parseHttpRespMessage;
/**
* Parse the response body from an HTTP request.
*
* @param {object} body - The response body.
* @return {object} parsedHttpRespMessage - The parsed response.
* @param {?error} parsedHttpRespMessage.err - An error detected.
* @param {object} parsedHttpRespMessage.body - The original body value provided
* will try to be JSON.parse'd. If it's successful, the parsed value will be
* returned here, otherwise the original value.
*/
function parseHttpRespBody(body) {
const parsedHttpRespBody = {
body: body,
};
if (is.string(body)) {
try {
parsedHttpRespBody.body = JSON.parse(body);
} catch (err) {
parsedHttpRespBody.err = new util.ApiError('Cannot parse JSON response');
}
}
if (parsedHttpRespBody.body && parsedHttpRespBody.body.error) {
// Error from JSON API.
parsedHttpRespBody.err = new util.ApiError(parsedHttpRespBody.body.error);
}
return parsedHttpRespBody;
}
util.parseHttpRespBody = parseHttpRespBody;
/**
* Take a Duplexify stream, fetch an authenticated connection header, and create
* an outgoing writable stream.
*
* @param {Duplexify} dup - Duplexify stream.
* @param {object} options - Configuration object.
* @param {module:common/connection} options.connection - A connection instance,
* used to get a token with and send the request through.
* @param {object} options.metadata - Metadata to send at the head of the
* request.
* @param {object} options.request - Request object, in the format of a standard
* Node.js http.request() object.
* @param {string=} options.request.method - Default: "POST".
* @param {string=} options.request.qs.uploadType - Default: "multipart".
* @param {string=} options.streamContentType - Default:
* "application/octet-stream".
* @param {function} onComplete - Callback, executed after the writable Request
* stream has completed.
*/
function makeWritableStream(dup, options, onComplete) {
onComplete = onComplete || util.noop;
const writeStream = through();
dup.setWritable(writeStream);
const defaultReqOpts = {
method: 'POST',
qs: {
uploadType: 'multipart',
},
};
const metadata = options.metadata || {};
const reqOpts = extend(true, defaultReqOpts, options.request, {
multipart: [
{
'Content-Type': 'application/json',
body: JSON.stringify(metadata),
},
{
'Content-Type': metadata.contentType || 'application/octet-stream',
body: writeStream,
},
],
});
options.makeAuthenticatedRequest(reqOpts, {
onAuthenticated: function(err, authenticatedReqOpts) {
if (err) {
dup.destroy(err);
return;
}
request(authenticatedReqOpts, function(err, resp, body) {
util.handleResp(err, resp, body, function(err, data) {
if (err) {
dup.destroy(err);
return;
}
dup.emit('response', resp);
onComplete(data);
});
});
},
});
}
util.makeWritableStream = makeWritableStream;
/**
* Returns true if the API request should be retried, given the error that was
* given the first time the request was attempted. This is used for rate limit
* related errors as well as intermittent server errors.
*
* @param {error} err - The API error to check if it is appropriate to retry.
* @return {boolean} True if the API request should be retried, false otherwise.
*/
function shouldRetryRequest(err) {
if (err) {
if ([429, 500, 502, 503].indexOf(err.code) !== -1) {
return true;
}
if (err.errors) {
for (const i in err.errors) {
const reason = err.errors[i].reason;
if (reason === 'rateLimitExceeded') {
return true;
}
if (reason === 'userRateLimitExceeded') {
return true;
}
}
}
}
return false;
}
util.shouldRetryRequest = shouldRetryRequest;
/**
* Get a function for making authenticated requests.
*
* @throws {Error} If a projectId is requested, but not able to be detected.
*
* @param {object} config - Configuration object.
* @param {boolean=} config.autoRetry - Automatically retry requests if the
* response is related to rate limits or certain intermittent server errors.
* We will exponentially backoff subsequent requests by default. (default:
* true)
* @param {object=} config.credentials - Credentials object.
* @param {boolean=} config.customEndpoint - If true, just return the provided
* request options. Default: false.
* @param {string=} config.email - Account email address, required for PEM/P12
* usage.
* @param {number=} config.maxRetries - Maximum number of automatic retries
* attempted before returning the error. (default: 3)
* @param {string=} config.keyFile - Path to a .json, .pem, or .p12 keyfile.
* @param {array} config.scopes - Array of scopes required for the API.
*/
function makeAuthenticatedRequestFactory(config) {
config = config || {};
const googleAutoAuthConfig = extend({}, config);
if (googleAutoAuthConfig.projectId === '{{projectId}}') {
delete googleAutoAuthConfig.projectId;
}
const authClient = googleAuth(googleAutoAuthConfig);
/**
* The returned function that will make an authenticated request.
*
* @param {type} reqOpts - Request options in the format `request` expects.
* @param {object|function} options - Configuration object or callback
* function.
* @param {function=} options.onAuthenticated - If provided, a request will
* not be made. Instead, this function is passed the error & authenticated
* request options.
*/
function makeAuthenticatedRequest(reqOpts, options) {
let stream;
const reqConfig = extend({}, config);
let activeRequest_;
if (!options) {
stream = duplexify();
reqConfig.stream = stream;
}
function onAuthenticated(err, authenticatedReqOpts) {
const autoAuthFailed =
err &&
err.message.indexOf('Could not load the default credentials') > -1;
if (autoAuthFailed) {
// Even though authentication failed, the API might not actually care.
authenticatedReqOpts = reqOpts;
}
if (!err || autoAuthFailed) {
let projectId = authClient.projectId;
if (config.projectId && config.projectId !== '{{projectId}}') {
projectId = config.projectId;
}
try {
authenticatedReqOpts = util.decorateRequest(
authenticatedReqOpts,
projectId
);
err = null;
} catch (e) {
// A projectId was required, but we don't have one.
// Re-use the "Could not load the default credentials error" if auto
// auth failed.
err = err || e;
}
}
if (err) {
if (stream) {
stream.destroy(err);
} else {
(options.onAuthenticated || options)(err);
}
return;
}
if (options && options.onAuthenticated) {
options.onAuthenticated(null, authenticatedReqOpts);
} else {
activeRequest_ = util.makeRequest(
authenticatedReqOpts,
reqConfig,
options
);
}
}
if (reqConfig.customEndpoint) {
// Using a custom API override. Do not use `google-auto-auth` for
// authentication. (ex: connecting to a local Datastore server)
onAuthenticated(null, reqOpts);
} else {
authClient.authorizeRequest(reqOpts, onAuthenticated);
}
if (stream) {
return stream;
}
return {
abort: function() {
if (activeRequest_) {
activeRequest_.abort();
activeRequest_ = null;
}
},
};
}
makeAuthenticatedRequest.getCredentials = authClient.getCredentials.bind(
authClient
);
makeAuthenticatedRequest.authClient = authClient;
return makeAuthenticatedRequest;
}
util.makeAuthenticatedRequestFactory = makeAuthenticatedRequestFactory;
/**
* Make a request through the `retryRequest` module with built-in error handling
* and exponential back off.
*
* @param {object} reqOpts - Request options in the format `request` expects.
* @param {object=} config - Configuration object.
* @param {boolean=} config.autoRetry - Automatically retry requests if the
* response is related to rate limits or certain intermittent server errors.
* We will exponentially backoff subsequent requests by default. (default:
* true)
* @param {number=} config.maxRetries - Maximum number of automatic retries
* attempted before returning the error. (default: 3)
* @param {function} callback - The callback function.
*/
function makeRequest(reqOpts, config, callback) {
if (is.fn(config)) {
callback = config;
config = {};
}
config = config || {};
const options = {
request: request,
retries: config.autoRetry !== false ? config.maxRetries || 3 : 0,
shouldRetryFn: function(httpRespMessage) {
const err = util.parseHttpRespMessage(httpRespMessage).err;
return err && util.shouldRetryRequest(err);
},
};
if (config.stream) {
const dup = config.stream;
let requestStream;
const isGetRequest = (reqOpts.method || 'GET').toUpperCase() === 'GET';
if (isGetRequest) {
requestStream = retryRequest(reqOpts, options);
dup.setReadable(requestStream);
} else {
// Streaming writable HTTP requests cannot be retried.
requestStream = request(reqOpts);
dup.setWritable(requestStream);
}
// Replay the Request events back to the stream.
requestStream
.on('error', dup.destroy.bind(dup))
.on('response', dup.emit.bind(dup, 'response'))
.on('complete', dup.emit.bind(dup, 'complete'));
dup.abort = requestStream.abort;
} else {
return retryRequest(reqOpts, options, function(err, response, body) {
util.handleResp(err, response, body, callback);
});
}
}
util.makeRequest = makeRequest;
/**
* Decorate the options about to be made in a request.
*
* @param {object} reqOpts - The options to be passed to `request`.
* @param {string} projectId - The project ID.
* @return {object} reqOpts - The decorated reqOpts.
*/
function decorateRequest(reqOpts, projectId) {
delete reqOpts.autoPaginate;
delete reqOpts.autoPaginateVal;
delete reqOpts.objectMode;
if (is.object(reqOpts.qs)) {
delete reqOpts.qs.autoPaginate;
delete reqOpts.qs.autoPaginateVal;
reqOpts.qs = util.replaceProjectIdToken(reqOpts.qs, projectId);
}
if (is.object(reqOpts.json)) {
delete reqOpts.json.autoPaginate;
delete reqOpts.json.autoPaginateVal;
reqOpts.json = util.replaceProjectIdToken(reqOpts.json, projectId);
}
reqOpts.uri = util.replaceProjectIdToken(reqOpts.uri, projectId);
return reqOpts;
}
util.decorateRequest = decorateRequest;
/**
* Populate the `{{projectId}}` placeholder.
*
* @throws {Error} If a projectId is required, but one is not provided.
*
* @param {*} - Any input value that may contain a placeholder. Arrays and
* objects will be looped.
* @param {string} projectId - A projectId. If not provided
* @return {*} - The original argument with all placeholders populated.
*/
function replaceProjectIdToken(value, projectId) {
if (is.array(value)) {
value = value.map(function(val) {
return replaceProjectIdToken(val, projectId);
});
}
if (is.object(value) && is.fn(value.hasOwnProperty)) {
for (const opt in value) {
if (value.hasOwnProperty(opt)) {
value[opt] = replaceProjectIdToken(value[opt], projectId);
}
}
}
if (is.string(value) && value.indexOf('{{projectId}}') > -1) {
if (!projectId || projectId === '{{projectId}}') {
throw new util.MissingProjectIdError();
}
value = value.replace(/{{projectId}}/g, projectId);
}
return value;
}
util.replaceProjectIdToken = replaceProjectIdToken;
/**
* Extend a global configuration object with user options provided at the time
* of sub-module instantiation.
*
* Connection details currently come in two ways: `credentials` or
* `keyFilename`. Because of this, we have a special exception when overriding a
* global configuration object. If a user provides either to the global
* configuration, then provides another at submodule instantiation-time, the
* latter is preferred.
*
* @param {object} globalConfig - The global configuration object.
* @param {object=} overrides - The instantiation-time configuration object.
* @return {object}
*/
function extendGlobalConfig(globalConfig, overrides) {
globalConfig = globalConfig || {};
overrides = overrides || {};
const defaultConfig = {};
if (process.env.GCLOUD_PROJECT) {
defaultConfig.projectId = process.env.GCLOUD_PROJECT;
}
const options = extend({}, globalConfig);
const hasGlobalConnection = options.credentials || options.keyFilename;
const isOverridingConnection = overrides.credentials || overrides.keyFilename;
if (hasGlobalConnection && isOverridingConnection) {
delete options.credentials;
delete options.keyFilename;
}
const extendedConfig = extend(true, defaultConfig, options, overrides);
// Preserve the original (not cloned) interceptors.
extendedConfig.interceptors_ = globalConfig.interceptors_;
return extendedConfig;
}
util.extendGlobalConfig = extendGlobalConfig;
/**
* Merge and validate API configurations.
*
* @param {object} globalContext - gcloud-level context.
* @param {object} globalContext.config_ - gcloud-level configuration.
* @param {object} localConfig - Service-level configurations.
* @return {object} config - Merged and validated configuration.
*/
function normalizeArguments(globalContext, localConfig) {
const globalConfig = globalContext && globalContext.config_;
return util.extendGlobalConfig(globalConfig, localConfig);
}
util.normalizeArguments = normalizeArguments;
/**
* Limit requests according to a `maxApiCalls` limit.
*
* @param {function} makeRequestFn - The function that will be called.
* @param {object=} options - Configuration object.
* @param {number} options.maxApiCalls - The maximum number of API calls to
* make.
* @param {object} options.streamOptions - Options to pass to the Stream
* constructor.
*/
function createLimiter(makeRequestFn, options) {
options = options || {};
const stream = streamEvents(through.obj(options.streamOptions));
let requestsMade = 0;
let requestsToMake = -1;
if (is.number(options.maxApiCalls)) {
requestsToMake = options.maxApiCalls;
}
return {
makeRequest: function() {
requestsMade++;
if (requestsToMake >= 0 && requestsMade > requestsToMake) {
stream.push(null);
return;
}
makeRequestFn.apply(null, arguments);
return stream;
},
stream: stream,
};
}
util.createLimiter = createLimiter;
function isCustomType(unknown, module) {
function getConstructorName(obj) {
return obj.constructor && obj.constructor.name.toLowerCase();
}
const moduleNameParts = module.split('/');
const parentModuleName =
moduleNameParts[0] && moduleNameParts[0].toLowerCase();
const subModuleName = moduleNameParts[1] && moduleNameParts[1].toLowerCase();
if (subModuleName && getConstructorName(unknown) !== subModuleName) {
return false;
}
let walkingModule = unknown;
do {
if (getConstructorName(walkingModule) === parentModuleName) {
return true;
}
} while ((walkingModule = walkingModule.parent));
return false;
}
util.isCustomType = isCustomType;
/**
* Create a properly-formatted User-Agent string from a package.json file.
*
* @param {object} packageJson - A module's package.json file.
* @return {string} userAgent - The formatted User-Agent string.
*/
function getUserAgentFromPackageJson(packageJson) {
const hyphenatedPackageName = packageJson.name
.replace('@google-cloud', 'gcloud-node') // For legacy purposes.
.replace('/', '-'); // For UA spec-compliance purposes.
return hyphenatedPackageName + '/' + packageJson.version;
}
util.getUserAgentFromPackageJson = getUserAgentFromPackageJson;
/**
* Wraps a callback style function to conditionally return a promise.
*
* @param {function} originalMethod - The method to promisify.
* @param {object=} options - Promise options.
* @param {boolean} options.singular - Resolve the promise with single arg
* instead of an array.
* @return {function} wrapped
*/
function promisify(originalMethod, options) {
if (originalMethod.promisified_) {
return originalMethod;
}
options = options || {};
const slice = Array.prototype.slice;
const wrapper = function() {
const context = this;
let last;
for (last = arguments.length - 1; last >= 0; last--) {
const arg = arguments[last];
if (is.undefined(arg)) {
continue; // skip trailing undefined.
}
if (!is.fn(arg)) {
break; // non-callback last argument found.
}
return originalMethod.apply(context, arguments);
}
// peel trailing undefined.
const args = slice.call(arguments, 0, last + 1);
let PromiseCtor = Promise;
// Because dedupe will likely create a single install of
// @google-cloud/common to be shared amongst all modules, we need to
// localize it at the Service level.
if (context && context.Promise) {
PromiseCtor = context.Promise;
}
return new PromiseCtor(function(resolve, reject) {
args.push(function() {
const callbackArgs = slice.call(arguments);
const err = callbackArgs.shift();
if (err) {
return reject(err);
}
if (options.singular && callbackArgs.length === 1) {
resolve(callbackArgs[0]);
} else {
resolve(callbackArgs);
}
});
originalMethod.apply(context, args);
});
};
wrapper.promisified_ = true;
return wrapper;
}
util.promisify = promisify;
/**
* Promisifies certain Class methods. This will not promisify private or
* streaming methods.
*
* @param {module:common/service} Class - Service class.
* @param {object=} options - Configuration object.
*/
function promisifyAll(Class, options) {
const exclude = (options && options.exclude) || [];
const methods = Object.keys(Class.prototype).filter(function(methodName) {
return (
is.fn(Class.prototype[methodName]) && // is it a function?
!/(^_|(Stream|_)|promise$)/.test(methodName) && // is it promisable?
exclude.indexOf(methodName) === -1
); // is it blacklisted?
});
methods.forEach(function(methodName) {
const originalMethod = Class.prototype[methodName];
if (!originalMethod.promisified_) {
Class.prototype[methodName] = util.promisify(originalMethod, options);
}
});
}
util.promisifyAll = promisifyAll;
/**
* This will mask properties of an object from console.log.
*
* @param {object} object - The object to assign the property to.
* @param {string} propName - Property name.
* @param {*} value - Value.
*/
function privatize(object, propName, value) {
Object.defineProperty(object, propName, {value, writable: true});
}
util.privatize = privatize;