Skip to content

Commit 7a51820

Browse files
feat(rover): subgraph checks
1 parent 5e484c7 commit 7a51820

File tree

7 files changed

+317
-8
lines changed

7 files changed

+317
-8
lines changed

crates/rover-client/src/blocking/client.rs

-2
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@ impl Client {
2929
) -> Result<Q::ResponseData, RoverClientError> {
3030
let h = headers::build(headers)?;
3131
let body = Q::build_query(variables);
32-
3332
let response = self.client.post(&self.uri).headers(h).json(&body).send()?;
34-
3533
Client::handle_response::<Q>(response)
3634
}
3735

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
mutation CheckPartialSchemaQuery (
2+
$graph_id: ID!
3+
$variant: String!
4+
$implementingServiceName: String!
5+
$partialSchema: PartialSchemaInput!
6+
) {
7+
service(id: $graph_id) {
8+
checkPartialSchema(
9+
graphVariant: $variant
10+
implementingServiceName: $implementingServiceName
11+
partialSchema: $partialSchema
12+
) {
13+
compositionValidationResult {
14+
errors {
15+
message
16+
}
17+
}
18+
checkSchemaResult {
19+
diffToPrevious {
20+
severity
21+
numberOfCheckedOperations
22+
changes {
23+
severity
24+
code
25+
description
26+
}
27+
}
28+
targetUrl
29+
}
30+
}
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use crate::blocking::StudioClient;
2+
use crate::RoverClientError;
3+
use graphql_client::*;
4+
5+
use reqwest::Url;
6+
7+
#[derive(GraphQLQuery)]
8+
// The paths are relative to the directory where your `Cargo.toml` is located.
9+
// Both json and the GraphQL schema language are supported as sources for the schema
10+
#[graphql(
11+
query_path = "src/query/subgraph/check.graphql",
12+
schema_path = ".schema/schema.graphql",
13+
response_derives = "PartialEq, Debug, Serialize, Deserialize",
14+
deprecated = "warn"
15+
)]
16+
/// This struct is used to generate the module containing `Variables` and
17+
/// `ResponseData` structs.
18+
/// Snake case of this name is the mod name. i.e. check_partial_schema_query
19+
pub struct CheckPartialSchemaQuery;
20+
21+
/// The main function to be used from this module.
22+
/// This function takes a proposed schema and validates it against a pushed
23+
/// schema.
24+
pub fn run(
25+
variables: check_partial_schema_query::Variables,
26+
client: &StudioClient,
27+
) -> Result<CheckResponse, RoverClientError> {
28+
let data = client.post::<CheckPartialSchemaQuery>(variables)?;
29+
get_check_response_from_data(data)
30+
}
31+
32+
pub enum CheckResponse {
33+
CompositionErrors(Vec<check_partial_schema_query::CheckPartialSchemaQueryServiceCheckPartialSchemaCompositionValidationResultErrors>),
34+
CheckResult(CheckResult)
35+
}
36+
37+
#[derive(Debug)]
38+
pub struct CheckResult {
39+
pub target_url: Option<Url>,
40+
pub number_of_checked_operations: i64,
41+
pub change_severity: check_partial_schema_query::ChangeSeverity,
42+
pub changes: Vec<check_partial_schema_query::CheckPartialSchemaQueryServiceCheckPartialSchemaCheckSchemaResultDiffToPreviousChanges>,
43+
}
44+
45+
fn get_check_response_from_data(
46+
data: check_partial_schema_query::ResponseData,
47+
) -> Result<CheckResponse, RoverClientError> {
48+
let service = data.service.ok_or(RoverClientError::NoService)?;
49+
50+
// for some reason this is a `Vec<Option<CompositionError>>`
51+
// we convert this to just `Vec<CompositionError>` because the `None`
52+
// errors would be useless.
53+
let composition_errors: Vec<check_partial_schema_query::CheckPartialSchemaQueryServiceCheckPartialSchemaCompositionValidationResultErrors> = service
54+
.check_partial_schema
55+
.composition_validation_result
56+
.errors
57+
.into_iter()
58+
.filter(|e| e.is_some())
59+
.map(|e| e.unwrap())
60+
.collect();
61+
62+
if composition_errors.is_empty() {
63+
// TODO: fix this error case
64+
let check_schema_result = service
65+
.check_partial_schema
66+
.check_schema_result
67+
.ok_or(RoverClientError::NoData)?;
68+
69+
let target_url = get_url(check_schema_result.target_url);
70+
71+
let diff_to_previous = check_schema_result.diff_to_previous;
72+
73+
let number_of_checked_operations =
74+
diff_to_previous.number_of_checked_operations.unwrap_or(0);
75+
76+
let change_severity = diff_to_previous.severity;
77+
let changes = diff_to_previous.changes;
78+
79+
let check_result = CheckResult {
80+
target_url,
81+
number_of_checked_operations,
82+
change_severity,
83+
changes,
84+
};
85+
86+
Ok(CheckResponse::CheckResult(check_result))
87+
} else {
88+
Ok(CheckResponse::CompositionErrors(composition_errors))
89+
}
90+
}
91+
92+
fn get_url(url: Option<String>) -> Option<Url> {
93+
match url {
94+
Some(url) => {
95+
let url = Url::parse(&url);
96+
match url {
97+
Ok(url) => Some(url),
98+
// if the API returns an invalid URL, don't put it in the response
99+
Err(_) => None,
100+
}
101+
}
102+
None => None,
103+
}
104+
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
/// "subgraph push" command execution
2-
pub mod push;
3-
41
/// "subgraph delete" command execution
52
pub mod delete;
63

4+
/// "subgraph check" command execution
5+
pub mod check;
6+
77
/// "subgraph fetch" command execution
88
pub mod fetch;
9+
10+
/// "subgraph push" command execution
11+
pub mod push;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"data": {
3+
"service": {
4+
"checkPartialSchema": {
5+
"compositionValidationResult": {
6+
"compositionValidationDetails": {
7+
"schemaHash": null
8+
},
9+
"graphCompositionID": "14334021-dfcd-4bc8-b595-c504c52f4856",
10+
"errors": [
11+
{
12+
"message": "[films] Person -> A @key directive specifies a field which is not found in this service. Add a field to this type with @external."
13+
},
14+
{
15+
"message": "[films] Person -> A @key selects iddd, but Person.iddd could not be found"
16+
},
17+
{
18+
"message": "[films] Person -> extends from people but specifies an invalid @key directive. Valid @key directives are specified by the originating type. Available @key directives for this type are:\n\t@key(fields: \"id\")"
19+
}
20+
]
21+
},
22+
"checkSchemaResult": null
23+
}
24+
}
25+
}
26+
}

