Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
48 changes: 27 additions & 21 deletions cli/src/commands/feature-subgraph/commands/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { websocketSubprotocolDescription } from '../../../constants.js';
import { BaseCommandOptions } from '../../../core/types/types.js';
import { handleCompositionResult } from '../../../handle-composition-result.js';
import { validateSubscriptionProtocols } from '../../../utils.js';
import { getBaseHeaders } from '../../../core/config.js';

export default (opts: BaseCommandOptions) => {
const command = new Command('publish');
Expand Down Expand Up @@ -94,27 +95,32 @@ export default (opts: BaseCommandOptions) => {
spinner.start();
}

const resp = await opts.client.platform.publishFederatedSubgraph({
baseSubgraphName: options.subgraph,
disableResolvabilityValidation: options.disableResolvabilityValidation,
isFeatureSubgraph: true,
labels: [],
name,
namespace: options.namespace,
// Publish schema only
// Optional when feature subgraph does not exist yet
routingUrl: options.routingUrl,
schema,
subscriptionProtocol: options.subscriptionProtocol
? parseGraphQLSubscriptionProtocol(options.subscriptionProtocol)
: undefined,
subscriptionUrl: options.subscriptionUrl,
websocketSubprotocol: options.websocketSubprotocol
? parseGraphQLWebsocketSubprotocol(options.websocketSubprotocol)
: undefined,
// passing Standard type to the backend, because the users have to use the 'wgc router plugin publish' command to publish the plugin
type: SubgraphType.STANDARD,
});
const resp = await opts.client.platform.publishFederatedSubgraph(
{
baseSubgraphName: options.subgraph,
disableResolvabilityValidation: options.disableResolvabilityValidation,
isFeatureSubgraph: true,
labels: [],
name,
namespace: options.namespace,
// Publish schema only
// Optional when feature subgraph does not exist yet
routingUrl: options.routingUrl,
schema,
subscriptionProtocol: options.subscriptionProtocol
? parseGraphQLSubscriptionProtocol(options.subscriptionProtocol)
: undefined,
subscriptionUrl: options.subscriptionUrl,
websocketSubprotocol: options.websocketSubprotocol
? parseGraphQLWebsocketSubprotocol(options.websocketSubprotocol)
: undefined,
// passing Standard type to the backend, because the users have to use the 'wgc router plugin publish' command to publish the plugin
type: SubgraphType.STANDARD,
},
{
headers: getBaseHeaders(),
},
);

try {
handleCompositionResult({
Expand Down
73 changes: 73 additions & 0 deletions cli/src/commands/grpc-service/commands/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
import { SubgraphType } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb';
import { splitLabel } from '@wundergraph/cosmo-shared';
import { Command, program } from 'commander';
import ora from 'ora';
import { resolve } from 'pathe';
import pc from 'picocolors';
import { getBaseHeaders } from '../../../core/config.js';
import { BaseCommandOptions } from '../../../core/types/types.js';

export default (opts: BaseCommandOptions) => {
const command = new Command('create');
command.description('Creates a federated grpc subgraph on the control plane.');
command.argument(
'<name>',
'The name of the grpc subgraph to create. It is used to uniquely identify your grpc subgraph.',
);
command.option('-n, --namespace [string]', 'The namespace of the grpc subgraph.');
command.requiredOption(
'-r, --routing-url <url>',
'The routing URL of your subgraph. This is the url at which the subgraph will be accessible.',
);
command.option(
'--label [labels...]',
'The labels to apply to the subgraph. The labels are passed in the format <key>=<value> <key>=<value>.',
);
command.option('--readme <path-to-readme>', 'The markdown file which describes the subgraph.');

command.action(async (name, options) => {
let readmeFile;
if (options.readme) {
readmeFile = resolve(options.readme);
if (!existsSync(readmeFile)) {
program.error(
pc.red(
pc.bold(`The readme file '${pc.bold(readmeFile)}' does not exist. Please check the path and try again.`),
),
);
}
}

const spinner = ora('GRPC Subgraph is being created...').start();
const resp = await opts.client.platform.createFederatedSubgraph(
{
name,
namespace: options.namespace,
labels: options.label ? options.label.map((label: string) => splitLabel(label)) : [],
routingUrl: options.routingUrl,
readme: readmeFile ? await readFile(readmeFile, 'utf8') : undefined,
type: SubgraphType.GRPC_SERVICE,
},
{
headers: getBaseHeaders(),
},
);

if (resp.response?.code === EnumStatusCode.OK) {
spinner.succeed('GRPC subgraph was created successfully.');
} else {
spinner.fail('Failed to create grpc subgraph.');
if (resp.response?.details) {
console.log(pc.red(pc.bold(resp.response?.details)));
}
process.exitCode = 1;
// eslint-disable-next-line no-useless-return
return;
}
});

return command;
};
156 changes: 156 additions & 0 deletions cli/src/commands/grpc-service/commands/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Command } from 'commander';
import pc from 'picocolors';
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
import inquirer from 'inquirer';
import Table from 'cli-table3';
import ora from 'ora';
import { BaseCommandOptions } from '../../../core/types/types.js';
import { getBaseHeaders } from '../../../core/config.js';

export default (opts: BaseCommandOptions) => {
const command = new Command('delete');
command.description('Deletes a gRPC subgraph on the control plane.');
command.argument('<name>', 'The name of the gRPC subgraph to delete.');
command.option('-n, --namespace [string]', 'The namespace of the gRPC subgraph.');
command.option('-f, --force', 'Flag to force the deletion (skip confirmation).');
command.option('--suppress-warnings', 'This flag suppresses any warnings produced by composition.');
command.action(async (name, options) => {
if (!options.force) {
const deletionConfirmed = await inquirer.prompt({
name: 'confirmDeletion',
type: 'confirm',
message: `Are you sure you want to delete the gRPC subgraph "${name}"?`,
});
if (!deletionConfirmed.confirmDeletion) {
process.exitCode = 1;
return;
}
}

const spinner = ora(`The gRPC subgraph "${name}" is being deleted...`).start();

const resp = await opts.client.platform.deleteFederatedSubgraph(
{
subgraphName: name,
namespace: options.namespace,
},
{
headers: getBaseHeaders(),
},
);

switch (resp.response?.code) {
case EnumStatusCode.OK: {
spinner.succeed(`The gRPC subgraph "${name}" was deleted successfully.`);
if (resp.proposalMatchMessage) {
console.log(pc.yellow(`Warning: Proposal match failed`));
console.log(pc.yellow(resp.proposalMatchMessage));
}
break;
}
case EnumStatusCode.ERR_SCHEMA_MISMATCH_WITH_APPROVED_PROPOSAL: {
spinner.fail(`Failed to delete gRPC subgraph "${name}".`);
console.log(pc.red(`Error: Proposal match failed`));
console.log(pc.red(resp.proposalMatchMessage));
break;
}
case EnumStatusCode.ERR_SUBGRAPH_COMPOSITION_FAILED: {
spinner.fail(`The gRPC subgraph "${name}" was deleted but with composition errors.`);

const compositionErrorsTable = new Table({
head: [
pc.bold(pc.white('FEDERATED_GRAPH_NAME')),
pc.bold(pc.white('NAMESPACE')),
pc.bold(pc.white('FEATURE_FLAG')),
pc.bold(pc.white('ERROR_MESSAGE')),
],
colWidths: [30, 30, 30, 120],
wordWrap: true,
});

console.log(
pc.red(
`There were composition errors when composing at least one federated graph related to the` +
` gRPC subgraph "${name}".\nThe router will continue to work with the latest valid schema.` +
`\n${pc.bold('Please check the errors below:')}`,
),
);
for (const compositionError of resp.compositionErrors) {
compositionErrorsTable.push([
compositionError.federatedGraphName,
compositionError.namespace,
compositionError.featureFlag || '-',
compositionError.message,
]);
}
// Don't exit here with 1 because the change was still applied
console.log(compositionErrorsTable.toString());

break;
}
case EnumStatusCode.ERR_DEPLOYMENT_FAILED: {
spinner.warn(
`The gRPC subgraph "${name}" was deleted, but the updated composition could not be deployed.` +
`\nThis means the updated composition is not accessible to the router.` +
`\n${pc.bold('Please check the errors below:')}`,
);

const deploymentErrorsTable = new Table({
head: [
pc.bold(pc.white('FEDERATED_GRAPH_NAME')),
pc.bold(pc.white('NAMESPACE')),
pc.bold(pc.white('ERROR_MESSAGE')),
],
colWidths: [30, 30, 120],
wordWrap: true,
});

for (const deploymentError of resp.deploymentErrors) {
deploymentErrorsTable.push([
deploymentError.federatedGraphName,
deploymentError.namespace,
deploymentError.message,
]);
}
// Don't exit here with 1 because the change was still applied
console.log(deploymentErrorsTable.toString());

break;
}
default: {
spinner.fail(`Failed to delete the gRPC subgraph "${name}".`);
if (resp.response?.details) {
console.log(pc.red(pc.bold(resp.response?.details)));
}
process.exitCode = 1;
return;
}
}

if (!options.suppressWarnings && resp.compositionWarnings.length > 0) {
const compositionWarningsTable = new Table({
head: [
pc.bold(pc.white('FEDERATED_GRAPH_NAME')),
pc.bold(pc.white('NAMESPACE')),
pc.bold(pc.white('FEATURE_FLAG')),
pc.bold(pc.white('WARNING_MESSAGE')),
],
colWidths: [30, 30, 30, 120],
wordWrap: true,
});

console.log(pc.yellow(`The following warnings were produced while composing the federated graph:`));
for (const compositionWarning of resp.compositionWarnings) {
compositionWarningsTable.push([
compositionWarning.federatedGraphName,
compositionWarning.namespace,
compositionWarning.featureFlag || '-',
compositionWarning.message,
]);
}
console.log(compositionWarningsTable.toString());
}
});

return command;
};
Loading
Loading