Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create subgraph fetch command #117

Merged
merged 10 commits into from
Dec 16, 2020
5 changes: 4 additions & 1 deletion crates/rover-client/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ pub enum RoverClientError {
/// when someone provides a bad service/variant combingation or isn't
/// validated properly, we don't know which reason is at fault for data.service
/// being empty, so this error tells them to check both.
#[error("No service found. Either the service/variant combination wasn't found or your API key is invalid.")]
#[error("No graph found. Either the graph@variant combination wasn't found or your API key is invalid.")]
NoService,

#[error("The graph `{graph_name}` is a non-federated graph. This operation is only possible for federated graphs")]
ExpectedFederatedGraph { graph_name: String },
}
15 changes: 15 additions & 0 deletions crates/rover-client/src/query/subgraph/fetch.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
query FetchSubgraphQuery($variant: String!, $graphID: ID!) {
service(id: $graphID) {
implementingServices(graphVariant: $variant) {
__typename
... on FederatedImplementingServices {
services {
name
activePartialSchema {
sdl
}
}
}
}
}
}
195 changes: 195 additions & 0 deletions crates/rover-client/src/query/subgraph/fetch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
use crate::blocking::StudioClient;
use crate::RoverClientError;
use graphql_client::*;

#[derive(GraphQLQuery)]
// The paths are relative to the directory where your `Cargo.toml` is located.
// Both json and the GraphQL schema language are supported as sources for the schema
#[graphql(
query_path = "src/query/subgraph/fetch.graphql",
schema_path = ".schema/schema.graphql",
response_derives = "PartialEq, Debug, Serialize, Deserialize",
deprecated = "warn"
)]
/// This struct is used to generate the module containing `Variables` and
/// `ResponseData` structs.
/// Snake case of this name is the mod name. i.e. fetch_subgraph_query
pub struct FetchSubgraphQuery;

/// Fetches a schema from apollo studio and returns its SDL (String)
pub fn run(
variables: fetch_subgraph_query::Variables,
client: &StudioClient,
// we can't specify this as a variable in the op, so we have to filter the
// operation response by this name
service_name: &str,
) -> Result<String, RoverClientError> {
let graph_name = variables.graph_id.clone();
let response_data = client.post::<FetchSubgraphQuery>(variables)?;
let services = get_services_from_response_data(response_data, &graph_name)?;
get_sdl_for_service(services, service_name)
// if we want json, we can parse & serialize it here
}

type ServiceList = Vec<fetch_subgraph_query::FetchSubgraphQueryServiceImplementingServicesOnFederatedImplementingServicesServices>;
fn get_services_from_response_data(
response_data: fetch_subgraph_query::ResponseData,
graph_name: &str,
) -> Result<ServiceList, RoverClientError> {
let service_data = match response_data.service {
Some(data) => Ok(data),
None => Err(RoverClientError::NoService),
}?;

// get list of services
let services = match service_data.implementing_services {
Some(services) => Ok(services),
// this case may be removable in the near future as unreachable, since
// you should still get an `implementingServices` response in the case
// of a non-federated graph. Fow now, this case still exists, but
// wont' for long. Check on this later (Jake) :)
None => Err(RoverClientError::ExpectedFederatedGraph {
graph_name: graph_name.to_string(),
}),
}?;

match services {
fetch_subgraph_query::FetchSubgraphQueryServiceImplementingServices::FederatedImplementingServices (services) => {
Ok(services.services)
},
fetch_subgraph_query::FetchSubgraphQueryServiceImplementingServices::NonFederatedImplementingService => {
Err(RoverClientError::ExpectedFederatedGraph { graph_name: graph_name.to_string() })
}
}
}

fn get_sdl_for_service(
services: ServiceList,
service_name: &str,
) -> Result<String, RoverClientError> {
// find the right service by name
let service = services.iter().find(|svc| svc.name == service_name);

// if there is a service, get it's active sdl, otherwise, error and list
// available services to fetch
if let Some(service) = service {
Ok(service.active_partial_schema.sdl.clone())
} else {
let all_service_names: Vec<String> = services.iter().map(|svc| svc.name.clone()).collect();
let msg = format!("Could not find service `{}` in list of implementing services. Available services to fetch: [{}]", service_name, all_service_names.join(", "));

Err(RoverClientError::HandleResponse { msg })
}
}

#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;

