wgc
Version:
The official CLI tool to manage the GraphQL Federation Platform Cosmo
425 lines • 17.1 kB
JavaScript
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