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 all 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),
// 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()
}),
}?;

// 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;
34 changes: 34 additions & 0 deletions docs/source/subgraphs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
34 changes: 34 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,35 @@ impl RoverStdout {
}
println!("{}", &hash);
}
RoverStdout::SubgraphList(details) => {
println!("Subgraphs:\n");

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))
}
}
Loading