UNPKG

apollo-gateway-subscription

Version:

Sidecar for subscriptions that work with Apollo Federation/Supergraphs

226 lines 8.92 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GatewayDataSource = void 0; const apollo_server_1 = require("apollo-server"); const core_1 = require("@apollo/client/core"); const apollo_datasource_1 = require("apollo-datasource"); const error_1 = require("@apollo/client/link/error"); const graphql_parse_resolve_info_1 = require("graphql-parse-resolve-info"); const context_1 = require("@apollo/client/link/context"); const node_fetch_1 = __importDefault(require("node-fetch")); const merge_1 = __importDefault(require("lodash/merge")); class GatewayDataSource extends apollo_datasource_1.DataSource { gatewayURL; context; constructor(gatewayURL) { super(); this.gatewayURL = gatewayURL; } initialize(config) { this.context = config.context; } composeLinks() { const uri = this.resolveUri(); return (0, core_1.from)([ this.onErrorLink(), this.onRequestLink(), (0, core_1.createHttpLink)({ fetch: node_fetch_1.default, uri }) ]); } didEncounterError(error) { const status = error.statusCode ? error.statusCode : null; const message = error.bodyText ? error.bodyText : null; let apolloError; switch (status) { case 401: apolloError = new apollo_server_1.AuthenticationError(message); break; case 403: apolloError = new apollo_server_1.ForbiddenError(message); break; case 502: apolloError = new apollo_server_1.ApolloError("Bad Gateway", status); break; default: apolloError = new apollo_server_1.ApolloError(message, status); } throw apolloError; } async query(query, options) { const link = this.composeLinks(); try { const response = await (0, core_1.toPromise)((0, core_1.execute)(link, { query, ...options })); return response; } catch (error) { this.didEncounterError(error); } } resolveUri() { const gatewayURL = this.gatewayURL; if (!gatewayURL) { throw new apollo_server_1.ApolloError("Cannot make request to GraphQL API, missing gatewayURL"); } return gatewayURL; } onRequestLink() { return (0, context_1.setContext)(request => { if (typeof this.willSendRequest === "function") { this.willSendRequest(request); } return request; }); } onErrorLink() { return (0, error_1.onError)(({ graphQLErrors, networkError }) => { if (graphQLErrors) { graphQLErrors.map(graphqlError => console.error(`[GraphQL error]: ${graphqlError.message}`)); } if (networkError) { console.log(`[Network Error]: ${networkError}`); } }); } addDelimiter(a, b) { return a ? `${a}.${b}` : b; } isObject(val) { return typeof val === "object" && !Array.isArray(val) && val !== null; } isFieldObject(obj) { return (this.isObject(obj) && obj.hasOwnProperty("args") && obj.hasOwnProperty("alias") && obj.hasOwnProperty("name")); } fieldPathsAsStrings(obj) { const paths = (obj = {}, head = "") => { return Object.entries(obj).reduce((acc, [key, value]) => { let fullPath = this.addDelimiter(head, key); return this.isObject(value) ? acc.concat(key, paths(value, fullPath)) : acc.concat(fullPath); }, []); }; return paths(obj); } fieldPathsAsMapFromResolveInfo(resolveInfo) { const paths = (obj = {}, head = "") => { return Object.entries(obj).reduce((acc, [key, value]) => { let fullPath = this.addDelimiter(head, key); if (this.isFieldObject(value) && Object.keys(value.fieldsByTypeName).length === 0) { const { alias, args, name } = value; return acc.concat([[fullPath, { alias, args, name }]]); } else if (this.isFieldObject(value)) { const { alias, args, name } = value; return acc.concat([[fullPath, { alias, args, name }]], paths(value, fullPath)); } else if (this.isObject(value)) { return acc.concat(paths(value, fullPath)); } return acc.concat([[fullPath, null]]); }, []); }; const resolveInfoFields = paths(resolveInfo); return Object.fromEntries(resolveInfoFields .filter(([_, options]) => options) .map(([path, { alias, args, name }]) => { const pathParts = path.split("."); const noTypeNames = pathParts.forEach((part, i) => { if (pathParts[i - 1] === "fieldsByTypeName") { pathParts.splice(i - 1, 2); } }); let keptOptions = { ...(name !== alias && { alias }), ...(Object.keys(args).length && { args }) }; return [ pathParts.join("."), Object.keys(keptOptions).length ? keptOptions : null ]; })); } buildSelection(selection, pathString, pathParts, fieldPathMap, index) { let formattedSelection = selection; let options; let parentOptions; if (pathParts.length > 1 && index < pathParts.length - 1) { const parentPathString = pathParts.slice(0, index + 1).join("."); parentOptions = fieldPathMap[parentPathString]; } else { options = fieldPathMap[pathString]; } if (parentOptions) { if (parentOptions.alias) { formattedSelection = `${parentOptions.alias}: ${formattedSelection}`; } if (parentOptions.args) { const formattedArgs = JSON.stringify(parentOptions.args) .slice(1, -1) .replace(/"([^"]+)":/g, "$1:"); formattedSelection = `${formattedSelection}(${formattedArgs})`; } } else if (options) { if (options.alias) { formattedSelection = `${options.alias}: ${formattedSelection}`; } if (options.args) { const formattedArgs = JSON.stringify(options.args) .slice(1, -1) .replace(/"([^"]+)":/g, "$1:"); formattedSelection = `${formattedSelection}(${formattedArgs})`; } } return formattedSelection; } buildNonPayloadSelections(payload, info) { const resolveInfo = (0, graphql_parse_resolve_info_1.parseResolveInfo)(info); const payloadFieldPaths = this.fieldPathsAsStrings(payload[resolveInfo?.name]); const operationFields = resolveInfo ? this.fieldPathsAsMapFromResolveInfo(resolveInfo) : {}; const operationFieldPaths = Object.keys(operationFields); return operationFieldPaths .filter(path => !payloadFieldPaths.includes(path)) .reduce((acc, curr, i, arr) => { const pathParts = curr.split("."); let selections = ""; pathParts.forEach((part, j) => { const hasSubFields = !!arr.slice(i + 1).find(item => { const itemParts = item.split("."); itemParts.pop(); const rejoinedItem = itemParts.join("."); return rejoinedItem === curr; }); if (hasSubFields) { return; } const sel = this.buildSelection(part, curr, pathParts, operationFields, j); if (j === 0) { selections = `${sel} `; } else if (j === 1) { selections = `${selections}{ ${sel} } `; } else { const char = -(j - 2) - j; selections = `${selections.slice(0, char)}{ ${sel} } ${selections.slice(char)}`; } }); return acc + selections; }, ""); } mergeFieldData(payloadFieldData, nonPayloadFieldData) { return (0, merge_1.default)(payloadFieldData, nonPayloadFieldData); } } exports.GatewayDataSource = GatewayDataSource; //# sourceMappingURL=index.js.map