From 7a9d562ee9e01e5b1333adf862f9b6454a7b1724 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 15 Jan 2021 12:12:47 -0500 Subject: [PATCH 1/9] feat: add subgraph list command --- .../src/query/subgraph/list.graphql | 15 ++ .../rover-client/src/query/subgraph/list.rs | 180 ++++++++++++++++++ crates/rover-client/src/query/subgraph/mod.rs | 3 + src/command/output.rs | 21 ++ src/command/subgraph/list.rs | 47 +++++ src/command/subgraph/mod.rs | 5 + 6 files changed, 271 insertions(+) create mode 100644 crates/rover-client/src/query/subgraph/list.graphql create mode 100644 crates/rover-client/src/query/subgraph/list.rs create mode 100644 src/command/subgraph/list.rs diff --git a/crates/rover-client/src/query/subgraph/list.graphql b/crates/rover-client/src/query/subgraph/list.graphql new file mode 100644 index 000000000..d733b65aa --- /dev/null +++ b/crates/rover-client/src/query/subgraph/list.graphql @@ -0,0 +1,15 @@ +query ListSubgraphsQuery($graphId: ID!, $variant: String!) { + frontendUrlRoot + service(id: $graphId) { + implementingServices(graphVariant: $variant) { + __typename + ... on FederatedImplementingServices { + services { + name + url + updatedAt + } + } + } + } +} diff --git a/crates/rover-client/src/query/subgraph/list.rs b/crates/rover-client/src/query/subgraph/list.rs new file mode 100644 index 000000000..2b3169a30 --- /dev/null +++ b/crates/rover-client/src/query/subgraph/list.rs @@ -0,0 +1,180 @@ +use crate::blocking::StudioClient; +use crate::RoverClientError; +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, // optional, and may not be a real url + pub updated_at: 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, RoverClientError> { + let graph_name = variables.graph_id.clone(); + let response_data = client.post::(variables)?; + let subgraphs = get_subgraphs_from_response_data(response_data, graph_name)?; + Ok(format_subgraphs(subgraphs)) +} + +type RawSubgraphInfo = list_subgraphs_query::ListSubgraphsQueryServiceImplementingServicesOnFederatedImplementingServicesServices; +fn get_subgraphs_from_response_data( + response_data: list_subgraphs_query::ResponseData, + graph_name: String, +) -> Result, 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.clone(), + }), + }?; + + // implementing_services.services + match services { + list_subgraphs_query::ListSubgraphsQueryServiceImplementingServices::FederatedImplementingServices (services) => { + Ok(services.services) + }, + list_subgraphs_query::ListSubgraphsQueryServiceImplementingServices::NonFederatedImplementingService => { + Err(RoverClientError::ExpectedFederatedGraph { graph_name }) + } + } +} + +/// 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: Vec) -> Vec { + let mut subgraphs: Vec = subgraphs + .iter() + .map(|subgraph| SubgraphInfo { + name: subgraph.name.clone(), + url: subgraph.url.clone(), + updated_at: subgraph.updated_at.clone(), + }) + .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.partial_cmp(&b.updated_at).unwrap().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 = + 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!({ + "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 = + 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()); + } +} diff --git a/crates/rover-client/src/query/subgraph/mod.rs b/crates/rover-client/src/query/subgraph/mod.rs index 08bc3180f..017a8c95d 100644 --- a/crates/rover-client/src/query/subgraph/mod.rs +++ b/crates/rover-client/src/query/subgraph/mod.rs @@ -9,3 +9,6 @@ pub mod fetch; /// "subgraph push" command execution pub mod push; + +/// "subgraph list" +pub mod list; diff --git a/src/command/output.rs b/src/command/output.rs index cdabd23cf..c006a7088 100644 --- a/src/command/output.rs +++ b/src/command/output.rs @@ -1,4 +1,6 @@ use atty::{self, Stream}; +use prettytable::{cell, row, Table}; +use rover_client::query::subgraph::list::SubgraphInfo; /// RoverStdout defines all of the different types of data that are printed /// to `stdout`. Every one of Rover's commands should return `anyhow::Result` @@ -12,6 +14,7 @@ use atty::{self, Stream}; pub enum RoverStdout { SDL(String), SchemaHash(String), + SubgraphList(Vec), None, } @@ -36,6 +39,24 @@ impl RoverStdout { } println!("{}", &hash); } + RoverStdout::SubgraphList(subgraphs) => { + 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 subgraphs { + table.add_row(row![ + subgraph.name, + subgraph.url.clone().unwrap_or_else(|| "".to_string()), + subgraph.updated_at + ]); + } + + println!("{}", table); + } RoverStdout::None => (), } } diff --git a/src/command/subgraph/list.rs b/src/command/subgraph/list.rs new file mode 100644 index 000000000..5144d6ba1 --- /dev/null +++ b/src/command/subgraph/list.rs @@ -0,0 +1,47 @@ +use anyhow::{Context, Result}; +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}; + +#[derive(Debug, Serialize, StructOpt)] +pub struct List { + /// @ of graph in Apollo Studio to list subgraphs from. + /// @ 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 { + 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 subgraphs = list::run( + list::list_subgraphs_query::Variables { + graph_id: self.graph.name.clone(), + variant: self.graph.variant.clone(), + }, + &client, + ) + .context("Failed while fetching subgraph list from Apollo Studio")?; + + Ok(RoverStdout::SubgraphList(subgraphs)) + } +} diff --git a/src/command/subgraph/mod.rs b/src/command/subgraph/mod.rs index a9d79b8bf..8fb0f7e9b 100644 --- a/src/command/subgraph/mod.rs +++ b/src/command/subgraph/mod.rs @@ -1,6 +1,7 @@ mod check; mod delete; mod fetch; +mod list; mod push; use serde::Serialize; @@ -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 { @@ -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), } } } From 83c6ddc256f0264db1e0f3b3c39fc6bc085d51db Mon Sep 17 00:00:00 2001 From: Jake Dawkins Date: Tue, 19 Jan 2021 09:43:52 -0500 Subject: [PATCH 2/9] feat: add studio url to full service info --- .../rover-client/src/query/subgraph/list.rs | 29 ++++++++++++++----- src/command/output.rs | 12 +++++--- src/command/subgraph/list.rs | 4 +-- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/crates/rover-client/src/query/subgraph/list.rs b/crates/rover-client/src/query/subgraph/list.rs index 2b3169a30..d88ae84bd 100644 --- a/crates/rover-client/src/query/subgraph/list.rs +++ b/crates/rover-client/src/query/subgraph/list.rs @@ -25,21 +25,33 @@ pub struct SubgraphInfo { pub updated_at: String, } +#[derive(Clone, PartialEq, Debug)] +pub struct ListDetails { + pub subgraphs: Vec, + 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, RoverClientError> { +) -> Result { let graph_name = variables.graph_id.clone(); let response_data = client.post::(variables)?; - let subgraphs = get_subgraphs_from_response_data(response_data, graph_name)?; - Ok(format_subgraphs(subgraphs)) + 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: String, + graph_name: &str, ) -> Result, RoverClientError> { let service_data = match response_data.service { Some(data) => Ok(data), @@ -54,7 +66,7 @@ fn get_subgraphs_from_response_data( // 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.clone(), + graph_name: graph_name.to_string(), }), }?; @@ -64,7 +76,7 @@ fn get_subgraphs_from_response_data( Ok(services.services) }, list_subgraphs_query::ListSubgraphsQueryServiceImplementingServices::NonFederatedImplementingService => { - Err(RoverClientError::ExpectedFederatedGraph { graph_name }) + Err(RoverClientError::ExpectedFederatedGraph { graph_name: graph_name.to_string() }) } } } @@ -118,7 +130,7 @@ mod tests { }); 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 output = get_subgraphs_from_response_data(data, &"service".to_string()); let expected_json = json!([ { @@ -142,13 +154,14 @@ mod tests { #[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()); + let output = get_subgraphs_from_response_data(data, &"service".to_string()); assert!(output.is_err()); } diff --git a/src/command/output.rs b/src/command/output.rs index c006a7088..ae24da01d 100644 --- a/src/command/output.rs +++ b/src/command/output.rs @@ -1,6 +1,6 @@ use atty::{self, Stream}; use prettytable::{cell, row, Table}; -use rover_client::query::subgraph::list::SubgraphInfo; +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` @@ -14,7 +14,7 @@ use rover_client::query::subgraph::list::SubgraphInfo; pub enum RoverStdout { SDL(String), SchemaHash(String), - SubgraphList(Vec), + SubgraphList(ListDetails), None, } @@ -39,7 +39,7 @@ impl RoverStdout { } println!("{}", &hash); } - RoverStdout::SubgraphList(subgraphs) => { + RoverStdout::SubgraphList(details) => { if atty::is(Stream::Stdout) { tracing::info!("Subgraphs:"); } @@ -47,7 +47,7 @@ impl RoverStdout { let mut table = Table::new(); table.add_row(row!["Name", "Routing Url", "Last Updated"]); - for subgraph in subgraphs { + for subgraph in &details.subgraphs { table.add_row(row![ subgraph.name, subgraph.url.clone().unwrap_or_else(|| "".to_string()), @@ -56,6 +56,10 @@ impl RoverStdout { } println!("{}", table); + println!( + "View full details at {}/graph/{}/service-list", + details.root_url, details.graph_name + ); } RoverStdout::None => (), } diff --git a/src/command/subgraph/list.rs b/src/command/subgraph/list.rs index 5144d6ba1..52d72be61 100644 --- a/src/command/subgraph/list.rs +++ b/src/command/subgraph/list.rs @@ -33,7 +33,7 @@ impl List { &self.profile_name ); - let subgraphs = list::run( + let list_details = list::run( list::list_subgraphs_query::Variables { graph_id: self.graph.name.clone(), variant: self.graph.variant.clone(), @@ -42,6 +42,6 @@ impl List { ) .context("Failed while fetching subgraph list from Apollo Studio")?; - Ok(RoverStdout::SubgraphList(subgraphs)) + Ok(RoverStdout::SubgraphList(list_details)) } } From a748eaa2134e19f286a55e4ed05de06878793a6f Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 25 Jan 2021 09:28:35 -0500 Subject: [PATCH 3/9] wip --- .vscode/launch.json | 16 ++++++++++++++++ Cargo.lock | 1 + crates/rover-client/Cargo.toml | 1 + crates/rover-client/src/query/subgraph/list.rs | 4 ++-- src/command/output.rs | 10 +++++----- 5 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..a2626296b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug", + "program": "${workspaceFolder}/", + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c2e865f88..0f066109a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1483,6 +1483,7 @@ name = "rover-client" version = "0.0.0" dependencies = [ "anyhow", + "chrono", "graphql_client", "http", "online", diff --git a/crates/rover-client/Cargo.toml b/crates/rover-client/Cargo.toml index b6e007798..bb6e13360 100644 --- a/crates/rover-client/Cargo.toml +++ b/crates/rover-client/Cargo.toml @@ -14,6 +14,7 @@ serde = "1" serde_json = "1" thiserror = "1" tracing = "0.1" +chrono = "0.4" [build-dependencies] online = "0.2.2" diff --git a/crates/rover-client/src/query/subgraph/list.rs b/crates/rover-client/src/query/subgraph/list.rs index d88ae84bd..10d3bc171 100644 --- a/crates/rover-client/src/query/subgraph/list.rs +++ b/crates/rover-client/src/query/subgraph/list.rs @@ -42,7 +42,7 @@ pub fn run( 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), + subgraphs: format_subgraphs(&subgraphs), root_url, graph_name, }) @@ -83,7 +83,7 @@ fn get_subgraphs_from_response_data( /// 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: Vec) -> Vec { +fn format_subgraphs(subgraphs: &[RawSubgraphInfo]) -> Vec { let mut subgraphs: Vec = subgraphs .iter() .map(|subgraph| SubgraphInfo { diff --git a/src/command/output.rs b/src/command/output.rs index ae24da01d..b5955060a 100644 --- a/src/command/output.rs +++ b/src/command/output.rs @@ -48,11 +48,11 @@ impl RoverStdout { table.add_row(row!["Name", "Routing Url", "Last Updated"]); for subgraph in &details.subgraphs { - table.add_row(row![ - subgraph.name, - subgraph.url.clone().unwrap_or_else(|| "".to_string()), - subgraph.updated_at - ]); + // 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 == "" { "N/A".to_string() } else { url }; + + table.add_row(row![subgraph.name, url, subgraph.updated_at]); } println!("{}", table); From 862c5f63848303351d132b5ea54d2d9b01d6a8e0 Mon Sep 17 00:00:00 2001 From: Jake Dawkins Date: Thu, 28 Jan 2021 22:20:04 -0500 Subject: [PATCH 4/9] feat(list): update subgraph list with chrono datetime --- Cargo.lock | 1 + Cargo.toml | 1 + .../rover-client/src/query/subgraph/list.rs | 55 ++++++++++--------- src/command/output.rs | 9 ++- src/command/subgraph/list.rs | 5 +- 5 files changed, 40 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f066109a..6d4fadbfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1459,6 +1459,7 @@ dependencies = [ "assert_fs", "atty", "binstall", + "chrono", "console", "heck", "houston", diff --git a/Cargo.toml b/Cargo.toml index 1667b3fc3..860e982ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/rover-client/src/query/subgraph/list.rs b/crates/rover-client/src/query/subgraph/list.rs index 10d3bc171..c5640d903 100644 --- a/crates/rover-client/src/query/subgraph/list.rs +++ b/crates/rover-client/src/query/subgraph/list.rs @@ -1,5 +1,6 @@ use crate::blocking::StudioClient; use crate::RoverClientError; +use chrono::prelude::*; use graphql_client::*; type Timestamp = String; @@ -22,7 +23,7 @@ pub struct ListSubgraphsQuery; pub struct SubgraphInfo { pub name: String, pub url: Option, // optional, and may not be a real url - pub updated_at: String, + pub updated_at: Option>, } #[derive(Clone, PartialEq, Debug)] @@ -89,7 +90,7 @@ fn format_subgraphs(subgraphs: &[RawSubgraphInfo]) -> Vec { .map(|subgraph| SubgraphInfo { name: subgraph.name.clone(), url: subgraph.url.clone(), - updated_at: subgraph.updated_at.clone(), + updated_at: subgraph.updated_at.clone().parse::>().ok(), }) .collect(); @@ -165,29 +166,29 @@ mod tests { 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 = - 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()); - } + // #[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 = + // 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()); + // } } diff --git a/src/command/output.rs b/src/command/output.rs index b5955060a..b8436f55f 100644 --- a/src/command/output.rs +++ b/src/command/output.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use atty::{self, Stream}; use prettytable::{cell, row, Table}; use rover_client::query::subgraph::list::ListDetails; @@ -51,8 +53,13 @@ impl RoverStdout { // 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 == "" { "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, subgraph.updated_at]); + table.add_row(row![subgraph.name, url, formatted_updated_at]); } println!("{}", table); diff --git a/src/command/subgraph/list.rs b/src/command/subgraph/list.rs index 52d72be61..b0ef80553 100644 --- a/src/command/subgraph/list.rs +++ b/src/command/subgraph/list.rs @@ -1,4 +1,3 @@ -use anyhow::{Context, Result}; use serde::Serialize; use structopt::StructOpt; @@ -7,6 +6,7 @@ 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 { @@ -39,8 +39,7 @@ impl List { variant: self.graph.variant.clone(), }, &client, - ) - .context("Failed while fetching subgraph list from Apollo Studio")?; + )?; Ok(RoverStdout::SubgraphList(list_details)) } From 12df861bdc2c91eddde2fd0af64c5cfdea0e0132 Mon Sep 17 00:00:00 2001 From: Jake Dawkins Date: Thu, 28 Jan 2021 22:21:27 -0500 Subject: [PATCH 5/9] fix: remove accidental commit --- .vscode/launch.json | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index a2626296b..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug", - "program": "${workspaceFolder}/", - "args": [], - "cwd": "${workspaceFolder}" - } - ] -} \ No newline at end of file From 7e2a1b9d880b6c023419c3cfc9bf85194e853baf Mon Sep 17 00:00:00 2001 From: Jake Dawkins Date: Thu, 28 Jan 2021 22:28:26 -0500 Subject: [PATCH 6/9] fix: formatting & replace partial cmp --- crates/rover-client/src/query/subgraph/list.rs | 2 +- src/command/output.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/rover-client/src/query/subgraph/list.rs b/crates/rover-client/src/query/subgraph/list.rs index c5640d903..83b59e9c1 100644 --- a/crates/rover-client/src/query/subgraph/list.rs +++ b/crates/rover-client/src/query/subgraph/list.rs @@ -97,7 +97,7 @@ fn format_subgraphs(subgraphs: &[RawSubgraphInfo]) -> Vec { // 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.partial_cmp(&b.updated_at).unwrap().reverse()); + subgraphs.sort_unstable_by(|a, b| a.updated_at.cmp(&b.updated_at).reverse()); subgraphs } diff --git a/src/command/output.rs b/src/command/output.rs index b8436f55f..426dda516 100644 --- a/src/command/output.rs +++ b/src/command/output.rs @@ -52,7 +52,11 @@ impl RoverStdout { 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 == "" { "N/A".to_string() } else { url }; + 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 { From 520eeca4ad5f6f31ebba288e1a80be1731dabf9f Mon Sep 17 00:00:00 2001 From: Jake Dawkins Date: Fri, 29 Jan 2021 10:03:34 -0500 Subject: [PATCH 7/9] fix: docs & nits :) --- crates/rover-client/src/query/subgraph/list.rs | 12 ++++++------ src/command/output.rs | 4 +--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/crates/rover-client/src/query/subgraph/list.rs b/crates/rover-client/src/query/subgraph/list.rs index 83b59e9c1..903209926 100644 --- a/crates/rover-client/src/query/subgraph/list.rs +++ b/crates/rover-client/src/query/subgraph/list.rs @@ -62,12 +62,12 @@ fn get_subgraphs_from_response_data( // 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(), + // TODO (api-error) + // this case is unreachable, since even non-federated graphs will return + // an implementing service, just under the NonFederatedImplementingService + // fragment spread + None => Err(RoverClientError::HandleResponse { + msg: "There was no response for implementing services of this graph. This error shouldn't ever happen.".to_string() }), }?; diff --git a/src/command/output.rs b/src/command/output.rs index 426dda516..d4e1199ef 100644 --- a/src/command/output.rs +++ b/src/command/output.rs @@ -42,9 +42,7 @@ impl RoverStdout { println!("{}", &hash); } RoverStdout::SubgraphList(details) => { - if atty::is(Stream::Stdout) { - tracing::info!("Subgraphs:"); - } + println!("Subgraphs:\n\n"); let mut table = Table::new(); table.add_row(row!["Name", "Routing Url", "Last Updated"]); From 33bf9a9ec3e5d348745098503055708bf26f4cdc Mon Sep 17 00:00:00 2001 From: Jake Dawkins Date: Fri, 29 Jan 2021 10:13:05 -0500 Subject: [PATCH 8/9] docs: add subgraph list to public docs --- docs/source/subgraphs.md | 34 ++++++++++++++++++++++++++++++++++ src/command/output.rs | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/docs/source/subgraphs.md b/docs/source/subgraphs.md index 527ffc3f2..c1ba4c955 100644 --- a/docs/source/subgraphs.md +++ b/docs/source/subgraphs.md @@ -68,6 +68,40 @@ rover subgraph introspect http://localhost:4000 > accounts-schema.graphql > For more on passing values via `stdout`, see [Essential concepts](./essentials#using-stdout). +## Listing subgraphs for a graph + +> This requires first [authenticating Rover with Apollo Studio](./configuring/#authenticating-with-apollo-studio). + +A federated graph is composed of multiple subgraphs. You can use Rover to list +the subgraphs available to work with in Apollo Studio using the `subgraph list` +command. + +```bash +rover subgraph list my-graph@dev +``` + +This command lists all subgraphs for a variant, including their routing urls +and when they were last updated (in local time), along with a link to view them +in Apollo Studio. + +``` +Subgraphs: + ++----------+-------------- --------------+----------------------------+ +| Name | Routing Url | Last Updated | ++----------+-----------------------------+----------------------------+ +| reviews | https://reviews.my-app.com | 2020-10-21 12:23:28 -04:00 | ++----------+----------------------------------------+-----------------+ +| books | https://books.my-app.com | 2020-09-20 13:58:27 -04:00 | ++----------+----------------------------------------+-----------------+ +| accounts | https://accounts.my-app.com | 2020-09-20 12:23:36 -04:00 | ++----------+----------------------------------------+-----------------+ +| products | https://products.my-app.com | 2020-09-20 12:23:28 -04:00 | ++----------+----------------------------------------+-----------------+ + +View full details at https://studio.apollographql.com/graph/my-graph/service-list +``` + ## Pushing a subgraph schema to Apollo Studio > This requires first [authenticating Rover with Apollo Studio](./configuring/#authenticating-with-apollo-studio). diff --git a/src/command/output.rs b/src/command/output.rs index d4e1199ef..fcdaf3f99 100644 --- a/src/command/output.rs +++ b/src/command/output.rs @@ -42,7 +42,7 @@ impl RoverStdout { println!("{}", &hash); } RoverStdout::SubgraphList(details) => { - println!("Subgraphs:\n\n"); + println!("Subgraphs:\n"); let mut table = Table::new(); table.add_row(row!["Name", "Routing Url", "Last Updated"]); From 24bb26ead7fba801ae1f06c7d8fa61b75f80ff8e Mon Sep 17 00:00:00 2001 From: Jake Dawkins Date: Fri, 29 Jan 2021 10:15:14 -0500 Subject: [PATCH 9/9] test: re-enable test --- .../rover-client/src/query/subgraph/list.rs | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/crates/rover-client/src/query/subgraph/list.rs b/crates/rover-client/src/query/subgraph/list.rs index 903209926..1cf86530e 100644 --- a/crates/rover-client/src/query/subgraph/list.rs +++ b/crates/rover-client/src/query/subgraph/list.rs @@ -166,29 +166,29 @@ mod tests { 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 = - // 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()); - // } + #[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 = + 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()); + } }