Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a8cc87c
feat: add support for config splitting
wilsonrivera May 26, 2026
4b3f6de
chore: update documentation
wilsonrivera May 26, 2026
3bea131
chore: do not create directory when not needed
wilsonrivera May 26, 2026
4bd7432
chore: remove duplicated message
wilsonrivera May 26, 2026
e32f726
chore: write `mapper.json` file
wilsonrivera May 26, 2026
63a6430
chore: add tests for `router compose` and update documentation
wilsonrivera May 26, 2026
e53e6bd
chore: fix tests
wilsonrivera May 26, 2026
861c514
chore: add more tests
wilsonrivera May 27, 2026
798d37a
chore: update test labels
wilsonrivera May 27, 2026
bd18e87
Merge branch 'main' into wilson/eng-9577-cli-wgc-federated-graph-fetc…
wilsonrivera May 27, 2026
d4bcac2
Merge branch 'main' into wilson/eng-9577-cli-wgc-federated-graph-fetc…
wilsonrivera May 28, 2026
f976055
chore: address comments
wilsonrivera May 28, 2026
f7d7a80
Merge branch 'main' into wilson/eng-9577-cli-wgc-federated-graph-fetc…
wilsonrivera May 29, 2026
515e109
chore: update wording
wilsonrivera May 29, 2026
e0f4778
Merge branch 'main' into wilson/eng-9577-cli-wgc-federated-graph-fetc…
wilsonrivera Jun 2, 2026
4c80f78
Merge branch 'main' into wilson/eng-9577-cli-wgc-federated-graph-fetc…
wilsonrivera Jun 8, 2026
c02f2d5
Merge branch 'main' into wilson/eng-9577-cli-wgc-federated-graph-fetc…
wilsonrivera Jun 15, 2026
452b33e
chore: refactor router compose
Aenimus Jun 17, 2026
bf0f425
chore: refactor dir creation
Aenimus Jun 17, 2026
c6dcf88
chore: import formatting
Aenimus Jun 17, 2026
b3b63ec
chore: import formatting (again)
Aenimus Jun 17, 2026
63d8686
Merge branch 'main' into wilson/eng-9577-cli-wgc-federated-graph-fetc…
Aenimus Jun 17, 2026
c7f1a41
chore: linting
wilsonrivera Jun 17, 2026
56e5565
chore: fix tests
wilsonrivera Jun 17, 2026
5df9890
chore: add uniqueness to tests
wilsonrivera Jun 17, 2026
e04430b
chore: create directory recursively
wilsonrivera Jun 17, 2026
2331489
chore: fix dir creation
wilsonrivera Jun 17, 2026
6a001d1
chore: fix e2e tests
wilsonrivera Jun 17, 2026
45ac19b
chore: restore non-recursive directory creation for split config
wilsonrivera Jun 17, 2026
e9ca493
chore: improvements
wilsonrivera Jun 17, 2026
0b5d51a
Merge branch 'main' into wilson/eng-9577-cli-wgc-federated-graph-fetc…
wilsonrivera Jun 17, 2026
0d559aa
chore: validate that the output directory is empty
wilsonrivera Jun 18, 2026
00b2544
Merge branch 'main' into wilson/eng-9577-cli-wgc-federated-graph-fetc…
wilsonrivera Jun 18, 2026
3f4ff60
Merge branch 'main' into wilson/eng-9577-cli-wgc-federated-graph-fetc…
wilsonrivera Jun 25, 2026
e0bbd16
chore: fix missing parameter
wilsonrivera Jun 25, 2026
c552dda
Merge branch 'main' into wilson/eng-9577-cli-wgc-federated-graph-fetc…
wilsonrivera Jul 1, 2026
d2c95f3
Merge branch 'main' into wilson/eng-9577-cli-wgc-federated-graph-fetc…
wilsonrivera Jul 1, 2026
2a08858
chore: update base path and add some comments
wilsonrivera Jul 1, 2026
69121dc
chore: add normalization util for feature flag name
wilsonrivera Jul 1, 2026
470cea1
chore: reject feature flags with invalid names
wilsonrivera Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cli/e2e/smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('Bun CLI', () => {

expect(exitCode, `exited with non-zero:\nstdout:\n${stdout.toString()}\n\nstderr:\n${stderr.toString()}`).toBe(0);

expect(stdout.toString()).toContain('Router config successfully written');
expect(stdout.toString()).toContain('Router execution config successfully written to');
});
});

Expand All @@ -52,6 +52,6 @@ describe('Node CLI', () => {

expect(exitCode, `exited with non-zero:\nstdout:\n${stdout.toString()}\n\nstderr:\n${stderr.toString()}`).toBe(0);

expect(stdout.toString()).toContain('Router config successfully written');
expect(stdout.toString()).toContain('Router execution config successfully written to');
});
});
3 changes: 3 additions & 0 deletions cli/src/commands/auth/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ export interface KeycloakTokenResponse {
refresh_expires_in: number;
}

