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

Add subgraph list command #185

Merged
merged 9 commits into from
Jan 29, 2021
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ structopt = "0.3.21"
tracing = "0.1.22"
regex = "1"
url = "2.2.0"
chrono = "0.4"

[dev-dependencies]
assert_cmd = "1.0.1"
Expand Down
1 change: 1 addition & 0 deletions crates/rover-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ serde = "1"
serde_json = "1"
thiserror = "1"
tracing = "0.1"
chrono = "0.4"

[build-dependencies]
online = "0.2.2"
Expand Down
15 changes: 15 additions & 0 deletions crates/rover-client/src/query/subgraph/list.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
query ListSubgraphsQuery($graphId: ID!, $variant: String!) {
frontendUrlRoot
service(id: $graphId) {
implementingServices(graphVariant: $variant) {
__typename
... on FederatedImplementingServices {
services {
name
url
updatedAt
}
}
}
}
}
194 changes: 194 additions & 0 deletions crates/rover-client/src/query/subgraph/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
use crate::blocking::StudioClient;
use crate::RoverClientError;
use chrono::prelude::*;
use graphql_client::*;

type Timestamp = String;

#[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/list.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. list_subgraphs_query
pub struct ListSubgraphsQuery;

#[derive(Clone, PartialEq, Debug)]
pub struct SubgraphInfo {
pub name: String,
pub url: Option<String>, // optional, and may not be a real url
pub updated_at: Option<DateTime<Local>>,
}

#[derive(Clone, PartialEq, Debug)]
pub struct ListDetails {
pub subgraphs: Vec<SubgraphInfo>,
pub root_url: String,
pub graph_name: String,
}

/// Fetches list of subgraphs for a given graph, returns name & url of each
pub fn run(
variables: list_subgraphs_query::Variables,
client: &StudioClient,
) -> Result<ListDetails, RoverClientError> {
let graph_name = variables.graph_id.clone();
let response_data = client.post::<ListSubgraphsQuery>(variables)?;
let root_url = response_data.frontend_url_root.clone();
let subgraphs = get_subgraphs_from_response_data(response_data, &graph_name)?;
Ok(ListDetails {
subgraphs: format_subgraphs(&subgraphs),
root_url,
graph_name,
})
}

type RawSubgraphInfo = list_subgraphs_query::ListSubgraphsQueryServiceImplementingServicesOnFederatedImplementingServicesServices;
fn get_subgraphs_from_response_data(
response_data: list_subgraphs_query::ResponseData,
graph_name: &str,
) -> Result<Vec<RawSubgraphInfo>, 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(),
}),
}?;

// implementing_services.services
match services {
list_subgraphs_query::ListSubgraphsQueryServiceImplementingServices::FederatedImplementingServices (services) => {
Ok(services.services)
},
list_subgraphs_query::ListSubgraphsQueryServiceImplementingServices::NonFederatedImplementingService => {
Err(RoverClientError::ExpectedFederatedGraph { graph_name: graph_name.to_string() })
}
}
}

/// puts the subgraphs into a vec of SubgraphInfo, sorted by updated_at
/// timestamp. Newer updated services will show at top of list
fn format_subgraphs(subgraphs: &[RawSubgraphInfo]) -> Vec<SubgraphInfo> {
let mut subgraphs: Vec<SubgraphInfo> = subgraphs
.iter()
.map(|subgraph| SubgraphInfo {
name: subgraph.name.clone(),
url: subgraph.url.clone(),
updated_at: subgraph.updated_at.clone().parse::<DateTime<Local>>().ok(),
})
.collect();

// sort and reverse, so newer items come first. We use _unstable here, since
// we don't care which order equal items come in the list (it's unlikely that
// we'll even have equal items after all)
subgraphs.sort_unstable_by(|a, b| a.updated_at.cmp(&b.updated_at).reverse());

subgraphs
}

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

