Skip to content

Commit 616ae59

Browse files
authored
Add subgraph list command (#185)
* feat: add subgraph list command * feat(list): update subgraph list with chrono datetime * docs: add subgraph list to public docs
1 parent 17fd355 commit 616ae59

File tree

10 files changed

+335
-0
lines changed

10 files changed

+335
-0
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ structopt = "0.3.21"
2828
tracing = "0.1.22"
2929
regex = "1"
3030
url = "2.2.0"
31+
chrono = "0.4"
3132

3233
[dev-dependencies]
3334
assert_cmd = "1.0.1"

crates/rover-client/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ serde = "1"
1414
serde_json = "1"
1515
thiserror = "1"
1616
tracing = "0.1"
17+
chrono = "0.4"
1718

1819
[build-dependencies]
1920
online = "0.2.2"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
query ListSubgraphsQuery($graphId: ID!, $variant: String!) {
2+
frontendUrlRoot
3+
service(id: $graphId) {
4+
implementingServices(graphVariant: $variant) {
5+
__typename
6+
... on FederatedImplementingServices {
7+
services {
8+
name
9+
url
10+
updatedAt
11+
}
12+
}
13+
}
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
use crate::blocking::StudioClient;
2+
use crate::RoverClientError;
3+
use chrono::prelude::*;
4+
use graphql_client::*;
5+
6+
type Timestamp = String;
7+
8+
#[derive(GraphQLQuery)]
9+
// The paths are relative to the directory where your `Cargo.toml` is located.
10+
// Both json and the GraphQL schema language are supported as sources for the schema
11+
#[graphql(
12+
query_path = "src/query/subgraph/list.graphql",
13+
schema_path = ".schema/schema.graphql",
14+
response_derives = "PartialEq, Debug, Serialize, Deserialize",
15+
deprecated = "warn"
16+
)]
17+
/// This struct is used to generate the module containing `Variables` and
18+
/// `ResponseData` structs.
19+
/// Snake case of this name is the mod name. i.e. list_subgraphs_query
20+
pub struct ListSubgraphsQuery;
21+
22+
#[derive(Clone, PartialEq, Debug)]
23+
pub struct SubgraphInfo {
24+
pub name: String,
25+
pub url: Option<String>, // optional, and may not be a real url
26+
pub updated_at: Option<DateTime<Local>>,
27+
}
28+
29+
#[derive(Clone, PartialEq, Debug)]
30+
pub struct ListDetails {
31+
pub subgraphs: Vec<SubgraphInfo>,
32+
pub root_url: String,
33+
pub graph_name: String,
34+
}
35+
36+
/// Fetches list of subgraphs for a given graph, returns name & url of each
37+
pub fn run(
38+
variables: list_subgraphs_query::Variables,
39+
client: &StudioClient,
40+
) -> Result<ListDetails, RoverClientError> {
41+
let graph_name = variables.graph_id.clone();
42+
let response_data = client.post::<ListSubgraphsQuery>(variables)?;
43+
let root_url = response_data.frontend_url_root.clone();
44+
let subgraphs = get_subgraphs_from_response_data(response_data, &graph_name)?;
45+
Ok(ListDetails {
46+
subgraphs: format_subgraphs(&subgraphs),
47+
root_url,
48+
graph_name,
49+
})
50+
}
51+
52+
type RawSubgraphInfo = list_subgraphs_query::ListSubgraphsQueryServiceImplementingServicesOnFederatedImplementingServicesServices;
53+
fn get_subgraphs_from_response_data(
54+
response_data: list_subgraphs_query::ResponseData,
55+
graph_name: &str,
56+
) -> Result<Vec<RawSubgraphInfo>, RoverClientError> {
57+
let service_data = match response_data.service {
58+
Some(data) => Ok(data),
59+
None => Err(RoverClientError::NoService),
60+
}?;
61+
62+
// get list of services
63+
let services = match service_data.implementing_services {
64+
Some(services) => Ok(services),
65+
// TODO (api-error)
66+
// this case is unreachable, since even non-federated graphs will return
67+
// an implementing service, just under the NonFederatedImplementingService
68+
// fragment spread
69+
None => Err(RoverClientError::HandleResponse {
70+
msg: "There was no response for implementing services of this graph. This error shouldn't ever happen.".to_string()
71+
}),
72+
}?;
73+
74+
// implementing_services.services
75+
match services {
76+
list_subgraphs_query::ListSubgraphsQueryServiceImplementingServices::FederatedImplementingServices (services) => {
77+
Ok(services.services)
78+
},
79+
list_subgraphs_query::ListSubgraphsQueryServiceImplementingServices::NonFederatedImplementingService => {
80+
Err(RoverClientError::ExpectedFederatedGraph { graph_name: graph_name.to_string() })
81+
}
82+
}
83+
}
84+
85+
/// puts the subgraphs into a vec of SubgraphInfo, sorted by updated_at
86+
/// timestamp. Newer updated services will show at top of list
87+
fn format_subgraphs(subgraphs: &[RawSubgraphInfo]) -> Vec<SubgraphInfo> {
88+
let mut subgraphs: Vec<SubgraphInfo> = subgraphs
89+
.iter()
90+
.map(|subgraph| SubgraphInfo {
91+
name: subgraph.name.clone(),
92+
url: subgraph.url.clone(),
93+
updated_at: subgraph.updated_at.clone().parse::<DateTime<Local>>().ok(),
94+
})
95+
.collect();
96+
97+
// sort and reverse, so newer items come first. We use _unstable here, since
98+
// we don't care which order equal items come in the list (it's unlikely that
99+
// we'll even have equal items after all)
100+
subgraphs.sort_unstable_by(|a, b| a.updated_at.cmp(&b.updated_at).reverse());
101+
102+
subgraphs
103+
}
104+
105+
#[cfg(test)]
106+
mod tests {
107+
use super::*;
108+
use serde_json::json;
109+
110+
#[test]
111+
fn get_subgraphs_from_response_data_works() {
112+
let json_response = json!({
113+
"frontendUrlRoot": "https://studio.apollographql.com/",
114+
"service": {
115+
"implementingServices": {
116+
"__typename": "FederatedImplementingServices",
117+
"services": [
118+
{
119+
"name": "accounts",
120+
"url": "https://localhost:3000",
121+
"updatedAt": "2020-09-24T18:53:08.683Z"
122+
},
123+
{
124+
"name": "products",
125+
"url": "https://localhost:3001",
126+
"updatedAt": "2020-09-16T19:22:06.420Z"
127+
}
128+
]
129+
}
130+
}
131+
});
132+
let data: list_subgraphs_query::ResponseData =
133+
serde_json::from_value(json_response).unwrap();
134+
let output = get_subgraphs_from_response_data(data, &"service".to_string());
135+
136+
let expected_json = json!([
137+
{
138+
"name": "accounts",
139+
"url": "https://localhost:3000",
140+
"updatedAt": "2020-09-24T18:53:08.683Z"
141+
},
142+
{
143+
"name": "products",
144+
"url": "https://localhost:3001",
145+
"updatedAt": "2020-09-16T19:22:06.420Z"
146+
}
147+
]);
148+
let expected_service_list: Vec<RawSubgraphInfo> =
149+
serde_json::from_value(expected_json).unwrap();
150+
151+
assert!(output.is_ok());
152+
assert_eq!(output.unwrap(), expected_service_list);
153+
}
154+
155+
#[test]
156+
fn get_subgraphs_from_response_data_errs_with_no_services() {
157+
let json_response = json!({
158+
"frontendUrlRoot": "https://harambe.com",
159+
"service": {
160+
"implementingServices": null
161+
}
162+
});
163+
let data: list_subgraphs_query::ResponseData =
164+
serde_json::from_value(json_response).unwrap();
165+
let output = get_subgraphs_from_response_data(data, &"service".to_string());
166+
assert!(output.is_err());
167+
}
168+
169+
#[test]
170+
fn format_subgraphs_builds_and_sorts_subgraphs() {
171+
let raw_info_json = json!([
172+
{
173+
"name": "accounts",
174+
"url": "https://localhost:3000",
175+
"updatedAt": "2020-09-24T18:53:08.683Z"
176+
},
177+
{
178+
"name": "shipping",
179+
"url": "https://localhost:3002",
180+
"updatedAt": "2020-09-16T17:22:06.420Z"
181+
},
182+
{
183+
"name": "products",
184+
"url": "https://localhost:3001",
185+
"updatedAt": "2020-09-16T19:22:06.420Z"
186+
}
187+
]);
188+
let raw_subgraph_list: Vec<RawSubgraphInfo> =
189+
serde_json::from_value(raw_info_json).unwrap();
190+
let formatted = format_subgraphs(&raw_subgraph_list);
191+
assert_eq!(formatted[0].name, "accounts".to_string());
192+
assert_eq!(formatted[2].name, "shipping".to_string());
193+
}
194+
}

crates/rover-client/src/query/subgraph/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ pub mod fetch;
99

1010
/// "subgraph push" command execution
1111
pub mod push;
12+
13+
/// "subgraph list"
14+
pub mod list;

docs/source/subgraphs.md

+34
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,40 @@ rover subgraph introspect http://localhost:4000 > accounts-schema.graphql
6868

6969
> For more on passing values via `stdout`, see [Essential concepts](./essentials#using-stdout).
7070
71+
## Listing subgraphs for a graph
72+
73+
> This requires first [authenticating Rover with Apollo Studio](./configuring/#authenticating-with-apollo-studio).
74+
75+
A federated graph is composed of multiple subgraphs. You can use Rover to list
76+
the subgraphs available to work with in Apollo Studio using the `subgraph list`
77+
command.
78+
79+
```bash
80+
rover subgraph list my-graph@dev
81+
```
82+
83+
This command lists all subgraphs for a variant, including their routing urls
84+
and when they were last updated (in local time), along with a link to view them
85+
in Apollo Studio.
86+
87+
```
88+
Subgraphs:
89+
90+
+----------+-------------- --------------+----------------------------+
91+
| Name | Routing Url | Last Updated |
92+
+----------+-----------------------------+----------------------------+
93+
| reviews | https://reviews.my-app.com | 2020-10-21 12:23:28 -04:00 |
94+
+----------+----------------------------------------+-----------------+
95+
| books | https://books.my-app.com | 2020-09-20 13:58:27 -04:00 |
96+
+----------+----------------------------------------+-----------------+
97+
| accounts | https://accounts.my-app.com | 2020-09-20 12:23:36 -04:00 |
98+
+----------+----------------------------------------+-----------------+
99+
| products | https://products.my-app.com | 2020-09-20 12:23:28 -04:00 |
100+
+----------+----------------------------------------+-----------------+
101+
102+
View full details at https://studio.apollographql.com/graph/my-graph/service-list
103+
```
104+
71105
## Pushing a subgraph schema to Apollo Studio
72106

73107
> This requires first [authenticating Rover with Apollo Studio](./configuring/#authenticating-with-apollo-studio).

src/command/output.rs

+34
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
use std::fmt::Debug;
2+
13
use atty::{self, Stream};
4+
use prettytable::{cell, row, Table};
5+
use rover_client::query::subgraph::list::ListDetails;
26

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

@@ -36,6 +41,35 @@ impl RoverStdout {
3641
}
3742
println!("{}", &hash);
3843
}
44+
RoverStdout::SubgraphList(details) => {
45+
println!("Subgraphs:\n");
46+
47+
let mut table = Table::new();
48+
table.add_row(row!["Name", "Routing Url", "Last Updated"]);
49+
50+
for subgraph in &details.subgraphs {
51+
// if the url is None or empty (""), then set it to "N/A"
52+
let url = subgraph.url.clone().unwrap_or_else(|| "N/A".to_string());
53+
let url = if url.is_empty() {
54+
"N/A".to_string()
55+
} else {
56+
url
57+
};
58+
let formatted_updated_at: String = if let Some(dt) = subgraph.updated_at {
59+
dt.format("%Y-%m-%d %H:%M:%S %Z").to_string()
60+
} else {
61+
"N/A".to_string()
62+
};
63+
64+
table.add_row(row![subgraph.name, url, formatted_updated_at]);
65+
}
66+
67+
println!("{}", table);
68+
println!(
69+
"View full details at {}/graph/{}/service-list",
70+
details.root_url, details.graph_name
71+
);
72+
}
3973
RoverStdout::None => (),
4074
}
4175
}

src/command/subgraph/list.rs

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use serde::Serialize;
2+
use structopt::StructOpt;
3+
4+
use rover_client::query::subgraph::list;
5+
6+
use crate::client::StudioClientConfig;
7+
use crate::command::RoverStdout;
8+
use crate::utils::parsers::{parse_graph_ref, GraphRef};
9+
use crate::Result;
10+
11+
#[derive(Debug, Serialize, StructOpt)]
12+
pub struct List {
13+
/// <NAME>@<VARIANT> of graph in Apollo Studio to list subgraphs from.
14+
/// @<VARIANT> may be left off, defaulting to @current
15+
#[structopt(name = "GRAPH_REF", parse(try_from_str = parse_graph_ref))]
16+
#[serde(skip_serializing)]
17+
graph: GraphRef,
18+
19+
/// Name of configuration profile to use
20+
#[structopt(long = "profile", default_value = "default")]
21+
#[serde(skip_serializing)]
22+
profile_name: String,
23+
}
24+
25+
impl List {
26+
pub fn run(&self, client_config: StudioClientConfig) -> Result<RoverStdout> {
27+
let client = client_config.get_client(&self.profile_name)?;
28+
29+
tracing::info!(
30+
"Listing subgraphs for {}@{}, mx. {}!",
31+
&self.graph.name,
32+
&self.graph.variant,
33+
&self.profile_name
34+
);
35+
36+
let list_details = list::run(
37+
list::list_subgraphs_query::Variables {
38+
graph_id: self.graph.name.clone(),
39+
variant: self.graph.variant.clone(),
40+
},
41+
&client,
42+
)?;
43+
44+
Ok(RoverStdout::SubgraphList(list_details))
45+
}
46+
}

0 commit comments

Comments
 (0)