Skip to content

Commit 02d448f

Browse files
committed
feat: handle federated graph creation
1 parent b6c9f96 commit 02d448f

File tree

1 file changed

+305
-3
lines changed

1 file changed

+305
-3
lines changed

cli/src/commands/demo/command.ts

Lines changed: 305 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import pc from 'picocolors';
55
import ora from 'ora';
66
import { program } from 'commander';
77
import { z } from 'zod';
8-
import type { WhoAmIResponse } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb';
8+
import type { FederatedGraph, Subgraph, WhoAmIResponse } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb';
99
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
1010
import { BaseCommandOptions } from '../../core/types/types.js';
1111
import { getBaseHeaders, config } from '../../core/config.js';
@@ -14,6 +14,10 @@ import { waitForKeyPress, rainbow, visibleLength } from '../../utils.js';
1414
const ONBOARDING_REPO = 'wundergraph/cosmo-onboarding';
1515
const ONBOARDING_BRANCH = 'main';
1616
const ONBOARDING_DIR_PREFIX = 'plugins/';
17+
const DEMO_GRAPH_NAME = 'demo' as const;
18+
const DEMO_NAMESPACES = 'default' as const;
19+
const DEMO_LABEL_MATCHER = `graph=demo` as const;
20+
const DEMO_ROUTER_PORT = 3002 as const;
1721

