Skip to content

Commit

Permalink
feat: operation checks (breaking change detection) (wundergraph#214)
Browse files Browse the repository at this point in the history
Co-authored-by: thisisnithin <nithinkumar5353@gmail.com>
  • Loading branch information
StarpTech and thisisnithin authored Nov 3, 2023
1 parent 9b784cf commit 0935413
Show file tree
Hide file tree
Showing 86 changed files with 5,435 additions and 1,210 deletions.
12 changes: 12 additions & 0 deletions .run/controlplane.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="controlplane" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/controlplane/package.json" />
<command value="run" />
<scripts>
<script value="debug" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"del-cli": "^5.0.0",
"eslint": "^8.52.0",
"eslint-config-unjs": "^0.2.1",
"prettier": "^2.8.8",
"prettier": "^3.0.3",
"tsx": "^3.14.0",
"typescript": "^5.2.2",
"vitest": "^0.34.1"
Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/federated-graph/commands/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default (opts: BaseCommandOptions) => {
changeMessage: cl.changeMessage,
createdAt: cl.createdAt,
})),
} as OutputFile[number]),
}) as OutputFile[number],
);
await writeFile(join(process.cwd(), options.out), JSON.stringify(output));
} else {
Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/federated-graph/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default (opts: BaseCommandOptions) => {
routingURL: g.routingURL,
isComposable: g.isComposable,
lastUpdatedAt: g.lastUpdatedAt,
} as OutputFile[number]),
}) as OutputFile[number],
);
await writeFile(join(process.cwd(), options.out), JSON.stringify(output));
process.exit(0);
Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/router/commands/token/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default (opts: BaseCommandOptions) => {
name: g.name,
token: g.token,
createdAt: g.createdAt,
} as OutputFile[number]),
}) as OutputFile[number],
);
await writeFile(join(process.cwd(), options.out), JSON.stringify(output));
process.exit(0);
Expand Down
51 changes: 40 additions & 11 deletions cli/src/commands/subgraph/commands/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export default (opts: BaseCommandOptions) => {
});

let success = false;
let finalStatement = '';

