UNPKG

wgc

Version:

The official CLI tool to manage the GraphQL Federation Platform Cosmo

425 lines 17.1 kB
import pc from 'picocolors'; import { program } from 'commander'; import open from 'open'; import { config } from '../../core/config.js'; import { createRouterToken, deleteRouterToken } from '../../core/router-token.js'; import { waitForKeyPress, rainbow } from '../../utils.js'; import { cleanUpFederatedGraph, createFederatedGraph, fetchFederatedGraphByName, fetchUserInfo, checkExistingOnboarding, } from './api.js'; import { captureOnboardingEvent, checkDockerReadiness, clearScreen, getDemoLogPath, prepareSupportingData, printLogo, publishAllPlugins, resetScreen, runRouterContainer, updateScreenWithUserInfo, demoSpinner, } from './util.js'; function printHello() { printLogo(); console.log(`\nThank you for choosing ${rainbow('WunderGraph')} - The open-source solution to building, maintaining, and collaborating on GraphQL Federation at Scale.`); console.log('This command will guide you through the inital setup to create your first federated graph.'); } async function handleGetFederatedGraphResponse(client, { onboarding, userInfo, }) { var _a, _b, _c; function retryFn() { resetScreen(userInfo); return handleGetFederatedGraphResponse(client, { onboarding, userInfo, }); } const spinner = demoSpinner().start(); const getFederatedGraphResponse = await fetchFederatedGraphByName(client, { name: config.demoGraphName, namespace: config.demoNamespace, }); if (getFederatedGraphResponse.error) { spinner.fail(`Failed to retrieve graph information ${getFederatedGraphResponse.error}`); return await waitForKeyPress({ r: retryFn, R: retryFn, }, 'Hit [r] to refresh. CTRL+C to quit'); } if ((_a = getFederatedGraphResponse.data) === null || _a === void 0 ? void 0 : _a.graph) { spinner.succeed(`Federated graph ${pc.bold((_c = (_b = getFederatedGraphResponse.data) === null || _b === void 0 ? void 0 : _b.graph) === null || _c === void 0 ? void 0 : _c.name)} exists.`); } else { spinner.stop(); } return getFederatedGraphResponse.data; } async function cleanupFederatedGraph(client, { graphData, userInfo, }) { let deleted = false; function retryFn() { resetScreen(userInfo); cleanupFederatedGraph(client, { graphData, userInfo }); } const spinner = demoSpinner(`Removing federated graph ${pc.bold(graphData.graph.name)}…`).start(); const deleteResponse = await cleanUpFederatedGraph(client, graphData); if (deleteResponse.error) { deleted = false; const failText = `Removing federated graph ${graphData.graph.name} failed.`; spinner.fail(failText); captureOnboardingEvent({ name: 'onboarding_step_failed', properties: { step_name: 'delete_federated_graph', entry_source: 'wgc', error_category: 'resource', error_message: `${failText}\n${deleteResponse.error.message}`, }, }); console.error(deleteResponse.error.message); return await waitForKeyPress({ Enter: () => undefined, r: retryFn, R: retryFn, }, `Failed to delete the federated graph ${pc.bold(graphData.graph.name)}. [ENTER] to continue, [r] to retry. CTRL+C to quit.`); } else { deleted = true; } if (deleted) { spinner.succeed(`Federated graph ${pc.bold(graphData.graph.name)} removed.`); captureOnboardingEvent({ name: 'onboarding_step_completed', properties: { step_name: 'delete_federated_graph', entry_source: 'wgc', }, }); } } async function handleCreateFederatedGraphResponse(client, { onboarding, userInfo, }) { function retryFn() { resetScreen(userInfo); handleCreateFederatedGraphResponse(client, { onboarding, userInfo }); } const routingUrl = new URL('graphql', 'http://localhost'); routingUrl.port = String(config.demoRouterPort); const federatedGraphSpinner = demoSpinner().start(); const createGraphResponse = await createFederatedGraph(client, { name: config.demoGraphName, namespace: config.demoNamespace, labelMatcher: config.demoLabelMatcher, routingUrl, }); if (createGraphResponse.error) { federatedGraphSpinner.fail(createGraphResponse.error.message); await waitForKeyPress({ r: retryFn, R: retryFn, }, 'Hit [r] to refresh. CTRL+C to quit'); return; } federatedGraphSpinner.succeed(`Federated graph ${pc.bold('demo')} succesfully created.`); } async function handleStep2(opts, { onboarding, userInfo, supportDir, signal, logPath, }) { var _a; function retryFn() { resetScreen(userInfo); return handleStep2(opts, { onboarding, userInfo, supportDir, signal, logPath }); } async function publishPlugins() { console.log(`\nPublishing plugins… ${pc.dim(`(logs: ${logPath})`)}`); const publishResult = await publishAllPlugins({ client: opts.client, supportDir, signal, logPath, }); if (publishResult.error) { await waitForKeyPress({ r: retryFn, R: retryFn, }, 'Hit [r] to retry. CTRL+C to quit.'); } } const graphData = await handleGetFederatedGraphResponse(opts.client, { onboarding, userInfo, }); const graph = graphData === null || graphData === void 0 ? void 0 : graphData.graph; const subgraphs = (_a = graphData === null || graphData === void 0 ? void 0 : graphData.subgraphs) !== null && _a !== void 0 ? _a : []; if (graph) { let deleted = false; const cleanupFn = async () => { await cleanupFederatedGraph(opts.client, { graphData: { graph, subgraphs }, userInfo, }); deleted = true; }; await waitForKeyPress({ Enter: () => undefined, d: cleanupFn, D: cleanupFn, }, 'Hit [ENTER] to continue or [d] to delete the federated graph and its subgraphs to start over. CTRL+C to quit.'); if (deleted) { console.log(pc.yellow('\nPlease restart the demo command to continue.\n')); process.exit(0); } await publishPlugins(); return { routingUrl: graph.routingURL }; } await handleCreateFederatedGraphResponse(opts.client, { onboarding, userInfo, }); const routingUrl = new URL('graphql', 'http://localhost'); routingUrl.port = String(config.demoRouterPort); await publishPlugins(); return { routingUrl: routingUrl.toString() }; } async function handleStep3(opts, { userInfo, routerBaseUrl, signal, logPath, }) { let firedQueries = 0; function retryFn() { firedQueries = 0; resetScreen(userInfo); return handleStep3(opts, { userInfo, routerBaseUrl, signal, logPath }); } const tokenParams = { client: opts.client, tokenName: config.demoRouterTokenName, graphName: config.demoGraphName, namespace: config.demoNamespace, }; // Delete existing token first (idempotent — no error if missing) const deleteResult = await deleteRouterToken(tokenParams); if (deleteResult.error) { const errorText = `Failed to clean up existing router token: ${deleteResult.error.message}`; console.error(errorText); captureOnboardingEvent({ name: 'onboarding_step_failed', properties: { step_name: 'run_router_send_metrics', entry_source: 'wgc', error_category: 'router', error_message: errorText, }, }); await waitForKeyPress({ r: retryFn, R: retryFn }, 'Hit [r] to retry. CTRL+C to quit.'); return; } const spinner = demoSpinner('Generating router token…').start(); const createResult = await createRouterToken(tokenParams); if (createResult.error) { const failText = `Failed to generate router token: ${createResult.error.message}`; spinner.fail(failText); captureOnboardingEvent({ name: 'onboarding_step_failed', properties: { step_name: 'run_router_send_metrics', entry_source: 'wgc', error_category: 'router', error_message: failText, }, }); await waitForKeyPress({ r: retryFn, R: retryFn }, 'Hit [r] to retry. CTRL+C to quit.'); return; } spinner.succeed('Router token generated.'); console.log(` ${pc.bold(createResult.token)}`); const sampleQuery = JSON.stringify({ query: `query GetProductWithReviews($id: ID!) { product(id: $id) { id title price { currency amount } reviews { id author rating contents } } }`, variables: { id: 'product-1' }, }); async function fireSampleQuery() { const querySpinner = demoSpinner('Sending sample query…').start(); try { const res = await fetch(`${routerBaseUrl}/graphql`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'GraphQL-Client-Name': 'wgc', }, body: sampleQuery, }); const body = await res.json(); querySpinner.succeed('Sample query response:'); console.log(pc.dim(JSON.stringify(body, null, 2))); if (firedQueries === 0) { captureOnboardingEvent({ name: 'onboarding_step_completed', properties: { step_name: 'run_router_send_metrics', entry_source: 'wgc', }, }); } firedQueries++; } catch (err) { const failText = `Sample query failed: ${err instanceof Error ? err.message : String(err)}`; captureOnboardingEvent({ name: 'onboarding_step_failed', properties: { step_name: 'run_router_send_metrics', entry_source: 'wgc', error_category: 'router', error_message: failText, }, }); querySpinner.fail(failText); } showQueryPrompt(); } function showQueryPrompt() { waitForKeyPress({ r: fireSampleQuery, R: fireSampleQuery }, 'Hit [r] to send a sample query. CTRL+C to stop the router.'); } const routerResult = await runRouterContainer({ routerToken: createResult.token, routerBaseUrl, signal, logPath, }); if (routerResult.error) { const errorText = `Router exited with error: ${routerResult.error.message}`; console.error(`\n${errorText}`); captureOnboardingEvent({ name: 'onboarding_step_failed', properties: { step_name: 'run_router_send_metrics', entry_source: 'wgc', error_category: 'router', error_message: errorText, }, }); await waitForKeyPress({ r: retryFn, R: retryFn }, 'Hit [r] to retry. CTRL+C to quit.'); } else { showQueryPrompt(); } } async function handleGetOnboardingResponse(client, userInfo) { const onboardingCheck = await checkExistingOnboarding(client); async function retryFn() { return await handleGetOnboardingResponse(client, userInfo); } switch (onboardingCheck.status) { case 'ok': { return onboardingCheck.onboarding; } case 'not-allowed': { const errorText = 'Only organization owners can trigger onboarding.'; captureOnboardingEvent({ name: 'onboarding_step_failed', properties: { step_name: 'check_onboarding', entry_source: 'wgc', error_category: 'resource', error_message: errorText, }, }); program.error(errorText); break; } case 'error': { const errorText = 'An issue occured while fetching the onboarding status'; console.error(errorText); console.error(onboardingCheck.error); captureOnboardingEvent({ name: 'onboarding_step_failed', properties: { step_name: 'check_onboarding', entry_source: 'wgc', error_category: 'resource', error_message: `${errorText}\n${onboardingCheck.error}`, }, }); await waitForKeyPress({ Enter: retryFn }, 'Hit Enter to retry. CTRL+C to quit.'); break; } default: { program.error('Invariant'); } } } async function handleStep1(opts, userInfo) { return await handleGetOnboardingResponse(opts.client, userInfo); } async function getUserInfo(client) { const spinner = demoSpinner('Retrieving information about you…').start(); const { userInfo, error } = await fetchUserInfo(client); if (error) { spinner.fail(error.message); captureOnboardingEvent({ name: 'onboarding_step_failed', properties: { step_name: 'init', entry_source: 'wgc', error_category: 'resource', error_message: error.message, }, }); program.error(error.message); } else if (!userInfo) { spinner.fail('Could not retrieve information about your account.'); program.error('Failed to retrieve user information.'); } spinner.succeed(`You are signed in as ${pc.bold(userInfo.userEmail)} in organization ${pc.bold(userInfo.organizationName)}.`); return userInfo; } export default function (opts) { return async function handleCommand() { const controller = new AbortController(); try { clearScreen(); printHello(); const supportDir = await prepareSupportingData(); await checkDockerReadiness(); const userInfo = await getUserInfo(opts.client); updateScreenWithUserInfo(userInfo); const onboardingUrl = new URL('/onboarding', config.webURL); async function openOnboardingUrl() { const browserUrl = new URL(onboardingUrl); browserUrl.searchParams.set('referrer', 'wgc'); const process = await open(browserUrl.toString()); process.on('error', (error) => { console.log(pc.yellow(`\nCouldn't open browser: ${error.message}`)); }); } await waitForKeyPress({ Enter: () => undefined, s: { callback: openOnboardingUrl, persistent: true }, S: { callback: openOnboardingUrl, persistent: true }, }, `It is recommended you run this command along the onboarding wizard at ${onboardingUrl} with the same account.\nPress [s] to open it in your browser, or [ENTER] to continue…`); resetScreen(userInfo); captureOnboardingEvent({ name: 'onboarding_step_completed', properties: { step_name: 'init', entry_source: 'wgc', }, }); const onboardingCheck = await handleStep1(opts, userInfo); if (!onboardingCheck) { return; } captureOnboardingEvent({ name: 'onboarding_step_completed', properties: { step_name: 'check_onboarding', entry_source: 'wgc', }, }); const logPath = getDemoLogPath(); const step2Result = await handleStep2(opts, { onboarding: onboardingCheck, userInfo, supportDir, signal: controller.signal, logPath, }); if (!step2Result) { return; } captureOnboardingEvent({ name: 'onboarding_step_completed', properties: { step_name: 'create_federated_graph', entry_source: 'wgc', }, }); const routerBaseUrl = new URL(step2Result.routingUrl).origin; await handleStep3(opts, { userInfo, routerBaseUrl, signal: controller.signal, logPath }); } finally { // no-op } }; } //# sourceMappingURL=command.js.map