@graphql-mesh/transport-grpc
Version:
130 lines (129 loc) • 4.86 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.toGrpcGraphQLError = toGrpcGraphQLError;
exports.buildGrpcMetadata = buildGrpcMetadata;
exports.addMetaDataToCall = addMetaDataToCall;
exports.addExecutionLogicToScalar = addExecutionLogicToScalar;
const tslib_1 = require("tslib");
const lodash_get_1 = tslib_1.__importDefault(require("lodash.get"));
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");
function isBlob(input) {
return input != null && input.stream instanceof Function;
}
function isGrpcServiceError(error) {
return (typeof error === 'object' &&
error != null &&
typeof error.code === 'number' &&
typeof error.details === 'string');
}
/**
* Convert a gRPC `ServiceError` into a GraphQLError so gateways do not mask it.
* Upstream details live under `extensions.grpc` (code, status, details, metadata).
* Non-ServiceError values are returned unchanged.
*/
function toGrpcGraphQLError(error) {
if (!isGrpcServiceError(error)) {
return error;
}
const status = grpc_js_1.status[error.code] != null ? String(grpc_js_1.status[error.code]) : 'UNKNOWN';
// Do not set `originalError` to the raw ServiceError — Envelop's maskedErrors
// walks originalError and would treat a non-GraphQLError root as unexpected.
return (0, utils_2.createGraphQLError)(error.message || status, {
extensions: {
code: 'DOWNSTREAM_SERVICE_ERROR',
grpc: {
code: error.code,
status,
details: error.details,
metadata: error.metadata?.getMap?.(),
},
},
});
}
function buildGrpcMetadata(metaData, resolverData) {
if (!metaData) {
return undefined;
}
const meta = new grpc_js_1.Metadata();
const entries = Array.isArray(metaData) ? Object.fromEntries(metaData) : metaData;
for (const [key, value] of Object.entries(entries)) {
let metaValue = value;
if (Array.isArray(value)) {
// Extract data from context
metaValue = (0, lodash_get_1.default)(resolverData.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 = string_interpolation_1.stringInterpolator.parse(metaValue, resolverData);
}
meta.add(key, metaValue);
}
return meta;
}
function addMetaDataToCall(callFn, input, resolverData, metaData, isResponseStream = false, requestTimeout) {
const callFnArguments = [];
if (!isBlob(input)) {
callFnArguments.push(input);
}
const meta = buildGrpcMetadata(metaData, resolverData);
const callOptions = {};
if (requestTimeout != null && requestTimeout > 0) {
callOptions.deadline = Date.now() + requestTimeout;
}
const hasCallOptions = Object.keys(callOptions).length > 0;
if (meta || hasCallOptions) {
// grpc-js accepts (argument, metadata, options, callback)
callFnArguments.push(meta ?? new grpc_js_1.Metadata());
if (hasCallOptions) {
callFnArguments.push(callOptions);
}
}
return new Promise((resolve, reject) => {
const call = callFn(...callFnArguments, (error, response) => {
if (error) {
reject(toGrpcGraphQLError(error));
return;
}
resolve(response);
});
if (isResponseStream) {
let isCancelled = false;
const responseStreamWithCancel = (0, utils_1.withCancel)(call, reason => {
if (!isCancelled) {
call.call?.cancelWithStatus(0, reason?.toString() || 'aborted');
isCancelled = true;
}
});
resolve(responseStreamWithCancel);
if (isBlob(input)) {
const blobStream = input.stream();
blobStream.pipe(call);
}
}
});
}
function addExecutionLogicToScalar(nonExecutableScalar, actualScalar) {
Object.defineProperties(nonExecutableScalar, {
serialize: {
value: actualScalar.serialize,
},
parseValue: {
value: actualScalar.parseValue,
},
parseLiteral: {
value: actualScalar.parseLiteral,
},
extensions: {
value: {
...actualScalar.extensions,
...nonExecutableScalar.extensions,
},
},
});
}