|
| 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 | +} |
0 commit comments