UNPKG

@graphql-hive/core

Version:
238 lines (237 loc) • 11.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.InvalidSupergraphResultError = exports.RemoteSupergraphCompositionError = exports.SupergraphRegistryApiError = exports.LocalSupergraphCompositionError = void 0; exports.composeSupergraphLocally = composeSupergraphLocally; exports.composeSupergraphRemotely = composeSupergraphRemotely; exports.createDevFetcher = createDevFetcher; const tslib_1 = require("tslib"); const promises_1 = require("node:fs/promises"); const node_path_1 = require("node:path"); const graphql_1 = require("graphql"); const federation_composition_1 = require("@theguild/federation-composition"); const circuit_js_1 = tslib_1.__importDefault(require("../circuit-breaker/circuit.js")); const circuit_breaker_js_1 = require("./circuit-breaker.js"); const http_client_js_1 = require("./http-client.js"); const utils_js_1 = require("./utils.js"); class LocalSupergraphCompositionError extends Error { constructor(compositionResult) { super('Local composition failed.'); this.compositionResult = compositionResult; } } exports.LocalSupergraphCompositionError = LocalSupergraphCompositionError; /** The registry API returned a GraphQL/API-level error while composing remotely. */ class SupergraphRegistryApiError extends Error { } exports.SupergraphRegistryApiError = SupergraphRegistryApiError; /** Remote composition finished but produced composition errors. */ class RemoteSupergraphCompositionError extends Error { constructor(errors) { super(`Remote composition failed:\n${errors.map(error => error.message).join('\n')}`); this.errors = errors; } } exports.RemoteSupergraphCompositionError = RemoteSupergraphCompositionError; /** Remote composition reported success but did not return a usable supergraph SDL. */ class InvalidSupergraphResultError extends Error { constructor(supergraphSdl) { super(`Remote composition resulted in an invalid supergraph: ${supergraphSdl}`); this.supergraphSdl = supergraphSdl; } } exports.InvalidSupergraphResultError = InvalidSupergraphResultError; async function composeSupergraphLocally(services) { const compositionResult = await new Promise((resolvePromise, reject) => { try { resolvePromise((0, federation_composition_1.composeServices)(services.map(service => ({ name: service.name, url: service.url, typeDefs: (0, graphql_1.parse)(service.sdl), })))); } catch (error) { // composeServices should not throw; this reject covers the offchance that // something unexpected happens under the hood, so the promise doesn't hang. reject(error); } }); if ((0, federation_composition_1.compositionHasErrors)(compositionResult)) { throw new LocalSupergraphCompositionError(compositionResult); } return compositionResult.supergraphSdl; } async function composeSupergraphRemotely(input) { var _a, _b; const response = await http_client_js_1.http.post(input.registry, JSON.stringify({ query: /* GraphQL */ ` mutation CreateDevFetcher_SchemaCompose($input: SchemaComposeInput!) { schemaCompose(input: $input) { __typename ... on SchemaComposeSuccess { valid compositionResult { supergraphSdl errors { edges { node { message } } } } } ... on SchemaComposeError { message } } } `, variables: { input: { useLatestComposableVersion: !input.unstable__forceLatest, services: input.services.map(service => ({ name: service.name, url: service.url, sdl: service.sdl, })), target: input.target, }, }, }), { headers: { 'content-type': 'application/json', authorization: `Bearer ${input.token}`, 'graphql-client-name': 'Hive Dev Fetcher', 'graphql-client-version': input.version, }, logger: input.logger, fetchImplementation: input.fetch, }); const body = await response.json(); if ((_a = body.errors) === null || _a === void 0 ? void 0 : _a.length) { throw new SupergraphRegistryApiError(body.errors.map(error => error.message).join(', ')); } const schemaCompose = (_b = body.data) === null || _b === void 0 ? void 0 : _b.schemaCompose; if (!schemaCompose) { throw new SupergraphRegistryApiError('Received an unexpected response from the registry.'); } if (schemaCompose.__typename === 'SchemaComposeError') { throw new SupergraphRegistryApiError(schemaCompose.message); } const { valid, compositionResult } = schemaCompose; if (!valid) { if (compositionResult.errors) { throw new RemoteSupergraphCompositionError(compositionResult.errors.edges.map(edge => edge.node)); } throw new InvalidSupergraphResultError(compositionResult.supergraphSdl); } if (typeof compositionResult.supergraphSdl !== 'string') { throw new InvalidSupergraphResultError(compositionResult.supergraphSdl); } return compositionResult.supergraphSdl; } async function introspectFederationService(service, logger, fetch) { var _a, _b, _c; const response = await http_client_js_1.http.post(service.url, JSON.stringify({ query: '{ _service { sdl } }' }), { headers: { 'content-type': 'application/json' }, logger, fetchImplementation: fetch, }); const body = await response.json(); if (((_a = body.errors) === null || _a === void 0 ? void 0 : _a.length) || !((_c = (_b = body.data) === null || _b === void 0 ? void 0 : _b._service) === null || _c === void 0 ? void 0 : _c.sdl)) { throw new Error(`Could not get a federation introspection result from the service "${service.name}". ` + `Make sure the service exposes a federation "_service { sdl }" field, or set its ` + `"introspection" option to "graphql" to use standard GraphQL introspection instead.`); } return body.data._service.sdl; } async function introspectGraphQLService(service, logger, fetch) { var _a; const response = await http_client_js_1.http.post(service.url, JSON.stringify({ query: (0, graphql_1.getIntrospectionQuery)() }), { headers: { 'content-type': 'application/json' }, logger, fetchImplementation: fetch, }); const body = await response.json(); if (((_a = body.errors) === null || _a === void 0 ? void 0 : _a.length) || !body.data) { throw new Error(`Could not get introspection result from the service "${service.name}". Make sure introspection is enabled by the server.`); } return (0, graphql_1.printSchema)((0, graphql_1.buildClientSchema)(body.data)); } async function resolveService(service, cwd, logger, fetch) { if (service.source === 'file') { const filePath = (0, node_path_1.resolve)(cwd, service.schema); const contents = await (0, promises_1.readFile)(filePath, 'utf8'); // `parse` here only validates the file's contents; `contents` is kept as-is rather than // reprinting it, since it's re-parsed anyway by whichever composition path consumes it. (0, graphql_1.parse)(contents); return { name: service.name, url: service.url, sdl: contents }; } const sdl = service.source === 'graphql' ? await introspectGraphQLService(service, logger, fetch) : await introspectFederationService(service, logger, fetch); return { name: service.name, url: service.url, sdl }; } async function resolveServices(services, cwd, logger, fetch) { return Promise.all(services.map(service => resolveService(service, cwd, logger, fetch))); } const CACHE_KEY = 'hive:dev-fetcher:supergraph'; function servicesUnchanged(previous, next) { if (previous.length !== next.length) { return false; } return next.every(service => { var _a; return ((_a = previous.find(p => p.name === service.name)) === null || _a === void 0 ? void 0 : _a.sdl) === service.sdl; }); } /** * Create a fetcher that can get subgraph definitions from a local file, graphql introspection, * or federated introspection (default), and then compose these services with the latest schema * stored in Hive with these subgraphs replaced (based on service name). * * This is an alternative to using `@graphql-hive/cli`'s dev command. * * The composed supergraph is cached and is only recomposed if the provided service SDLs change. But * introspection and file reading is ran on every call, so if using Hive Gateway's polling interval, * set the interval accordingly. Composition is also CircuitBreaked, so that the expensive composition * request is guaranteed not to run too frequently. */ function createDevFetcher(options) { var _a, _b; const logger = (0, utils_js_1.chooseLogger)(options.logger); const cwd = (_a = options.cwd) !== null && _a !== void 0 ? _a : process.cwd(); const circuitBreakerConfig = (_b = options.circuitBreaker) !== null && _b !== void 0 ? _b : circuit_breaker_js_1.defaultCircuitBreakerConfiguration; const composeBreaker = new circuit_js_1.default(async (services) => { var _a, _b, _c; if (options.remote) { if (!options.registry || !options.token) { throw new Error('`registry` and `token` are required when `remote` is enabled.'); } return await composeSupergraphRemotely({ services, registry: options.registry, token: options.token, unstable__forceLatest: (_a = options.unstable__forceLatest) !== null && _a !== void 0 ? _a : false, target: (_b = options.target) !== null && _b !== void 0 ? _b : null, version: (_c = options.version) !== null && _c !== void 0 ? _c : 'unknown', logger, fetch: options.fetch, }); } return await composeSupergraphLocally(services); }, Object.assign(Object.assign({}, circuitBreakerConfig), { timeout: false })); return { async fetch() { var _a, _b; const services = await resolveServices(options.services, cwd, logger, options.fetch); const cached = await ((_a = options.cache) === null || _a === void 0 ? void 0 : _a.get(CACHE_KEY)); if (cached && servicesUnchanged(cached.services, services)) { return cached.supergraphSdl; } const supergraphSdl = await composeBreaker.fire(services); await ((_b = options.cache) === null || _b === void 0 ? void 0 : _b.set(CACHE_KEY, { services, supergraphSdl })); return supergraphSdl; }, dispose() { composeBreaker.shutdown(); }, }; }