src/command/subgraph/check.rs

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
use anyhow::{Context, Result};
2+
use prettytable::{cell, row, Table};
3+
use serde::Serialize;
4+
use structopt::StructOpt;
5+
6+
use rover_client::query::subgraph::check;
7+
8+
use crate::client::StudioClientConfig;
9+
use crate::command::RoverStdout;
10+
use crate::utils::loaders::load_schema_from_flag;
11+
use crate::utils::parsers::{parse_graph_ref, parse_schema_source, GraphRef, SchemaSource};
12+
13+
#[derive(Debug, Serialize, StructOpt)]
14+
pub struct Check {
15+
/// <NAME>@<VARIANT> of graph in Apollo Studio to validate.
16+
/// @<VARIANT> may be left off, defaulting to @current
17+
#[structopt(name = "GRAPH_REF", parse(try_from_str = parse_graph_ref))]
18+
#[serde(skip_serializing)]
19+
graph: GraphRef,
20+
21+
/// Name of the implementing service to validate
22+
#[structopt(long = "service", required = true)]
23+
#[serde(skip_serializing)]
24+
service_name: String,
25+
26+
/// Name of configuration profile to use
27+
#[structopt(long = "profile", default_value = "default")]
28+
#[serde(skip_serializing)]
29+
profile_name: String,
30+
31+
/// The schema file to push
32+
/// Can pass `-` to use stdin instead of a file
33+
#[structopt(long, short = "s", parse(try_from_str = parse_schema_source))]
34+
#[serde(skip_serializing)]
35+
schema: SchemaSource,
36+
}
37+
38+
impl Check {
39+
pub fn run(&self, client_config: StudioClientConfig) -> Result<RoverStdout> {
40+
let client = client_config.get_client(&self.profile_name)?;
41+
42+
let sdl = load_schema_from_flag(&self.schema, std::io::stdin())?;
43+
44+
let partial_schema = check::check_partial_schema_query::PartialSchemaInput {
45+
sdl: Some(sdl),
46+
// we never need to send the hash since the back end computes it from SDL
47+
hash: None,
48+
};
49+
let res = check::run(
50+
check::check_partial_schema_query::Variables {
51+
graph_id: self.graph.name.clone(),
52+
variant: self.graph.variant.clone(),
53+
partial_schema,
54+
implementing_service_name: self.service_name.clone(),
55+
},
56+
&client,
57+
)
58+
.context("Failed to validate schema")?;
59+
60+
tracing::info!(
61+
"Checked the proposed subgraph against {}@{}",
62+
&self.graph.name,
63+
&self.graph.variant
64+
);
65+
66+
match res {
67+
check::CheckResponse::CompositionErrors(composition_errors) => {
68+
handle_composition_errors(&composition_errors)
69+
}
70+
check::CheckResponse::CheckResult(check_result) => handle_checks(check_result),
71+
}
72+
}
73+
}
74+
75+
fn handle_checks(check_result: check::CheckResult) -> Result<RoverStdout> {
76+
let num_changes = check_result.changes.len();
77+
78+
let msg = match num_changes {
79+
0 => "There were no changes detected between the proposed subgraph and the subgraph that already exists in the graph registry.".to_string(),
80+
_ => format!("Compared {} schema changes against {} operations", check_result.changes.len(), check_result.number_of_checked_operations),
81+
};
82+
83+
tracing::info!("{}", &msg);
84+
85+
let mut num_failures = 0;
86+
87+
if !check_result.changes.is_empty() {
88+
let mut table = Table::new();
89+
table.add_row(row!["Change", "Code", "Description"]);
90+
for check in check_result.changes {
91+
let change = match check.severity {
92+
check::check_partial_schema_query::ChangeSeverity::NOTICE => "PASS",
93+
check::check_partial_schema_query::ChangeSeverity::FAILURE => {
94+
num_failures += 1;
95+
"FAIL"
96+
}
97+
_ => unreachable!("Unknown change severity"),
98+
};
99+
table.add_row(row![change, check.code, check.description]);
100+
}
101+
102+
eprintln!("{}", table);
103+
}
104+
105+
if let Some(url) = check_result.target_url {
106+
tracing::info!("View full details here");
107+
tracing::info!("{}", url.to_string());
108+
}
109+
110+
match num_failures {
111+
0 => Ok(RoverStdout::None),
112+
1 => Err(anyhow::anyhow!(
113+
"Encountered 1 failure while checking your subgraph."
114+
)),
115+
_ => Err(anyhow::anyhow!(
116+
"Encountered {} failures while checking your subgraph.",
117+
num_failures
118+
)),
119+
}
120+
}
121+
122+
fn handle_composition_errors(
123+
composition_errors: &[check::check_partial_schema_query::CheckPartialSchemaQueryServiceCheckPartialSchemaCompositionValidationResultErrors],
124+
) -> Result<RoverStdout> {
125+
let mut num_failures = 0;
126+
for error in composition_errors {
127+
num_failures += 1;
128+
tracing::error!("{}", &error.message);
129+
}
130+
match num_failures {
131+
0 => Ok(RoverStdout::None),
132+
1 => Err(anyhow::anyhow!(
133+
"Encountered 1 composition error while composing the subgraph."
134+
)),
135+
_ => Err(anyhow::anyhow!(
136+
"Encountered {} composition errors while composing the subgraph.",
137+
num_failures
138+
)),
139+
}
140+
}

src/command/subgraph/mod.rs

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod check;
12
mod delete;
23
mod fetch;
34
mod push;
@@ -17,13 +18,17 @@ pub struct Subgraph {
1718

1819
#[derive(Debug, Serialize, StructOpt)]
1920
pub enum Command {
20-
/// Push an implementing service schema from a local file
21-
Push(push::Push),
21+
/// Check changes to an implementing service
22+
Check(check::Check),
2223

2324
/// Delete an implementing service and trigger composition
2425
Delete(delete::Delete),
25-
/// ⬇️ Fetch an implementing service's schema from Apollo Studio
26+
27+
/// Fetch an implementing service's schema from Apollo Studio
2628
Fetch(fetch::Fetch),
29+
30+
/// Push an implementing service schema from a local file
31+
Push(push::Push),
2732
}
2833

2934
impl Subgraph {
@@ -32,6 +37,7 @@ impl Subgraph {
3237
Command::Push(command) => command.run(client_config),
3338
Command::Delete(command) => command.run(client_config),
3439
Command::Fetch(command) => command.run(client_config),
40+
Command::Check(command) => command.run(client_config),
3541
}
3642
}
3743
}

0 commit comments

Comments
 (0)