#[test]
fn get_services_from_response_data_works() {
let json_response = json!({
"service": {
"implementingServices": {
"__typename": "FederatedImplementingServices",
"services": [
{
"name": "accounts",
"activePartialSchema": {
"sdl": "type Query {\n me: User\n}\n\ntype User @key(fields: \"id\") {\n id: ID!\n}\n"
}
},
{
"name": "accounts2",
"activePartialSchema": {
"sdl": "extend type User @key(fields: \"id\") {\n id: ID! @external\n age: Int\n}\n"
}
}
]
}
}
});
let data: fetch_subgraph_query::ResponseData =
serde_json::from_value(json_response).unwrap();
let output = get_services_from_response_data(data, "service");

let expected_json = json!([
{
"name": "accounts",
"activePartialSchema": {
"sdl": "type Query {\n me: User\n}\n\ntype User @key(fields: \"id\") {\n id: ID!\n}\n"
}
},
{
"name": "accounts2",
"activePartialSchema": {
"sdl": "extend type User @key(fields: \"id\") {\n id: ID! @external\n age: Int\n}\n"
}
}
]);
let expected_service_list: ServiceList = serde_json::from_value(expected_json).unwrap();

assert!(output.is_ok());
assert_eq!(output.unwrap(), expected_service_list);
}

#[test]
fn get_services_from_response_data_errs_with_no_services() {
let json_response = json!({
"service": {
"implementingServices": null
}
});
let data: fetch_subgraph_query::ResponseData =
serde_json::from_value(json_response).unwrap();
let output = get_services_from_response_data(data, "service");
assert!(output.is_err());
}

#[test]
fn get_sdl_for_service_returns_correct_sdl() {
let json_service_list = json!([
{
"name": "accounts",
"activePartialSchema": {
"sdl": "type Query {\n me: User\n}\n\ntype User @key(fields: \"id\") {\n id: ID!\n}\n"
}
},
{
"name": "accounts2",
"activePartialSchema": {
"sdl": "extend type User @key(fields: \"id\") {\n id: ID! @external\n age: Int\n}\n"
}
}
]);
let service_list: ServiceList = serde_json::from_value(json_service_list).unwrap();
let output = get_sdl_for_service(service_list, "accounts2");
assert_eq!(
output.unwrap(),
"extend type User @key(fields: \"id\") {\n id: ID! @external\n age: Int\n}\n"
.to_string()
);
}

#[test]
fn get_sdl_for_service_errs_on_invalid_name() {
let json_service_list = json!([
{
"name": "accounts",
"activePartialSchema": {
"sdl": "type Query {\n me: User\n}\n\ntype User @key(fields: \"id\") {\n id: ID!\n}\n"
}
},
{
"name": "accounts2",
"activePartialSchema": {
"sdl": "extend type User @key(fields: \"id\") {\n id: ID! @external\n age: Int\n}\n"
}
}
]);
let service_list: ServiceList = serde_json::from_value(json_service_list).unwrap();
let output = get_sdl_for_service(service_list, "harambe-was-an-inside-job");
assert!(output.is_err());
}
}
3 changes: 3 additions & 0 deletions crates/rover-client/src/query/subgraph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ pub mod push;

/// "subgraph delete" command execution
pub mod delete;

/// "subgraph fetch" command execution
pub mod fetch;
55 changes: 55 additions & 0 deletions src/command/subgraph/fetch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use anyhow::{Context, Result};
use serde::Serialize;
use structopt::StructOpt;

use rover_client::query::subgraph::fetch;

use crate::client::get_studio_client;
use crate::command::RoverStdout;
use crate::utils::parsers::{parse_graph_ref, GraphRef};

#[derive(Debug, Serialize, StructOpt)]
pub struct Fetch {
/// <NAME>@<VARIANT> of graph in Apollo Studio to fetch from.
/// @<VARIANT> may be left off, defaulting to @current
#[structopt(name = "GRAPH_REF", parse(try_from_str = parse_graph_ref))]
#[serde(skip_serializing)]
graph: GraphRef,

/// Name of configuration profile to use
#[structopt(long = "profile", default_value = "default")]
#[serde(skip_serializing)]
profile_name: String,

/// Name of implementing service in federated graph to update
#[structopt(long)]
#[serde(skip_serializing)]
service_name: String,
}

impl Fetch {
pub fn run(&self) -> Result<RoverStdout> {
let client =
get_studio_client(&self.profile_name).context("Failed to get studio client")?;

tracing::info!(
"Let's get this schema, {}@{} (service: {}), mx. {}!",
&self.graph.name,
&self.graph.variant,
&self.service_name,
&self.profile_name
);

let sdl = fetch::run(
fetch::fetch_subgraph_query::Variables {
graph_id: self.graph.name.clone(),
variant: self.graph.variant.clone(),
},
&client,
&self.service_name,
)
.context("Failed while fetching from Apollo Studio")?;

Ok(RoverStdout::SDL(sdl))
}
}
4 changes: 4 additions & 0 deletions src/command/subgraph/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod delete;
mod fetch;
mod push;

use anyhow::Result;
Expand All @@ -19,13 +20,16 @@ pub enum Command {
Push(push::Push),
/// Delete an implementing service and trigger composition
Delete(delete::Delete),
/// ⬇️ Fetch an implementing service's schema from Apollo Studio
Fetch(fetch::Fetch),
}

impl Subgraph {
pub fn run(&self) -> Result<RoverStdout> {
match &self.command {
Command::Push(command) => command.run(),
Command::Delete(command) => command.run(),
Command::Fetch(command) => command.run(),
}
}
}