export type GraphTokenFeature = 'split-config-loading';

export interface GraphToken {
iss?: string;
iat?: number;
federated_graph_id: string;
organization_id: string;
features?: GraphTokenFeature[];
}

export interface DecodedAccessToken {
Expand Down
71 changes: 54 additions & 17 deletions cli/src/commands/graph/federated-graph/commands/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { existsSync } from 'node:fs';
import { mkdir, readdir, writeFile } from 'node:fs/promises';
import { Command } from 'commander';
import yaml from 'js-yaml';
import { join, resolve } from 'pathe';
import pc from 'picocolors';
import { BaseCommandOptions } from '../../../../core/types/types.js';
import { fetchRouterConfig, getFederatedGraphSchemas, getSubgraphSDL, getSubgraphsOfFedGraph } from '../utils.js';
import { getFederatedGraphSchemas, getSubgraphSDL, getSubgraphsOfFedGraph } from '../utils.js';
import { featureFlagsDir, fetchRouterConfig, writeFeatureFlagConfigToFile } from '../../../router/utils.js';

const cosmoConfigFile = 'cosmoConfig.json';
const cosmoMapperFile = 'cosmo-mapper.json';
const cosmoSchemaFile = 'cosmoSchema.graphql';
const cosmoClientSchemaFile = 'cosmoClientSchema.graphql';
const cosmoCompositionFile = 'cosmo-composition.yaml';
const roverCompositionFile = 'rover-composition.yaml';
const apolloSchemaFile = 'apolloSchema.graphql';
const apolloScriptFile = 'apollo.sh';

export default (opts: BaseCommandOptions) => {
const cmd = new Command('fetch');
Expand All @@ -29,32 +40,59 @@ export default (opts: BaseCommandOptions) => {
namespace: options.namespace,
});

const basePath = resolve(options.out, `${name}${options.namespace ? `-${options.namespace}` : ''}`);
let basePath = options.out ? resolve(options.out) : resolve();
basePath = join(basePath, `${name}-${options.namespace || 'default'}`);
if (!existsSync(basePath)) {
await mkdir(basePath, { recursive: true });
}

const entries = await readdir(basePath);
if (entries.length > 0) {
console.log(pc.red(`Output directory "${basePath}" is not empty. Please provide an empty directory path.`));
process.exitCode = 1;
return;
}

const superGraphPath = join(basePath, '/supergraph/');
const subgraphPath = join(basePath, '/subgraphs/');
const scriptsPath = join(basePath, '/scripts/');

if (!existsSync(superGraphPath)) {
mkdirSync(superGraphPath, { recursive: true });
await mkdir(superGraphPath, { recursive: true });
}
if (!existsSync(subgraphPath)) {
mkdirSync(subgraphPath, { recursive: true });
await mkdir(subgraphPath, { recursive: true });
}
if (!existsSync(scriptsPath) && options.apolloCompatibility) {
mkdirSync(scriptsPath, { recursive: true });
await mkdir(scriptsPath, { recursive: true });
}

const routerConfig = await fetchRouterConfig({
client: opts.client,
name,
namespace: options.namespace,
});
writeFileSync(join(superGraphPath, `cosmoConfig.json`), routerConfig);
await writeFile(join(superGraphPath, cosmoConfigFile), routerConfig.routerConfig);
if (routerConfig.mapper) {
Comment thread
Aenimus marked this conversation as resolved.
// The mapper file is only available when `split-config-loading` is enabled
await writeFile(join(basePath, cosmoMapperFile), JSON.stringify(routerConfig.mapper));
}

writeFileSync(join(superGraphPath, `cosmoSchema.graphql`), fedGraphSchemas.sdl);
if (routerConfig.featureFlags?.size) {
// Same as the mapper, feature flags are only available when `split-config-loading` is enabled
const featureFlagsPath = join(basePath, featureFlagsDir);
if (!existsSync(featureFlagsPath)) {
await mkdir(featureFlagsPath);
}

for (const [featureFlagName, featureFlagConfig] of routerConfig.featureFlags) {
await writeFeatureFlagConfigToFile(featureFlagsPath, featureFlagName, featureFlagConfig);
}
}

await writeFile(join(superGraphPath, cosmoSchemaFile), fedGraphSchemas.sdl);
if (fedGraphSchemas.clientSchema) {
writeFileSync(join(superGraphPath, `cosmoClientSchema.graphql`), fedGraphSchemas.clientSchema);
await writeFile(join(superGraphPath, cosmoClientSchemaFile), fedGraphSchemas.clientSchema);
}

const subgraphs = await getSubgraphsOfFedGraph({ client: opts.client, name, namespace: options.namespace });
Expand Down Expand Up @@ -125,14 +163,14 @@ export default (opts: BaseCommandOptions) => {
};
}
}
writeFileSync(filePath, subgraphSDL);
await writeFile(filePath, subgraphSDL);
}

