@graphql-mesh/grpc
Version:
549 lines (540 loc) • 25.3 kB
JavaScript
'use strict';
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
const Long = _interopDefault(require('long'));
const stringInterpolation = require('@graphql-mesh/string-interpolation');
const grpcJs = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const graphqlCompose = require('graphql-compose');
const graphqlScalars = require('graphql-scalars');
const lodashGet = _interopDefault(require('lodash.get'));
const lodashHas = _interopDefault(require('lodash.has'));
const protobufjs = _interopDefault(require('protobufjs'));
const grpcReflection = _interopDefault(require('@ardatan/grpc-reflection-js'));
const descriptor = require('protobufjs/ext/descriptor/index.js');
const descriptor__default = _interopDefault(descriptor);
const utils = require('@graphql-mesh/utils');
const crossHelpers = require('@graphql-mesh/cross-helpers');
const graphql = require('graphql');
const globby = _interopDefault(require('globby'));
function patchLongJs() {
const originalLongFromValue = Long.fromValue.bind(Long);
Long.fromValue = (value) => {
if (typeof value === 'bigint') {
return Long.fromValue(value.toString());
}
return originalLongFromValue(value);
};
}
patchLongJs();
const SCALARS = new Map([
['bool', 'Boolean'],
['bytes', 'Byte'],
['double', 'Float'],
['fixed32', 'Int'],
['fixed64', 'BigInt'],
['float', 'Float'],
['int32', 'Int'],
['int64', 'BigInt'],
['sfixed32', 'Int'],
['sfixed64', 'BigInt'],
['sint32', 'Int'],
['sint64', 'BigInt'],
['string', 'String'],
['uint32', 'UnsignedInt'],
['uint64', 'BigInt'], // A new scalar might be needed
]);
function isScalarType(type) {
return SCALARS.has(type);
}
function getGraphQLScalar(scalarType) {
const gqlScalar = SCALARS.get(scalarType);
if (!gqlScalar) {
throw new Error(`Could not find GraphQL Scalar for type ${scalarType}`);
}
return SCALARS.get(scalarType);
}
function getTypeName(schemaComposer, pathWithName, isInput) {
if (pathWithName === null || pathWithName === void 0 ? void 0 : pathWithName.length) {
const baseTypeName = pathWithName.filter(Boolean).join('_');
if (isScalarType(baseTypeName)) {
return getGraphQLScalar(baseTypeName);
}
if (schemaComposer.isEnumType(baseTypeName)) {
return baseTypeName;
}
return isInput ? baseTypeName + '_Input' : baseTypeName;
}
return 'Void';
}
function addIncludePathResolver(root, includePaths) {
const originalResolvePath = root.resolvePath;
root.resolvePath = (origin, target) => {
if (crossHelpers.path.isAbsolute(target)) {
return target;
}
for (const directory of includePaths) {
const fullPath = crossHelpers.path.join(directory, target);
if (crossHelpers.fs.existsSync(fullPath)) {
return fullPath;
}
}
const path = originalResolvePath(origin, target);
if (path === null) {
console.warn(`${target} not found in any of the include paths ${includePaths}`);
}
return path;
};
}
function isBlob(input) {
return input != null && input.stream instanceof Function;
}
function addMetaDataToCall(callFn, input, context, metaData, isResponseStream = false) {
const callFnArguments = [];
if (!isBlob(input)) {
callFnArguments.push(input);
}
if (metaData) {
const meta = new grpcJs.Metadata();
for (const [key, value] of Object.entries(metaData)) {
let metaValue = value;
if (Array.isArray(value)) {
// Extract data from context
metaValue = lodashGet(context, value);
}
// Ensure that the metadata is compatible with what node-grpc expects
if (typeof metaValue !== 'string' && !(metaValue instanceof Buffer)) {
metaValue = JSON.stringify(metaValue);
}
if (typeof metaValue === 'string') {
metaValue = stringInterpolation.stringInterpolator.parse(metaValue, context);
}
meta.add(key, metaValue);
}
callFnArguments.push(meta);
}
return new Promise((resolve, reject) => {
const call = callFn(...callFnArguments, (error, response) => {
if (error) {
reject(error);
}
resolve(response);
});
if (isResponseStream) {
let isCancelled = false;
const responseStreamWithCancel = utils.withCancel(call, () => {
var _a;
if (!isCancelled) {
(_a = call.call) === null || _a === void 0 ? void 0 : _a.cancelWithStatus(0, 'Cancelled by GraphQL Mesh');
isCancelled = true;
}
});
resolve(responseStreamWithCancel);
if (isBlob(input)) {
input.stream().pipe(call);
}
}
});
}
/* eslint-disable import/no-duplicates */
const { Root } = protobufjs;
const QUERY_METHOD_PREFIXES = ['get', 'list'];
class GrpcHandler {
constructor({ config, baseDir, store, logger }) {
this.schemaComposer = new graphqlCompose.SchemaComposer();
this.logger = logger;
this.config = config;
this.baseDir = baseDir;
this.rootJsonAndDecodedDescriptorSets = store.proxy('descriptorSet.proto', {
codify: rootJsonAndDecodedDescriptorSets => `
import { FileDescriptorSet } from 'protobufjs/ext/descriptor/index.js';
export default [
${rootJsonAndDecodedDescriptorSets
.map(({ name, rootJson, decodedDescriptorSet }) => `
{
name: ${JSON.stringify(name)},
decodedDescriptorSet: FileDescriptorSet.fromObject(${JSON.stringify(decodedDescriptorSet.toJSON(), null, 2)}),
rootJson: ${JSON.stringify(rootJson, null, 2)},
},
`)
.join('\n')}
];
`.trim(),
fromJSON: jsonData => {
return jsonData.map(({ name, rootJson, decodedDescriptorSet }) => ({
name,
rootJson,
decodedDescriptorSet: descriptor.FileDescriptorSet.fromObject(decodedDescriptorSet),
}));
},
toJSON: rootJsonAndDecodedDescriptorSets => {
return rootJsonAndDecodedDescriptorSets.map(({ name, rootJson, decodedDescriptorSet }) => {
return {
name,
rootJson,
decodedDescriptorSet: decodedDescriptorSet.toJSON(),
};
});
},
validate: () => { },
});
}
async getRootPromisesFromReflection(creds) {
this.logger.debug(`Using the reflection`);
const grpcReflectionServer = this.config.endpoint;
this.logger.debug(`Creating gRPC Reflection Client`);
const reflectionClient = new grpcReflection.Client(grpcReflectionServer, creds);
const services = await reflectionClient.listServices();
const userServices = services.filter(service => service && !(service === null || service === void 0 ? void 0 : service.startsWith('grpc.')));
return userServices.map(async (service) => {
this.logger.debug(`Resolving root of Service: ${service} from the reflection response`);
const serviceRoot = await reflectionClient.fileContainingSymbol(service);
return serviceRoot;
});
}
async getRootPromiseFromDescriptorFilePath() {
var _a;
let fileName;
let options;
if (typeof this.config.descriptorSetFilePath === 'object') {
fileName = this.config.descriptorSetFilePath.file;
options = {
...this.config.descriptorSetFilePath.load,
includeDirs: (_a = this.config.descriptorSetFilePath.load.includeDirs) === null || _a === void 0 ? void 0 : _a.map(includeDir => crossHelpers.path.isAbsolute(includeDir) ? includeDir : crossHelpers.path.join(this.baseDir, includeDir)),
};
}
else {
fileName = this.config.descriptorSetFilePath;
}
const absoluteFilePath = crossHelpers.path.isAbsolute(fileName) ? fileName : crossHelpers.path.join(this.baseDir, fileName);
this.logger.debug(`Using the descriptor set from ${absoluteFilePath} `);
const descriptorSetBuffer = await crossHelpers.fs.promises.readFile(absoluteFilePath);
this.logger.debug(`Reading ${absoluteFilePath} `);
let decodedDescriptorSet;
if (absoluteFilePath.endsWith('json')) {
this.logger.debug(`Parsing ${absoluteFilePath} as json`);
const descriptorSetJSON = JSON.parse(descriptorSetBuffer.toString());
decodedDescriptorSet = descriptor__default.FileDescriptorSet.fromObject(descriptorSetJSON);
}
else {
decodedDescriptorSet = descriptor__default.FileDescriptorSet.decode(descriptorSetBuffer);
}
this.logger.debug(`Creating root from descriptor set`);
const rootFromDescriptor = Root.fromDescriptor(decodedDescriptorSet);
if (options.includeDirs) {
if (!Array.isArray(options.includeDirs)) {
return Promise.reject(new Error('The includeDirs option must be an array'));
}
addIncludePathResolver(rootFromDescriptor, options.includeDirs);
}
return rootFromDescriptor;
}
async getRootPromiseFromProtoFilePath() {
var _a, _b;
this.logger.debug(`Using proto file(s)`);
let protoRoot = new Root();
let fileGlob;
let options = {
keepCase: true,
alternateCommentMode: true,
};
if (typeof this.config.protoFilePath === 'object') {
fileGlob = this.config.protoFilePath.file;
options = {
...options,
...this.config.protoFilePath.load,
includeDirs: (_b = (_a = this.config.protoFilePath.load) === null || _a === void 0 ? void 0 : _a.includeDirs) === null || _b === void 0 ? void 0 : _b.map(includeDir => crossHelpers.path.isAbsolute(includeDir) ? includeDir : crossHelpers.path.join(this.baseDir, includeDir)),
};
if (options.includeDirs) {
if (!Array.isArray(options.includeDirs)) {
return Promise.reject(new Error('The includeDirs option must be an array'));
}
addIncludePathResolver(protoRoot, options.includeDirs);
}
}
else {
fileGlob = this.config.protoFilePath;
}
const fileNames = await globby(fileGlob, {
cwd: this.baseDir,
});
this.logger.debug(`Loading proto files(${fileGlob}); \n ${fileNames.join('\n')} `);
protoRoot = await protoRoot.load(fileNames.map(filePath => (crossHelpers.path.isAbsolute(filePath) ? filePath : crossHelpers.path.join(this.baseDir, filePath))), options);
this.logger.debug(`Adding proto content to the root`);
return protoRoot;
}
getCachedDescriptorSets(creds) {
return this.rootJsonAndDecodedDescriptorSets.getWithSet(async () => {
const rootPromises = [];
this.logger.debug(`Building Roots`);
if (this.config.useReflection) {
const reflectionPromises = await this.getRootPromisesFromReflection(creds);
rootPromises.push(...reflectionPromises);
}
if (this.config.descriptorSetFilePath) {
const rootPromise = this.getRootPromiseFromDescriptorFilePath();
rootPromises.push(rootPromise);
}
if (this.config.protoFilePath) {
const rootPromise = this.getRootPromiseFromProtoFilePath();
rootPromises.push(rootPromise);
}
return Promise.all(rootPromises.map(async (root$, i) => {
const root = await root$;
const rootName = root.name || `Root${i}`;
const rootLogger = this.logger.child(rootName);
rootLogger.debug(`Resolving entire the root tree`);
root.resolveAll();
rootLogger.debug(`Creating artifacts from descriptor set and root`);
return {
name: rootName,
rootJson: root.toJSON({
keepComments: true,
}),
decodedDescriptorSet: root.toDescriptor('proto3'),
};
}));
});
}
async getCredentials() {
if (this.config.credentialsSsl) {
this.logger.debug(() => `Using SSL Connection with credentials at ${this.config.credentialsSsl.privateKey} & ${this.config.credentialsSsl.certChain}`);
const absolutePrivateKeyPath = crossHelpers.path.isAbsolute(this.config.credentialsSsl.privateKey)
? this.config.credentialsSsl.privateKey
: crossHelpers.path.join(this.baseDir, this.config.credentialsSsl.privateKey);
const absoluteCertChainPath = crossHelpers.path.isAbsolute(this.config.credentialsSsl.certChain)
? this.config.credentialsSsl.certChain
: crossHelpers.path.join(this.baseDir, this.config.credentialsSsl.certChain);
const sslFiles = [crossHelpers.fs.promises.readFile(absolutePrivateKeyPath), crossHelpers.fs.promises.readFile(absoluteCertChainPath)];
if (this.config.credentialsSsl.rootCA !== 'rootCA') {
const absoluteRootCAPath = crossHelpers.path.isAbsolute(this.config.credentialsSsl.rootCA)
? this.config.credentialsSsl.rootCA
: crossHelpers.path.join(this.baseDir, this.config.credentialsSsl.rootCA);
sslFiles.unshift(crossHelpers.fs.promises.readFile(absoluteRootCAPath));
}
const [rootCA, privateKey, certChain] = await Promise.all(sslFiles);
return grpcJs.credentials.createSsl(rootCA, privateKey, certChain);
}
else if (this.config.useHTTPS) {
this.logger.debug(`Using SSL Connection`);
return grpcJs.credentials.createSsl();
}
this.logger.debug(`Using insecure connection`);
return grpcJs.credentials.createInsecure();
}
walkToFindTypePath(rootJson, pathWithName, baseTypePath) {
const currentWalkingPath = [...pathWithName];
while (!lodashHas(rootJson.nested, currentWalkingPath.concat(baseTypePath).join('.nested.'))) {
if (!currentWalkingPath.length) {
break;
}
currentWalkingPath.pop();
}
return currentWalkingPath.concat(baseTypePath);
}
visit({ nested, name, currentPath, rootJson, creds, grpcObject, rootLogger: logger, }) {
var _a;
const pathWithName = [...currentPath, ...name.split('.')].filter(Boolean);
if ('nested' in nested) {
for (const key in nested.nested) {
logger.debug(`Visiting ${currentPath}.nested[${key}]`);
const currentNested = nested.nested[key];
this.visit({
nested: currentNested,
name: key,
currentPath: pathWithName,
rootJson,
creds,
grpcObject,
rootLogger: logger,
});
}
}
const typeName = pathWithName.join('_');
if ('values' in nested) {
const enumTypeConfig = {
name: typeName,
values: {},
description: nested.comment,
};
const commentMap = nested.comments;
for (const [key, value] of Object.entries(nested.values)) {
logger.debug(`Visiting ${currentPath}.nested.values[${key}]`);
enumTypeConfig.values[key] = {
value,
description: commentMap === null || commentMap === void 0 ? void 0 : commentMap[key],
};
}
this.schemaComposer.createEnumTC(enumTypeConfig);
}
else if ('fields' in nested) {
const inputTypeName = typeName + '_Input';
const outputTypeName = typeName;
const description = nested.comment;
const fieldEntries = Object.entries(nested.fields);
if (fieldEntries.length) {
const inputTC = this.schemaComposer.createInputTC({
name: inputTypeName,
description,
fields: {},
});
const outputTC = this.schemaComposer.createObjectTC({
name: outputTypeName,
description,
fields: {},
});
for (const [fieldName, { type, rule, comment }] of fieldEntries) {
logger.debug(`Visiting ${currentPath}.nested.fields[${fieldName}]`);
const baseFieldTypePath = type.split('.');
inputTC.addFields({
[fieldName]: {
type: () => {
const fieldTypePath = this.walkToFindTypePath(rootJson, pathWithName, baseFieldTypePath);
const fieldInputTypeName = getTypeName(this.schemaComposer, fieldTypePath, true);
return rule === 'repeated' ? `[${fieldInputTypeName}]` : fieldInputTypeName;
},
description: comment,
},
});
outputTC.addFields({
[fieldName]: {
type: () => {
const fieldTypePath = this.walkToFindTypePath(rootJson, pathWithName, baseFieldTypePath);
const fieldTypeName = getTypeName(this.schemaComposer, fieldTypePath, false);
return rule === 'repeated' ? `[${fieldTypeName}]` : fieldTypeName;
},
description: comment,
},
});
}
}
else {
this.schemaComposer.createScalarTC({
...graphqlScalars.GraphQLJSON.toConfig(),
name: inputTypeName,
description,
});
this.schemaComposer.createScalarTC({
...graphqlScalars.GraphQLJSON.toConfig(),
name: outputTypeName,
description,
});
}
}
else if ('methods' in nested) {
const objPath = pathWithName.join('.');
const ServiceClient = lodashGet(grpcObject, objPath);
if (typeof ServiceClient !== 'function') {
throw new Error(`Object at path ${objPath} is not a Service constructor`);
}
const client = new ServiceClient((_a = stringInterpolation.stringInterpolator.parse(this.config.endpoint, { env: crossHelpers.process.env })) !== null && _a !== void 0 ? _a : this.config.endpoint, creds);
for (const methodName in nested.methods) {
const method = nested.methods[methodName];
const rootFieldName = [...pathWithName, methodName].join('_');
const fieldConfig = {
type: () => {
var _a;
const baseResponseTypePath = (_a = method.responseType) === null || _a === void 0 ? void 0 : _a.split('.');
if (baseResponseTypePath) {
const responseTypePath = this.walkToFindTypePath(rootJson, pathWithName, baseResponseTypePath);
return getTypeName(this.schemaComposer, responseTypePath, false);
}
return 'Void';
},
description: method.comment,
};
fieldConfig.args = {
input: () => {
var _a;
if (method.requestStream) {
return 'File';
}
const baseRequestTypePath = (_a = method.requestType) === null || _a === void 0 ? void 0 : _a.split('.');
if (baseRequestTypePath) {
const requestTypePath = this.walkToFindTypePath(rootJson, pathWithName, baseRequestTypePath);
const requestTypeName = getTypeName(this.schemaComposer, requestTypePath, true);
return requestTypeName;
}
return undefined;
},
};
if (method.responseStream) {
this.schemaComposer.Subscription.addFields({
[rootFieldName]: {
...fieldConfig,
subscribe: (__, args, context) => addMetaDataToCall(client[methodName].bind(client), args.input, context, this.config.metaData, true),
resolve: (payload) => payload,
},
});
}
else {
const methodNameLowerCased = methodName.toLowerCase();
const prefixQueryMethod = this.config.prefixQueryMethod || QUERY_METHOD_PREFIXES;
const rootTypeComposer = prefixQueryMethod.some(prefix => methodNameLowerCased.startsWith(prefix))
? this.schemaComposer.Query
: this.schemaComposer.Mutation;
rootTypeComposer.addFields({
[rootFieldName]: {
...fieldConfig,
resolve: (_, args, context) => addMetaDataToCall(client[methodName].bind(client), args.input, context, this.config.metaData),
},
});
}
}
const connectivityStateFieldName = pathWithName.join('_') + '_connectivityState';
this.schemaComposer.Query.addFields({
[connectivityStateFieldName]: {
type: 'ConnectivityState',
args: {
tryToConnect: {
type: 'Boolean',
},
},
resolve: (_, { tryToConnect }) => client.getChannel().getConnectivityState(tryToConnect),
},
});
}
}
async getMeshSource() {
this.config.requestTimeout = this.config.requestTimeout || 200000;
this.schemaComposer.add(graphqlScalars.GraphQLBigInt);
this.schemaComposer.add(graphqlScalars.GraphQLByte);
this.schemaComposer.add(graphqlScalars.GraphQLUnsignedInt);
this.schemaComposer.add(graphqlScalars.GraphQLVoid);
this.schemaComposer.add(graphqlScalars.GraphQLJSON);
this.schemaComposer.createScalarTC({
name: 'File',
});
// identical of grpc's ConnectivityState
this.schemaComposer.createEnumTC({
name: 'ConnectivityState',
values: {
IDLE: { value: 0 },
CONNECTING: { value: 1 },
READY: { value: 2 },
TRANSIENT_FAILURE: { value: 3 },
SHUTDOWN: { value: 4 },
},
});
this.logger.debug(`Getting channel credentials`);
const creds = await this.getCredentials();
this.logger.debug(`Getting stored root and decoded descriptor set objects`);
const artifacts = await this.getCachedDescriptorSets(creds);
for (const { name, rootJson, decodedDescriptorSet } of artifacts) {
const rootLogger = this.logger.child(name);
rootLogger.debug(`Creating package definition from file descriptor set object`);
const packageDefinition = protoLoader.loadFileDescriptorSetFromObject(decodedDescriptorSet);
rootLogger.debug(`Creating service client for package definition`);
const grpcObject = grpcJs.loadPackageDefinition(packageDefinition);
this.logger.debug(`Building the schema structure based on the root object`);
this.visit({ nested: rootJson, name: '', currentPath: [], rootJson, creds, grpcObject, rootLogger });
}
// graphql-compose doesn't add @defer and @stream to the schema
graphql.specifiedDirectives.forEach(directive => this.schemaComposer.addDirective(directive));
this.logger.debug(`Building the final GraphQL Schema`);
const schema = this.schemaComposer.buildSchema();
return {
schema,
};
}
}
module.exports = GrpcHandler;