Skip to content

Commit 428c7ac

Browse files
committed
feat: handle federated graph creation
1 parent 9a66866 commit 428c7ac

File tree

3 files changed

+326
-4
lines changed

3 files changed

+326
-4
lines changed

cli/src/commands/demo/api.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
2+
import type { FederatedGraph, Subgraph } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb';
23
import type { BaseCommandOptions } from '../../core/types/types.js';
34
import { getBaseHeaders } from '../../core/config.js';
45

@@ -65,3 +66,136 @@ export async function checkExistingOnboarding(client: BaseCommandOptions['client
6566
status: 'ok',
6667
} as const;
6768
}
69+
70+
/**
71+
* Retrieves federated graph by [name] *demo*. Missing federated graph
72+
* is a valid state.
73+
*/
74+
export async function fetchFederatedGraphByName(
75+
client: BaseCommandOptions['client'],
76+
{ name, namespace }: { name: string; namespace: string },
77+
) {
78+
const { response, graph, subgraphs } = await client.platform.getFederatedGraphByName(
79+
{
80+
name,
81+
namespace,
82+
},
83+
{
84+
headers: getBaseHeaders(),
85+
},
86+
);
87+
88+
switch (response?.code) {
89+
case EnumStatusCode.OK: {
90+
return { data: { graph, subgraphs }, error: null };
91+
}
92+
case EnumStatusCode.ERR_NOT_FOUND: {
93+
return { data: null, error: null };
94+
}
95+
default: {
96+
return {
97+
data: null,
98+
error: new Error(response?.details ?? 'An unknown error occured'),
99+
};
100+
}
101+
}
102+
}
103+
104+
/**
105+
* Cleans up the federated graph by [name] _demo_ and its related
106+
* subgraphs.
107+
*/
108+
export async function cleanUpFederatedGraph(
109+
client: BaseCommandOptions['client'],
110+
graphData: {
111+
graph: FederatedGraph;
112+
subgraphs: Subgraph[];
113+
},
114+
) {
115+
const subgraphDeleteResponses = await Promise.all(
116+
graphData.subgraphs.map(({ name, namespace }) =>
117+
client.platform.deleteFederatedSubgraph(
118+
{
119+
namespace,
120+
subgraphName: name,
121+
disableResolvabilityValidation: false,
122+
},
123+
{
124+
headers: getBaseHeaders(),
125+
},
126+
),
127+
),
128+
);
129+
130+
const failedSubgraphDeleteResponses = subgraphDeleteResponses.filter(
131+
({ response }) => response?.code !== EnumStatusCode.OK,
132+
);
133+
134+
if (failedSubgraphDeleteResponses.length > 0) {
135+
return {
136+
error: new Error(
137+
failedSubgraphDeleteResponses.map(({ response }) => response?.details ?? 'Unknown error occurred.').join('. '),
138+
),
139+
};
140+
}
141+
142+
const federatedGraphDeleteResponse = await client.platform.deleteFederatedGraph(
143+
{
144+
name: graphData.graph.name,
145+
namespace: graphData.graph.namespace,
146+
},
147+
{
148+
headers: getBaseHeaders(),
149+
},
150+
);
151+
152+
switch (federatedGraphDeleteResponse.response?.code) {
153+
case EnumStatusCode.OK: {
154+
return {
155+
error: null,
156+
};
157+
}
158+
default: {
159+
return {
160+
error: new Error(federatedGraphDeleteResponse.response?.details ?? 'Unknown error occurred.'),
161+
};
162+
}
163+
}
164+
}
165+
166+
/**
167+
* Creates federated graph using default [name] and [namespace], with pre-defined
168+
* [labelMatcher] which identify the graph as _demo_.
169+
*/
170+
export async function createFederatedGraph(
171+
client: BaseCommandOptions['client'],
172+
options: {
173+
name: string;
174+
namespace: string;
175+
labelMatcher: string;
176+
routingUrl: URL;
177+
},
178+
) {
179+
const createFedGraphResponse = await client.platform.createFederatedGraph(
180+
{
181+
name: options.name,
182+
namespace: options.namespace,
183+
routingUrl: options.routingUrl.toString(),
184+
labelMatchers: [options.labelMatcher],
185+
},
186+
{
187+
headers: getBaseHeaders(),
188+
},
189+
);
190+
191+
switch (createFedGraphResponse.response?.code) {
192+
case EnumStatusCode.OK: {
193+
return { error: null };
194+
}
195+
default: {
196+
return {
197+
error: new Error(createFedGraphResponse.response?.details ?? 'An unknown error occured'),
198+
};
199+
}
200+
}
201+
}

