UNPKG

ran-boilerplate

Version:

React . Apollo (GraphQL) . Next.js Toolkit

968 lines (836 loc) 23.7 kB
/*! * Copyright 2015 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 commonGrpc/service */ 'use strict'; var dotProp = require('dot-prop'); var duplexify = require('duplexify'); var extend = require('extend'); var grpc = require('grpc'); var is = require('is'); var nodeutil = require('util'); var retryRequest = require('retry-request'); var Service = require('@google-cloud/common').Service; var through = require('through2'); var util = require('@google-cloud/common').util; /** * @const {object} - A cache of proto objects. * @private */ var protoObjectCache = {}; /** * @const {object} - A map of protobuf codes to HTTP status codes. * @private */ var GRPC_ERROR_CODE_TO_HTTP = { 0: { code: 200, message: 'OK', }, 1: { code: 499, message: 'Client Closed Request', }, 2: { code: 500, message: 'Internal Server Error', }, 3: { code: 400, message: 'Bad Request', }, 4: { code: 504, message: 'Gateway Timeout', }, 5: { code: 404, message: 'Not Found', }, 6: { code: 409, message: 'Conflict', }, 7: { code: 403, message: 'Forbidden', }, 8: { code: 429, message: 'Too Many Requests', }, 9: { code: 412, message: 'Precondition Failed', }, 10: { code: 409, message: 'Conflict', }, 11: { code: 400, message: 'Bad Request', }, 12: { code: 501, message: 'Not Implemented', }, 13: { code: 500, message: 'Internal Server Error', }, 14: { code: 503, message: 'Service Unavailable', }, 15: { code: 500, message: 'Internal Server Error', }, 16: { code: 401, message: 'Unauthorized', }, }; /** * The default configuration for all gRPC Service instantions. * * @resource [All options]{@link https://github.com/grpc/grpc/blob/13e185419cd177b7fb552601665e43820321a96b/include/grpc/impl/codegen/grpc_types.h#L148} * * @private * * @type {object} */ var GRPC_SERVICE_OPTIONS = { // RE: https://github.com/GoogleCloudPlatform/google-cloud-node/issues/1991 'grpc.max_send_message_length': -1, // unlimited 'grpc.max_receive_message_length': -1, // unlimited // RE: https://github.com/grpc/grpc/issues/8839 // RE: https://github.com/grpc/grpc/issues/8382 // RE: https://github.com/GoogleCloudPlatform/google-cloud-node/issues/1991 'grpc.initial_reconnect_backoff_ms': 5000, }; /** * Service is a base class, meant to be inherited from by a "service," like * BigQuery or Storage. * * This handles making authenticated requests by exposing a `makeReq_` function. * * @constructor * @alias module:common/grpc-service * * @param {object} config - Configuration object. * @param {string} config.baseUrl - The base URL to make API requests to. * @param {object} config.grpcMetadata - Metadata to send with every request. * @param {string[]} config.scopes - The scopes required for the request. * @param {string} config.protosDir - The root directory where proto files live. * @param {object} config.protoServices - Directly provide the required proto * files. This is useful when a single class requires multiple services. * @param {object} options - [Configuration object](#/docs/?method=gcloud). */ function GrpcService(config, options) { if (global.GCLOUD_SANDBOX_ENV) { // gRPC has a tendency to cause our doc unit tests to fail, so we prevent // any calls to that library from going through. // Reference: https://github.com/GoogleCloudPlatform/google-cloud-node/pull/1137#issuecomment-193315047 return global.GCLOUD_SANDBOX_ENV; } Service.call(this, config, options); if (config.customEndpoint) { this.grpcCredentials = grpc.credentials.createInsecure(); } this.grpcMetadata = new grpc.Metadata(); this.grpcMetadata.add( 'x-goog-api-client', [ 'gl-node/' + process.versions.node, 'gccl/' + config.packageJson.version, 'grpc/' + require('grpc/package.json').version, ].join(' ') ); if (config.grpcMetadata) { for (var prop in config.grpcMetadata) { if (config.grpcMetadata.hasOwnProperty(prop)) { this.grpcMetadata.add(prop, config.grpcMetadata[prop]); } } } this.maxRetries = options.maxRetries; this.userAgent = util.getUserAgentFromPackageJson(config.packageJson); this.activeServiceMap_ = new Map(); this.protos = {}; var protoServices = config.protoServices; var self = this; Object.keys(protoServices).forEach(function(name) { var protoConfig = protoServices[name]; var service = self.loadProtoFile_(protoConfig, config); self.protos[name] = service; if (protoConfig.baseUrl) { service.baseUrl = protoConfig.baseUrl; } }); } nodeutil.inherits(GrpcService, Service); /** * Make an authenticated request with gRPC. * * @param {object} protoOpts - The proto options. * @param {string} protoOpts.service - The service name. * @param {string} protoOpts.method - The method name. * @param {number=} protoOpts.timeout - After how many milliseconds should the * request cancel. * @param {object} reqOpts - The request options. * @param {function=} callback - The callback function. */ GrpcService.prototype.request = function(protoOpts, reqOpts, callback) { if (global.GCLOUD_SANDBOX_ENV) { return global.GCLOUD_SANDBOX_ENV; } var self = this; if (!this.grpcCredentials) { // We must establish an authClient to give to grpc. this.getGrpcCredentials_(function(err, credentials) { if (err) { callback(err); return; } self.grpcCredentials = credentials; self.request(protoOpts, reqOpts, callback); }); return; } var service = this.getService_(protoOpts); var metadata = this.grpcMetadata; var grpcOpts = {}; if (is.number(protoOpts.timeout)) { grpcOpts.deadline = GrpcService.createDeadline_(protoOpts.timeout); } try { reqOpts = this.decorateRequest_(reqOpts); } catch (e) { callback(e); return; } // Retains a reference to an error from the response. If the final callback is // executed with this as the "response", we return it to the user as an error. var respError; var retryOpts = extend( { retries: this.maxRetries, currentRetryAttempt: 0, shouldRetryFn: GrpcService.shouldRetryRequest_, // retry-request determines if it should retry from the incoming HTTP // response status. gRPC always returns an error proto message. We pass that // "error" into retry-request to act as the HTTP response, so it can use the // status code to determine if it should retry. request: function(_, onResponse) { respError = null; return service[protoOpts.method](reqOpts, metadata, grpcOpts, function( err, resp ) { if (err) { respError = GrpcService.decorateError_(err); if (respError) { onResponse(null, respError); return; } onResponse(err, resp); return; } onResponse(null, resp); }); }, }, protoOpts.retryOpts ); return retryRequest(null, retryOpts, function(err, resp) { if (!err && resp === respError) { err = respError; resp = null; } callback(err, resp); }); }; /** * Make an authenticated streaming request with gRPC. * * @param {object} protoOpts - The proto options. * @param {string} protoOpts.service - The service. * @param {string} protoOpts.method - The method name. * @param {number=} protoOpts.timeout - After how many milliseconds should the * request cancel. * @param {object} reqOpts - The request options. */ GrpcService.prototype.requestStream = function(protoOpts, reqOpts) { if (global.GCLOUD_SANDBOX_ENV) { return through.obj(); } var self = this; if (!protoOpts.stream) { protoOpts.stream = through.obj(); } var stream = protoOpts.stream; if (!this.grpcCredentials) { // We must establish an authClient to give to grpc. this.getGrpcCredentials_(function(err, credentials) { if (err) { stream.destroy(err); return; } self.grpcCredentials = credentials; self.requestStream(protoOpts, reqOpts); }); return stream; } var objectMode = !!reqOpts.objectMode; var service = this.getService_(protoOpts); var grpcMetadata = this.grpcMetadata; var grpcOpts = {}; if (is.number(protoOpts.timeout)) { grpcOpts.deadline = GrpcService.createDeadline_(protoOpts.timeout); } try { reqOpts = this.decorateRequest_(reqOpts); } catch (e) { setImmediate(function() { stream.destroy(e); }); return stream; } var retryOpts = extend( { retries: this.maxRetries, currentRetryAttempt: 0, objectMode: objectMode, shouldRetryFn: GrpcService.shouldRetryRequest_, request: function() { return service[protoOpts.method](reqOpts, grpcMetadata, grpcOpts).on( 'metadata', function() { // retry-request requires a server response before it starts emitting // data. The closest mechanism grpc provides is a metadata event, but // this does not provide any kind of response status. So we're faking // it here with code `0` which translates to HTTP 200. // // https://github.com/GoogleCloudPlatform/google-cloud-node/pull/1444#discussion_r71812636 var grcpStatus = GrpcService.decorateStatus_({code: 0}); this.emit('response', grcpStatus); } ); }, }, protoOpts.retryOpts ); return retryRequest(null, retryOpts) .on('error', function(err) { var grpcError = GrpcService.decorateError_(err); stream.destroy(grpcError || err); }) .on('request', stream.emit.bind(stream, 'request')) .pipe(stream); }; /** * Make an authenticated writable streaming request with gRPC. * * @param {object} protoOpts - The proto options. * @param {string} protoOpts.service - The service. * @param {string} protoOpts.method - The method name. * @param {number=} protoOpts.timeout - After how many milliseconds should the * request cancel. * @param {object} reqOpts - The request options. */ GrpcService.prototype.requestWritableStream = function(protoOpts, reqOpts) { var stream = (protoOpts.stream = protoOpts.stream || duplexify.obj()); if (global.GCLOUD_SANDBOX_ENV) { return stream; } var self = this; if (!this.grpcCredentials) { // We must establish an authClient to give to grpc. this.getGrpcCredentials_(function(err, credentials) { if (err) { stream.destroy(err); return; } self.grpcCredentials = credentials; self.requestWritableStream(protoOpts, reqOpts); }); return stream; } var service = this.getService_(protoOpts); var grpcMetadata = this.grpcMetadata; var grpcOpts = {}; if (is.number(protoOpts.timeout)) { grpcOpts.deadline = GrpcService.createDeadline_(protoOpts.timeout); } try { reqOpts = this.decorateRequest_(reqOpts); } catch (e) { setImmediate(function() { stream.destroy(e); }); return stream; } var grpcStream = service[protoOpts.method](reqOpts, grpcMetadata, grpcOpts) .on('status', function(status) { var grcpStatus = GrpcService.decorateStatus_(status); stream.emit('response', grcpStatus || status); }) .on('error', function(err) { var grpcError = GrpcService.decorateError_(err); stream.destroy(grpcError || err); }); stream.setReadable(grpcStream); stream.setWritable(grpcStream); return stream; }; /** * Decode a protobuf Struct's value. * * @private * * @param {object} value - A Struct's Field message. * @return {*} - The decoded value. */ GrpcService.decodeValue_ = function(value) { switch (value.kind) { case 'structValue': { return GrpcService.structToObj_(value.structValue); } case 'nullValue': { return null; } case 'listValue': { return value.listValue.values.map(GrpcService.decodeValue_); } default: { return value[value.kind]; } } }; /** * Convert a raw value to a type-denoted protobuf message-friendly object. * * @private * * @param {*} value - The input value. * @return {*} - The encoded value. * * @example * ObjectToStructConverter.encodeValue('Hi'); * // { * // stringValue: 'Hello!' * // } */ GrpcService.encodeValue_ = function(value) { return new GrpcService.ObjectToStructConverter().encodeValue_(value); }; /** * Creates a deadline. * * @private * * @param {number} timeout - Timeout in miliseconds. * @return {date} deadline - The deadline in Date object form. */ GrpcService.createDeadline_ = function(timeout) { return new Date(Date.now() + timeout); }; /** * Checks for a grpc status code and extends the error object with additional * information. * * @private * * @param {error|object} err - The grpc error. * @return {error|null} */ GrpcService.decorateError_ = function(err) { var errorObj = is.error(err) ? err : {}; return GrpcService.decorateGrpcResponse_(errorObj, err); }; /** * Checks for a grpc status code and extends the supplied object with additional * information. * * @private * * @param {object} obj - The object to be extended. * @param {object} response - The grpc response. * @return {object|null} */ GrpcService.decorateGrpcResponse_ = function(obj, response) { if (response && GRPC_ERROR_CODE_TO_HTTP[response.code]) { var defaultResponseDetails = GRPC_ERROR_CODE_TO_HTTP[response.code]; var message = defaultResponseDetails.message; if (response.message) { // gRPC error messages can be either stringified JSON or strings. try { message = JSON.parse(response.message).description; } catch (e) { message = response.message; } } return extend(true, obj, response, { code: defaultResponseDetails.code, message: message, }); } return null; }; /** * Checks for grpc status code and extends the status object with additional * information * * @private * @param {object} status - The grpc status. * @return {object|null} */ GrpcService.decorateStatus_ = function(status) { return GrpcService.decorateGrpcResponse_({}, status); }; /** * Function to decide whether or not a request retry could occur. * * @private * * @param {object} response - The request response. * @return {boolean} shouldRetry */ GrpcService.shouldRetryRequest_ = function(response) { return [429, 500, 502, 503].indexOf(response.code) > -1; }; /** * Convert an object to a struct. * * @private * * @param {object} obj - An object to convert. * @param {object=} options - Configuration object. * @param {boolean} options.removeCircular - Remove circular references in the * object with a placeholder string. * @param {boolean} options.stringify - Stringify un-recognized types. * @return {array} - The converted object. * * @example * GrpcService.objToStruct_({ * greeting: 'Hello!', * favNumber: 7, * friendIds: [ * 1004, * 1006 * ], * userDetails: { * termsSigned: true * } * }); * // { * // fields: { * // greeting: { * // stringValue: 'Hello!' * // }, * // favNumber: { * // numberValue: 7 * // }, * // friendIds: { * // listValue: [ * // { * // numberValue: 1004 * // }, * // { * // numberValue: 1006 * // } * // ] * // }, * // userDetails: { * // fields: { * // termsSigned: { * // booleanValue: true * // } * // } * // } * // } * // } */ GrpcService.objToStruct_ = function(obj, options) { return new GrpcService.ObjectToStructConverter(options).convert(obj); }; /** * Condense a protobuf Struct into an object of only its values. * * @private * * @param {object} struct - A protobuf Struct message. * @return {object} - The simplified object. * * @example * GrpcService.structToObj_({ * fields: { * name: { * kind: 'stringValue', * stringValue: 'Stephen' * } * } * }); * // { * // name: 'Stephen' * // } */ GrpcService.structToObj_ = function(struct) { var convertedObject = {}; for (var prop in struct.fields) { if (struct.fields.hasOwnProperty(prop)) { var value = struct.fields[prop]; convertedObject[prop] = GrpcService.decodeValue_(value); } } return convertedObject; }; /** * Assign a projectId if one is specified to all request options. * * @param {object} reqOpts - The request options. * @return {object} - The decorated request object. */ GrpcService.prototype.decorateRequest_ = function(reqOpts) { reqOpts = extend({}, reqOpts); delete reqOpts.autoPaginate; delete reqOpts.autoPaginateVal; delete reqOpts.objectMode; return util.replaceProjectIdToken(reqOpts, this.projectId); }; /** * To authorize requests through gRPC, we must get the raw google-auth-library * auth client object. * * @private * * @param {function} callback - The callback function. * @param {?error} callback.err - An error getting an auth client. */ GrpcService.prototype.getGrpcCredentials_ = function(callback) { var self = this; this.authClient.getAuthClient(function(err, authClient) { if (err) { callback(err); return; } var credentials = grpc.credentials.combineChannelCredentials( grpc.credentials.createSsl(), grpc.credentials.createFromGoogleCredential(authClient) ); if (!self.projectId || self.projectId === '{{projectId}}') { self.projectId = self.authClient.projectId; } callback(null, credentials); }); }; /** * Loads a proto file, useful when handling multiple proto files/services * within a single instance of GrpcService. * * @private * * @param {object} protoConfig - The proto specific configs for this file. * @param {object} config - The base config for the GrpcService. * @return {object} protoObject - The loaded proto object. */ GrpcService.prototype.loadProtoFile_ = function(protoConfig, config) { var grpcOpts = { binaryAsBase64: true, convertFieldsToCamelCase: true, }; if (is.string(protoConfig)) { protoConfig = { path: protoConfig, }; } var protoObjectCacheKey = [ config.protosDir, protoConfig.path, protoConfig.service, ].join('$'); if (!protoObjectCache[protoObjectCacheKey]) { var services = grpc.load( { root: config.protosDir, file: protoConfig.path, }, 'proto', grpcOpts ); var service = dotProp.get(services.google, protoConfig.service); protoObjectCache[protoObjectCacheKey] = service; } return protoObjectCache[protoObjectCacheKey]; }; /** * Retrieves the service object used to make the grpc requests. * * @private * * @param {object} protoOpts - The proto options. * @return {object} service - The proto service. */ GrpcService.prototype.getService_ = function(protoOpts) { var proto = this.protos[protoOpts.service]; var service = this.activeServiceMap_.get(protoOpts.service); if (!service) { service = new proto[protoOpts.service]( proto.baseUrl || this.baseUrl, this.grpcCredentials, extend( { 'grpc.primary_user_agent': this.userAgent, }, GRPC_SERVICE_OPTIONS ) ); this.activeServiceMap_.set(protoOpts.service, service); } return service; }; /** * A class that can be used to convert an object to a struct. Optionally this * class can be used to erase/throw on circular references during conversion. * * @private * * @param {object=} options - Configuration object. * @param {boolean} options.removeCircular - Remove circular references in the * object with a placeholder string. (Default: `false`) * @param {boolean} options.stringify - Stringify un-recognized types. (Default: * `false`) */ function ObjectToStructConverter(options) { options = options || {}; this.seenObjects = new Set(); this.removeCircular = options.removeCircular === true; this.stringify = options.stringify === true; } /** * Begin the conversion process from a JS object to an encoded gRPC Value * message. * * @param {*} value - The input value. * @return {object} - The encoded value. * * @example * ObjectToStructConverter.convert({ * aString: 'Hi' * }); * // { * // fields: { * // aString: { * // stringValue: 'Hello!' * // } * // } * // } */ ObjectToStructConverter.prototype.convert = function(obj) { var convertedObject = { fields: {}, }; this.seenObjects.add(obj); for (var prop in obj) { if (obj.hasOwnProperty(prop)) { var value = obj[prop]; if (is.undefined(value)) { continue; } convertedObject.fields[prop] = this.encodeValue_(value); } } this.seenObjects.delete(obj); return convertedObject; }; /** * Convert a raw value to a type-denoted protobuf message-friendly object. * * @private * * @param {*} value - The input value. * @return {*} - The encoded value. * * @example * ObjectToStructConverter.encodeValue('Hi'); * // { * // stringValue: 'Hello!' * // } */ ObjectToStructConverter.prototype.encodeValue_ = function(value) { var convertedValue; if (is.null(value)) { convertedValue = { nullValue: 0, }; } else if (is.number(value)) { convertedValue = { numberValue: value, }; } else if (is.string(value)) { convertedValue = { stringValue: value, }; } else if (is.boolean(value)) { convertedValue = { boolValue: value, }; } else if (Buffer.isBuffer(value)) { convertedValue = { blobValue: value, }; } else if (is.object(value)) { if (this.seenObjects.has(value)) { // Circular reference. if (!this.removeCircular) { throw new Error( [ 'This object contains a circular reference. To automatically', 'remove it, set the `removeCircular` option to true.', ].join(' ') ); } convertedValue = { stringValue: '[Circular]', }; } else { convertedValue = { structValue: this.convert(value), }; } } else if (is.array(value)) { convertedValue = { listValue: { values: value.map(this.encodeValue_.bind(this)), }, }; } else { if (!this.stringify) { throw new Error('Value of type ' + typeof value + ' not recognized.'); } convertedValue = { stringValue: String(value), }; } return convertedValue; }; module.exports = GrpcService; module.exports.GRPC_ERROR_CODE_TO_HTTP = GRPC_ERROR_CODE_TO_HTTP; module.exports.GRPC_SERVICE_OPTIONS = GRPC_SERVICE_OPTIONS; module.exports.ObjectToStructConverter = ObjectToStructConverter;