switch (resp.response?.code) {
case EnumStatusCode.OK: {
Expand All @@ -91,25 +92,46 @@ export default (opts: BaseCommandOptions) => {
resp.breakingChanges.length === 0 &&
resp.compositionErrors.length === 0
) {
console.log('\nDetected no changes.');
console.log('\nDetected no changes.\n');

success = true;

break;
}

success = resp.breakingChanges.length === 0 && resp.compositionErrors.length === 0;
console.log(`\nChecking the proposed schema for subgraph ${pc.bold(name)}.`);

// No operations imply no clients and no subgraphs
if (resp.operationUsageStats) {
if (resp.operationUsageStats?.totalOperations === 0) {
// Composition errors are still considered failures
success = resp.compositionErrors.length === 0;
console.log(`No operations were affected by this schema change.`);
finalStatement = `This schema change didn't affect any operations from existing client traffic.`;
} else {
success = resp.breakingChanges.length === 0 && resp.compositionErrors.length === 0;

console.log(
logSymbols.warning +
` Compared ${pc.bold(resp.breakingChanges.length)} breaking change's impacting ${pc.bold(
resp.operationUsageStats.totalOperations,
)} operations.\nFound client activity between ` +
pc.underline(new Date(resp.operationUsageStats.firstSeenAt).toLocaleString()) +
` and ` +
pc.underline(new Date(resp.operationUsageStats.lastSeenAt).toLocaleString()),
);
finalStatement = `This check has encountered ${pc.bold(
`${resp.breakingChanges.length}`,
)} breaking change's that would break operations from existing client traffic.`;
}
}

if (resp.nonBreakingChanges.length > 0 || resp.breakingChanges.length > 0) {
console.log('\nDetected the following changes.');
console.log('\nDetected the following changes:');

if (resp.breakingChanges.length > 0) {
for (const breakingChange of resp.breakingChanges) {
changesTable.push([
pc.red('BREAKING'),
pc.red(breakingChange.changeType),
pc.red(breakingChange.message),
]);
changesTable.push([pc.red('BREAKING'), breakingChange.changeType, breakingChange.message]);
}
}

Expand All @@ -123,17 +145,24 @@ export default (opts: BaseCommandOptions) => {
}

if (resp.compositionErrors.length > 0) {
console.log(pc.red('\nDetected composition errors.'));
console.log(pc.red('\nDetected composition errors:'));
for (const compositionError of resp.compositionErrors) {
compositionErrorsTable.push([compositionError.federatedGraphName, compositionError.message]);
}
console.log(compositionErrorsTable.toString());
}

if (success) {
console.log('\n' + logSymbols.success + pc.green(' Schema check passed.'));
console.log('\n' + logSymbols.success + pc.green(` Schema check passed. ${finalStatement}`) + '\n');
} else {
console.log('\n' + logSymbols.error + pc.red(' Schema check failed.'));
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',
);
}
break;
}
Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/subgraph/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default (opts: BaseCommandOptions) => {
labels: g.labels.map((l) => joinLabel(l)),
routingURL: g.routingURL,
lastUpdatedAt: g.lastUpdatedAt,
} as OutputFile[number]),
}) as OutputFile[number],
);
await writeFile(join(process.cwd(), options.out), JSON.stringify(output));
process.exit(0);
Expand Down
1 change: 1 addition & 0 deletions cli/src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const config = {
baseURL: process.env.COSMO_API_URL || 'https://cosmo-cp.wundergraph.com',
apiKey: getLoginDetails()?.accessToken || process.env.COSMO_API_KEY,
kcApiURL: process.env.KC_API_URL || 'https://accounts.wundergraph.com/auth',
webURL: process.env.COSMO_WEB_URL || 'https://cosmo.wundergraph.com',
kcClientId: process.env.KC_CLIENT_ID || 'cosmo-cli',
kcRealm: process.env.KC_REALM || 'cosmo',
version: info.version,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { createQueryService } from "@connectrpc/connect-query";
import { MethodIdempotency, MethodKind } from "@bufbuild/protobuf";
import { CheckFederatedGraphRequest, CheckFederatedGraphResponse, CheckSubgraphSchemaRequest, CheckSubgraphSchemaResponse, CreateAPIKeyRequest, CreateAPIKeyResponse, CreateFederatedGraphRequest, CreateFederatedGraphResponse, CreateFederatedGraphTokenRequest, CreateFederatedGraphTokenResponse, CreateFederatedSubgraphRequest, CreateFederatedSubgraphResponse, CreateIntegrationRequest, CreateIntegrationResponse, CreateOrganizationWebhookConfigRequest, CreateOrganizationWebhookConfigResponse, DeleteAPIKeyRequest, DeleteAPIKeyResponse, DeleteFederatedGraphRequest, DeleteFederatedGraphResponse, DeleteFederatedSubgraphRequest, DeleteFederatedSubgraphResponse, DeleteIntegrationRequest, DeleteIntegrationResponse, DeleteOrganizationRequest, DeleteOrganizationResponse, DeleteOrganizationWebhookConfigRequest, DeleteOrganizationWebhookConfigResponse, DeleteRouterTokenRequest, DeleteRouterTokenResponse, FixSubgraphSchemaRequest, FixSubgraphSchemaResponse, ForceCheckSuccessRequest, ForceCheckSuccessResponse, GetAnalyticsViewRequest, GetAnalyticsViewResponse, GetAPIKeysRequest, GetAPIKeysResponse, GetCheckDetailsRequest, GetCheckDetailsResponse, GetChecksByFederatedGraphNameRequest, GetChecksByFederatedGraphNameResponse, GetDashboardAnalyticsViewRequest, GetDashboardAnalyticsViewResponse, GetFederatedGraphByNameRequest, GetFederatedGraphByNameResponse, GetFederatedGraphChangelogRequest, GetFederatedGraphChangelogResponse, GetFederatedGraphSDLByNameRequest, GetFederatedGraphSDLByNameResponse, GetFederatedGraphsRequest, GetFederatedGraphsResponse, GetFederatedSubgraphSDLByNameRequest, GetFederatedSubgraphSDLByNameResponse, GetFieldUsageRequest, GetFieldUsageResponse, GetGraphMetricsRequest, GetGraphMetricsResponse, GetMetricsErrorRateRequest, GetMetricsErrorRateResponse, GetOrganizationIntegrationsRequest, GetOrganizationIntegrationsResponse, GetOrganizationMembersRequest, GetOrganizationMembersResponse, GetOrganizationWebhookConfigsRequest, GetOrganizationWebhookConfigsResponse, GetOrganizationWebhookMetaRequest, GetOrganizationWebhookMetaResponse, GetRouterTokensRequest, GetRouterTokensResponse, GetSubgraphByNameRequest, GetSubgraphByNameResponse, GetSubgraphsRequest, GetSubgraphsResponse, GetTraceRequest, GetTraceResponse, InviteUserRequest, InviteUserResponse, IsGitHubAppInstalledRequest, IsGitHubAppInstalledResponse, LeaveOrganizationRequest, LeaveOrganizationResponse, MigrateFromApolloRequest, MigrateFromApolloResponse, PublishFederatedSubgraphRequest, PublishFederatedSubgraphResponse, RemoveInvitationRequest, RemoveInvitationResponse, UpdateFederatedGraphRequest, UpdateFederatedGraphResponse, UpdateIntegrationConfigRequest, UpdateIntegrationConfigResponse, UpdateOrganizationDetailsRequest, UpdateOrganizationDetailsResponse, UpdateOrganizationWebhookConfigRequest, UpdateOrganizationWebhookConfigResponse, UpdateOrgMemberRoleRequest, UpdateOrgMemberRoleResponse, UpdateSubgraphRequest, UpdateSubgraphResponse, WhoAmIRequest, WhoAmIResponse } from "./platform_pb.js";
import { CheckFederatedGraphRequest, CheckFederatedGraphResponse, CheckSubgraphSchemaRequest, CheckSubgraphSchemaResponse, CreateAPIKeyRequest, CreateAPIKeyResponse, CreateFederatedGraphRequest, CreateFederatedGraphResponse, CreateFederatedGraphTokenRequest, CreateFederatedGraphTokenResponse, CreateFederatedSubgraphRequest, CreateFederatedSubgraphResponse, CreateIntegrationRequest, CreateIntegrationResponse, CreateOrganizationWebhookConfigRequest, CreateOrganizationWebhookConfigResponse, DeleteAPIKeyRequest, DeleteAPIKeyResponse, DeleteFederatedGraphRequest, DeleteFederatedGraphResponse, DeleteFederatedSubgraphRequest, DeleteFederatedSubgraphResponse, DeleteIntegrationRequest, DeleteIntegrationResponse, DeleteOrganizationRequest, DeleteOrganizationResponse, DeleteOrganizationWebhookConfigRequest, DeleteOrganizationWebhookConfigResponse, DeleteRouterTokenRequest, DeleteRouterTokenResponse, FixSubgraphSchemaRequest, FixSubgraphSchemaResponse, ForceCheckSuccessRequest, ForceCheckSuccessResponse, GetAnalyticsViewRequest, GetAnalyticsViewResponse, GetAPIKeysRequest, GetAPIKeysResponse, GetCheckDetailsRequest, GetCheckDetailsResponse, GetCheckOperationsRequest, GetCheckOperationsResponse, GetChecksByFederatedGraphNameRequest, GetChecksByFederatedGraphNameResponse, GetCheckSummaryRequest, GetCheckSummaryResponse, GetDashboardAnalyticsViewRequest, GetDashboardAnalyticsViewResponse, GetFederatedGraphByNameRequest, GetFederatedGraphByNameResponse, GetFederatedGraphChangelogRequest, GetFederatedGraphChangelogResponse, GetFederatedGraphSDLByNameRequest, GetFederatedGraphSDLByNameResponse, GetFederatedGraphsRequest, GetFederatedGraphsResponse, GetFederatedSubgraphSDLByNameRequest, GetFederatedSubgraphSDLByNameResponse, GetFieldUsageRequest, GetFieldUsageResponse, GetGraphMetricsRequest, GetGraphMetricsResponse, GetMetricsErrorRateRequest, GetMetricsErrorRateResponse, GetOperationContentRequest, GetOperationContentResponse, GetOrganizationIntegrationsRequest, GetOrganizationIntegrationsResponse, GetOrganizationMembersRequest, GetOrganizationMembersResponse, GetOrganizationWebhookConfigsRequest, GetOrganizationWebhookConfigsResponse, GetOrganizationWebhookMetaRequest, GetOrganizationWebhookMetaResponse, GetRouterTokensRequest, GetRouterTokensResponse, GetSubgraphByNameRequest, GetSubgraphByNameResponse, GetSubgraphsRequest, GetSubgraphsResponse, GetTraceRequest, GetTraceResponse, InviteUserRequest, InviteUserResponse, IsGitHubAppInstalledRequest, IsGitHubAppInstalledResponse, LeaveOrganizationRequest, LeaveOrganizationResponse, MigrateFromApolloRequest, MigrateFromApolloResponse, PublishFederatedSubgraphRequest, PublishFederatedSubgraphResponse, RemoveInvitationRequest, RemoveInvitationResponse, UpdateFederatedGraphRequest, UpdateFederatedGraphResponse, UpdateIntegrationConfigRequest, UpdateIntegrationConfigResponse, UpdateOrganizationDetailsRequest, UpdateOrganizationDetailsResponse, UpdateOrganizationWebhookConfigRequest, UpdateOrganizationWebhookConfigResponse, UpdateOrgMemberRoleRequest, UpdateOrgMemberRoleResponse, UpdateSubgraphRequest, UpdateSubgraphResponse, WhoAmIRequest, WhoAmIResponse } from "./platform_pb.js";
import { GetConfigRequest, GetConfigResponse } from "../../node/v1/node_pb.js";

export const typeName = "wg.cosmo.platform.v1.PlatformService";
Expand Down Expand Up @@ -390,6 +390,25 @@ export const getChecksByFederatedGraphName = createQueryService({
},
}).getChecksByFederatedGraphName;