1822
const GitHubTreeSchema = z.object({
1923
tree: z.array(
@@ -89,6 +93,298 @@ function printHello() {
8993
console.log('This command will guide you through the inital setup to create your first federated graph.');
9094
}
9195

96+
async function fetchFederatedGraphByName(
97+
client: BaseCommandOptions['client'],
98+
{ name, namespace }: { name: string; namespace: string },
99+
) {
100+
const { response, graph, subgraphs } = await client.platform.getFederatedGraphByName(
101+
{
102+
name,
103+
namespace,
104+
},
105+
{
106+
headers: getBaseHeaders(),
107+
},
108+
);
109+
110+
switch (response?.code) {
111+
case EnumStatusCode.OK: {
112+
return { data: { graph, subgraphs }, error: null };
113+
}
114+
case EnumStatusCode.ERR_NOT_FOUND: {
115+
return { data: null, error: null };
116+
}
117+
default: {
118+
return {
119+
data: null,
120+
error: new Error(response?.details ?? 'An unknown error occured'),
121+
};
122+
}
123+
}
124+
}
125+
126+
async function cleanUpFederatedGraph(
127+
client: BaseCommandOptions['client'],
128+
graphData: {
129+
graph: FederatedGraph;
130+
subgraphs: Subgraph[];
131+
},
132+
) {
133+
const subgraphDeleteResponses = await Promise.all(
134+
graphData.subgraphs.map(({ name, namespace }) =>
135+
client.platform.deleteFederatedSubgraph(
136+
{
137+
namespace,
138+
subgraphName: name,
139+
disableResolvabilityValidation: false,
140+
},
141+
{
142+
headers: getBaseHeaders(),
143+
},
144+
),
145+
),
146+
);
147+
148+
const failedSubgraphDeleteResponses = subgraphDeleteResponses.filter(
149+
({ response }) => response?.code !== EnumStatusCode.OK,
150+
);
151+
152+
if (failedSubgraphDeleteResponses.length > 0) {
153+
return {
154+
error: new Error(
155+
failedSubgraphDeleteResponses.map(({ response }) => response?.details ?? 'Unknown error occurred.').join('. '),
156+
),
157+
};
158+
}
159+
160+
const federatedGraphDeleteResponse = await client.platform.deleteFederatedGraph(
161+
{
162+
name: graphData.graph.name,
163+
namespace: graphData.graph.namespace,
164+
},
165+
{
166+
headers: getBaseHeaders(),
167+
},
168+
);
169+
170+
switch (federatedGraphDeleteResponse.response?.code) {
171+
case EnumStatusCode.OK: {
172+
return {
173+
error: null,
174+
};
175+
}
176+
default: {
177+
return {
178+
error: new Error(federatedGraphDeleteResponse.response?.details ?? 'Unknown error occurred.'),
179+
};
180+
}
181+
}
182+
}
183+
184+
async function createFederatedGraph(
185+
client: BaseCommandOptions['client'],
186+
options: {
187+
name: string;
188+
namespace: string;
189+
labelMatcher: string;
190+
routingUrl: URL;
191+
},
192+
) {
193+
const createFedGraphResponse = await client.platform.createFederatedGraph(
194+
{
195+
name: options.name,
196+
namespace: options.namespace,
197+
routingUrl: options.routingUrl.toString(),
198+
labelMatchers: [options.labelMatcher],
199+
},
200+
{
201+
headers: getBaseHeaders(),
202+
},
203+
);
204+
205+
switch (createFedGraphResponse.response?.code) {
206+
case EnumStatusCode.OK: {
207+
return { error: null };
208+
}
209+
default: {
210+
return {
211+
error: new Error(createFedGraphResponse.response?.details ?? 'An unknown error occured'),
212+
};
213+
}
214+
}
215+
}
216+
217+
async function handleGetFederatedGraphResponse(
218+
client: BaseCommandOptions['client'],
219+
{
220+
onboarding,
221+
userInfo,
222+
}: {
223+
onboarding: {
224+
finishedAt?: string;
225+
};
226+
userInfo: UserInfo;
227+
},
228+
) {
229+
function retryFn() {
230+
resetScreen(userInfo);
231+
return handleGetFederatedGraphResponse(client, {
232+
onboarding,
233+
userInfo,
234+
});
235+
}
236+
237+
const spinner = ora().start();
238+
const getFederatedGraphResponse = await fetchFederatedGraphByName(client, {
239+
name: DEMO_GRAPH_NAME,
240+
namespace: DEMO_NAMESPACES,
241+
});
242+
243+
if (getFederatedGraphResponse.error) {
244+
spinner.fail(`Failed to retrieve graph information ${getFederatedGraphResponse.error}`);
245+
await waitForKeyPress(
246+
{
247+
r: retryFn,
248+
R: retryFn,
249+
},
250+
'Hit [r] to refresh. CTRL+C to quit',
251+
);
252+
return;
253+
}
254+
255+
if (getFederatedGraphResponse.data?.graph) {
256+
spinner.succeed(`Federated graph ${pc.bold(getFederatedGraphResponse.data?.graph?.name)} exists.`);
257+
} else {
258+
spinner.stop();
259+
}
260+
261+
return getFederatedGraphResponse.data;
262+
}
263+
264+
async function cleanupFederatedGraph(
265+
client: BaseCommandOptions['client'],
266+
{
267+
graphData,
268+
userInfo,
269+
}: {
270+
graphData: {
271+
graph: FederatedGraph;
272+
subgraphs: Subgraph[];
273+
};
274+
userInfo: UserInfo;
275+
},
276+
) {
277+
function retryFn() {
278+
resetScreen(userInfo);
279+
cleanupFederatedGraph(client, { graphData, userInfo });
280+
}
281+
282+
const spinner = ora().start(`Removing federated graph ${pc.bold(graphData.graph.name)}…`);
283+
const deleteResponse = await cleanUpFederatedGraph(client, graphData);
284+
285+
if (deleteResponse.error) {
286+
spinner.fail(`Removing federated graph ${graphData.graph.name} failed.`);
287+
console.error(deleteResponse.error.message);
288+
289+
await waitForKeyPress(
290+
{
291+
Enter: () => undefined,
292+
r: retryFn,
293+
R: retryFn,
294+
},
295+
`Failed to delete the federated graph ${pc.bold(graphData.graph.name)}. [ENTER] to continue, [r] to retry. CTRL+C to quit.`,
296+
);
297+
}
298+
299+
spinner.succeed(`Federated graph ${pc.bold(graphData.graph.name)} removed.`);
300+
}
301+
302+
async function handleCreateFederatedGraphResponse(
303+
client: BaseCommandOptions['client'],
304+
{
305+
onboarding,
306+
userInfo,
307+
}: {
308+
onboarding: {
309+
finishedAt?: string;
310+
};
311+
userInfo: UserInfo;
312+
},
313+
) {
314+
function retryFn() {
315+
resetScreen(userInfo);
316+
handleCreateFederatedGraphResponse(client, { onboarding, userInfo });
317+
}
318+
319+
const routingUrl = new URL('http://localhost');
320+
routingUrl.port = String(DEMO_ROUTER_PORT);
321+
322+
const federatedGraphSpinner = ora().start();
323+
const createGraphResponse = await createFederatedGraph(client, {
324+
name: DEMO_GRAPH_NAME,
325+
namespace: DEMO_NAMESPACES,
326+
labelMatcher: DEMO_LABEL_MATCHER,
327+
routingUrl,
328+
});
329+
330+
if (createGraphResponse.error) {
331+
federatedGraphSpinner.fail(createGraphResponse.error.message);
332+
333+
await waitForKeyPress(
334+
{
335+
r: retryFn,
336+
R: retryFn,
337+
},
338+
'Hit [r] to refresh. CTRL+C to quit',
339+
);
340+
return;
341+
}
342+
343+
federatedGraphSpinner.succeed(`Federated graph ${pc.bold('demo')} succesfully created.`);
344+
}
345+
346+
async function handleStep2(
347+
opts: BaseCommandOptions,
348+
{
349+
onboarding,
350+
userInfo,
351+
}: {
352+
onboarding: {
353+
finishedAt?: string;
354+
};
355+
userInfo: UserInfo;
356+
},
357+
) {
358+
const graphData = await handleGetFederatedGraphResponse(opts.client, {
359+
onboarding,
360+
userInfo,
361+
});
362+
363+
const graph = graphData?.graph;
364+
const subgraphs = graphData?.subgraphs ?? [];
365+
if (graph) {
366+
const cleanupFn = async () =>
367+
await cleanupFederatedGraph(opts.client, {
368+
graphData: { graph, subgraphs },
369+
userInfo,
370+
});
371+
await waitForKeyPress(
372+
{
373+
Enter: () => undefined,
374+
d: cleanupFn,
375+
D: cleanupFn,
376+
},
377+
'Hit [ENTER] to continue or [d] to delete the federated graph and its subgraphs to start over. CTRL+C to quit.',
378+
);
379+
return;
380+
}
381+
382+
await handleCreateFederatedGraphResponse(opts.client, {
383+
onboarding,
384+
userInfo,
385+
});
386+
}
387+
92388
async function checkExistingOnboarding(client: BaseCommandOptions['client']) {
93389
const { response, finishedAt, enabled } = await client.platform.getOnboarding(
94390
{},
@@ -148,7 +444,7 @@ async function handleGetOnboardingResponse(client: BaseCommandOptions['client'],
148444
}
149445

150446
async function handleStep1(opts: BaseCommandOptions, userInfo: UserInfo) {
151-
await handleGetOnboardingResponse(opts.client, userInfo);
447+
return await handleGetOnboardingResponse(opts.client, userInfo);
152448
}
153449

154450
async function fetchUserInfo(client: BaseCommandOptions['client']) {
@@ -267,6 +563,12 @@ export default function (opts: BaseCommandOptions) {
267563
const userInfo = await getUserInfo(opts.client);
268564
resetScreen(userInfo);
269565

270-
await handleStep1(opts, userInfo);
566+
const onboardingCheck = await handleStep1(opts, userInfo);
567+
568+
if (!onboardingCheck) {
569+
return;
570+
}
571+
572+
await handleStep2(opts, { onboarding: onboardingCheck, userInfo });
271573
};
272574
}

0 commit comments

Comments
 (0)