wgc
Version:
The official CLI tool to manage the GraphQL Federation Platform Cosmo
442 lines • 22.7 kB
JavaScript
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
import Table from 'cli-table3';
import { program } from 'commander';
import logSymbols from 'log-symbols';
import pc from 'picocolors';
import { config } from './core/config.js';
import { JsonCheckSchemaOutputBuilder } from './json-check-schema-output-builder.js';
// operationUsageStats is required — caller must guard with `if (response.operationUsageStats)` before calling
const handleTrafficCheck = (response, operationUsageStats, jsonBuilder, shouldOutputJson) => {
const { clientTrafficCheckSkipped, compositionErrors, lintErrors, graphPruneErrors, breakingChanges, composedSchemaBreakingChanges, } = response;
const { totalOperations, safeOperations, firstSeenAt, lastSeenAt } = operationUsageStats;
if (totalOperations === 0 && !clientTrafficCheckSkipped) {
// Composition errors are still considered failures, otherwise we can consider this a success
// because no operations were affected by the change
const success = compositionErrors.length === 0 && lintErrors.length === 0 && graphPruneErrors.length === 0;
const message = 'No operations were affected by this schema change.';
jsonBuilder.setTraffic(message);
if (!shouldOutputJson) {
console.log(message);
}
return { success, finalStatement: `This schema change didn't affect any operations from existing client traffic.` };
}
if (totalOperations === safeOperations && !clientTrafficCheckSkipped) {
// This is also a success because changes to these operations were marked as safe
const success = compositionErrors.length === 0 && lintErrors.length === 0 && graphPruneErrors.length === 0;
const message = `${totalOperations} operations were considered safe due to overrides.`;
jsonBuilder.setTraffic(message);
if (!shouldOutputJson) {
console.log(message);
}
return { success, finalStatement: `This schema change affected operations with safe overrides.` };
}
// Composition and breaking errors are considered failures because operations were affected
const success = breakingChanges.length === 0 &&
composedSchemaBreakingChanges.length === 0 &&
compositionErrors.length === 0 &&
lintErrors.length === 0 &&
graphPruneErrors.length === 0;
let finalStatement = '';
const totalBreakingChanges = breakingChanges.length + composedSchemaBreakingChanges.length;
if (breakingChanges.length > 0 || composedSchemaBreakingChanges.length > 0) {
jsonBuilder.addBreakingChanges(breakingChanges);
const warningMessage = [logSymbols.warning, ` Found ${pc.bold(totalBreakingChanges)} breaking changes.`];
const jsonMessage = [`Found ${totalBreakingChanges} breaking changes.`];
if (totalOperations > 0) {
warningMessage.push(`${pc.bold(totalOperations - safeOperations)} operations impacted.`);
jsonMessage.push(`${totalOperations - safeOperations} operations impacted.`);
}
if (safeOperations > 0) {
warningMessage.push(`In addition, ${safeOperations} operations marked safe due to overrides.`);
jsonMessage.push(`In addition, ${safeOperations} operations marked safe due to overrides.`);
}
if (!clientTrafficCheckSkipped) {
warningMessage.push(`\nFound client activity between ${pc.underline(new Date(firstSeenAt).toLocaleString())} and ${pc.underline(new Date(lastSeenAt).toLocaleString())}.`);
jsonMessage.push(`Found client activity between ${new Date(firstSeenAt).toLocaleString()} and ${new Date(lastSeenAt).toLocaleString()}.`);
jsonBuilder.setTraffic(jsonMessage.join(' '));
jsonBuilder.setOperationUsageStats(operationUsageStats);
}
if (!shouldOutputJson) {
console.log(warningMessage.join(''));
}
finalStatement = `This check has encountered ${pc.bold(`${totalBreakingChanges}`)} breaking changes${clientTrafficCheckSkipped ? `.` : ` that would break operations from existing client traffic.`}`;
}
return { success, finalStatement };
};
const handleSchemaChanges = (response, jsonBuilder, shouldOutputJson) => {
if (response.breakingChanges.length > 0) {
jsonBuilder.addBreakingChanges(response.breakingChanges);
}
if (response.nonBreakingChanges.length > 0) {
jsonBuilder.addNonBreakingChanges(response.nonBreakingChanges);
}
if (shouldOutputJson) {
return;
}
console.log('\nDetected the following subgraph schema changes:');
const changesTable = new Table({
head: [pc.bold(pc.white('CHANGE')), pc.bold(pc.white('TYPE')), pc.bold(pc.white('DESCRIPTION'))],
wordWrap: true,
});
for (const change of response.breakingChanges) {
changesTable.push([`${logSymbols.error} ${pc.red('BREAKING')}`, change.changeType, change.message]);
}
for (const change of response.nonBreakingChanges) {
changesTable.push([`${logSymbols.success} NON-BREAKING`, change.changeType, change.message]);
}
console.log(changesTable.toString());
};
const handleComposedSchemaBreakingChanges = (response, jsonBuilder, shouldOutputJson) => {
jsonBuilder.addComposedSchemaBreakingChanges(response.composedSchemaBreakingChanges);
if (shouldOutputJson) {
return;
}
const composedSchemaChangesTable = new Table({
head: [
pc.bold(pc.white('CHANGE')),
pc.bold(pc.white('TYPE')),
pc.bold(pc.white('FEDERATED_GRAPH')),
pc.bold(pc.white('FEATURE_FLAG')),
pc.bold(pc.white('DESCRIPTION')),
],
wordWrap: true,
});
for (const change of response.composedSchemaBreakingChanges) {
composedSchemaChangesTable.push([
`${logSymbols.error} ${pc.red('BREAKING')}`,
change.changeType,
change.federatedGraphName,
change.featureFlag || '—',
change.message,
]);
}
console.log(pc.red('\nDetected the following federated graph schema breaking changes:'));
console.log(pc.dim('These breaking changes were detected in the composed federated graph schema after composition. They are not reported above because they only become visible when all subgraphs are composed together (e.g., field type or nullability conflicts between subgraphs).'));
console.log(composedSchemaChangesTable.toString());
};
const handleCompositionErrors = (response, jsonBuilder, shouldOutputJson) => {
jsonBuilder.addCompositionErrors(response.compositionErrors);
if (!shouldOutputJson) {
const compositionErrorsTable = new Table({
head: [
pc.bold(pc.white('GRAPH_NAME')),
pc.bold(pc.white('NAMESPACE')),
pc.bold(pc.white('FEATURE_FLAG')),
pc.bold(pc.white('ERROR_MESSAGE')),
],
colWidths: [30, 30, 20, 120],
wordWrap: true,
});
for (const error of response.compositionErrors) {
compositionErrorsTable.push([error.federatedGraphName, error.namespace, error.featureFlag || '—', error.message]);
}
console.log(pc.red('\nDetected composition errors:'));
console.log(compositionErrorsTable.toString());
}
};
const handleCompositionWarnings = (response, jsonBuilder, shouldOutputJson) => {
jsonBuilder.addCompositionWarnings(response.compositionWarnings);
if (!shouldOutputJson) {
const compositionWarningsTable = new Table({
head: [
pc.bold(pc.white('GRAPH_NAME')),
pc.bold(pc.white('NAMESPACE')),
pc.bold(pc.white('FEATURE_FLAG')),
pc.bold(pc.white('WARNING_MESSAGE')),
],
colWidths: [30, 30, 20, 120],
wordWrap: true,
});
for (const warning of response.compositionWarnings) {
compositionWarningsTable.push([
warning.federatedGraphName,
warning.namespace,
warning.featureFlag || '—',
warning.message,
]);
}
console.log(pc.yellow(`\nDetected composition warnings:`));
console.log(compositionWarningsTable.toString());
}
};
const handleLintIssues = (response, jsonBuilder, shouldOutputJson) => {
var _a, _b;
jsonBuilder.addLintErrors(response.lintErrors);
jsonBuilder.addLintWarnings(response.lintWarnings);
if (!shouldOutputJson) {
const lintIssuesTable = new Table({
head: [pc.bold(pc.white('LINT_RULE')), pc.bold(pc.white('ERROR_MESSAGE')), pc.bold(pc.white('LINE NUMBER'))],
colAligns: ['left', 'left', 'center'],
wordWrap: true,
});
for (const error of response.lintErrors) {
lintIssuesTable.push([
`${logSymbols.error} ${pc.red(error.lintRuleType)}`,
error.message,
(_a = error.issueLocation) === null || _a === void 0 ? void 0 : _a.line,
]);
}
for (const warning of response.lintWarnings) {
lintIssuesTable.push([
`${logSymbols.warning} ${pc.yellow(warning.lintRuleType)}`,
warning.message,
(_b = warning.issueLocation) === null || _b === void 0 ? void 0 : _b.line,
]);
}
console.log('\nDetected lint issues:');
console.log(lintIssuesTable.toString());
}
};
const handleGraphPruneIssues = (response, jsonBuilder, shouldOutputJson) => {
var _a, _b;
jsonBuilder.addGraphPruneErrors(response.graphPruneErrors);
jsonBuilder.addGraphPruneWarnings(response.graphPruneWarnings);
if (!shouldOutputJson) {
const graphPruningIssuesTable = new Table({
head: [
pc.bold(pc.white('RULE')),
pc.bold(pc.white('FEDERATED_GRAPH_NAME')),
pc.bold(pc.white('FIELD_PATH')),
pc.bold(pc.white('MESSAGE')),
pc.bold(pc.white('LINE NUMBER')),
],
colAligns: ['left', 'left', 'left', 'left', 'center'],
wordWrap: true,
});
for (const error of response.graphPruneErrors) {
graphPruningIssuesTable.push([
`${logSymbols.error} ${pc.red(error.graphPruningRuleType)}`,
error.federatedGraphName,
error.fieldPath,
error.message,
((_a = error.issueLocation) === null || _a === void 0 ? void 0 : _a.line) || '-',
]);
}
for (const warning of response.graphPruneWarnings) {
graphPruningIssuesTable.push([
`${logSymbols.warning} ${pc.yellow(warning.graphPruningRuleType)}`,
warning.federatedGraphName,
warning.fieldPath,
warning.message,
((_b = warning.issueLocation) === null || _b === void 0 ? void 0 : _b.line) || '-',
]);
}
console.log('\nDetected graph pruning issues:');
console.log(graphPruningIssuesTable.toString());
}
};
// currentSuccess determines which sentence variant to use in the returned statement
const handleLinkedCheckFailures = (response, jsonBuilder, currentSuccess) => {
let additionalStatement = currentSuccess
? `\n\n But this schema change has been linked to a target subgraph and the target subgraph check has failed.`
: `\n\n This schema change has been linked to a target subgraph and the target subgraph check has failed.`;
if (response.isLinkedTrafficCheckFailed) {
const message = 'The target subgraph check has failed because of client traffic issues.';
additionalStatement += `\n\n ${message}`;
jsonBuilder.markTrafficLinkedFailed(message);
}
if (response.isLinkedPruningCheckFailed) {
jsonBuilder.markGraphPruneLinkedFailed();
additionalStatement += `\n\n The target subgraph check has failed because of graph pruning issues.`;
}
return additionalStatement;
};
const handleOkResult = ({ response, jsonBuilder, rowLimit, shouldOutputJson, studioCheckDestination, }) => {
var _a;
let success = false;
let finalStatement = '';
// Proposal match warning — always build json, conditionally print
if (response.proposalMatchMessage) {
jsonBuilder.setProposals(response.proposalMatchMessage);
if (!shouldOutputJson) {
console.log(pc.yellow(`Warning: Proposal match failed`));
console.log(pc.yellow(response.proposalMatchMessage));
}
}
// Feature subgraph not assigned to any enabled feature flag — informational, no composition ran.
// Always build json; print before the "nothing to report" early-exit below so it always surfaces.
if (response.featureSubgraphCheckMessage) {
jsonBuilder.setFeatureSubgraphCheck(response.featureSubgraphCheckMessage);
if (!shouldOutputJson) {
console.log(response.featureSubgraphCheckMessage);
}
}
// Early exit: nothing to report
const hasNoIssues = response.nonBreakingChanges.length === 0 &&
response.breakingChanges.length === 0 &&
response.composedSchemaBreakingChanges.length === 0 &&
response.compositionErrors.length === 0 &&
response.lintErrors.length === 0 &&
response.lintWarnings.length === 0 &&
response.graphPruneErrors.length === 0 &&
response.graphPruneWarnings.length === 0 &&
((_a = response.isCheckExtensionSkipped) !== null && _a !== void 0 ? _a : true);
if (hasNoIssues) {
jsonBuilder.initProposals('Detected no changes. Detected no lint issues. Detected no graph pruning issues.');
jsonBuilder.setStatus(true);
if (!shouldOutputJson) {
console.log(`\nDetected no changes.\nDetected no lint issues.\nDetected no graph pruning issues.\n\n${studioCheckDestination}\n`);
}
return { success: true };
}
if (!shouldOutputJson) {
console.log(`\nChecking the proposed schema`);
}
// No operations usage stats mean the check was not performed against any live traffic
if (response.operationUsageStats) {
({ success, finalStatement } = handleTrafficCheck(response, response.operationUsageStats, jsonBuilder, shouldOutputJson !== null && shouldOutputJson !== void 0 ? shouldOutputJson : false));
}
// Schema changes — build json always, build + print table only for text output
if (response.nonBreakingChanges.length > 0 || response.breakingChanges.length > 0) {
handleSchemaChanges(response, jsonBuilder, shouldOutputJson !== null && shouldOutputJson !== void 0 ? shouldOutputJson : false);
}
// Composed federated graph schema breaking changes
if (response.composedSchemaBreakingChanges.length > 0) {
handleComposedSchemaBreakingChanges(response, jsonBuilder, shouldOutputJson !== null && shouldOutputJson !== void 0 ? shouldOutputJson : false);
}
// Composition errors
if (response.compositionErrors.length > 0) {
handleCompositionErrors(response, jsonBuilder, shouldOutputJson !== null && shouldOutputJson !== void 0 ? shouldOutputJson : false);
}
// Composition warnings
if (response.compositionWarnings.length > 0) {
handleCompositionWarnings(response, jsonBuilder, shouldOutputJson !== null && shouldOutputJson !== void 0 ? shouldOutputJson : false);
}
// Lint issues
if (response.lintErrors.length > 0 || response.lintWarnings.length > 0) {
handleLintIssues(response, jsonBuilder, shouldOutputJson !== null && shouldOutputJson !== void 0 ? shouldOutputJson : false);
}
// Graph pruning issues
if (response.graphPruneErrors.length > 0 || response.graphPruneWarnings.length > 0) {
handleGraphPruneIssues(response, jsonBuilder, shouldOutputJson !== null && shouldOutputJson !== void 0 ? shouldOutputJson : false);
}
// Linked subgraph check failures
if (response.isLinkedTrafficCheckFailed || response.isLinkedPruningCheckFailed) {
finalStatement += handleLinkedCheckFailures(response, jsonBuilder, success);
success = false;
}
// Extension error
if (response.checkExtensionErrorMessage) {
const message = `Subgraph extension check failed with message: ${response.checkExtensionErrorMessage}`;
jsonBuilder.setExtensionError(message);
success = false;
finalStatement += `\n${logSymbols.error} ${message}`;
}
// Row limit exceeded message
let moreEntriesAvailableMessage = '';
if (response.counts) {
const hasExceeded = response.counts.lintWarnings + response.counts.lintErrors > rowLimit ||
response.counts.breakingChanges + response.counts.nonBreakingChanges > rowLimit ||
response.counts.graphPruneErrors + response.counts.graphPruneWarnings > rowLimit ||
response.counts.compositionErrors > rowLimit ||
response.counts.compositionWarnings > rowLimit ||
response.counts.composedSchemaBreakingChanges > rowLimit;
jsonBuilder.setExceededRowLimit(hasExceeded);
if (hasExceeded) {
if (studioCheckDestination !== '') {
moreEntriesAvailableMessage += `\n\n`;
}
moreEntriesAvailableMessage += pc.red(`Some results were truncated due to exceeding the limit of ${rowLimit} rows.`);
if (studioCheckDestination !== '') {
moreEntriesAvailableMessage += ` They can be viewed in the studio dashboard.`;
}
}
}
// Finalise json state, then print text output if not in JSON mode
jsonBuilder.setStatus(success).setMessage(finalStatement);
if (!shouldOutputJson) {
if (success) {
console.log('\n' +
logSymbols.success +
pc.green(` Schema check passed. ${finalStatement}`) +
'\n\n' +
studioCheckDestination +
moreEntriesAvailableMessage +
'\n');
}
else {
program.error('\n' +
logSymbols.error +
pc.red(` Schema check failed. ${finalStatement}\nSee https://cosmo-docs.wundergraph.com/studio/schema-checks for more information on resolving operation check errors.\n${studioCheckDestination}${moreEntriesAvailableMessage}\n`) +
'\n');
}
}
return { success };
};
export const handleCheckResult = async ({ response, rowLimit, shouldOutputJson, outFile, }) => {
var _a, _b, _c, _d, _e, _f, _g;
const jsonBuilder = new JsonCheckSchemaOutputBuilder(EnumStatusCode.ERR, rowLimit, outFile);
let studioCheckDestination = '';
if (response.checkId && response.checkedFederatedGraphs.length > 0) {
const url = `${config.webURL}/${encodeURIComponent(response.checkedFederatedGraphs[0].organizationSlug)}/${encodeURIComponent(response.checkedFederatedGraphs[0].namespace)}/graph/${encodeURIComponent(response.checkedFederatedGraphs[0].name)}/checks/${encodeURIComponent(response.checkId)}`;
jsonBuilder.setUrl(url);
studioCheckDestination = `${pc.bold('Open in studio')}: ${url}`;
}
switch ((_a = response.response) === null || _a === void 0 ? void 0 : _a.code) {
case EnumStatusCode.OK: {
const { success } = handleOkResult({ response, jsonBuilder, rowLimit, shouldOutputJson, studioCheckDestination });
if (shouldOutputJson) {
await jsonBuilder.write();
}
return success;
}
case EnumStatusCode.ERR_SCHEMA_MISMATCH_WITH_APPROVED_PROPOSAL: {
const message = 'Error: Proposal match failed';
if (shouldOutputJson) {
await jsonBuilder
.setCode(EnumStatusCode.ERR_SCHEMA_MISMATCH_WITH_APPROVED_PROPOSAL)
.setDetails(response.proposalMatchMessage)
.setMessage(message)
.setStatus(false)
.write();
}
else {
console.log(pc.red(message));
console.log(pc.red(response.proposalMatchMessage));
console.log(logSymbols.error +
pc.red(`Schema check failed.\nSee https://cosmo-docs.wundergraph.com/studio/schema-checks for more information on resolving operation check errors.\n${studioCheckDestination}\n`));
}
return false;
}
case EnumStatusCode.ERR_INVALID_SUBGRAPH_SCHEMA: {
const message = 'Check has failed early because the schema could not be built. Please ensure that the schema is valid GraphQL and try again.';
if (shouldOutputJson) {
await jsonBuilder
.setCode(EnumStatusCode.ERR_INVALID_SUBGRAPH_SCHEMA)
.setDetails((_b = response.response) === null || _b === void 0 ? void 0 : _b.details)
.setMessage(message)
.setStatus(false)
.write();
return false;
}
else {
console.log('\n' + message);
if ((_c = response.response) === null || _c === void 0 ? void 0 : _c.details) {
console.log(pc.red(pc.bold((_d = response.response) === null || _d === void 0 ? void 0 : _d.details)));
}
}
program.error(logSymbols.error + pc.red(' Schema check failed.'));
break;
}
default: {
const message = 'Failed to perform the check operation.';
if (shouldOutputJson) {
await jsonBuilder
.setCode(EnumStatusCode.ERR)
.setMessage(message)
.setDetails((_e = response.response) === null || _e === void 0 ? void 0 : _e.details)
.setStatus(false)
.write();
return false;
}
else {
console.log('\nFailed to perform the check operation.');
}
if (((_f = response.response) === null || _f === void 0 ? void 0 : _f.details) && !shouldOutputJson) {
console.log(pc.red(pc.bold((_g = response.response) === null || _g === void 0 ? void 0 : _g.details)));
}
program.error(logSymbols.error + pc.red(' Schema check failed.'));
}
}
};
//# sourceMappingURL=handle-check-result.js.map