const cosmoCompositionConfig = yaml.dump({
version: 1,
subgraphs: cosmoSubgraphsConfig,
});
writeFileSync(join(basePath, `cosmo-composition.yaml`), cosmoCompositionConfig);
await writeFile(join(basePath, cosmoCompositionFile), cosmoCompositionConfig);

if (options.apolloCompatibility) {
const roverCompositionConfig = yaml.dump({
Expand All @@ -156,15 +194,14 @@ export default (opts: BaseCommandOptions) => {
},
},
});
writeFileSync(join(basePath, `rover-composition.yaml`), roverCompositionConfig);

const absRoverCompositionFile = join(basePath, roverCompositionFile);
await writeFile(absRoverCompositionFile, roverCompositionConfig);

const apolloScript = `npm install -g @apollo/rover
rover supergraph compose --config '${join(basePath, `rover-composition.yaml`)}' --output '${join(
superGraphPath,
'apolloSchema.graphql',
)}'
rover supergraph compose --config '${absRoverCompositionFile}' --output '${join(superGraphPath, apolloSchemaFile)}'
`;
writeFileSync(join(scriptsPath, `apollo.sh`), apolloScript);
await writeFile(join(scriptsPath, apolloScriptFile), apolloScript);
}

console.log(
Expand Down
65 changes: 1 addition & 64 deletions cli/src/commands/graph/federated-graph/utils.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,8 @@
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
import { Subgraph as ProtoSubgraph } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb';
import { program } from 'commander';
import jwtDecode from 'jwt-decode';
import pc from 'picocolors';
import { Client } from '../../../core/client/client.js';
import { config, getBaseHeaders } from '../../../core/config.js';
import { GraphToken } from '../../auth/utils.js';

export const fetchRouterConfig = async ({
client,
name,
namespace,
}: {
client: Client;
name: string;
namespace?: string;
}) => {
const resp = await client.platform.generateRouterToken(
{
fedGraphName: name,
namespace,
},
{
headers: getBaseHeaders(),
},
);

if (resp.response?.code !== EnumStatusCode.OK) {
throw new Error(
`${pc.red(`Could not fetch the router config for the graph ${pc.bold(name)}`)} \n${pc.red(
pc.bold(resp.response?.details || ''),
)}`,
);
}

let decoded: GraphToken;

try {
decoded = jwtDecode<GraphToken>(resp.token);
} catch {
program.error('Could not fetch the router config. Please try again');
}

const requestBody = JSON.stringify({
Version: '',
});

const headers = new Headers();
headers.append('Content-Type', 'application/json; charset=UTF-8');
headers.append('Authorization', 'Bearer ' + resp.token);
headers.append('Accept-Encoding', 'gzip');

const url = new URL(
`/${decoded.organization_id}/${decoded.federated_graph_id}/routerconfigs/latest.json`,
config.cdnURL,
);

const response = await fetch(url, {
method: 'POST',
headers,
body: requestBody,
});

const routerConfig = await response.text();

return routerConfig;
};
import { getBaseHeaders } from '../../../core/config.js';

Comment thread
Aenimus marked this conversation as resolved.
export interface Subgraph {
name: string;
Expand Down
11 changes: 4 additions & 7 deletions cli/src/commands/mcp/tools/federated-graph-tools.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
import { z } from 'zod';
import { getBaseHeaders } from '../../../core/config.js';
import {
fetchRouterConfig,
getFederatedGraphSchemas,
getSubgraphsOfFedGraph,
} from '../../graph/federated-graph/utils.js';
import { getFederatedGraphSchemas, getSubgraphsOfFedGraph } from '../../graph/federated-graph/utils.js';
import { fetchRouterConfig } from '../../router/utils.js';
import { ToolContext } from './types.js';

export const registerFederatedGraphTools = ({ server, opts }: ToolContext) => {
Expand Down Expand Up @@ -121,14 +118,14 @@ export const registerFederatedGraphTools = ({ server, opts }: ToolContext) => {
},
async ({ name, namespace }) => {
try {
const routerConfig = await fetchRouterConfig({
const result = await fetchRouterConfig({
client: opts.client,
name,
namespace,
});

return {
content: [{ type: 'text', text: routerConfig }],
content: [{ type: 'text', text: result.routerConfig }],
};
} catch (e: any) {
throw new Error(`Failed to fetch router config: ${e.message}`);
Expand Down
Loading
Loading