UNPKG

@graphql-hive/cli

Version:

A CLI util to manage and control your GraphQL Hive

368 lines • 13.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.resolveServices = resolveServices; const tslib_1 = require("tslib"); const promises_1 = require("node:fs/promises"); const node_path_1 = require("node:path"); const core_1 = require("@graphql-hive/core"); const core_2 = require("@oclif/core"); const base_command_1 = tslib_1.__importDefault(require("../base-command")); const gql_1 = require("../gql"); const config_1 = require("../helpers/config"); const errors_1 = require("../helpers/errors"); const schema_1 = require("../helpers/schema"); const TargetInput = tslib_1.__importStar(require("../helpers/target-input")); const validation_1 = require("../helpers/validation"); async function resolveServices(services, logger) { return await Promise.all(services.map(async (input) => { if (input.sdl) { return { name: input.name, url: input.url, sdl: await resolveSdlFromPath(input.sdl, logger), input: { kind: 'file', path: input.sdl, }, }; } return { name: input.name, url: input.url, sdl: await resolveSdlFromUrl(input.name, input.url, logger), input: { kind: 'url', url: input.url, }, }; })); } async function resolveSdlFromPath(path, logger) { const sdl = await (0, schema_1.loadSchema)(null, path, { logger }); (0, validation_1.invariant)(typeof sdl === 'string' && sdl.length > 0, `Read empty schema from ${path}`); return sdl; } async function resolveSdlFromUrl(serviceName, url, logger) { const sdl = await (0, schema_1.loadSchema)('only-federation-introspection', url, { logger }).catch(err => { logger.error(err); throw err; }); if (!sdl) { throw new errors_1.IntrospectionError(serviceName); } return sdl; } class Dev extends base_command_1.default { async run() { const { flags } = await this.parse(Dev); const { unstable__forceLatest } = flags; if (flags.service.length !== flags.url.length) { throw new errors_1.ServiceAndUrlLengthMismatch(flags.service, flags.url); } const isRemote = flags.remote === true; const serviceInputs = flags.service.map((name, i) => { const url = flags.url[i]; const sdl = flags.schema ? flags.schema[i] : undefined; return { name, url, sdl, }; }); let target = null; if (flags.target) { const result = TargetInput.parse(flags.target); if (result.type === 'error') { throw new errors_1.InvalidTargetError(); } target = result.data; } if (flags.watch === true) { if (isRemote) { let registry, token; try { registry = this.ensure({ key: 'registry.endpoint', legacyFlagName: 'registry', args: flags, defaultValue: config_1.graphqlEndpoint, env: 'HIVE_REGISTRY', description: Dev.flags['registry.endpoint'].description, }); } catch (e) { this.logDebug(e); throw new errors_1.MissingEndpointError(); } try { token = this.ensure({ key: 'registry.accessToken', legacyFlagName: 'token', args: flags, env: 'HIVE_TOKEN', description: Dev.flags['registry.accessToken'].description, }); } catch (e) { this.logDebug(e); throw new errors_1.MissingRegistryTokenError(); } void this.watch(flags.watchInterval, serviceInputs, services => this.compose({ services, registry, token, write: flags.write, unstable__forceLatest, target, onError: error => { // watch mode should not exit. Log instead. this.logFailure(error.message); }, })); return; } void this.watch(flags.watchInterval, serviceInputs, services => this.composeLocally({ services, write: flags.write, onError: error => { // watch mode should not exit. Log instead. this.logFailure(error.message); }, })); return; } const services = await this.resolveServices(serviceInputs); if (isRemote) { let registry, token; try { registry = this.ensure({ key: 'registry.endpoint', legacyFlagName: 'registry', args: flags, defaultValue: config_1.graphqlEndpoint, env: 'HIVE_REGISTRY', description: Dev.flags['registry.endpoint'].description, }); } catch (e) { this.logDebug(e); throw new errors_1.MissingEndpointError(); } try { token = this.ensure({ key: 'registry.accessToken', legacyFlagName: 'token', args: flags, env: 'HIVE_TOKEN', description: Dev.flags['registry.accessToken'].description, }); } catch (e) { this.logDebug(e); throw new errors_1.MissingRegistryTokenError(); } return this.compose({ services, registry, token, write: flags.write, unstable__forceLatest, target, onError: error => { throw error; }, }); } return this.composeLocally({ services, write: flags.write, onError: error => { throw error; }, }); } async composeLocally(input) { let supergraphSdl; try { supergraphSdl = await (0, core_1.composeSupergraphLocally)(input.services); } catch (error) { if (error instanceof core_1.LocalSupergraphCompositionError) { input.onError(new errors_1.LocalCompositionError(error.compositionResult)); return; } throw error; } this.logSuccess('Composition successful'); this.log(`Saving supergraph schema to ${input.write}`); await (0, promises_1.writeFile)((0, node_path_1.resolve)(process.cwd(), input.write), supergraphSdl, 'utf-8'); } async compose(input) { let supergraphSdl; try { supergraphSdl = await (0, core_1.composeSupergraphRemotely)({ services: input.services, registry: input.registry, token: input.token, unstable__forceLatest: input.unstable__forceLatest, target: input.target, version: this.config.version, logger: this.logger, }); } catch (error) { if (error instanceof core_1.SupergraphRegistryApiError) { input.onError(new errors_1.APIError(error.message)); return; } if (error instanceof core_1.RemoteSupergraphCompositionError) { input.onError(new errors_1.RemoteCompositionError((0, gql_1.makeFragmentData)({ edges: error.errors.map(e => ({ node: { message: e.message } })) }, schema_1.RenderErrors_SchemaErrorConnectionFragment))); return; } if (error instanceof core_1.InvalidSupergraphResultError) { input.onError(new errors_1.InvalidCompositionResultError(error.supergraphSdl)); return; } throw error; } this.logSuccess('Composition successful'); this.log(`Saving supergraph schema to ${input.write}`); try { await (0, promises_1.writeFile)((0, node_path_1.resolve)(process.cwd(), input.write), supergraphSdl, 'utf-8'); } catch (e) { input.onError(new errors_1.UnexpectedError(e)); } } async watch(watchInterval, serviceInputs, compose) { this.logInfo('Watch mode enabled'); let services; try { services = await this.resolveServices(serviceInputs); await compose(services); } catch (e) { throw new errors_1.UnexpectedError(e); } this.logInfo('Watching for changes'); let resolveWatchMode; const watchPromise = new Promise(resolve => { resolveWatchMode = resolve; }); let timeoutId; const watch = async () => { try { const newServices = await this.resolveServices(serviceInputs); if (newServices.some(service => services.find(s => s.name === service.name).sdl !== service.sdl)) { this.logInfo('Detected changes, recomposing'); await compose(newServices); services = newServices; } } catch (error) { this.logFailure(new errors_1.UnexpectedError(error)); } timeoutId = setTimeout(watch, watchInterval); }; process.once('SIGINT', () => { this.logInfo('Exiting watch mode'); clearTimeout(timeoutId); resolveWatchMode(); }); process.once('SIGTERM', () => { this.logInfo('Exiting watch mode'); clearTimeout(timeoutId); resolveWatchMode(); }); void watch(); return watchPromise; } async resolveServices(services) { return await resolveServices(services, this.logger); } } Dev.description = [ 'Develop and compose Supergraph with your local services.', 'Only available for Federation projects.', '', 'Two modes are available:', " 1. Local mode (default): Compose provided services locally. (Uses Hive's native Federation v2 composition)", ' 2. Remote mode: Perform composition remotely (according to project settings) using all services registered in the registry.', ].join('\n'); Dev.flags = { 'registry.endpoint': core_2.Flags.string({ description: 'registry endpoint', dependsOn: ['remote'], }), /** @deprecated */ registry: core_2.Flags.string({ description: 'registry address (deprecated in favor of --registry.endpoint)', deprecated: { message: 'use --registry.endpoint instead', version: '0.21.0', }, dependsOn: ['remote'], }), 'registry.accessToken': core_2.Flags.string({ description: 'registry access token', dependsOn: ['remote'], }), /** @deprecated */ token: core_2.Flags.string({ description: 'api token (deprecated in favor of --registry.accessToken)', deprecated: { message: 'use --registry.accessToken instead', version: '0.21.0', }, dependsOn: ['remote'], }), service: core_2.Flags.string({ description: 'Service name', required: true, multiple: true, helpValue: '<string>', }), url: core_2.Flags.string({ description: 'Service url', required: true, multiple: true, helpValue: '<address>', dependsOn: ['service'], }), schema: core_2.Flags.string({ description: 'Service sdl. If not provided, will be introspected from the service', multiple: true, helpValue: '<filepath>', dependsOn: ['service'], }), watch: core_2.Flags.boolean({ description: 'Watch mode', default: false, }), watchInterval: core_2.Flags.integer({ description: 'Watch interval in milliseconds', default: 1000, }), write: core_2.Flags.string({ description: 'Where to save the supergraph schema file', default: 'supergraph.graphql', }), remote: core_2.Flags.boolean({ // TODO: improve description description: 'Compose provided services remotely', default: false, }), unstable__forceLatest: core_2.Flags.boolean({ hidden: true, description: 'Force the command to use the latest version of the CLI, not the latest composable version.', default: false, dependsOn: ['remote'], }), target: core_2.Flags.string({ description: 'The target to use for composition (slug or ID).' + ' This can either be a slug following the format "$organizationSlug/$projectSlug/$targetSlug" (e.g "the-guild/graphql-hive/staging")' + ' or an UUID (e.g. "a0f4c605-6541-4350-8cfe-b31f21a4bf80").', }), }; exports.default = Dev; //# sourceMappingURL=dev.js.map