UNPKG

@graphql-mesh/grpc

Version:
549 lines (540 loc) • 25.3 kB
'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;