@graphql-mesh/grpc
Version:
632 lines (631 loc) • 30.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
/* eslint-disable import/no-duplicates */
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 lodash_get_1 = tslib_1.__importDefault(require("lodash.get"));
const lodash_has_1 = tslib_1.__importDefault(require("lodash.has"));
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 store_1 = require("@graphql-mesh/store");
const string_interpolation_1 = require("@graphql-mesh/string-interpolation");
const utils_1 = require("@graphql-mesh/utils");
const utils_2 = require("@graphql-tools/utils");
const grpc_js_1 = require("@grpc/grpc-js");
const proto_loader_1 = require("@grpc/proto-loader");
const directives_js_1 = require("./directives.js");
require("./patchLongJs.js");
const graphql_scalars_2 = require("graphql-scalars");
const scalars_js_1 = require("./scalars.js");
const utils_js_1 = require("./utils.js");
const { Root } = protobufjs_1.default;
const QUERY_METHOD_PREFIXES = ['get', 'list', 'search'];
class GrpcHandler {
constructor({ config, baseDir, store, logger, pubsub, importFn, }) {
this.grpcObjectByserviceClientByObjPath = new WeakMap();
this.schemaComposer = new graphql_compose_1.SchemaComposer();
this.logger = logger;
this.config = config;
this.baseDir = baseDir;
this.schemaWithAnnotationsProxy = store.proxy('schemaWithAnnotations', store_1.PredefinedProxyOptions.GraphQLSchemaWithDiffing);
this.pubsub = pubsub;
this.importFn = importFn;
}
async processReflection(creds) {
this.logger.debug(`Using the reflection`);
const reflectionEndpoint = string_interpolation_1.stringInterpolator.parse(this.config.endpoint, { env: cross_helpers_1.process.env });
this.logger.debug(`Creating gRPC Reflection Client`);
const reflectionClient = new grpc_reflection_js_1.Client(reflectionEndpoint, creds);
const subId = this.pubsub.subscribe('destroy', () => {
reflectionClient.grpcClient.close();
this.pubsub.unsubscribe(subId);
});
const services = await reflectionClient.listServices();
const userServices = services.filter(service => service && !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 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: cross_helpers_1.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: cross_helpers_1.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(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,
}),
};
}));
}
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 = 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));
}
const [rootCA, privateKey, certChain] = await Promise.all(sslFiles);
return 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();
}
walkToFindTypePath(rootJson, pathWithName, baseTypePath) {
const currentWalkingPath = [...pathWithName];
while (!(0, lodash_has_1.default)(rootJson.nested, currentWalkingPath.concat(baseTypePath).join('.nested.'))) {
if (!currentWalkingPath.length) {
break;
}
currentWalkingPath.pop();
}
return currentWalkingPath.concat(baseTypePath);
}
getGrpcObject({ rootJson, loadOptions, rootLogger, }) {
const packageDefinition = (0, proto_loader_1.fromJSON)(rootJson, loadOptions);
rootLogger.debug(`Creating service client for package definition`);
const grpcObject = (0, grpc_js_1.loadPackageDefinition)(packageDefinition);
return grpcObject;
}
getServiceClient({ grpcObject, objPath, creds, }) {
let serviceClientByObjPath = this.grpcObjectByserviceClientByObjPath.get(grpcObject);
if (!serviceClientByObjPath) {
serviceClientByObjPath = new Map();
this.grpcObjectByserviceClientByObjPath.set(grpcObject, serviceClientByObjPath);
}
let client = serviceClientByObjPath.get(objPath);
if (!client) {
const ServiceClient = (0, lodash_get_1.default)(grpcObject, objPath);
if (typeof ServiceClient !== 'function') {
throw new Error(`Object at path ${objPath} is not a Service constructor`);
}
client = new ServiceClient(string_interpolation_1.stringInterpolator.parse(this.config.endpoint, { env: cross_helpers_1.process.env }) ??
this.config.endpoint, creds);
const subId = this.pubsub.subscribe('destroy', () => {
client.close();
this.pubsub.unsubscribe(subId);
});
serviceClientByObjPath.set(objPath, client);
}
return client;
}
getFieldResolver({ client, methodName, isResponseStream, }) {
const metaData = this.config.metaData;
const clientMethod = client[methodName].bind(client);
return function grpcFieldResolver(root, args, context) {
return (0, utils_js_1.addMetaDataToCall)(clientMethod, args.input, {
root,
args,
context,
env: cross_helpers_1.process.env,
}, metaData, isResponseStream);
};
}
getConnectivityStateResolver({ client, }) {
return function connectivityStateResolver(_, { tryToConnect }) {
return client.getChannel().getConnectivityState(tryToConnect);
};
}
processDirectives({ schema, creds }) {
const schemaTypeMap = schema.getTypeMap();
for (const scalarTypeName in graphql_scalars_2.resolvers) {
if (scalarTypeName in schemaTypeMap) {
(0, scalars_js_1.addExecutionLogicToScalar)(schemaTypeMap[scalarTypeName], graphql_scalars_2.resolvers[scalarTypeName]);
}
}
if ('ObjMap' in schemaTypeMap) {
(0, scalars_js_1.addExecutionLogicToScalar)(schemaTypeMap.ObjMap, directives_js_1.ObjMapScalar);
}
const queryType = schema.getQueryType();
const rootJsonAnnotations = (0, utils_2.getDirective)(schema, queryType, 'grpcRootJson');
const rootJsonMap = new Map();
const grpcObjectByRootJsonName = new Map();
for (const { name, rootJson, loadOptions } of rootJsonAnnotations) {
rootJsonMap.set(name, rootJson);
const rootLogger = this.logger.child(name);
grpcObjectByRootJsonName.set(name, this.getGrpcObject({ rootJson, loadOptions, rootLogger }));
}
const rootTypes = (0, utils_2.getRootTypes)(schema);
for (const rootType of rootTypes) {
const rootTypeFields = rootType.getFields();
for (const fieldName in rootTypeFields) {
const field = rootTypeFields[fieldName];
const directives = (0, utils_2.getDirectives)(schema, field);
if (directives?.length) {
for (const directiveObj of directives) {
switch (directiveObj.name) {
case 'grpcMethod': {
const { rootJsonName, objPath, methodName, responseStream } = directiveObj.args;
const grpcObject = grpcObjectByRootJsonName.get(rootJsonName);
const client = this.getServiceClient({
grpcObject,
objPath,
creds,
});
if (rootType.name === 'Subscription') {
field.subscribe = this.getFieldResolver({
client,
methodName,
isResponseStream: responseStream,
});
field.resolve = function identityFn(root) {
return root;
};
}
else {
field.resolve = this.getFieldResolver({
client,
methodName,
isResponseStream: responseStream,
});
}
break;
}
case 'grpcConnectivityState': {
const { rootJsonName, objPath } = directiveObj.args;
const grpcObject = grpcObjectByRootJsonName.get(rootJsonName);
const client = this.getServiceClient({
grpcObject,
objPath,
creds,
});
field.resolve = this.getConnectivityStateResolver({ client });
break;
}
}
}
}
}
}
const typeMap = schema.getTypeMap();
for (const typeName in typeMap) {
const type = typeMap[typeName];
if ((0, graphql_1.isEnumType)(type)) {
const values = type.getValues();
for (const value of values) {
const enumAnnotations = (0, utils_2.getDirective)(schema, value, 'enum');
if (enumAnnotations?.length) {
for (const enumAnnotation of enumAnnotations) {
const enumSerializedValue = enumAnnotation.value;
if (enumSerializedValue) {
const serializedValue = JSON.parse(enumSerializedValue);
value.value = serializedValue;
type._valueLookup.set(serializedValue, value);
}
}
}
}
}
}
}
visit({ nested, name, currentPath, rootJsonName, rootJson, rootLogger: logger, loadOptions, }) {
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: {
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 = this.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 = this.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 = this.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 = this.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;
const 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: {
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: {
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: {
rootJsonName,
objPath,
},
},
],
},
});
}
}
async getCachedNonExecutableSchema(creds) {
const interpolatedSource = this.config.source?.toString();
if (interpolatedSource?.endsWith('.graphql')) {
this.logger.info(`Fetching GraphQL Schema with annotations`);
const sdl = await (0, utils_1.readFileOrUrl)(interpolatedSource, {
allowUnknownExtensions: true,
cwd: this.baseDir,
fetch: this.fetchFn,
importFn: this.importFn,
logger: this.logger,
headers: this.config.schemaHeaders,
});
return (0, graphql_1.buildSchema)(sdl, {
assumeValidSDL: true,
assumeValid: true,
});
}
return this.schemaWithAnnotationsProxy.getWithSet(async () => {
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.logger.debug(`Getting stored root and decoded descriptor set objects`);
const descriptorSets = await this.getDescriptorSets(creds);
for (const { name: rootJsonName, rootJson } of descriptorSets) {
const rootLogger = this.logger.child(rootJsonName);
rootLogger.debug(`Creating package definition from file descriptor set object`);
let loadOptions;
if (typeof this.config.source === 'object') {
loadOptions = this.config.source.load;
}
this.logger.debug(`Building the schema structure based on the root object`);
this.visit({
nested: rootJson,
name: '',
currentPath: [],
rootJsonName,
rootJson,
rootLogger,
loadOptions,
});
this.schemaComposer.addDirective(directives_js_1.grpcRootJsonDirective);
this.schemaComposer.Query.setDirectiveByName('grpcRootJson', {
name: rootJsonName,
rootJson,
});
}
// 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_2.GraphQLStreamDirective);
}
this.logger.debug(`Building the final GraphQL Schema`);
const schema = this.schemaComposer.buildSchema();
return schema;
});
}
async getMeshSource({ fetchFn }) {
this.fetchFn = fetchFn;
this.config.requestTimeout = this.config.requestTimeout || 200000;
this.logger.debug(`Getting channel credentials`);
const creds = await this.getCredentials();
const schema = await this.getCachedNonExecutableSchema(creds);
this.processDirectives({ schema, creds });
return {
schema,
};
}
}
exports.default = GrpcHandler;