mirror of
https://github.com/titanscouting/tra-analysis.git
synced 2025-01-03 20:41:22 +00:00
831 lines
23 KiB
JavaScript
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;
|