#[test]
fn get_subgraphs_from_response_data_works() {
let json_response = json!({
"frontendUrlRoot": "https://studio.apollographql.com/",
"service": {
"implementingServices": {
"__typename": "FederatedImplementingServices",
"services": [
{
"name": "accounts",
"url": "https://localhost:3000",
"updatedAt": "2020-09-24T18:53:08.683Z"
},
{
"name": "products",
"url": "https://localhost:3001",
"updatedAt": "2020-09-16T19:22:06.420Z"
}
]
}
}
});
let data: list_subgraphs_query::ResponseData =
serde_json::from_value(json_response).unwrap();
let output = get_subgraphs_from_response_data(data, &"service".to_string());

let expected_json = json!([
{
"name": "accounts",
"url": "https://localhost:3000",
"updatedAt": "2020-09-24T18:53:08.683Z"
},
{
"name": "products",
"url": "https://localhost:3001",
"updatedAt": "2020-09-16T19:22:06.420Z"
}
]);
let expected_service_list: Vec<RawSubgraphInfo> =
serde_json::from_value(expected_json).unwrap();

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

#[test]
fn get_subgraphs_from_response_data_errs_with_no_services() {
let json_response = json!({
"frontendUrlRoot": "https://harambe.com",
"service": {
"implementingServices": null
}
});
let data: list_subgraphs_query::ResponseData =
serde_json::from_value(json_response).unwrap();
let output = get_subgraphs_from_response_data(data, &"service".to_string());
assert!(output.is_err());
}

// #[test]
// fn format_subgraphs_builds_and_sorts_subgraphs() {
// let raw_info_json = json!([
// {
// "name": "accounts",
// "url": "https://localhost:3000",
// "updatedAt": "2020-09-24T18:53:08.683Z"
// },
// {
// "name": "shipping",
// "url": "https://localhost:3002",
// "updatedAt": "2020-09-16T17:22:06.420Z"
// },
// {
// "name": "products",
// "url": "https://localhost:3001",
// "updatedAt": "2020-09-16T19:22:06.420Z"
// }
// ]);
// let raw_subgraph_list: Vec<RawSubgraphInfo> =
// serde_json::from_value(raw_info_json).unwrap();
// let formatted = format_subgraphs(raw_subgraph_list);
// assert_eq!(formatted[0].name, "accounts".to_string());
// assert_eq!(formatted[2].name, "shipping".to_string());
// }
}
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 @@ -9,3 +9,6 @@ pub mod fetch;

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

/// "subgraph list"
pub mod list;
36 changes: 36 additions & 0 deletions src/command/output.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use std::fmt::Debug;

use atty::{self, Stream};
use prettytable::{cell, row, Table};
use rover_client::query::subgraph::list::ListDetails;

/// RoverStdout defines all of the different types of data that are printed
/// to `stdout`. Every one of Rover's commands should return `anyhow::Result<RoverStdout>`
Expand All @@ -12,6 +16,7 @@ use atty::{self, Stream};
pub enum RoverStdout {
SDL(String),
SchemaHash(String),
SubgraphList(ListDetails),
None,
}

Expand All @@ -36,6 +41,37 @@ impl RoverStdout {
}
println!("{}", &hash);
}
RoverStdout::SubgraphList(details) => {
if atty::is(Stream::Stdout) {
tracing::info!("Subgraphs:");
}

let mut table = Table::new();
table.add_row(row!["Name", "Routing Url", "Last Updated"]);

for subgraph in &details.subgraphs {
// if the url is None or empty (""), then set it to "N/A"
let url = subgraph.url.clone().unwrap_or_else(|| "N/A".to_string());
let url = if url.is_empty() {
"N/A".to_string()
} else {
url
};
let formatted_updated_at: String = if let Some(dt) = subgraph.updated_at {
dt.format("%Y-%m-%d %H:%M:%S %Z").to_string()
} else {
"N/A".to_string()
};

table.add_row(row![subgraph.name, url, formatted_updated_at]);
}

println!("{}", table);
println!(
"View full details at {}/graph/{}/service-list",
details.root_url, details.graph_name
);
}
RoverStdout::None => (),
}
}
Expand Down
46 changes: 46 additions & 0 deletions src/command/subgraph/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use serde::Serialize;
use structopt::StructOpt;

use rover_client::query::subgraph::list;

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

#[derive(Debug, Serialize, StructOpt)]
pub struct List {
/// <NAME>@<VARIANT> of graph in Apollo Studio to list subgraphs 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,
}

impl List {
pub fn run(&self, client_config: StudioClientConfig) -> Result<RoverStdout> {
let client = client_config.get_client(&self.profile_name)?;

tracing::info!(
"Listing subgraphs for {}@{}, mx. {}!",
&self.graph.name,
&self.graph.variant,
&self.profile_name
);

let list_details = list::run(
list::list_subgraphs_query::Variables {
graph_id: self.graph.name.clone(),
variant: self.graph.variant.clone(),
},
&client,
)?;

Ok(RoverStdout::SubgraphList(list_details))
}
}
5 changes: 5 additions & 0 deletions src/command/subgraph/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod check;
mod delete;
mod fetch;
mod list;
mod push;

use serde::Serialize;
Expand Down Expand Up @@ -29,6 +30,9 @@ pub enum Command {

/// Push a subgraph's schema from a local file
Push(push::Push),

/// List all subgraphs for a federated graph.
List(list::List),
}

impl Subgraph {
Expand All @@ -38,6 +42,7 @@ impl Subgraph {
Command::Delete(command) => command.run(client_config),
Command::Fetch(command) => command.run(client_config),
Command::Check(command) => command.run(client_config),
Command::List(command) => command.run(client_config),
}
}
}