cli/src/commands/demo/command.ts

Lines changed: 188 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import pc from 'picocolors';
22
import ora from 'ora';
33
import { program } from 'commander';
4-
import { BaseCommandOptions } from '../../core/types/types.js';
4+
import type { FederatedGraph, Subgraph, WhoAmIResponse } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb';
55
import { config } from '../../core/config.js';
6+
import { BaseCommandOptions } from '../../core/types/types.js';
67
import { waitForKeyPress, rainbow } from '../../utils.js';
7-
import { fetchUserInfo, checkExistingOnboarding } from './api.js';
88
import type { UserInfo } from './types.js';
9+
import {
10+
cleanUpFederatedGraph,
11+
createFederatedGraph,
12+
fetchFederatedGraphByName,
13+
fetchUserInfo,
14+
checkExistingOnboarding,
15+
} from './api.js';
916
import {
1017
checkDockerReadiness,
1118
clearScreen,
@@ -23,6 +30,177 @@ function printHello() {
2330
console.log('This command will guide you through the inital setup to create your first federated graph.');
2431
}
2532

33+
async function handleGetFederatedGraphResponse(
34+
client: BaseCommandOptions['client'],
35+
{
36+
onboarding,
37+
userInfo,
38+
}: {
39+
onboarding: {
40+
finishedAt?: string;
41+
};
42+
userInfo: UserInfo;
43+
},
44+
) {
45+
function retryFn() {
46+
resetScreen(userInfo);
47+
return handleGetFederatedGraphResponse(client, {
48+
onboarding,
49+
userInfo,
50+
});
51+
}
52+
53+
const spinner = ora().start();
54+
const getFederatedGraphResponse = await fetchFederatedGraphByName(client, {
55+
name: config.demoGraphName,
56+
namespace: config.demoNamespace,
57+
});
58+
59+
if (getFederatedGraphResponse.error) {
60+
spinner.fail(`Failed to retrieve graph information ${getFederatedGraphResponse.error}`);
61+
await waitForKeyPress(
62+
{
63+
r: retryFn,
64+
R: retryFn,
65+
},
66+
'Hit [r] to refresh. CTRL+C to quit',
67+
);
68+
return;
69+
}
70+
71+
if (getFederatedGraphResponse.data?.graph) {
72+
spinner.succeed(`Federated graph ${pc.bold(getFederatedGraphResponse.data?.graph?.name)} exists.`);
73+
} else {
74+
spinner.stop();
75+
}
76+
77+
return getFederatedGraphResponse.data;
78+
}
79+
80+
async function cleanupFederatedGraph(
81+
client: BaseCommandOptions['client'],
82+
{
83+
graphData,
84+
userInfo,
85+
}: {
86+
graphData: {
87+
graph: FederatedGraph;
88+
subgraphs: Subgraph[];
89+
};
90+
userInfo: UserInfo;
91+
},
92+
) {
93+
function retryFn() {
94+
resetScreen(userInfo);
95+
cleanupFederatedGraph(client, { graphData, userInfo });
96+
}
97+
98+
const spinner = ora().start(`Removing federated graph ${pc.bold(graphData.graph.name)}…`);
99+
const deleteResponse = await cleanUpFederatedGraph(client, graphData);
100+
101+
if (deleteResponse.error) {
102+
spinner.fail(`Removing federated graph ${graphData.graph.name} failed.`);
103+
console.error(deleteResponse.error.message);
104+
105+
await waitForKeyPress(
106+
{
107+
Enter: () => undefined,
108+
r: retryFn,
109+
R: retryFn,
110+
},
111+
`Failed to delete the federated graph ${pc.bold(graphData.graph.name)}. [ENTER] to continue, [r] to retry. CTRL+C to quit.`,
112+
);
113+
}
114+
115+
spinner.succeed(`Federated graph ${pc.bold(graphData.graph.name)} removed.`);
116+
}
117+
118+
async function handleCreateFederatedGraphResponse(
119+
client: BaseCommandOptions['client'],
120+
{
121+
onboarding,
122+
userInfo,
123+
}: {
124+
onboarding: {
125+
finishedAt?: string;
126+
};
127+
userInfo: UserInfo;
128+
},
129+
) {
130+
function retryFn() {
131+
resetScreen(userInfo);
132+
handleCreateFederatedGraphResponse(client, { onboarding, userInfo });
133+
}
134+
135+
const routingUrl = new URL('graphql', 'http://localhost');
136+
routingUrl.port = String(config.demoRouterPort);
137+
138+
const federatedGraphSpinner = ora().start();
139+
const createGraphResponse = await createFederatedGraph(client, {
140+
name: config.demoGraphName,
141+
namespace: config.demoNamespace,
142+
labelMatcher: config.demoLabelMatcher,
143+
routingUrl,
144+
});
145+
146+
if (createGraphResponse.error) {
147+
federatedGraphSpinner.fail(createGraphResponse.error.message);
148+
149+
await waitForKeyPress(
150+
{
151+
r: retryFn,
152+
R: retryFn,
153+
},
154+
'Hit [r] to refresh. CTRL+C to quit',
155+
);
156+
return;
157+
}
158+
159+
federatedGraphSpinner.succeed(`Federated graph ${pc.bold('demo')} succesfully created.`);
160+
}
161+
162+
async function handleStep2(
163+
opts: BaseCommandOptions,
164+
{
165+
onboarding,
166+
userInfo,
167+
}: {
168+
onboarding: {
169+
finishedAt?: string;
170+
};
171+
userInfo: UserInfo;
172+
},
173+
) {
174+
const graphData = await handleGetFederatedGraphResponse(opts.client, {
175+
onboarding,
176+
userInfo,
177+
});
178+
179+
const graph = graphData?.graph;
180+
const subgraphs = graphData?.subgraphs ?? [];
181+
if (graph) {
182+
const cleanupFn = async () =>
183+
await cleanupFederatedGraph(opts.client, {
184+
graphData: { graph, subgraphs },
185+
userInfo,
186+
});
187+
await waitForKeyPress(
188+
{
189+
Enter: () => undefined,
190+
d: cleanupFn,
191+
D: cleanupFn,
192+
},
193+
'Hit [ENTER] to continue or [d] to delete the federated graph and its subgraphs to start over. CTRL+C to quit.',
194+
);
195+
return;
196+
}
197+
198+
await handleCreateFederatedGraphResponse(opts.client, {
199+
onboarding,
200+
userInfo,
201+
});
202+
}
203+
26204
async function handleGetOnboardingResponse(client: BaseCommandOptions['client'], userInfo: UserInfo) {
27205
const onboardingCheck = await checkExistingOnboarding(client);
28206

@@ -53,7 +231,7 @@ async function handleGetOnboardingResponse(client: BaseCommandOptions['client'],
53231
}
54232

55233
async function handleStep1(opts: BaseCommandOptions, userInfo: UserInfo) {
56-
await handleGetOnboardingResponse(opts.client, userInfo);
234+
return await handleGetOnboardingResponse(opts.client, userInfo);
57235
}
58236

59237
async function getUserInfo(client: BaseCommandOptions['client']) {
@@ -90,6 +268,12 @@ export default function (opts: BaseCommandOptions) {
90268

91269
resetScreen(userInfo);
92270

93-
await handleStep1(opts, userInfo);
271+
const onboardingCheck = await handleStep1(opts, userInfo);
272+
273+
if (!onboardingCheck) {
274+
return;
275+
}
276+
277+
await handleStep2(opts, { onboarding: onboardingCheck, userInfo });
94278
};
95279
}

cli/src/core/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,13 @@ export const config = {
3636
checkCommitSha: process.env.COSMO_VCS_COMMIT || '',
3737
checkBranch: process.env.COSMO_VCS_BRANCH || '',
3838
pluginRegistryURL: process.env.PLUGIN_REGISTRY_URL || 'cosmo-registry.wundergraph.com',
39+
demoLabelMatcher: 'graph=demo' as const,
40+
demoGraphName: 'demo' as const,
41+
demoNamespace: 'default' as const,
3942
demoOnboardingRepositoryName: 'wundergraph/cosmo-onboarding' as const,
4043
demoOnboardingRepositoryBranch: 'main' as const,
4144
dockerBuilderName: 'cosmo-builder' as const,
45+
demoRouterPort: 3002 as const,
4246
};
4347

4448
export const getBaseHeaders = (): HeadersInit => {

0 commit comments

Comments
 (0)