/**
* GetCheckSummary returns top level information about a schema check
*
* @generated from rpc wg.cosmo.platform.v1.PlatformService.GetCheckSummary
*/
export const getCheckSummary = createQueryService({
service: {
methods: {
getCheckSummary: {
name: "GetCheckSummary",
kind: MethodKind.Unary,
I: GetCheckSummaryRequest,
O: GetCheckSummaryResponse,
},
},
typeName: "wg.cosmo.platform.v1.PlatformService",
},
}).getCheckSummary;

/**
* GetCheckDetails returns changes and composition errors recorded for a check
*
Expand All @@ -409,6 +428,25 @@ export const getCheckDetails = createQueryService({
},
}).getCheckDetails;

/**
* GetCheckOperations returns affected operations for a check
*
* @generated from rpc wg.cosmo.platform.v1.PlatformService.GetCheckOperations
*/
export const getCheckOperations = createQueryService({
service: {
methods: {
getCheckOperations: {
name: "GetCheckOperations",
kind: MethodKind.Unary,
I: GetCheckOperationsRequest,
O: GetCheckOperationsResponse,
},
},
typeName: "wg.cosmo.platform.v1.PlatformService",
},
}).getCheckOperations;

/**
* ForceCheckSuccess forces a failed check to be marked as successful
*
Expand All @@ -428,6 +466,25 @@ export const forceCheckSuccess = createQueryService({
},
}).forceCheckSuccess;

/**
* GetOperationContent returns the operation body by searching using the hash
*
* @generated from rpc wg.cosmo.platform.v1.PlatformService.GetOperationContent
*/
export const getOperationContent = createQueryService({
service: {
methods: {
getOperationContent: {
name: "GetOperationContent",
kind: MethodKind.Unary,
I: GetOperationContentRequest,
O: GetOperationContentResponse,
},
},
typeName: "wg.cosmo.platform.v1.PlatformService",
},
}).getOperationContent;

/**
* GetFederatedGraphChangelog returns the changelog of the federated graph.
*
Expand Down
Loading

0 comments on commit 0935413

Please sign in to comment.