UNPKG

@omnigraph/grpc

Version:
481 lines (480 loc) 23.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GrpcLoaderHelper = void 0; const tslib_1 = require("tslib"); const globby_1 = tslib_1.__importDefault(require("globby")); const graphql_1 = require("graphql"); const graphql_compose_1 = require("graphql-compose"); const graphql_scalars_1 = require("graphql-scalars"); const micromatch_1 = tslib_1.__importDefault(require("micromatch")); const protobufjs_1 = tslib_1.__importDefault(require("protobufjs")); const index_js_1 = tslib_1.__importDefault(require("protobufjs/ext/descriptor/index.js")); const grpc_reflection_js_1 = require("@ardatan/grpc-reflection-js"); const cross_helpers_1 = require("@graphql-mesh/cross-helpers"); const string_interpolation_1 = require("@graphql-mesh/string-interpolation"); const utils_1 = require("@graphql-tools/utils"); const grpc_js_1 = require("@grpc/grpc-js"); const disposablestack_1 = require("@whatwg-node/disposablestack"); const directives_js_1 = require("./directives.js"); const utils_js_1 = require("./utils.js"); const { Root } = protobufjs_1.default; const QUERY_METHOD_PREFIXES = ['get', 'list', 'search']; class GrpcLoaderHelper extends disposablestack_1.DisposableStack { constructor(subgraphName, baseDir, logger, config) { super(); this.subgraphName = subgraphName; this.baseDir = baseDir; this.logger = logger; this.config = config; this.schemaComposer = new graphql_compose_1.SchemaComposer(); } async buildSchema() { this.schemaComposer.add(graphql_scalars_1.GraphQLBigInt); this.schemaComposer.add(graphql_scalars_1.GraphQLByte); this.schemaComposer.add(graphql_scalars_1.GraphQLUnsignedInt); this.schemaComposer.add(graphql_scalars_1.GraphQLVoid); this.schemaComposer.add(graphql_scalars_1.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.config.requestTimeout = this.config.requestTimeout || 200000; this.logger.debug(`Getting channel credentials`); const creds = await this.getCredentials(); if ('_unref' in creds && typeof creds._unref === 'function') { this.defer(() => creds._unref()); } this.logger.debug(`Getting stored root and decoded descriptor set objects`); const descriptorSets = await this.getDescriptorSets(creds); const directives = []; const roots = []; for (const { name: rootJsonName, rootJson } of descriptorSets) { const rootLogger = this.logger.child({ root: rootJsonName }); this.logger.debug(`Building the schema structure based on the root object`); this.visit({ nested: rootJson, name: '', currentPath: [], rootJsonName, rootJson, rootLogger, }); roots.push({ name: rootJsonName, rootJson: JSON.stringify(rootJson) }); } this.schemaComposer.Query.setDirectives(directives); // graphql-compose doesn't add @defer and @stream to the schema graphql_1.specifiedDirectives.forEach(directive => this.schemaComposer.addDirective(directive)); if (!this.schemaComposer.hasDirective('stream')) { this.schemaComposer.addDirective(utils_1.GraphQLStreamDirective); } this.logger.debug(`Building the final GraphQL Schema`); this.schemaComposer.addDirective(directives_js_1.transportDirective); const schema = this.schemaComposer.buildSchema(); const schemaExtensions = (schema.extensions = schema.extensions || {}); const directiveExtensions = (schemaExtensions.directives = schemaExtensions.directives || {}); directiveExtensions.transport = { subgraph: this.subgraphName, kind: 'grpc', location: this.config.endpoint, options: { requestTimeout: this.config.requestTimeout, credentialsSsl: this.config.credentialsSsl, useHTTPS: this.config.useHTTPS, metaData: this.config.metaData, roots, }, }; return schema; } processReflection(creds) { this.logger.debug(`Using the reflection`); const reflectionEndpoint = string_interpolation_1.stringInterpolator.parse(this.config.endpoint, { env: process.env }); this.logger.debug(`Creating gRPC Reflection Client`); const reflectionClient = new grpc_reflection_js_1.Client(reflectionEndpoint, creds); this.defer(() => reflectionClient.grpcClient.close()); return reflectionClient.listServices().then(services => services.filter(service => service && !service?.startsWith('grpc.')).map(service => { this.logger.debug(`Resolving root of Service: ${service} from the reflection response`); return reflectionClient.fileContainingSymbol(service); })); } async processDescriptorFile() { let fileName; let options; if (typeof this.config.source === 'object') { fileName = this.config.source.file; options = { ...this.config.source.load, includeDirs: this.config.source.load.includeDirs?.map(includeDir => cross_helpers_1.path.isAbsolute(includeDir) ? includeDir : cross_helpers_1.path.join(this.baseDir, includeDir)), }; } else { fileName = this.config.source; } fileName = string_interpolation_1.stringInterpolator.parse(fileName, { env: process.env }); const absoluteFilePath = cross_helpers_1.path.isAbsolute(fileName) ? fileName : cross_helpers_1.path.join(this.baseDir, fileName); this.logger.debug(`Using the descriptor set from ${absoluteFilePath} `); const descriptorSetBuffer = await cross_helpers_1.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 = index_js_1.default.FileDescriptorSet.fromObject(descriptorSetJSON); } else { decodedDescriptorSet = index_js_1.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')); } (0, utils_js_1.addIncludePathResolver)(rootFromDescriptor, options.includeDirs); } return rootFromDescriptor; } async processProtoFile() { this.logger.debug(`Using proto file(s)`); let protoRoot = new Root(); let fileGlob; let options = { keepCase: true, alternateCommentMode: true, }; if (typeof this.config.source === 'object') { fileGlob = this.config.source.file; options = { ...options, ...this.config.source.load, includeDirs: this.config.source.load?.includeDirs?.map(includeDir => cross_helpers_1.path.isAbsolute(includeDir) ? includeDir : cross_helpers_1.path.join(this.baseDir, includeDir)), }; if (options.includeDirs) { if (!Array.isArray(options.includeDirs)) { throw new Error('The includeDirs option must be an array'); } (0, utils_js_1.addIncludePathResolver)(protoRoot, options.includeDirs); } } else { fileGlob = this.config.source; } fileGlob = string_interpolation_1.stringInterpolator.parse(fileGlob, { env: process.env }); const fileNames = await (0, globby_1.default)(fileGlob, { cwd: this.baseDir, }); this.logger.debug(`Loading proto files(${fileGlob}); \n ${fileNames.join('\n')} `); protoRoot = await protoRoot.load(fileNames.map(filePath => cross_helpers_1.path.isAbsolute(filePath) ? filePath : cross_helpers_1.path.join(this.baseDir, filePath)), options); this.logger.debug(`Adding proto content to the root`); return protoRoot; } async getDescriptorSets(creds) { const rootPromises = []; this.logger.debug(`Building Roots`); if (this.config.source) { const filePath = typeof this.config.source === 'string' ? this.config.source : this.config.source.file; if (filePath.endsWith('json')) { rootPromises.push(this.processDescriptorFile()); } else if (filePath.endsWith('proto')) { rootPromises.push(this.processProtoFile()); } } else { const reflectionPromises = await this.processReflection(creds); rootPromises.push(...reflectionPromises); } return Promise.all(rootPromises.map(async (root$, i) => { const root = await root$; const rootName = root.name || `Root${i}`; const rootLogger = this.logger.child({ root: 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, }), }; })); } getCredentials() { this.logger.debug(`Getting channel credentials`); if (this.config.credentialsSsl) { this.logger.debug(() => `Using SSL Connection with credentials at ${this.config.credentialsSsl.privateKey} & ${this.config.credentialsSsl.certChain}`); const absolutePrivateKeyPath = cross_helpers_1.path.isAbsolute(this.config.credentialsSsl.privateKey) ? this.config.credentialsSsl.privateKey : cross_helpers_1.path.join(this.baseDir, this.config.credentialsSsl.privateKey); const absoluteCertChainPath = cross_helpers_1.path.isAbsolute(this.config.credentialsSsl.certChain) ? this.config.credentialsSsl.certChain : cross_helpers_1.path.join(this.baseDir, this.config.credentialsSsl.certChain); const sslFiles = [ cross_helpers_1.fs.promises.readFile(absolutePrivateKeyPath), cross_helpers_1.fs.promises.readFile(absoluteCertChainPath), ]; if (this.config.credentialsSsl.rootCA !== 'rootCA') { const absoluteRootCAPath = cross_helpers_1.path.isAbsolute(this.config.credentialsSsl.rootCA) ? this.config.credentialsSsl.rootCA : cross_helpers_1.path.join(this.baseDir, this.config.credentialsSsl.rootCA); sslFiles.unshift(cross_helpers_1.fs.promises.readFile(absoluteRootCAPath)); } return Promise.all(sslFiles).then(([rootCA, privateKey, certChain]) => grpc_js_1.credentials.createSsl(rootCA, privateKey, certChain)); } else if (this.config.useHTTPS) { this.logger.debug(`Using SSL Connection`); return grpc_js_1.credentials.createSsl(); } this.logger.debug(`Using insecure connection`); return grpc_js_1.credentials.createInsecure(); } visit({ nested, name, currentPath, rootJsonName, rootJson, rootLogger: logger, }) { 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, rootJsonName, rootJson, rootLogger: logger, }); } } const typeName = pathWithName.join('__'); if ('values' in nested) { const enumValues = {}; const commentMap = nested.comments; for (const [key, value] of Object.entries(nested.values)) { logger.debug(`Visiting ${currentPath}.nested.values[${key}]`); enumValues[key] = { directives: [ { name: 'enum', args: { subgraph: this.subgraphName, value: JSON.stringify(value), }, }, ], description: commentMap?.[key], }; } this.schemaComposer.addDirective(directives_js_1.EnumDirective); this.schemaComposer.createEnumTC({ name: typeName, values: enumValues, description: nested.comment, }); } 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, keyType }] of fieldEntries) { logger.debug(`Visiting ${currentPath}.nested.fields[${fieldName}]`); const baseFieldTypePath = type.split('.'); inputTC.addFields({ [fieldName]: { type: () => { let fieldInputTypeName; if (keyType) { fieldInputTypeName = 'JSON'; } else { const fieldTypePath = (0, utils_js_1.walkToFindTypePath)(rootJson, pathWithName, baseFieldTypePath); fieldInputTypeName = (0, utils_js_1.getTypeName)(this.schemaComposer, fieldTypePath, true); } return rule === 'repeated' ? `[${fieldInputTypeName}]` : fieldInputTypeName; }, description: comment, }, }); outputTC.addFields({ [fieldName]: { type: () => { let fieldTypeName; if (keyType) { fieldTypeName = 'JSON'; } else { const fieldTypePath = (0, utils_js_1.walkToFindTypePath)(rootJson, pathWithName, baseFieldTypePath); fieldTypeName = (0, utils_js_1.getTypeName)(this.schemaComposer, fieldTypePath, false); } return rule === 'repeated' ? `[${fieldTypeName}]` : fieldTypeName; }, description: comment, }, }); } } else { this.schemaComposer.createScalarTC({ ...graphql_scalars_1.GraphQLJSON.toConfig(), name: inputTypeName, description, }); this.schemaComposer.createScalarTC({ ...graphql_scalars_1.GraphQLJSON.toConfig(), name: outputTypeName, description, }); } } else if ('methods' in nested) { const objPath = pathWithName.join('.'); for (const methodName in nested.methods) { const method = nested.methods[methodName]; const rootFieldName = [...pathWithName, methodName].join('_'); const fieldConfigTypeFactory = () => { const baseResponseTypePath = method.responseType?.split('.'); if (baseResponseTypePath) { const responseTypePath = (0, utils_js_1.walkToFindTypePath)(rootJson, pathWithName, baseResponseTypePath); return (0, utils_js_1.getTypeName)(this.schemaComposer, responseTypePath, false); } return 'Void'; }; const fieldConfig = { type: () => { const typeName = fieldConfigTypeFactory(); if (method.responseStream) { return `[${typeName}]`; } return typeName; }, description: method.comment, }; const fieldConfigArgs = { input: () => { if (method.requestStream) { return 'File'; } const baseRequestTypePath = method.requestType?.split('.'); if (baseRequestTypePath) { const requestTypePath = (0, utils_js_1.walkToFindTypePath)(rootJson, pathWithName, baseRequestTypePath); const requestTypeName = (0, utils_js_1.getTypeName)(this.schemaComposer, requestTypePath, true); return requestTypeName; } return undefined; }, }; fieldConfig.args = fieldConfigArgs; const methodNameLowerCased = methodName.toLowerCase(); const prefixQueryMethod = this.config.prefixQueryMethod || QUERY_METHOD_PREFIXES; let rootTypeComposer; if (this.config.selectQueryOrMutationField) { const selection = this.config.selectQueryOrMutationField.find(selection => (0, micromatch_1.default)([rootFieldName], selection.fieldName).length > 0); const rootTypeName = selection?.type?.toLowerCase(); if (rootTypeName) { if (rootTypeName === 'query') { rootTypeComposer = this.schemaComposer.Query; } else if (rootTypeName === 'mutation') { rootTypeComposer = this.schemaComposer.Mutation; } else if (rootTypeName === 'subscription') { rootTypeComposer = this.schemaComposer.Subscription; } else { throw new Error(`Unknown type provided ${selection.type} for ${rootFieldName}; available options are Query, Mutation and Subscription`); } } } if (rootTypeComposer == null) { rootTypeComposer = prefixQueryMethod.some(prefix => methodNameLowerCased.startsWith(prefix)) ? this.schemaComposer.Query : this.schemaComposer.Mutation; } this.schemaComposer.addDirective(directives_js_1.grpcMethodDirective); rootTypeComposer.addFields({ [rootFieldName]: { ...fieldConfig, directives: [ { name: 'grpcMethod', args: { subgraph: this.subgraphName, rootJsonName, objPath, methodName, responseStream: !!method.responseStream, }, }, ], }, }); if (method.responseStream) { this.schemaComposer.Subscription.addFields({ [rootFieldName]: { args: fieldConfigArgs, description: method.comment, type: fieldConfigTypeFactory, directives: [ { name: 'grpcMethod', args: { subgraph: this.subgraphName, rootJsonName, objPath, methodName, responseStream: true, }, }, ], }, }); } } const connectivityStateFieldName = pathWithName.join('_') + '_connectivityState'; this.schemaComposer.addDirective(directives_js_1.grpcConnectivityStateDirective); this.schemaComposer.Query.addFields({ [connectivityStateFieldName]: { type: 'ConnectivityState', args: { tryToConnect: { type: 'Boolean', }, }, directives: [ { name: 'grpcConnectivityState', args: { subgraph: this.subgraphName, rootJsonName, objPath, }, }, ], }, }); } } } exports.GrpcLoaderHelper = GrpcLoaderHelper;