diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2932cfe2b..1b117879a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -116,8 +116,8 @@ A minimal command in Rover would be laid out exactly like this: pub struct MyNewCommand { } impl MyNewCommand { - pub fn run(&self) -> Result { - Ok(RoverStdout::None) + pub fn run(&self) -> Result { + Ok(RoverOutput::None) } } ``` @@ -128,16 +128,16 @@ For our `graph hello` command, we'll add a new `hello.rs` file under `src/comman use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::Result; #[derive(Debug, Serialize, StructOpt)] pub struct Hello { } impl Hello { - pub fn run(&self) -> Result { + pub fn run(&self) -> Result { eprintln!("Hello, world!"); - Ok(RoverStdout::None) + Ok(RoverOutput::None) } } ``` @@ -195,7 +195,7 @@ To add these to our new `graph hello` command, we can copy and paste the field f pub struct Hello { /// @ of graph in Apollo Studio to publish to. /// @ may be left off, defaulting to @current - #[structopt(name = "GRAPH_REF", parse(try_from_str = parse_graph_ref))] + #[structopt(name = "GRAPH_REF"))] #[serde(skip_serializing)] graph: GraphRef @@ -206,12 +206,6 @@ pub struct Hello { } ``` -We'll have to also add some import statements at the top of our file to support parsing this new argument: - -```rust -use crate::utils::parsers::{parse_graph_ref, GraphRef}; -``` - Now if we run the command again, it will complain if we don't provide a graph ref: ```console @@ -228,13 +222,13 @@ For more information try --help ##### Setting up a command to work with `rover-client` -Most of Rover's commands make requests to Apollo Studio's API. Rather than handling the request logic in the repository's main package, Rover is structured so that this logic lives in `crates/rover-client`. This is helpful for separation of concerns and testing. +Most of Rover's commands make requests to Apollo Studio's API, or to another GraphQL API. Rather than handling the request logic in the repository's main package, Rover is structured so that this logic lives in `crates/rover-client`. This is helpful for separation of concerns and testing. To access functionality from `rover-client` in our `rover graph hello` command, we'll need to pass down a client from the entry to our command in `src/command/graph/mod.rs`. You can do this by changing the `Command::Hello(command) => command.run(),` line to `Command::Hello(command) => command.run(client_config),`. -Then you'll need to change `Hello::run` to accept a `client_config: StudioClientConfig` parameter in `src/command/graph/hello.rs`, and add a `use crate::utils::client::StudioClientConfig` import statement. Then, at the top of the run function, you can create a `StudioClient` by adding `let client = client_config.get_client(&self.profile_name)?;`. You can see examples of this in the other commands. +Then you'll need to change `Hello::run` to accept a `client_config: StudioClientConfig` parameter in `src/command/graph/hello.rs`, and add a `use crate::utils::client::StudioClientConfig` import statement. Then, at the top of the run function, you can create a `StudioClient` by adding `let client = client_config.get_authenticated_client(&self.profile_name)?;`. You can see examples of this in the other commands. ##### Auto-generated help command @@ -271,19 +265,19 @@ Whenever you create a new command, make sure to add `#[serde(skip_serializing)]` ##### Adding a query to Apollo Studio -The only piece of the `rover-client` crate that we need to be concerned with for now is the `src/query` directory. This is where all the queries to Apollo Studio live. This directory is roughly organized by the command names as well, but there might be some queries in these directories that are used by multiple commands. +The only piece of the `rover-client` crate that we need to be concerned with for now is the `src/operations` directory. This is where all the queries to Apollo Studio live. This directory is roughly organized by the command names as well, but there might be some queries in these directories that are used by multiple commands. -You can see in the `src/query/graph` directory a number of `.rs` files paired with `.graphql` files. The `.graphql` files are the files where the GraphQL operations live, and the matching `.rs` files contain the logic needed to execute those operations. +You can see in the `src/operations/graph` directory a number of `.rs` files paired with `.graphql` files. The `.graphql` files are the files where the GraphQL operations live, and the matching `.rs` files contain the logic needed to execute those operations. ##### Writing a GraphQL operation For our basic `graph hello` command, we're going to make a request to Apollo Studio that inquires about the existence of a particular graph, and nothing else. For this, we can use the `Query.service` field. -Create a `hello.graphql` file in `crates/rover-client/src/query/graph` and paste the following into it: +Create a `hello_query.graphql` file in `crates/rover-client/src/operations/graph` and paste the following into it: ```graphql -query GraphHello($graphId: ID!) { - service(id: $graphId) { +query GraphHello($graph_id: ID!) { + service(id: $graph_id) { deletedAt } } @@ -295,17 +289,19 @@ This basic GraphQL operation uses a graph's unique ID (which we get from the `Gr This project uses [graphql-client](https://docs.rs/graphql_client/latest/graphql_client/) to generate types for each raw `.graphql` query that we write. -First, create an empty file at `crates/rover-client/src/query/graph/hello.rs`. +First, create an empty directory at `crates/rover-client/src/operations/graph/hello`, and then in that directory, create a `mod.rs` file to initialize the module. -To start compiling this file, we need to export the module in `crates/rover-client/src/query/graph/mod.rs`: +To start compiling this file, we need to export the module in `crates/rover-client/src/operations/graph/mod.rs`: ```rust ... -/// "Graph hello" command execution +/// "graph hello" command execution pub mod hello; ``` -Back in `hello.rs`, we'll import the following types: +Back in our `hello` module, we'll create a `runner.rs`, and add `mod runner` to our `mod.rs` file. + +Then, in `runner.rs`, import the following types: ```rust use crate::blocking::StudioClient; @@ -313,14 +309,14 @@ use crate::RoverClientError; use graphql_client::*; ``` -Then, we'll create a new struct that will have auto-generated types for the `hello.graphql` file that we created earlier: +Then, we'll create a new struct that will have auto-generated types for the `hello_query.graphql` file that we created earlier: ```rust #[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/graph/hello.graphql", + query_path = "src/operations/graph/hello/hello_query.graphql", schema_path = ".schema/schema.graphql", response_derives = "PartialEq, Debug, Serialize, Deserialize", deprecated = "warn" @@ -352,7 +348,7 @@ Before we go any further, lets make sure everything is set up properly. We're go It should look something like this (you should make sure you are following the style of other commands when creating new ones): ```rust -pub fn run(&self, client_config: StudioClientConfig) -> Result { +pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_client(&self.profile_name)?; let graph_ref = self.graph.to_string(); eprintln!( @@ -366,7 +362,10 @@ pub fn run(&self, client_config: StudioClientConfig) -> Result { }, &client, )?; - Ok(RoverStdout::PlainText(deleted_at)) + println!("{:?}", deleted_at); + + // TODO: Add a new output type! + Ok(RoverOutput::None) } ``` @@ -395,19 +394,40 @@ fn build_response( } ``` -This should get you to the point where you can run `rover graph hello ` and see if and when the last graph was deleted. From here, you should be able to follow the examples of other commands to write out tests for the `build_response` function. This is left as an exercise for the reader. +This should get you to the point where you can run `rover graph hello ` and see if and when the last graph was deleted. From here, you should be able to follow the examples of other commands to write out tests for the `build_response` function. + +##### Clean up the API -##### `RoverStdout` +Unfortunately this is not the cleanest API and doesn't match the pattern set by the rest of the commands. Each `rover-client` operation has an input type and an output type, along with a `run` function that takes in a `reqwest::blocking::Client`. -Now that you can actually execute the `hello::run` query and return its result, you should create a new variant of `RoverStdout` in `src/command/output.rs` that is not `PlainText`. Your new variant should print the descriptor using the `print_descriptor` function, and print the raw content using `print_content`. +You'll want to define all of the types scoped to this command in `types.rs`, and re-export them from the top level `hello` module, and nothing else. -To do so, change the line `Ok(RoverStdout::PlainText(deleted_at))` to `Ok(RoverStdout::DeletedAt(deleted_at))`, add a new `DeletedAt(String)` variant to `RoverStdout`, and then match on it in `pub fn print(&self)`: +##### `RoverOutput` + +Now that you can actually execute the `hello::run` query and return its result, you should create a new variant of `RoverOutput` in `src/command/output.rs` that is not `None`. Your new variant should print the descriptor using the `print_descriptor` function, and print the raw content using `print_content`. + +To do so, change the line `Ok(RoverOutput::None)` to `Ok(RoverOutput::DeletedAt(deleted_at))`, add a new `DeletedAt(String)` variant to `RoverOutput`, and then match on it in `pub fn print(&self)` and `pub fn get_json(&self)`: ```rust -... -RoverStdout::DeletedAt(timestamp) => { - print_descriptor("Deleted At"); - print_content(×tamp); +pub fn print(&self) { + match self { + ... + RoverOutput::DeletedAt(timestamp) => { + print_descriptor("Deleted At"); + print_content(×tamp); + } + ... + } +} + +pub fn get_json(&self) -> Value { + match self { + ... + RoverOutput::DeletedAt(timestamp) => { + json!({ "deleted_at": timestamp.to_string() }) + } + ... + } } ``` diff --git a/Cargo.lock b/Cargo.lock index 2de86aef3..4d617f085 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,6 +83,16 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" +[[package]] +name = "assert-json-diff" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f1c3703dd33532d7f0ca049168930e9099ecac238e23cf932f3a69c42f06da" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "assert_cmd" version = "1.0.7" @@ -321,6 +331,7 @@ dependencies = [ "libc", "num-integer", "num-traits", + "serde", "time", "winapi", ] @@ -1107,9 +1118,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.9" +version = "0.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07d6baa1b441335f3ce5098ac421fb6547c46dda735ca1bc6d0153c838f9dd83" +checksum = "7728a72c4c7d72665fde02204bcbd93b247721025b222ef78606f14513e0fd03" dependencies = [ "bytes", "futures-channel", @@ -1192,9 +1203,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" dependencies = [ "cfg-if", ] @@ -1246,9 +1257,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.97" +version = "0.2.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" [[package]] name = "libgit2-sys" @@ -1915,6 +1926,7 @@ version = "0.1.9" dependencies = [ "ansi_term 0.12.1", "anyhow", + "assert-json-diff", "assert_cmd", "assert_fs", "atty", @@ -1929,7 +1941,6 @@ dependencies = [ "harmonizer", "heck", "houston", - "humantime", "opener", "os_info", "predicates", @@ -1939,7 +1950,6 @@ dependencies = [ "robot-panic", "rover-client", "sdl-encoder", - "semver", "serde", "serde_json", "serde_yaml", @@ -1963,15 +1973,20 @@ version = "0.0.0" dependencies = [ "camino", "chrono", + "git-url-parse", + "git2", "graphql_client", "houston", "http", + "humantime", "indoc", "online", "pretty_assertions", + "prettytable-rs", "regex", "reqwest", "sdl-encoder", + "semver", "serde", "serde_json", "thiserror", @@ -2347,9 +2362,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +checksum = "474aaa926faa1603c40b7885a9eaea29b444d1cb2850cb7c0e37bb1a4182f4fa" dependencies = [ "proc-macro2", "quote", @@ -2498,9 +2513,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "570c2eb13b3ab38208130eccd41be92520388791207fde783bda7c1e8ace28d4" +checksum = "98c8b05dc14c75ea83d63dd391100353789f5f24b8b3866542a5e85c8be8e985" dependencies = [ "autocfg", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 2a6ea99bb..6e123d5d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,13 +53,10 @@ git-url-parse = "0.3.1" git2 = { version = "0.13.20", default-features = false, features = ["vendored-openssl"] } harmonizer = { version = "0.27.0", optional = true } heck = "0.3.3" -humantime = "2.1.0" opener = "0.5.0" os_info = "3.0" prettytable-rs = "0.8.0" -reqwest = {version = "0.11", default-features = false, features = ["blocking", "brotli", "gzip", "json", "native-tls-vendored"]} -regex = "1" -semver = "1" +reqwest = {version = "0.11.4", default-features = false, features = ["blocking"] } serde = "1.0" serde_json = "1.0" serde_yaml = "0.8" @@ -75,6 +72,7 @@ url = { version = "2.2.2", features = ["serde"] } [dev-dependencies] assert_cmd = "1.0.7" assert_fs = "1.0.3" +assert-json-diff = "2.0.1" predicates = "2.0.0" reqwest = { version = "0.11.4", default-features = false, features = ["blocking", "native-tls-vendored"] } serial_test = "0.5.0" diff --git a/crates/rover-client/.eslintrc.json b/crates/rover-client/.eslintrc.json new file mode 100644 index 000000000..fc09e7e9a --- /dev/null +++ b/crates/rover-client/.eslintrc.json @@ -0,0 +1,60 @@ +{ + "root": true, + "overrides": [ + { + "files": ["*.graphql"], + "parser": "@graphql-eslint/eslint-plugin", + "plugins": ["@graphql-eslint"], + "parserOptions": { + "schema": "./.schema/schema.graphql", + "operations": ["./src/operations/**/*.graphql"] + }, + "rules": { + "@graphql-eslint/avoid-duplicate-fields": 2, + "@graphql-eslint/avoid-operation-name-prefix": 2, + "@graphql-eslint/avoid-typename-prefix": 2, + "@graphql-eslint/description-style": 2, + "@graphql-eslint/executable-definitions": 2, + "@graphql-eslint/fields-on-correct-type": 2, + "@graphql-eslint/fragments-on-composite-type": 2, + "@graphql-eslint/input-name": 2, + "@graphql-eslint/known-argument-names": 2, + "@graphql-eslint/known-directives": 2, + "@graphql-eslint/known-fragment-names": 2, + "@graphql-eslint/known-type-names": 2, + "@graphql-eslint/lone-anonymous-operation": 2, + "@graphql-eslint/no-anonymous-operations": 2, + "@graphql-eslint/no-case-insensitive-enum-values-duplicates": 2, + "@graphql-eslint/no-deprecated": 2, + "@graphql-eslint/no-fragment-cycles": 2, + "@graphql-eslint/no-hashtag-description": 2, + "@graphql-eslint/no-undefined-variables": 2, + "@graphql-eslint/no-unused-fields": 2, + "@graphql-eslint/no-unused-fragments": 2, + "@graphql-eslint/no-unused-variables": 2, + "@graphql-eslint/one-field-subscriptions": 2, + "@graphql-eslint/overlapping-fields-can-be-merged": 2, + "@graphql-eslint/possible-fragment-spread": 2, + "@graphql-eslint/possible-type-extension": 2, + "@graphql-eslint/provided-required-arguments": 2, + "@graphql-eslint/require-deprecation-reason": 2, + "@graphql-eslint/scalar-leafs": 2, + "@graphql-eslint/strict-id-in-types": 2, + "@graphql-eslint/unique-argument-names": 2, + "@graphql-eslint/unique-directive-names": 2, + "@graphql-eslint/unique-directive-names-per-location": 2, + "@graphql-eslint/unique-enum-value-names": 2, + "@graphql-eslint/unique-field-definition-names": 2, + "@graphql-eslint/unique-fragment-name": 2, + "@graphql-eslint/unique-input-field-names": 2, + "@graphql-eslint/unique-operation-name": 2, + "@graphql-eslint/unique-operation-types": 2, + "@graphql-eslint/unique-type-names": 2, + "@graphql-eslint/unique-variable-names": 2, + "@graphql-eslint/value-literals-of-correct-type": 2, + "@graphql-eslint/variables-are-input-types": 2, + "@graphql-eslint/variables-in-allowed-position": 2 + } + } + ] +} diff --git a/crates/rover-client/Cargo.toml b/crates/rover-client/Cargo.toml index 03de68888..e1c5315de 100644 --- a/crates/rover-client/Cargo.toml +++ b/crates/rover-client/Cargo.toml @@ -12,12 +12,17 @@ houston = {path = "../houston"} # crates.io deps camino = "1" -chrono = "0.4" +chrono = { version = "0.4", features = ["serde"] } +git-url-parse = "0.3.1" +git2 = { version = "0.13.20", default-features = false, features = ["vendored-openssl"] } graphql_client = "0.9" http = "0.2" -regex = "1.5.4" -reqwest = {version = "0.11", default-features = false, features = ["blocking", "json"]} +humantime = "2.1.0" +prettytable-rs = "0.8.0" +reqwest = { version = "0.11", default-features = false, features = ["blocking", "brotli", "gzip", "json", "native-tls-vendored"] } +regex = "1" sdl-encoder = {path = "../sdl-encoder"} +semver = "1" serde = "1" serde_json = "1" thiserror = "1" diff --git a/crates/rover-client/package-lock.json b/crates/rover-client/package-lock.json new file mode 100644 index 000000000..45898cc82 --- /dev/null +++ b/crates/rover-client/package-lock.json @@ -0,0 +1,5041 @@ +{ + "name": "rover-client", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "version": "0.0.0", + "devDependencies": { + "@graphql-eslint/eslint-plugin": "^1.1.3", + "eslint": "^7.30.0", + "graphql": "^15.5.1" + } + }, + "node_modules/@ardatan/aggregate-error": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@ardatan/aggregate-error/-/aggregate-error-0.0.6.tgz", + "integrity": "sha512-vyrkEHG1jrukmzTPtyWB4NLPauUw5bQeg4uhn8f+1SSynmrOcyvlb1GKQjjgoBzElLdfXCRYX8UnBlhklOHYRQ==", + "dev": true, + "dependencies": { + "tslib": "~2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ardatan/aggregate-error/node_modules/tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "dev": true + }, + "node_modules/@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz", + "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.14.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", + "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.14.5", + "@babel/template": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name/node_modules/@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-get-function-arity": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", + "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-get-function-arity/node_modules/@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", + "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", + "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.12.16", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.16.tgz", + "integrity": "sha512-c/+u9cqV6F0+4Hpq01jnJO+GLp2DdT63ppz9Xa+6cHaajM9VFzK/iDXiKK65YtpeVwu+ctfS6iqlMqRgQRzeCw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", + "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/parser": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz", + "integrity": "sha512-3Zb4w7eE/OslI0fTp8c7b286/cQps3+vdLW3UcwC8VSJC6GbKn55aeVVu2QJNuCDoeKyptLOFrPq8WqZZBodyA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.12.13", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "node_modules/@babel/types": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz", + "integrity": "sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@endemolshinegroup/cosmiconfig-typescript-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.0.2.tgz", + "integrity": "sha512-QRVtqJuS1mcT56oHpVegkKBlgtWjXw/gHNWO3eL9oyB5Sc7HBoc2OLG/nYpVfT/Jejvo3NUrD0Udk7XgoyDKkA==", + "dev": true, + "dependencies": { + "lodash.get": "^4", + "make-error": "^1", + "ts-node": "^9", + "tslib": "^2" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "cosmiconfig": ">=6" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.2.tgz", + "integrity": "sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz", + "integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@graphql-eslint/eslint-plugin": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@graphql-eslint/eslint-plugin/-/eslint-plugin-1.1.3.tgz", + "integrity": "sha512-PLbNLFkfXOFI62qN644OiPk5iuX3YUpTs6KNCHGxyO9ZgHPxnU2XpOIW6rlQ8pqgJdOlEVLUn6u+7aGKOonGKA==", + "dev": true, + "dependencies": { + "@graphql-tools/code-file-loader": "~6.3.0", + "@graphql-tools/graphql-file-loader": "~6.2.0", + "@graphql-tools/graphql-tag-pluck": "~6.5.0", + "@graphql-tools/import": "^6.3.1", + "@graphql-tools/json-file-loader": "~6.2.6", + "@graphql-tools/load": "~6.2.0", + "@graphql-tools/url-loader": "~6.10.0", + "@graphql-tools/utils": "~7.10.0", + "graphql-config": "^3.2.0", + "graphql-depth-limit": "1.1.0" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" + } + }, + "node_modules/@graphql-tools/batch-execute": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-7.1.2.tgz", + "integrity": "sha512-IuR2SB2MnC2ztA/XeTMTfWcA0Wy7ZH5u+nDkDNLAdX+AaSyDnsQS35sCmHqG0VOGTl7rzoyBWLCKGwSJplgtwg==", + "dev": true, + "dependencies": { + "@graphql-tools/utils": "^7.7.0", + "dataloader": "2.0.0", + "tslib": "~2.2.0", + "value-or-promise": "1.0.6" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@graphql-tools/batch-execute/node_modules/tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + }, + "node_modules/@graphql-tools/code-file-loader": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-6.3.1.tgz", + "integrity": "sha512-ZJimcm2ig+avgsEOWWVvAaxZrXXhiiSZyYYOJi0hk9wh5BxZcLUNKkTp6EFnZE/jmGUwuos3pIjUD3Hwi3Bwhg==", + "dev": true, + "dependencies": { + "@graphql-tools/graphql-tag-pluck": "^6.5.1", + "@graphql-tools/utils": "^7.0.0", + "tslib": "~2.1.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@graphql-tools/delegate": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-7.1.5.tgz", + "integrity": "sha512-bQu+hDd37e+FZ0CQGEEczmRSfQRnnXeUxI/0miDV+NV/zCbEdIJj5tYFNrKT03W6wgdqx8U06d8L23LxvGri/g==", + "dev": true, + "dependencies": { + "@ardatan/aggregate-error": "0.0.6", + "@graphql-tools/batch-execute": "^7.1.2", + "@graphql-tools/schema": "^7.1.5", + "@graphql-tools/utils": "^7.7.1", + "dataloader": "2.0.0", + "tslib": "~2.2.0", + "value-or-promise": "1.0.6" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@graphql-tools/delegate/node_modules/tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + }, + "node_modules/@graphql-tools/graphql-file-loader": { + "version": "6.2.7", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-6.2.7.tgz", + "integrity": "sha512-5k2SNz0W87tDcymhEMZMkd6/vs6QawDyjQXWtqkuLTBF3vxjxPD1I4dwHoxgWPIjjANhXybvulD7E+St/7s9TQ==", + "dev": true, + "dependencies": { + "@graphql-tools/import": "^6.2.6", + "@graphql-tools/utils": "^7.0.0", + "tslib": "~2.1.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@graphql-tools/graphql-tag-pluck": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-6.5.1.tgz", + "integrity": "sha512-7qkm82iFmcpb8M6/yRgzjShtW6Qu2OlCSZp8uatA3J0eMl87TxyJoUmL3M3UMMOSundAK8GmoyNVFUrueueV5Q==", + "dev": true, + "dependencies": { + "@babel/parser": "7.12.16", + "@babel/traverse": "7.12.13", + "@babel/types": "7.12.13", + "@graphql-tools/utils": "^7.0.0", + "tslib": "~2.1.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@graphql-tools/import": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-6.3.1.tgz", + "integrity": "sha512-1szR19JI6WPibjYurMLdadHKZoG9C//8I/FZ0Dt4vJSbrMdVNp8WFxg4QnZrDeMG4MzZc90etsyF5ofKjcC+jw==", + "dev": true, + "dependencies": { + "resolve-from": "5.0.0", + "tslib": "~2.2.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@graphql-tools/import/node_modules/tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + }, + "node_modules/@graphql-tools/json-file-loader": { + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-6.2.6.tgz", + "integrity": "sha512-CnfwBSY5926zyb6fkDBHnlTblHnHI4hoBALFYXnrg0Ev4yWU8B04DZl/pBRUc459VNgO2x8/mxGIZj2hPJG1EA==", + "dev": true, + "dependencies": { + "@graphql-tools/utils": "^7.0.0", + "tslib": "~2.0.1" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@graphql-tools/json-file-loader/node_modules/tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "dev": true + }, + "node_modules/@graphql-tools/load": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-6.2.8.tgz", + "integrity": "sha512-JpbyXOXd8fJXdBh2ta0Q4w8ia6uK5FHzrTNmcvYBvflFuWly2LDTk2abbSl81zKkzswQMEd2UIYghXELRg8eTA==", + "dev": true, + "dependencies": { + "@graphql-tools/merge": "^6.2.12", + "@graphql-tools/utils": "^7.5.0", + "globby": "11.0.3", + "import-from": "3.0.0", + "is-glob": "4.0.1", + "p-limit": "3.1.0", + "tslib": "~2.2.0", + "unixify": "1.0.0", + "valid-url": "1.0.9" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@graphql-tools/load/node_modules/tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + }, + "node_modules/@graphql-tools/merge": { + "version": "6.2.14", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.2.14.tgz", + "integrity": "sha512-RWT4Td0ROJai2eR66NHejgf8UwnXJqZxXgDWDI+7hua5vNA2OW8Mf9K1Wav1ZkjWnuRp4ztNtkZGie5ISw55ow==", + "dev": true, + "dependencies": { + "@graphql-tools/schema": "^7.0.0", + "@graphql-tools/utils": "^7.7.0", + "tslib": "~2.2.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@graphql-tools/merge/node_modules/tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + }, + "node_modules/@graphql-tools/schema": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-7.1.5.tgz", + "integrity": "sha512-uyn3HSNSckf4mvQSq0Q07CPaVZMNFCYEVxroApOaw802m9DcZPgf9XVPy/gda5GWj9AhbijfRYVTZQgHnJ4CXA==", + "dev": true, + "dependencies": { + "@graphql-tools/utils": "^7.1.2", + "tslib": "~2.2.0", + "value-or-promise": "1.0.6" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@graphql-tools/schema/node_modules/tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + }, + "node_modules/@graphql-tools/url-loader": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-6.10.1.tgz", + "integrity": "sha512-DSDrbhQIv7fheQ60pfDpGD256ixUQIR6Hhf9Z5bRjVkXOCvO5XrkwoWLiU7iHL81GB1r0Ba31bf+sl+D4nyyfw==", + "dev": true, + "dependencies": { + "@graphql-tools/delegate": "^7.0.1", + "@graphql-tools/utils": "^7.9.0", + "@graphql-tools/wrap": "^7.0.4", + "@microsoft/fetch-event-source": "2.0.1", + "@types/websocket": "1.0.2", + "abort-controller": "3.0.0", + "cross-fetch": "3.1.4", + "extract-files": "9.0.0", + "form-data": "4.0.0", + "graphql-ws": "^4.4.1", + "is-promise": "4.0.0", + "isomorphic-ws": "4.0.1", + "lodash": "4.17.21", + "meros": "1.1.4", + "subscriptions-transport-ws": "^0.9.18", + "sync-fetch": "0.3.0", + "tslib": "~2.2.0", + "valid-url": "1.0.9", + "ws": "7.4.5" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@graphql-tools/url-loader/node_modules/tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + }, + "node_modules/@graphql-tools/utils": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-7.10.0.tgz", + "integrity": "sha512-d334r6bo9mxdSqZW6zWboEnnOOFRrAPVQJ7LkU8/6grglrbcu6WhwCLzHb90E94JI3TD3ricC3YGbUqIi9Xg0w==", + "dev": true, + "dependencies": { + "@ardatan/aggregate-error": "0.0.6", + "camel-case": "4.1.2", + "tslib": "~2.2.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@graphql-tools/utils/node_modules/tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + }, + "node_modules/@graphql-tools/wrap": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-7.0.8.tgz", + "integrity": "sha512-1NDUymworsOlb53Qfh7fonDi2STvqCtbeE68ntKY9K/Ju/be2ZNxrFSbrBHwnxWcN9PjISNnLcAyJ1L5tCUyhg==", + "dev": true, + "dependencies": { + "@graphql-tools/delegate": "^7.1.5", + "@graphql-tools/schema": "^7.1.5", + "@graphql-tools/utils": "^7.8.1", + "tslib": "~2.2.0", + "value-or-promise": "1.0.6" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0" + } + }, + "node_modules/@graphql-tools/wrap/node_modules/tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", + "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "dev": true + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", + "dev": true + }, + "node_modules/@microsoft/fetch-event-source": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz", + "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/node": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.2.tgz", + "integrity": "sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw==", + "dev": true + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "node_modules/@types/websocket": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.2.tgz", + "integrity": "sha512-B5m9aq7cbbD/5/jThEr33nUY8WEfVi6A2YKCTOvw5Ldy7mtsOkqRvGjnzy6g7iMMDsgu7xREuCzqATLDLQVKcQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "node_modules/backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig-toml-loader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-toml-loader/-/cosmiconfig-toml-loader-1.0.0.tgz", + "integrity": "sha512-H/2gurFWVi7xXvCyvsWRLCMekl4tITJcX0QEsDMpzxtuxDyM59xLatYNg4s/k9AA/HdtCYfj2su8mgA0GSDLDA==", + "dev": true, + "dependencies": { + "@iarna/toml": "^2.2.5" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-fetch": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz", + "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==", + "dev": true, + "dependencies": { + "node-fetch": "2.6.1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dataloader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.0.0.tgz", + "integrity": "sha512-YzhyDAwA4TaQIhM5go+vCLmU0UikghC/t9DTQYZR2M/UvZ1MdOhPezSDZcjj9uqQJOMqjLcpWtyW2iNINdlatQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz", + "integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.2", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz", + "integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", + "dev": true + }, + "node_modules/extract-files": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz", + "integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==", + "dev": true, + "engines": { + "node": "^10.17.0 || ^12.0.0 || >= 13.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/jaydenseric" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/fastq": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.1.tgz", + "integrity": "sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.1.tgz", + "integrity": "sha512-OMQjaErSFHmHqZe+PSidH5n8j3O0F2DdnVh8JB4j4eUQ2k6KvB0qGfrKIhapvez5JerBbmWkaLYUYWISaESoXg==", + "dev": true + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", + "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphql": { + "version": "15.5.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.5.1.tgz", + "integrity": "sha512-FeTRX67T3LoE3LWAxxOlW2K3Bz+rMYAC18rRguK4wgXaTZMiJwSUwDmPFo3UadAKbzirKIg5Qy+sNJXbpPRnQw==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/graphql-config": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-3.3.0.tgz", + "integrity": "sha512-mSQIsPMssr7QrgqhnjI+CyVH6oQgCrgS6irHsTvwf7RFDRnR2k9kqpQOQgVoOytBSn0DOYryS0w0SAg9xor/Jw==", + "dev": true, + "dependencies": { + "@endemolshinegroup/cosmiconfig-typescript-loader": "3.0.2", + "@graphql-tools/graphql-file-loader": "^6.0.0", + "@graphql-tools/json-file-loader": "^6.0.0", + "@graphql-tools/load": "^6.0.0", + "@graphql-tools/merge": "^6.0.0", + "@graphql-tools/url-loader": "^6.0.0", + "@graphql-tools/utils": "^7.0.0", + "cosmiconfig": "7.0.0", + "cosmiconfig-toml-loader": "1.0.0", + "minimatch": "3.0.4", + "string-env-interpolation": "1.0.1" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" + } + }, + "node_modules/graphql-depth-limit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/graphql-depth-limit/-/graphql-depth-limit-1.1.0.tgz", + "integrity": "sha512-+3B2BaG8qQ8E18kzk9yiSdAa75i/hnnOwgSeAxVJctGQPvmeiLtqKOYF6HETCyRjiF7Xfsyal0HbLlxCQkgkrw==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "graphql": "*" + } + }, + "node_modules/graphql-ws": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.9.0.tgz", + "integrity": "sha512-sHkK9+lUm20/BGawNEWNtVAeJzhZeBg21VmvmLoT5NdGVeZWv5PdIhkcayQIAgjSyyQ17WMKmbDijIPG2On+Ag==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": ">=0.11 <=15" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "dev": true, + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/iterall": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", + "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/meros": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.1.4.tgz", + "integrity": "sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@types/node": ">=12" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", + "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.31", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", + "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", + "dev": true, + "dependencies": { + "mime-db": "1.48.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "dev": true, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/string-env-interpolation": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz", + "integrity": "sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/subscriptions-transport-ws": { + "version": "0.9.19", + "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.19.tgz", + "integrity": "sha512-dxdemxFFB0ppCLg10FTtRqH/31FNRL1y1BQv8209MK5I4CwALb7iihQg+7p65lFcIl8MHatINWBLOqpgU4Kyyw==", + "dev": true, + "dependencies": { + "backo2": "^1.0.2", + "eventemitter3": "^3.1.0", + "iterall": "^1.2.1", + "symbol-observable": "^1.0.4", + "ws": "^5.2.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependencies": { + "graphql": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sync-fetch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.3.0.tgz", + "integrity": "sha512-dJp4qg+x4JwSEW1HibAuMi0IIrBI3wuQr2GimmqB7OXR50wmwzfdusG+p39R9w3R6aFtZ2mzvxvWKQ3Bd/vx3g==", + "dev": true, + "dependencies": { + "buffer": "^5.7.0", + "node-fetch": "^2.6.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", + "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.clonedeep": "^4.5.0", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.1.tgz", + "integrity": "sha512-42VLtQUOLefAvKFAQIxIZDaThq6om/PrfP0CYk3/vn+y4BMNkKnbli8ON2QCiHov4KkzOSJ/xSoBJdayiiYvVQ==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "dependencies": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "typescript": ">=2.7" + } + }, + "node_modules/tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unixify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz", + "integrity": "sha1-OmQcjC/7zk2mg6XHDwOkYpQMIJA=", + "dev": true, + "dependencies": { + "normalize-path": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=", + "dev": true + }, + "node_modules/value-or-promise": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.6.tgz", + "integrity": "sha512-9r0wQsWD8z/BxPOvnwbPf05ZvFngXyouE9EKB+5GbYix+BYnAwrIChCUyFIinfbf2FL/U71z+CPpbnmTdxrwBg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/ws": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@ardatan/aggregate-error": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@ardatan/aggregate-error/-/aggregate-error-0.0.6.tgz", + "integrity": "sha512-vyrkEHG1jrukmzTPtyWB4NLPauUw5bQeg4uhn8f+1SSynmrOcyvlb1GKQjjgoBzElLdfXCRYX8UnBlhklOHYRQ==", + "dev": true, + "requires": { + "tslib": "~2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "dev": true + } + } + }, + "@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/generator": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz", + "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-function-name": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", + "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.14.5", + "@babel/template": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-get-function-arity": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", + "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", + "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", + "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.12.16", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.16.tgz", + "integrity": "sha512-c/+u9cqV6F0+4Hpq01jnJO+GLp2DdT63ppz9Xa+6cHaajM9VFzK/iDXiKK65YtpeVwu+ctfS6iqlMqRgQRzeCw==", + "dev": true + }, + "@babel/template": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", + "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/parser": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==", + "dev": true + }, + "@babel/types": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", + "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/traverse": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz", + "integrity": "sha512-3Zb4w7eE/OslI0fTp8c7b286/cQps3+vdLW3UcwC8VSJC6GbKn55aeVVu2QJNuCDoeKyptLOFrPq8WqZZBodyA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.12.13", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "@babel/types": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz", + "integrity": "sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "@endemolshinegroup/cosmiconfig-typescript-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.0.2.tgz", + "integrity": "sha512-QRVtqJuS1mcT56oHpVegkKBlgtWjXw/gHNWO3eL9oyB5Sc7HBoc2OLG/nYpVfT/Jejvo3NUrD0Udk7XgoyDKkA==", + "dev": true, + "requires": { + "lodash.get": "^4", + "make-error": "^1", + "ts-node": "^9", + "tslib": "^2" + } + }, + "@eslint/eslintrc": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.2.tgz", + "integrity": "sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "globals": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz", + "integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + } + } + }, + "@graphql-eslint/eslint-plugin": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@graphql-eslint/eslint-plugin/-/eslint-plugin-1.1.3.tgz", + "integrity": "sha512-PLbNLFkfXOFI62qN644OiPk5iuX3YUpTs6KNCHGxyO9ZgHPxnU2XpOIW6rlQ8pqgJdOlEVLUn6u+7aGKOonGKA==", + "dev": true, + "requires": { + "@graphql-tools/code-file-loader": "~6.3.0", + "@graphql-tools/graphql-file-loader": "~6.2.0", + "@graphql-tools/graphql-tag-pluck": "~6.5.0", + "@graphql-tools/import": "^6.3.1", + "@graphql-tools/json-file-loader": "~6.2.6", + "@graphql-tools/load": "~6.2.0", + "@graphql-tools/url-loader": "~6.10.0", + "@graphql-tools/utils": "~7.10.0", + "graphql-config": "^3.2.0", + "graphql-depth-limit": "1.1.0" + } + }, + "@graphql-tools/batch-execute": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-7.1.2.tgz", + "integrity": "sha512-IuR2SB2MnC2ztA/XeTMTfWcA0Wy7ZH5u+nDkDNLAdX+AaSyDnsQS35sCmHqG0VOGTl7rzoyBWLCKGwSJplgtwg==", + "dev": true, + "requires": { + "@graphql-tools/utils": "^7.7.0", + "dataloader": "2.0.0", + "tslib": "~2.2.0", + "value-or-promise": "1.0.6" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } + } + }, + "@graphql-tools/code-file-loader": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-6.3.1.tgz", + "integrity": "sha512-ZJimcm2ig+avgsEOWWVvAaxZrXXhiiSZyYYOJi0hk9wh5BxZcLUNKkTp6EFnZE/jmGUwuos3pIjUD3Hwi3Bwhg==", + "dev": true, + "requires": { + "@graphql-tools/graphql-tag-pluck": "^6.5.1", + "@graphql-tools/utils": "^7.0.0", + "tslib": "~2.1.0" + } + }, + "@graphql-tools/delegate": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-7.1.5.tgz", + "integrity": "sha512-bQu+hDd37e+FZ0CQGEEczmRSfQRnnXeUxI/0miDV+NV/zCbEdIJj5tYFNrKT03W6wgdqx8U06d8L23LxvGri/g==", + "dev": true, + "requires": { + "@ardatan/aggregate-error": "0.0.6", + "@graphql-tools/batch-execute": "^7.1.2", + "@graphql-tools/schema": "^7.1.5", + "@graphql-tools/utils": "^7.7.1", + "dataloader": "2.0.0", + "tslib": "~2.2.0", + "value-or-promise": "1.0.6" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } + } + }, + "@graphql-tools/graphql-file-loader": { + "version": "6.2.7", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-6.2.7.tgz", + "integrity": "sha512-5k2SNz0W87tDcymhEMZMkd6/vs6QawDyjQXWtqkuLTBF3vxjxPD1I4dwHoxgWPIjjANhXybvulD7E+St/7s9TQ==", + "dev": true, + "requires": { + "@graphql-tools/import": "^6.2.6", + "@graphql-tools/utils": "^7.0.0", + "tslib": "~2.1.0" + } + }, + "@graphql-tools/graphql-tag-pluck": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-6.5.1.tgz", + "integrity": "sha512-7qkm82iFmcpb8M6/yRgzjShtW6Qu2OlCSZp8uatA3J0eMl87TxyJoUmL3M3UMMOSundAK8GmoyNVFUrueueV5Q==", + "dev": true, + "requires": { + "@babel/parser": "7.12.16", + "@babel/traverse": "7.12.13", + "@babel/types": "7.12.13", + "@graphql-tools/utils": "^7.0.0", + "tslib": "~2.1.0" + } + }, + "@graphql-tools/import": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-6.3.1.tgz", + "integrity": "sha512-1szR19JI6WPibjYurMLdadHKZoG9C//8I/FZ0Dt4vJSbrMdVNp8WFxg4QnZrDeMG4MzZc90etsyF5ofKjcC+jw==", + "dev": true, + "requires": { + "resolve-from": "5.0.0", + "tslib": "~2.2.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } + } + }, + "@graphql-tools/json-file-loader": { + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-6.2.6.tgz", + "integrity": "sha512-CnfwBSY5926zyb6fkDBHnlTblHnHI4hoBALFYXnrg0Ev4yWU8B04DZl/pBRUc459VNgO2x8/mxGIZj2hPJG1EA==", + "dev": true, + "requires": { + "@graphql-tools/utils": "^7.0.0", + "tslib": "~2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==", + "dev": true + } + } + }, + "@graphql-tools/load": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-6.2.8.tgz", + "integrity": "sha512-JpbyXOXd8fJXdBh2ta0Q4w8ia6uK5FHzrTNmcvYBvflFuWly2LDTk2abbSl81zKkzswQMEd2UIYghXELRg8eTA==", + "dev": true, + "requires": { + "@graphql-tools/merge": "^6.2.12", + "@graphql-tools/utils": "^7.5.0", + "globby": "11.0.3", + "import-from": "3.0.0", + "is-glob": "4.0.1", + "p-limit": "3.1.0", + "tslib": "~2.2.0", + "unixify": "1.0.0", + "valid-url": "1.0.9" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } + } + }, + "@graphql-tools/merge": { + "version": "6.2.14", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.2.14.tgz", + "integrity": "sha512-RWT4Td0ROJai2eR66NHejgf8UwnXJqZxXgDWDI+7hua5vNA2OW8Mf9K1Wav1ZkjWnuRp4ztNtkZGie5ISw55ow==", + "dev": true, + "requires": { + "@graphql-tools/schema": "^7.0.0", + "@graphql-tools/utils": "^7.7.0", + "tslib": "~2.2.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } + } + }, + "@graphql-tools/schema": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-7.1.5.tgz", + "integrity": "sha512-uyn3HSNSckf4mvQSq0Q07CPaVZMNFCYEVxroApOaw802m9DcZPgf9XVPy/gda5GWj9AhbijfRYVTZQgHnJ4CXA==", + "dev": true, + "requires": { + "@graphql-tools/utils": "^7.1.2", + "tslib": "~2.2.0", + "value-or-promise": "1.0.6" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } + } + }, + "@graphql-tools/url-loader": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-6.10.1.tgz", + "integrity": "sha512-DSDrbhQIv7fheQ60pfDpGD256ixUQIR6Hhf9Z5bRjVkXOCvO5XrkwoWLiU7iHL81GB1r0Ba31bf+sl+D4nyyfw==", + "dev": true, + "requires": { + "@graphql-tools/delegate": "^7.0.1", + "@graphql-tools/utils": "^7.9.0", + "@graphql-tools/wrap": "^7.0.4", + "@microsoft/fetch-event-source": "2.0.1", + "@types/websocket": "1.0.2", + "abort-controller": "3.0.0", + "cross-fetch": "3.1.4", + "extract-files": "9.0.0", + "form-data": "4.0.0", + "graphql-ws": "^4.4.1", + "is-promise": "4.0.0", + "isomorphic-ws": "4.0.1", + "lodash": "4.17.21", + "meros": "1.1.4", + "subscriptions-transport-ws": "^0.9.18", + "sync-fetch": "0.3.0", + "tslib": "~2.2.0", + "valid-url": "1.0.9", + "ws": "7.4.5" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } + } + }, + "@graphql-tools/utils": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-7.10.0.tgz", + "integrity": "sha512-d334r6bo9mxdSqZW6zWboEnnOOFRrAPVQJ7LkU8/6grglrbcu6WhwCLzHb90E94JI3TD3ricC3YGbUqIi9Xg0w==", + "dev": true, + "requires": { + "@ardatan/aggregate-error": "0.0.6", + "camel-case": "4.1.2", + "tslib": "~2.2.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } + } + }, + "@graphql-tools/wrap": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-7.0.8.tgz", + "integrity": "sha512-1NDUymworsOlb53Qfh7fonDi2STvqCtbeE68ntKY9K/Ju/be2ZNxrFSbrBHwnxWcN9PjISNnLcAyJ1L5tCUyhg==", + "dev": true, + "requires": { + "@graphql-tools/delegate": "^7.1.5", + "@graphql-tools/schema": "^7.1.5", + "@graphql-tools/utils": "^7.8.1", + "tslib": "~2.2.0", + "value-or-promise": "1.0.6" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } + } + }, + "@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", + "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "dev": true + }, + "@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", + "dev": true + }, + "@microsoft/fetch-event-source": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz", + "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@types/node": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.2.tgz", + "integrity": "sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "@types/websocket": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.2.tgz", + "integrity": "sha512-B5m9aq7cbbD/5/jThEr33nUY8WEfVi6A2YKCTOvw5Ldy7mtsOkqRvGjnzy6g7iMMDsgu7xREuCzqATLDLQVKcQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "cosmiconfig-toml-loader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-toml-loader/-/cosmiconfig-toml-loader-1.0.0.tgz", + "integrity": "sha512-H/2gurFWVi7xXvCyvsWRLCMekl4tITJcX0QEsDMpzxtuxDyM59xLatYNg4s/k9AA/HdtCYfj2su8mgA0GSDLDA==", + "dev": true, + "requires": { + "@iarna/toml": "^2.2.5" + } + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "cross-fetch": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz", + "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==", + "dev": true, + "requires": { + "node-fetch": "2.6.1" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "dataloader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.0.0.tgz", + "integrity": "sha512-YzhyDAwA4TaQIhM5go+vCLmU0UikghC/t9DTQYZR2M/UvZ1MdOhPezSDZcjj9uqQJOMqjLcpWtyW2iNINdlatQ==", + "dev": true + }, + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.30.0.tgz", + "integrity": "sha512-VLqz80i3as3NdloY44BQSJpFw534L9Oh+6zJOUaViV4JPd+DaHwutqP7tcpkW3YiXbK6s05RZl7yl7cQn+lijg==", + "dev": true, + "requires": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.2", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "globals": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz", + "integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + }, + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true + }, + "eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", + "dev": true + }, + "extract-files": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz", + "integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fastq": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.1.tgz", + "integrity": "sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.1.tgz", + "integrity": "sha512-OMQjaErSFHmHqZe+PSidH5n8j3O0F2DdnVh8JB4j4eUQ2k6KvB0qGfrKIhapvez5JerBbmWkaLYUYWISaESoXg==", + "dev": true + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", + "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "graphql": { + "version": "15.5.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.5.1.tgz", + "integrity": "sha512-FeTRX67T3LoE3LWAxxOlW2K3Bz+rMYAC18rRguK4wgXaTZMiJwSUwDmPFo3UadAKbzirKIg5Qy+sNJXbpPRnQw==", + "dev": true + }, + "graphql-config": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-3.3.0.tgz", + "integrity": "sha512-mSQIsPMssr7QrgqhnjI+CyVH6oQgCrgS6irHsTvwf7RFDRnR2k9kqpQOQgVoOytBSn0DOYryS0w0SAg9xor/Jw==", + "dev": true, + "requires": { + "@endemolshinegroup/cosmiconfig-typescript-loader": "3.0.2", + "@graphql-tools/graphql-file-loader": "^6.0.0", + "@graphql-tools/json-file-loader": "^6.0.0", + "@graphql-tools/load": "^6.0.0", + "@graphql-tools/merge": "^6.0.0", + "@graphql-tools/url-loader": "^6.0.0", + "@graphql-tools/utils": "^7.0.0", + "cosmiconfig": "7.0.0", + "cosmiconfig-toml-loader": "1.0.0", + "minimatch": "3.0.4", + "string-env-interpolation": "1.0.1" + } + }, + "graphql-depth-limit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/graphql-depth-limit/-/graphql-depth-limit-1.1.0.tgz", + "integrity": "sha512-+3B2BaG8qQ8E18kzk9yiSdAa75i/hnnOwgSeAxVJctGQPvmeiLtqKOYF6HETCyRjiF7Xfsyal0HbLlxCQkgkrw==", + "dev": true, + "requires": { + "arrify": "^1.0.1" + } + }, + "graphql-ws": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.9.0.tgz", + "integrity": "sha512-sHkK9+lUm20/BGawNEWNtVAeJzhZeBg21VmvmLoT5NdGVeZWv5PdIhkcayQIAgjSyyQ17WMKmbDijIPG2On+Ag==", + "dev": true, + "requires": {} + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, + "import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "dev": true, + "requires": {} + }, + "iterall": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", + "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "meros": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.1.4.tgz", + "integrity": "sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ==", + "dev": true, + "requires": {} + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "mime-db": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", + "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.31", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", + "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", + "dev": true, + "requires": { + "mime-db": "1.48.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "dev": true + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-env-interpolation": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz", + "integrity": "sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==", + "dev": true + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "subscriptions-transport-ws": { + "version": "0.9.19", + "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.19.tgz", + "integrity": "sha512-dxdemxFFB0ppCLg10FTtRqH/31FNRL1y1BQv8209MK5I4CwALb7iihQg+7p65lFcIl8MHatINWBLOqpgU4Kyyw==", + "dev": true, + "requires": { + "backo2": "^1.0.2", + "eventemitter3": "^3.1.0", + "iterall": "^1.2.1", + "symbol-observable": "^1.0.4", + "ws": "^5.2.0 || ^6.0.0 || ^7.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true + }, + "sync-fetch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.3.0.tgz", + "integrity": "sha512-dJp4qg+x4JwSEW1HibAuMi0IIrBI3wuQr2GimmqB7OXR50wmwzfdusG+p39R9w3R6aFtZ2mzvxvWKQ3Bd/vx3g==", + "dev": true, + "requires": { + "buffer": "^5.7.0", + "node-fetch": "^2.6.1" + } + }, + "table": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", + "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", + "dev": true, + "requires": { + "ajv": "^8.0.1", + "lodash.clonedeep": "^4.5.0", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.1.tgz", + "integrity": "sha512-42VLtQUOLefAvKFAQIxIZDaThq6om/PrfP0CYk3/vn+y4BMNkKnbli8ON2QCiHov4KkzOSJ/xSoBJdayiiYvVQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + }, + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typescript": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "dev": true, + "peer": true + }, + "unixify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz", + "integrity": "sha1-OmQcjC/7zk2mg6XHDwOkYpQMIJA=", + "dev": true, + "requires": { + "normalize-path": "^2.1.1" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=", + "dev": true + }, + "value-or-promise": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.6.tgz", + "integrity": "sha512-9r0wQsWD8z/BxPOvnwbPf05ZvFngXyouE9EKB+5GbYix+BYnAwrIChCUyFIinfbf2FL/U71z+CPpbnmTdxrwBg==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", + "dev": true, + "requires": {} + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/crates/rover-client/package.json b/crates/rover-client/package.json new file mode 100644 index 000000000..18a0f9a59 --- /dev/null +++ b/crates/rover-client/package.json @@ -0,0 +1,14 @@ +{ + "name": "rover-client", + "version": "0.0.0", + "description": "This package is solely used for linting GraphQL schemas", + "scripts": { + "lint": "eslint ." + }, + "author": "opensource@apollographql.com", + "devDependencies": { + "@graphql-eslint/eslint-plugin": "^1.1.3", + "eslint": "^7.30.0", + "graphql": "^15.5.1" + } +} diff --git a/crates/rover-client/src/blocking/client.rs b/crates/rover-client/src/blocking/client.rs index c734a63ad..05fcaa78c 100644 --- a/crates/rover-client/src/blocking/client.rs +++ b/crates/rover-client/src/blocking/client.rs @@ -1,13 +1,16 @@ -use crate::{headers, RoverClientError}; +use crate::RoverClientError; use graphql_client::{Error as GraphQLError, GraphQLQuery, Response as GraphQLResponse}; use reqwest::{ blocking::{Client as ReqwestClient, Response}, - header::HeaderMap, + header::{HeaderMap, HeaderName, HeaderValue}, Error as ReqwestError, StatusCode, }; use std::collections::HashMap; +pub(crate) const JSON_CONTENT_TYPE: &str = "application/json"; +pub(crate) const CLIENT_NAME: &str = "rover-client"; + /// Represents a generic GraphQL client for making http requests. pub struct GraphQLClient { graphql_endpoint: String, @@ -35,7 +38,7 @@ impl GraphQLClient { variables: Q::Variables, header_map: &HashMap, ) -> Result { - let header_map = headers::build(header_map)?; + let header_map = build_headers(header_map)?; let response = self.execute::(variables, header_map)?; GraphQLClient::handle_response::(response) } @@ -125,6 +128,26 @@ fn handle_graphql_body_errors(errors: Vec) -> Result<(), RoverClie } } +/// Function for building a [HeaderMap] for making http requests. Use for +/// Generic requests to any graphql endpoint. +/// +/// Takes a single argument, list of header key/value pairs +fn build_headers(header_map: &HashMap) -> Result { + let mut headers = HeaderMap::new(); + + // this should be consistent for any graphql requests + let content_type = HeaderValue::from_str(JSON_CONTENT_TYPE)?; + headers.append("Content-Type", content_type); + + for (key, value) in header_map { + let header_key = HeaderName::from_bytes(key.as_bytes())?; + let header_value = HeaderValue::from_str(&value)?; + headers.append(header_key, header_value); + } + + Ok(headers) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/rover-client/src/blocking/mod.rs b/crates/rover-client/src/blocking/mod.rs index e51e31143..d89a42510 100644 --- a/crates/rover-client/src/blocking/mod.rs +++ b/crates/rover-client/src/blocking/mod.rs @@ -3,3 +3,5 @@ mod studio_client; pub use client::GraphQLClient; pub use studio_client::StudioClient; + +pub(crate) use client::{CLIENT_NAME, JSON_CONTENT_TYPE}; diff --git a/crates/rover-client/src/blocking/studio_client.rs b/crates/rover-client/src/blocking/studio_client.rs index a7a4d2fbc..405945f86 100644 --- a/crates/rover-client/src/blocking/studio_client.rs +++ b/crates/rover-client/src/blocking/studio_client.rs @@ -1,12 +1,17 @@ -use crate::{blocking::GraphQLClient, headers, RoverClientError}; -use houston::Credential; +use crate::{ + blocking::{GraphQLClient, CLIENT_NAME, JSON_CONTENT_TYPE}, + RoverClientError, +}; + +use houston::{Credential, CredentialOrigin}; use graphql_client::GraphQLQuery; +use reqwest::header::{HeaderMap, HeaderValue}; use reqwest::{blocking::Client as ReqwestClient, Error as ReqwestError}; /// Represents a client for making GraphQL requests to Apollo Studio. pub struct StudioClient { - pub credential: Credential, + credential: Credential, client: GraphQLClient, version: String, } @@ -34,8 +39,44 @@ impl StudioClient { &self, variables: Q::Variables, ) -> Result { - let header_map = headers::build_studio_headers(&self.credential.api_key, &self.version)?; + let header_map = self.build_studio_headers()?; let response = self.client.execute::(variables, header_map)?; GraphQLClient::handle_response::(response) } + + /// Function for building a [HeaderMap] for making http requests. Use for making + /// requests to Apollo Studio. We're leaving this separate from `build` since we + /// need to be able to mark the api_key as sensitive (at the bottom) + /// + /// Takes an `api_key` and a `client_version`, and returns a [HeaderMap]. + pub fn build_studio_headers(&self) -> Result { + let mut headers = HeaderMap::new(); + + let content_type = HeaderValue::from_str(JSON_CONTENT_TYPE)?; + headers.insert("Content-Type", content_type); + + // The headers "apollographql-client-name" and "apollographql-client-version" + // are used for client identification in Apollo Studio. + + // This provides metrics in Studio that help keep track of what parts of the schema + // Rover uses, which ensures future changes to the API do not break Rover users. + // more info here: + // https://www.apollographql.com/docs/studio/client-awareness/#using-apollo-server-and-apollo-client + + let client_name = HeaderValue::from_str(CLIENT_NAME)?; + headers.insert("apollographql-client-name", client_name); + tracing::debug!(?self.version); + let client_version = HeaderValue::from_str(&self.version)?; + headers.insert("apollographql-client-version", client_version); + + let mut api_key = HeaderValue::from_str(&self.credential.api_key)?; + api_key.set_sensitive(true); + headers.insert("x-api-key", api_key); + + Ok(headers) + } + + pub fn get_credential_origin(&self) -> CredentialOrigin { + self.credential.origin.clone() + } } diff --git a/crates/rover-client/src/error.rs b/crates/rover-client/src/error.rs index 3f92d95bd..47f2568c8 100644 --- a/crates/rover-client/src/error.rs +++ b/crates/rover-client/src/error.rs @@ -1,6 +1,8 @@ use reqwest::Url; use thiserror::Error; +use crate::shared::{BuildErrors, CheckResponse, GraphRef}; + /// RoverClientError represents all possible failures that can occur during a client request. #[derive(Error, Debug)] pub enum RoverClientError { @@ -58,14 +60,11 @@ pub enum RoverClientError { /// The Studio API could not find a variant for a graph #[error( - "The graph registry does not contain variant \"{invalid_variant}\" for graph \"{graph}\"" + "The graph registry does not contain variant \"{}\" for graph \"{}\"", graph_ref.variant, graph_ref.name )] NoSchemaForVariant { - /// The name of the graph. - graph: String, - - /// The non-existent variant. - invalid_variant: String, + /// The graph ref. + graph_ref: GraphRef, /// Valid variants. valid_variants: Vec, @@ -93,15 +92,25 @@ pub enum RoverClientError { /// when someone provides a bad graph/variant combination or isn't /// validated properly, we don't know which reason is at fault for data.service /// being empty, so this error tells them to check both. - #[error("Could not find graph with name \"{graph}\"")] - NoService { graph: String }, + #[error("Could not find graph with name \"{graph_ref}\"")] + GraphNotFound { graph_ref: GraphRef }, /// if someone attempts to get a core schema from a supergraph that has - /// no composition results we return this error. - #[error("No supergraph SDL exists for \"{graph}\" because its subgraphs failed to compose.")] - NoCompositionPublishes { - graph: String, - composition_errors: Vec, + /// no successful build in the API, we return this error. + #[error("No supergraph SDL exists for \"{graph_ref}\" because its subgraphs failed to build.")] + NoSupergraphBuilds { + graph_ref: GraphRef, + source: BuildErrors, + }, + + #[error("Encountered {} while trying to build a supergraph.", .source.length_string())] + BuildErrors { source: BuildErrors }, + + #[error("Encountered {} while trying to build subgraph \"{subgraph}\" into supergraph \"{graph_ref}\".", .source.length_string())] + SubgraphBuildErrors { + subgraph: String, + graph_ref: GraphRef, + source: BuildErrors, }, /// This error occurs when the Studio API returns no implementing services for a graph @@ -114,9 +123,9 @@ pub enum RoverClientError { /// `can_operation_convert` is only set to true when a non-federated graph /// was encountered during an operation that could potentially convert a non-federated graph /// to a federated graph. - #[error("The graph `{graph}` is a non-federated graph. This operation is only possible for federated graphs.")] + #[error("The graph `{graph_ref}` is a non-federated graph. This operation is only possible for federated graphs.")] ExpectedFederatedGraph { - graph: String, + graph_ref: GraphRef, can_operation_convert: bool, }, @@ -124,6 +133,27 @@ pub enum RoverClientError { #[error("Invalid ChangeSeverity.")] InvalidSeverity, + /// The user supplied an invalid validation period + #[error("You can only specify a duration as granular as seconds.")] + ValidationPeriodTooGranular, + + /// The user supplied an invalid validation period duration + #[error(transparent)] + InvalidValidationPeriodDuration(#[from] humantime::DurationError), + + /// While checking the proposed schema, we encountered changes that would break existing operations + // we nest the CheckResponse here because we want to print the entire response even + // if there were failures + #[error("{}", operation_check_error_msg(.check_response))] + OperationCheckFailure { + graph_ref: GraphRef, + check_response: CheckResponse, + }, + + /// This error occurs when a user has a malformed Graph Ref + #[error("Graph IDs must be in the format or @, where can only contain letters, numbers, or the characters `-` or `_`, and must be 64 characters or less. must be 64 characters or less.")] + InvalidGraphRef, + /// This error occurs when a user has a malformed API key #[error( "The API key you provided is malformed. An API key must have three parts separated by a colon." @@ -134,10 +164,26 @@ pub enum RoverClientError { #[error("The registry did not recognize the provided API key")] InvalidKey, - /// could not parse the latest version - #[error("Could not get the latest release version")] - UnparseableReleaseVersion, + /// Could not parse the latest version + #[error("Could not parse the latest release version")] + UnparseableReleaseVersion { source: semver::Error }, + + /// Encountered an error while processing the request for the latest version + #[error("There's something wrong with the latest GitHub release URL")] + BadReleaseUrl, #[error("This endpoint doesn't support subgraph introspection via the Query._service field")] SubgraphIntrospectionNotAvailable, } + +fn operation_check_error_msg(check_response: &CheckResponse) -> String { + let failure_count = check_response.get_failure_count(); + let plural = match failure_count { + 1 => "", + _ => "s", + }; + format!( + "This operation check has encountered {} schema change{} that would break operations from existing client traffic.", + failure_count, plural + ) +} diff --git a/crates/rover-client/src/headers.rs b/crates/rover-client/src/headers.rs deleted file mode 100644 index 6bcf47762..000000000 --- a/crates/rover-client/src/headers.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::RoverClientError; -use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; -use std::collections::HashMap; - -const JSON_CONTENT_TYPE: &str = "application/json"; -const CLIENT_NAME: &str = "rover-client"; - -/// Function for building a [HeaderMap] for making http requests. Use for -/// Generic requests to any graphql endpoint. -/// -/// Takes a single argument, list of header key/value pairs -pub fn build(header_map: &HashMap) -> Result { - let mut headers = HeaderMap::new(); - - // this should be consistent for any graphql requests - let content_type = HeaderValue::from_str(JSON_CONTENT_TYPE)?; - headers.append("Content-Type", content_type); - - for (key, value) in header_map { - let header_key = HeaderName::from_bytes(key.as_bytes())?; - let header_value = HeaderValue::from_str(&value)?; - headers.append(header_key, header_value); - } - - Ok(headers) -} - -/// Function for building a [HeaderMap] for making http requests. Use for making -/// requests to Apollo Studio. We're leaving this separate from `build` since we -/// need to be able to mark the api_key as sensitive (at the bottom) -/// -/// Takes an `api_key` and a `client_version`, and returns a [HeaderMap]. -pub fn build_studio_headers( - api_key: &str, - client_version: &str, -) -> Result { - let mut headers = HeaderMap::new(); - - let content_type = HeaderValue::from_str(JSON_CONTENT_TYPE)?; - headers.insert("Content-Type", content_type); - - // The headers "apollographql-client-name" and "apollographql-client-version" - // are used for client identification in Apollo Studio. - - // This provides metrics in Studio that help keep track of what parts of the schema - // Rover uses, which ensures future changes to the API do not break Rover users. - // more info here: - // https://www.apollographql.com/docs/studio/client-awareness/#using-apollo-server-and-apollo-client - - let client_name = HeaderValue::from_str(CLIENT_NAME)?; - headers.insert("apollographql-client-name", client_name); - tracing::debug!(?client_version); - let client_version = HeaderValue::from_str(&client_version)?; - headers.insert("apollographql-client-version", client_version); - - let mut api_key = HeaderValue::from_str(api_key)?; - api_key.set_sensitive(true); - headers.insert("x-api-key", api_key); - - Ok(headers) -} diff --git a/crates/rover-client/src/introspection/mod.rs b/crates/rover-client/src/introspection/mod.rs deleted file mode 100644 index dd9d7b111..000000000 --- a/crates/rover-client/src/introspection/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod schema; -pub use schema::Schema; diff --git a/crates/rover-client/src/introspection/schema.rs b/crates/rover-client/src/introspection/schema.rs deleted file mode 100644 index c5744f894..000000000 --- a/crates/rover-client/src/introspection/schema.rs +++ /dev/null @@ -1,271 +0,0 @@ -//! Schema encoding module used to work with Introspection result. -//! -//! More information on Schema Definition language(SDL) can be found in [this -//! documentation](https://www.apollographql.com/docs/apollo-server/schema/schema/). -//! -use crate::query::graph::introspect; -use sdl_encoder::{ - Directive, EnumDef, EnumValue, Field, InputField, InputObjectDef, InputValue, InterfaceDef, - ObjectDef, ScalarDef, Schema as SDL, SchemaDef, Type_, UnionDef, -}; -use serde::Deserialize; -use std::convert::TryFrom; - -pub type FullTypeField = introspect::introspection_query::FullTypeFields; -pub type FullTypeInputField = introspect::introspection_query::FullTypeInputFields; -pub type FullTypeFieldArg = introspect::introspection_query::FullTypeFieldsArgs; -pub type IntrospectionResult = introspect::introspection_query::ResponseData; -pub type SchemaMutationType = introspect::introspection_query::IntrospectionQuerySchemaMutationType; -pub type SchemaQueryType = introspect::introspection_query::IntrospectionQuerySchemaQueryType; -pub type SchemaType = introspect::introspection_query::IntrospectionQuerySchemaTypes; -pub type SchemaDirective = introspect::introspection_query::IntrospectionQuerySchemaDirectives; -pub type SchemaSubscriptionType = - introspect::introspection_query::IntrospectionQuerySchemaSubscriptionType; -pub type __TypeKind = introspect::introspection_query::__TypeKind; - -// Represents GraphQL types we will not be encoding to SDL. -const GRAPHQL_NAMED_TYPES: [&str; 12] = [ - "__Schema", - "__Type", - "__TypeKind", - "__Field", - "__InputValue", - "__EnumValue", - "__DirectiveLocation", - "__Directive", - "Boolean", - "String", - "Int", - "ID", -]; - -// Represents GraphQL directives we will not be encoding to SDL. -const SPECIFIED_DIRECTIVES: [&str; 3] = ["skip", "include", "deprecated"]; - -/// A representation of a GraphQL Schema. -/// -/// Contains Schema Types and Directives. -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Schema { - types: Vec, - directives: Vec, - mutation_type: Option, - query_type: SchemaQueryType, - subscription_type: Option, -} - -impl Schema { - /// Encode Schema into an SDL. - pub fn encode(self) -> String { - let mut sdl = SDL::new(); - - // When we have a defined mutation and subscription, we record - // everything to Schema Definition. - // https://www.apollographql.com/docs/graphql-subscriptions/subscriptions-to-schema/ - if self.mutation_type.is_some() | self.subscription_type.is_some() { - let mut schema_def = SchemaDef::new(); - if let Some(mutation_type) = self.mutation_type { - schema_def.mutation(mutation_type.name.unwrap()); - } - if let Some(subscription_type) = self.subscription_type { - schema_def.subscription(subscription_type.name.unwrap()); - } - if let Some(name) = self.query_type.name { - schema_def.query(name); - } - sdl.schema(schema_def); - } else if let Some(name) = self.query_type.name { - // If we don't have a mutation or a subscription, but do have a - // query type, only create a Schema Definition when it's something - // other than `Query`. - if name != "Query" { - let mut schema_def = SchemaDef::new(); - schema_def.query(name); - sdl.schema(schema_def); - } - } - - // Exclude GraphQL directives like 'skip' and 'include' before encoding directives. - self.directives - .into_iter() - .filter(|directive| !SPECIFIED_DIRECTIVES.contains(&directive.name.as_str())) - .for_each(|directive| Self::encode_directives(directive, &mut sdl)); - - // Exclude GraphQL named types like __Schema before encoding full type. - self.types - .into_iter() - .filter(|type_| match type_.full_type.name.as_deref() { - Some(name) => !GRAPHQL_NAMED_TYPES.contains(&name), - None => false, - }) - .for_each(|type_| Self::encode_full_type(type_, &mut sdl)); - - sdl.finish() - } - - fn encode_directives(directive: SchemaDirective, sdl: &mut SDL) { - let mut directive_ = Directive::new(directive.name); - directive_.description(directive.description); - for location in directive.locations { - // Location is of a __DirectiveLocation enum that doesn't implement - // Display (meaning we can't just do .to_string). This next line - // just forces it into a String with format! debug. - directive_.location(format!("{:?}", location)); - } - - sdl.directive(directive_) - } - - fn encode_full_type(type_: SchemaType, sdl: &mut SDL) { - let ty = type_.full_type; - - match ty.kind { - __TypeKind::OBJECT => { - let mut object_def = ObjectDef::new(ty.name.unwrap_or_else(String::new)); - object_def.description(ty.description); - if let Some(interfaces) = ty.interfaces { - for interface in interfaces { - object_def.interface(interface.type_ref.name.unwrap_or_else(String::new)); - } - } - if let Some(field) = ty.fields { - for f in field { - let field_def = Self::encode_field(f); - object_def.field(field_def); - } - sdl.object(object_def); - } - } - __TypeKind::INPUT_OBJECT => { - let mut input_def = InputObjectDef::new(ty.name.unwrap_or_else(String::new)); - input_def.description(ty.description); - if let Some(field) = ty.input_fields { - for f in field { - let input_field_def = Self::encode_input_field(f); - input_def.field(input_field_def); - } - sdl.input(input_def); - } - } - __TypeKind::INTERFACE => { - let mut interface_def = InterfaceDef::new(ty.name.unwrap_or_else(String::new)); - interface_def.description(ty.description); - if let Some(interfaces) = ty.interfaces { - for interface in interfaces { - interface_def - .interface(interface.type_ref.name.unwrap_or_else(String::new)); - } - } - if let Some(field) = ty.fields { - for f in field { - let field_def = Self::encode_field(f); - interface_def.field(field_def); - } - sdl.interface(interface_def); - } - } - __TypeKind::SCALAR => { - let mut scalar_def = ScalarDef::new(ty.name.unwrap_or_else(String::new)); - scalar_def.description(ty.description); - sdl.scalar(scalar_def); - } - __TypeKind::UNION => { - let mut union_def = UnionDef::new(ty.name.unwrap_or_else(String::new)); - union_def.description(ty.description); - if let Some(possible_types) = ty.possible_types { - for possible_type in possible_types { - union_def.member(possible_type.type_ref.name.unwrap_or_else(String::new)); - } - } - sdl.union(union_def); - } - __TypeKind::ENUM => { - let mut enum_def = EnumDef::new(ty.name.unwrap_or_else(String::new)); - if let Some(enums) = ty.enum_values { - for enum_ in enums { - let mut enum_value = EnumValue::new(enum_.name); - enum_value.description(enum_.description); - - if enum_.is_deprecated { - enum_value.deprecated(enum_.deprecation_reason); - } - - enum_def.value(enum_value); - } - } - sdl.enum_(enum_def); - } - _ => (), - } - } - - fn encode_field(field: FullTypeField) -> Field { - let ty = Self::encode_type(field.type_.type_ref); - let mut field_def = Field::new(field.name, ty); - - for value in field.args { - let field_value = Self::encode_arg(value); - field_def.arg(field_value); - } - - if field.is_deprecated { - field_def.deprecated(field.deprecation_reason); - } - field_def.description(field.description); - field_def - } - - fn encode_input_field(field: FullTypeInputField) -> InputField { - let ty = Self::encode_type(field.input_value.type_.type_ref); - let mut field_def = InputField::new(field.input_value.name, ty); - - field_def.default(field.input_value.default_value); - field_def.description(field.input_value.description); - field_def - } - - fn encode_arg(value: FullTypeFieldArg) -> InputValue { - let ty = Self::encode_type(value.input_value.type_.type_ref); - let mut value_def = InputValue::new(value.input_value.name, ty); - - value_def.default(value.input_value.default_value); - value_def.description(value.input_value.description); - value_def - } - - fn encode_type(ty: impl introspect::OfType) -> Type_ { - use introspect::introspection_query::__TypeKind::*; - match ty.kind() { - SCALAR | OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT => Type_::NamedType { - name: ty.name().unwrap().to_string(), - }, - NON_NULL => { - let ty = Self::encode_type(ty.of_type().unwrap()); - Type_::NonNull { ty: Box::new(ty) } - } - LIST => { - let ty = Self::encode_type(ty.of_type().unwrap()); - Type_::List { ty: Box::new(ty) } - } - Other(ty) => panic!("Unknown type: {}", ty), - } - } -} - -impl TryFrom for Schema { - type Error = &'static str; - - fn try_from(src: IntrospectionResult) -> Result { - match src.schema { - Some(s) => Ok(Self { - types: s.types, - directives: s.directives, - mutation_type: s.mutation_type, - query_type: s.query_type, - subscription_type: s.subscription_type, - }), - None => Err("Schema not found in Introspection Result."), - } - } -} diff --git a/crates/rover-client/src/lib.rs b/crates/rover-client/src/lib.rs index 0b59cf52f..5c8f3e490 100644 --- a/crates/rover-client/src/lib.rs +++ b/crates/rover-client/src/lib.rs @@ -2,22 +2,20 @@ //! HTTP client for making GraphQL requests for the Rover CLI tool. -/// Module related to blocking http client. -pub mod blocking; mod error; -/// Module related to constructing request headers. -pub mod headers; - -/// Module related to building an SDL from an introspection response. -pub mod introspection; +/// Module related to blocking http client. +pub mod blocking; /// Module for client related errors. pub use error::RoverClientError; #[allow(clippy::upper_case_acronyms)] /// Module for actually querying studio -pub mod query; +pub mod operations; /// Module for getting release info pub mod releases; + +/// Module for shared functionality +pub mod shared; diff --git a/crates/rover-client/src/operations/config/is_federated/is_federated_query.graphql b/crates/rover-client/src/operations/config/is_federated/is_federated_query.graphql new file mode 100644 index 000000000..4007e1af7 --- /dev/null +++ b/crates/rover-client/src/operations/config/is_federated/is_federated_query.graphql @@ -0,0 +1,7 @@ +query IsFederatedGraph($graph_id: ID!, $variant: String!) { + service(id: $graph_id) { + implementingServices(graphVariant: $variant) { + __typename + } + } +} \ No newline at end of file diff --git a/crates/rover-client/src/operations/config/is_federated/mod.rs b/crates/rover-client/src/operations/config/is_federated/mod.rs new file mode 100644 index 000000000..36375e300 --- /dev/null +++ b/crates/rover-client/src/operations/config/is_federated/mod.rs @@ -0,0 +1,5 @@ +mod runner; +mod types; + +pub(crate) use runner::run; +pub(crate) use types::IsFederatedInput; diff --git a/crates/rover-client/src/query/config/is_federated.rs b/crates/rover-client/src/operations/config/is_federated/runner.rs similarity index 71% rename from crates/rover-client/src/query/config/is_federated.rs rename to crates/rover-client/src/operations/config/is_federated/runner.rs index b8fb68fa6..35084ce68 100644 --- a/crates/rover-client/src/query/config/is_federated.rs +++ b/crates/rover-client/src/operations/config/is_federated/runner.rs @@ -1,13 +1,15 @@ -// PublishPartialSchemaMutation use crate::blocking::StudioClient; +use crate::operations::config::is_federated::IsFederatedInput; +use crate::shared::GraphRef; use crate::RoverClientError; + use graphql_client::*; #[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/config/is_federated.graphql", + query_path = "src/operations/config/is_federated/is_federated_query.graphql", schema_path = ".schema/schema.graphql", response_derives = "PartialEq, Debug, Serialize, Deserialize", deprecated = "warn" @@ -18,21 +20,23 @@ use graphql_client::*; pub(crate) struct IsFederatedGraph; pub(crate) fn run( - variables: is_federated_graph::Variables, + input: IsFederatedInput, client: &StudioClient, ) -> Result { - let graph = variables.graph_id.clone(); - let data = client.post::(variables)?; - build_response(data, graph) + let graph_ref = input.graph_ref.clone(); + let data = client.post::(input.into())?; + build_response(data, graph_ref) } type ImplementingServices = is_federated_graph::IsFederatedGraphServiceImplementingServices; fn build_response( data: is_federated_graph::ResponseData, - graph: String, + graph_ref: GraphRef, ) -> Result { - let service = data.service.ok_or(RoverClientError::NoService { graph })?; + let service = data + .service + .ok_or(RoverClientError::GraphNotFound { graph_ref })?; Ok(match service.implementing_services { Some(typename) => match typename { ImplementingServices::FederatedImplementingServices => true, diff --git a/crates/rover-client/src/operations/config/is_federated/types.rs b/crates/rover-client/src/operations/config/is_federated/types.rs new file mode 100644 index 000000000..3f6f22e52 --- /dev/null +++ b/crates/rover-client/src/operations/config/is_federated/types.rs @@ -0,0 +1,18 @@ +use crate::operations::config::is_federated::runner::is_federated_graph; +use crate::shared::GraphRef; + +type QueryVariables = is_federated_graph::Variables; + +#[derive(Debug, Clone, PartialEq)] +pub struct IsFederatedInput { + pub graph_ref: GraphRef, +} + +impl From for QueryVariables { + fn from(input: IsFederatedInput) -> Self { + Self { + graph_id: input.graph_ref.name, + variant: input.graph_ref.variant, + } + } +} diff --git a/crates/rover-client/src/query/config/mod.rs b/crates/rover-client/src/operations/config/mod.rs similarity index 83% rename from crates/rover-client/src/query/config/mod.rs rename to crates/rover-client/src/operations/config/mod.rs index ab814a569..e5e9f78ca 100644 --- a/crates/rover-client/src/query/config/mod.rs +++ b/crates/rover-client/src/operations/config/mod.rs @@ -1,5 +1,5 @@ /// runner for rover config whoami -pub mod whoami; +pub mod who_am_i; /// runner is_federated check pub mod is_federated; diff --git a/crates/rover-client/src/operations/config/who_am_i/mod.rs b/crates/rover-client/src/operations/config/who_am_i/mod.rs new file mode 100644 index 000000000..ccb47268d --- /dev/null +++ b/crates/rover-client/src/operations/config/who_am_i/mod.rs @@ -0,0 +1,5 @@ +mod runner; +mod types; + +pub use runner::run; +pub use types::{Actor, ConfigWhoAmIInput, RegistryIdentity}; diff --git a/crates/rover-client/src/query/config/whoami.rs b/crates/rover-client/src/operations/config/who_am_i/runner.rs similarity index 76% rename from crates/rover-client/src/query/config/whoami.rs rename to crates/rover-client/src/operations/config/who_am_i/runner.rs index 238440eb1..0974cd49d 100644 --- a/crates/rover-client/src/query/config/whoami.rs +++ b/crates/rover-client/src/operations/config/who_am_i/runner.rs @@ -1,5 +1,10 @@ use crate::blocking::StudioClient; +use crate::operations::config::who_am_i::{ + types::{QueryActorType, QueryResponseData, RegistryIdentity}, + Actor, ConfigWhoAmIInput, +}; use crate::RoverClientError; + use houston::CredentialOrigin; use graphql_client::*; @@ -8,43 +13,28 @@ use graphql_client::*; // 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/config/whoami.graphql", + query_path = "src/operations/config/who_am_i/who_am_i_query.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. who_am_i_query -pub struct WhoAmIQuery; - -#[derive(Debug, PartialEq)] -pub struct RegistryIdentity { - pub id: String, - pub graph_title: Option, - pub key_actor_type: Actor, - pub credential_origin: CredentialOrigin, -} - -#[derive(Debug, PartialEq)] -pub enum Actor { - GRAPH, - USER, - OTHER, -} +/// Snake case of this name is the mod name. i.e. config_who_am_i_query +pub(crate) struct ConfigWhoAmIQuery; /// Get info from the registry about an API key, i.e. the name/id of the /// user/graph and what kind of key it is (GRAPH/USER/Other) pub fn run( - variables: who_am_i_query::Variables, + input: ConfigWhoAmIInput, client: &StudioClient, ) -> Result { - let response_data = client.post::(variables)?; - get_identity_from_response_data(response_data, client.credential.origin.clone()) + let response_data = client.post::(input.into())?; + get_identity_from_response_data(response_data, client.get_credential_origin()) } fn get_identity_from_response_data( - response_data: who_am_i_query::ResponseData, + response_data: QueryResponseData, credential_origin: CredentialOrigin, ) -> Result { if let Some(me) = response_data.me { @@ -54,13 +44,13 @@ fn get_identity_from_response_data( // more here: https://studio-staging.apollographql.com/graph/engine/schema/reference/enums/ActorType?variant=prod let key_actor_type = match me.as_actor.type_ { - who_am_i_query::ActorType::GRAPH => Actor::GRAPH, - who_am_i_query::ActorType::USER => Actor::USER, + QueryActorType::GRAPH => Actor::GRAPH, + QueryActorType::USER => Actor::USER, _ => Actor::OTHER, }; let graph_title = match me.on { - who_am_i_query::WhoAmIQueryMeOn::Service(s) => Some(s.title), + config_who_am_i_query::ConfigWhoAmIQueryMeOn::Service(s) => Some(s.title), _ => None, }; @@ -91,7 +81,8 @@ mod tests { }, } }); - let data: who_am_i_query::ResponseData = serde_json::from_value(json_response).unwrap(); + let data: config_who_am_i_query::ResponseData = + serde_json::from_value(json_response).unwrap(); let output = get_identity_from_response_data(data, CredentialOrigin::EnvVar); let expected_identity = RegistryIdentity { @@ -116,7 +107,8 @@ mod tests { }, } }); - let data: who_am_i_query::ResponseData = serde_json::from_value(json_response).unwrap(); + let data: config_who_am_i_query::ResponseData = + serde_json::from_value(json_response).unwrap(); let output = get_identity_from_response_data(data, CredentialOrigin::EnvVar); let expected_identity = RegistryIdentity { diff --git a/crates/rover-client/src/operations/config/who_am_i/types.rs b/crates/rover-client/src/operations/config/who_am_i/types.rs new file mode 100644 index 000000000..d2d3c4e93 --- /dev/null +++ b/crates/rover-client/src/operations/config/who_am_i/types.rs @@ -0,0 +1,31 @@ +use super::runner::config_who_am_i_query; + +use houston::CredentialOrigin; + +pub(crate) type QueryResponseData = config_who_am_i_query::ResponseData; +pub(crate) type QueryVariables = config_who_am_i_query::Variables; +pub(crate) type QueryActorType = config_who_am_i_query::ActorType; + +#[derive(Debug, PartialEq)] +pub struct RegistryIdentity { + pub id: String, + pub graph_title: Option, + pub key_actor_type: Actor, + pub credential_origin: CredentialOrigin, +} + +#[derive(Debug, PartialEq)] +pub enum Actor { + GRAPH, + USER, + OTHER, +} + +#[derive(Debug, PartialEq)] +pub struct ConfigWhoAmIInput {} + +impl From for QueryVariables { + fn from(_input: ConfigWhoAmIInput) -> Self { + Self {} + } +} diff --git a/crates/rover-client/src/query/config/whoami.graphql b/crates/rover-client/src/operations/config/who_am_i/who_am_i_query.graphql similarity index 80% rename from crates/rover-client/src/query/config/whoami.graphql rename to crates/rover-client/src/operations/config/who_am_i/who_am_i_query.graphql index 9bf0e18f6..4f131f2fa 100644 --- a/crates/rover-client/src/query/config/whoami.graphql +++ b/crates/rover-client/src/operations/config/who_am_i/who_am_i_query.graphql @@ -1,4 +1,4 @@ -query WhoAmIQuery { +query ConfigWhoAmIQuery { me { __typename ... on Service { diff --git a/crates/rover-client/src/query/graph/check.graphql b/crates/rover-client/src/operations/graph/check/check_mutation.graphql similarity index 61% rename from crates/rover-client/src/query/graph/check.graphql rename to crates/rover-client/src/operations/graph/check/check_mutation.graphql index a891ba7eb..513688e1b 100644 --- a/crates/rover-client/src/query/graph/check.graphql +++ b/crates/rover-client/src/operations/graph/check/check_mutation.graphql @@ -1,15 +1,15 @@ -mutation CheckSchemaQuery( - $graphId: ID! +mutation GraphCheckMutation( + $graph_id: ID! $variant: String - $schema: String - $gitContext: GitContextInput! + $proposed_schema: String + $git_context: GitContextInput! $config: HistoricQueryParameters! ) { - service(id: $graphId) { + service(id: $graph_id) { checkSchema( - proposedSchemaDocument: $schema + proposedSchemaDocument: $proposed_schema baseSchemaTag: $variant - gitContext: $gitContext + gitContext: $git_context historicParameters: $config ) { targetUrl @@ -24,4 +24,4 @@ mutation CheckSchemaQuery( } } } -} \ No newline at end of file +} diff --git a/crates/rover-client/src/operations/graph/check/mod.rs b/crates/rover-client/src/operations/graph/check/mod.rs new file mode 100644 index 000000000..23c23b7e8 --- /dev/null +++ b/crates/rover-client/src/operations/graph/check/mod.rs @@ -0,0 +1,5 @@ +mod runner; +mod types; + +pub use runner::run; +pub use types::GraphCheckInput; diff --git a/crates/rover-client/src/operations/graph/check/runner.rs b/crates/rover-client/src/operations/graph/check/runner.rs new file mode 100644 index 000000000..152ab808e --- /dev/null +++ b/crates/rover-client/src/operations/graph/check/runner.rs @@ -0,0 +1,61 @@ +use crate::blocking::StudioClient; +use crate::operations::graph::check::types::{GraphCheckInput, MutationResponseData}; +use crate::shared::{CheckResponse, GraphRef}; +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/operations/graph/check/check_mutation.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. graph_check_mutation +pub(crate) struct GraphCheckMutation; + +/// The main function to be used from this module. +/// This function takes a proposed schema and validates it against a published +/// schema. +pub fn run( + input: GraphCheckInput, + client: &StudioClient, +) -> Result { + let graph_ref = input.graph_ref.clone(); + let data = client.post::(input.into())?; + get_check_response_from_data(data, graph_ref) +} + +fn get_check_response_from_data( + data: MutationResponseData, + graph_ref: GraphRef, +) -> Result { + let service = data.service.ok_or(RoverClientError::GraphNotFound { + graph_ref: graph_ref.clone(), + })?; + let target_url = service.check_schema.target_url; + + let diff_to_previous = service.check_schema.diff_to_previous; + + let operation_check_count = diff_to_previous.number_of_checked_operations.unwrap_or(0) as u64; + + let result = diff_to_previous.severity.into(); + let mut changes = Vec::with_capacity(diff_to_previous.changes.len()); + for change in diff_to_previous.changes { + changes.push(change.into()); + } + + CheckResponse::try_new( + target_url, + operation_check_count, + changes, + result, + graph_ref, + ) +} diff --git a/crates/rover-client/src/operations/graph/check/types.rs b/crates/rover-client/src/operations/graph/check/types.rs new file mode 100644 index 000000000..9991eed14 --- /dev/null +++ b/crates/rover-client/src/operations/graph/check/types.rs @@ -0,0 +1,103 @@ +use crate::operations::graph::check::runner::graph_check_mutation; +use crate::shared::{ChangeSeverity, CheckConfig, GitContext, GraphRef, SchemaChange}; + +#[derive(Debug, Clone, PartialEq)] +pub struct GraphCheckInput { + pub graph_ref: GraphRef, + pub proposed_schema: String, + pub git_context: GitContext, + pub config: CheckConfig, +} + +impl From for MutationVariables { + fn from(input: GraphCheckInput) -> Self { + Self { + graph_id: input.graph_ref.name, + variant: Some(input.graph_ref.variant), + proposed_schema: Some(input.proposed_schema), + config: input.config.into(), + git_context: input.git_context.into(), + } + } +} + +type MutationConfig = graph_check_mutation::HistoricQueryParameters; +impl From for MutationConfig { + fn from(input: CheckConfig) -> Self { + Self { + query_count_threshold: input.query_count_threshold, + query_count_threshold_percentage: input.query_count_threshold_percentage, + from: Some( + input + .validation_period + .clone() + .unwrap_or_default() + .from + .to_string(), + ), + to: Some(input.validation_period.unwrap_or_default().to.to_string()), + // we don't support configuring these, but we can't leave them out + excluded_clients: None, + ignored_operations: None, + included_variants: None, + } + } +} + +type MutationVariables = graph_check_mutation::Variables; +pub(crate) type MutationResponseData = graph_check_mutation::ResponseData; + +pub(crate) type MutationChangeSeverity = graph_check_mutation::ChangeSeverity; +impl From for ChangeSeverity { + fn from(severity: MutationChangeSeverity) -> Self { + match severity { + MutationChangeSeverity::NOTICE => ChangeSeverity::PASS, + MutationChangeSeverity::FAILURE => ChangeSeverity::FAIL, + _ => ChangeSeverity::unreachable(), + } + } +} + +impl From for MutationChangeSeverity { + fn from(severity: ChangeSeverity) -> Self { + match severity { + ChangeSeverity::PASS => MutationChangeSeverity::NOTICE, + ChangeSeverity::FAIL => MutationChangeSeverity::FAILURE, + } + } +} + +type MutationSchemaChange = + graph_check_mutation::GraphCheckMutationServiceCheckSchemaDiffToPreviousChanges; +impl From for MutationSchemaChange { + fn from(schema_change: SchemaChange) -> MutationSchemaChange { + MutationSchemaChange { + severity: schema_change.severity.into(), + code: schema_change.code, + description: schema_change.description, + } + } +} + +impl From for SchemaChange { + fn from(schema_change: MutationSchemaChange) -> SchemaChange { + SchemaChange { + severity: schema_change.severity.into(), + code: schema_change.code, + description: schema_change.description, + } + } +} + +type MutationGitContextInput = graph_check_mutation::GitContextInput; +impl From for MutationGitContextInput { + fn from(git_context: GitContext) -> MutationGitContextInput { + MutationGitContextInput { + branch: git_context.branch, + commit: git_context.commit, + committer: git_context.author, + remote_url: git_context.remote_url, + message: None, + } + } +} diff --git a/crates/rover-client/src/operations/graph/fetch/fetch_query.graphql b/crates/rover-client/src/operations/graph/fetch/fetch_query.graphql new file mode 100644 index 000000000..8a4cc851e --- /dev/null +++ b/crates/rover-client/src/operations/graph/fetch/fetch_query.graphql @@ -0,0 +1,11 @@ +query GraphFetchQuery($graph_id: ID!, $variant: String) { + frontendUrlRoot, + service(id: $graph_id) { + schema(tag: $variant) { + document + } + variants { + name + } + } +} diff --git a/crates/rover-client/src/operations/graph/fetch/mod.rs b/crates/rover-client/src/operations/graph/fetch/mod.rs new file mode 100644 index 000000000..7713aca96 --- /dev/null +++ b/crates/rover-client/src/operations/graph/fetch/mod.rs @@ -0,0 +1,5 @@ +mod runner; +mod types; + +pub use runner::run; +pub use types::GraphFetchInput; diff --git a/crates/rover-client/src/query/graph/fetch.rs b/crates/rover-client/src/operations/graph/fetch/runner.rs similarity index 58% rename from crates/rover-client/src/query/graph/fetch.rs rename to crates/rover-client/src/operations/graph/fetch/runner.rs index 2890334e0..fc72a3b53 100644 --- a/crates/rover-client/src/query/graph/fetch.rs +++ b/crates/rover-client/src/operations/graph/fetch/runner.rs @@ -1,5 +1,8 @@ use crate::blocking::StudioClient; +use crate::operations::graph::fetch::GraphFetchInput; +use crate::shared::{FetchResponse, GraphRef, Sdl, SdlType}; use crate::RoverClientError; + use graphql_client::*; // I'm not sure where this should live long-term @@ -10,40 +13,42 @@ type GraphQLDocument = String; // 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/graph/fetch.graphql", + query_path = "src/operations/graph/fetch/fetch_query.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. fetch_schema_query -pub struct FetchSchemaQuery; +/// Snake case of this name is the mod name. i.e. graph_fetch_query +pub(crate) struct GraphFetchQuery; /// The main function to be used from this module. This function fetches a /// schema from apollo studio and returns it in either sdl (default) or json format pub fn run( - variables: fetch_schema_query::Variables, + input: GraphFetchInput, client: &StudioClient, -) -> Result { - let graph = variables.graph_id.clone(); - let invalid_variant = variables - .variant - .clone() - .unwrap_or_else(|| "current".to_string()); - let response_data = client.post::(variables)?; - get_schema_from_response_data(response_data, graph, invalid_variant) - // if we want json, we can parse & serialize it here +) -> Result { + let graph_ref = input.graph_ref.clone(); + let response_data = client.post::(input.into())?; + let sdl_contents = get_schema_from_response_data(response_data, graph_ref)?; + Ok(FetchResponse { + sdl: Sdl { + contents: sdl_contents, + r#type: SdlType::Graph, + }, + }) } fn get_schema_from_response_data( - response_data: fetch_schema_query::ResponseData, - graph: String, - invalid_variant: String, + response_data: graph_fetch_query::ResponseData, + graph_ref: GraphRef, ) -> Result { - let service_data = response_data.service.ok_or(RoverClientError::NoService { - graph: graph.clone(), - })?; + let service_data = response_data + .service + .ok_or(RoverClientError::GraphNotFound { + graph_ref: graph_ref.clone(), + })?; let mut valid_variants = Vec::new(); @@ -55,8 +60,7 @@ fn get_schema_from_response_data( Ok(schema.document) } else { Err(RoverClientError::NoSchemaForVariant { - graph, - invalid_variant, + graph_ref, valid_variants, frontend_url_root: response_data.frontend_url_root, }) @@ -78,9 +82,9 @@ mod tests { "variants": [] } }); - let data: fetch_schema_query::ResponseData = serde_json::from_value(json_response).unwrap(); - let (graph, invalid_variant) = mock_vars(); - let output = get_schema_from_response_data(data, graph, invalid_variant); + let data: graph_fetch_query::ResponseData = serde_json::from_value(json_response).unwrap(); + let graph_ref = mock_graph_ref(); + let output = get_schema_from_response_data(data, graph_ref); assert!(output.is_ok()); assert_eq!(output.unwrap(), "type Query { hello: String }".to_string()); @@ -90,9 +94,9 @@ mod tests { fn get_schema_from_response_data_errs_on_no_service() { let json_response = json!({ "service": null, "frontendUrlRoot": "https://studio.apollographql.com" }); - let data: fetch_schema_query::ResponseData = serde_json::from_value(json_response).unwrap(); - let (graph, invalid_variant) = mock_vars(); - let output = get_schema_from_response_data(data, graph, invalid_variant); + let data: graph_fetch_query::ResponseData = serde_json::from_value(json_response).unwrap(); + let graph_ref = mock_graph_ref(); + let output = get_schema_from_response_data(data, graph_ref); assert!(output.is_err()); } @@ -106,14 +110,17 @@ mod tests { "variants": [], }, }); - let data: fetch_schema_query::ResponseData = serde_json::from_value(json_response).unwrap(); - let (graph, invalid_variant) = mock_vars(); - let output = get_schema_from_response_data(data, graph, invalid_variant); + let data: graph_fetch_query::ResponseData = serde_json::from_value(json_response).unwrap(); + let graph_ref = mock_graph_ref(); + let output = get_schema_from_response_data(data, graph_ref); assert!(output.is_err()); } - fn mock_vars() -> (String, String) { - ("mygraph".to_string(), "current".to_string()) + fn mock_graph_ref() -> GraphRef { + GraphRef { + name: "mygraph".to_string(), + variant: "current".to_string(), + } } } diff --git a/crates/rover-client/src/operations/graph/fetch/types.rs b/crates/rover-client/src/operations/graph/fetch/types.rs new file mode 100644 index 000000000..bbfe7fc7b --- /dev/null +++ b/crates/rover-client/src/operations/graph/fetch/types.rs @@ -0,0 +1,18 @@ +use crate::operations::graph::fetch::runner::graph_fetch_query; +use crate::shared::GraphRef; + +type QueryVariables = graph_fetch_query::Variables; + +#[derive(Debug, Clone, PartialEq)] +pub struct GraphFetchInput { + pub graph_ref: GraphRef, +} + +impl From for QueryVariables { + fn from(input: GraphFetchInput) -> Self { + Self { + graph_id: input.graph_ref.name, + variant: Some(input.graph_ref.variant), + } + } +} diff --git a/crates/rover-client/tests/fixtures/interfaces.json b/crates/rover-client/src/operations/graph/introspect/fixtures/interfaces.json similarity index 100% rename from crates/rover-client/tests/fixtures/interfaces.json rename to crates/rover-client/src/operations/graph/introspect/fixtures/interfaces.json diff --git a/crates/rover-client/tests/fixtures/simple.json b/crates/rover-client/src/operations/graph/introspect/fixtures/simple.json similarity index 100% rename from crates/rover-client/tests/fixtures/simple.json rename to crates/rover-client/src/operations/graph/introspect/fixtures/simple.json diff --git a/crates/rover-client/tests/fixtures/swapi.json b/crates/rover-client/src/operations/graph/introspect/fixtures/swapi.json similarity index 100% rename from crates/rover-client/tests/fixtures/swapi.json rename to crates/rover-client/src/operations/graph/introspect/fixtures/swapi.json diff --git a/crates/rover-client/src/query/graph/introspect_query.graphql b/crates/rover-client/src/operations/graph/introspect/introspect_query.graphql similarity index 97% rename from crates/rover-client/src/query/graph/introspect_query.graphql rename to crates/rover-client/src/operations/graph/introspect/introspect_query.graphql index 996742faf..c1c64031a 100644 --- a/crates/rover-client/src/query/graph/introspect_query.graphql +++ b/crates/rover-client/src/operations/graph/introspect/introspect_query.graphql @@ -1,4 +1,4 @@ -query IntrospectionQuery { +query GraphIntrospectQuery { __schema { queryType { name diff --git a/crates/rover-client/src/query/graph/introspect_schema.graphql b/crates/rover-client/src/operations/graph/introspect/introspect_schema.graphql similarity index 76% rename from crates/rover-client/src/query/graph/introspect_schema.graphql rename to crates/rover-client/src/operations/graph/introspect/introspect_schema.graphql index 805ad4486..c9030af48 100644 --- a/crates/rover-client/src/query/graph/introspect_schema.graphql +++ b/crates/rover-client/src/operations/graph/introspect/introspect_schema.graphql @@ -1,11 +1,14 @@ +# eslint-disable-next-line schema { query: Query } +# eslint-disable-next-line type Query { __schema: __Schema } +# eslint-disable-next-line type __Schema { types: [__Type!]! queryType: __Type! @@ -14,30 +17,32 @@ type __Schema { directives: [__Directive!]! } +# eslint-disable-next-line type __Type { kind: __TypeKind! name: String description: String - # OBJECT and INTERFACE only + "OBJECT and INTERFACE only" fields(includeDeprecated: Boolean = false): [__Field!] - # OBJECT only + "OBJECT only" interfaces: [__Type!] - # INTERFACE and UNION only + "INTERFACE and UNION only" possibleTypes: [__Type!] - # ENUM only + "ENUM only" enumValues(includeDeprecated: Boolean = false): [__EnumValue!] - # INPUT_OBJECT only + "INPUT_OBJECT only" inputFields: [__InputValue!] - # NON_NULL and LIST only + "NON_NULL and LIST only" ofType: __Type } +# eslint-disable-next-line type __Field { name: String! description: String @@ -47,6 +52,7 @@ type __Field { deprecationReason: String } +# eslint-disable-next-line type __InputValue { name: String! description: String @@ -54,6 +60,7 @@ type __InputValue { defaultValue: String } +# eslint-disable-next-line type __EnumValue { name: String! description: String @@ -61,6 +68,7 @@ type __EnumValue { deprecationReason: String } +# eslint-disable-next-line enum __TypeKind { SCALAR OBJECT @@ -72,6 +80,7 @@ enum __TypeKind { NON_NULL } +# eslint-disable-next-line type __Directive { name: String! description: String @@ -79,6 +88,7 @@ type __Directive { args: [__InputValue!]! } +# eslint-disable-next-line enum __DirectiveLocation { QUERY MUTATION diff --git a/crates/rover-client/src/operations/graph/introspect/mod.rs b/crates/rover-client/src/operations/graph/introspect/mod.rs new file mode 100644 index 000000000..541b12ff2 --- /dev/null +++ b/crates/rover-client/src/operations/graph/introspect/mod.rs @@ -0,0 +1,7 @@ +mod runner; +mod schema; +mod types; + +pub use runner::run; +pub use schema::Schema; +pub use types::{GraphIntrospectInput, GraphIntrospectResponse}; diff --git a/crates/rover-client/src/operations/graph/introspect/runner.rs b/crates/rover-client/src/operations/graph/introspect/runner.rs new file mode 100644 index 000000000..2734beff5 --- /dev/null +++ b/crates/rover-client/src/operations/graph/introspect/runner.rs @@ -0,0 +1,41 @@ +use crate::blocking::GraphQLClient; +use crate::operations::graph::introspect::{types::*, Schema}; +use crate::RoverClientError; +use graphql_client::*; + +use std::convert::TryFrom; + +#[derive(GraphQLQuery)] +#[graphql( + query_path = "src/operations/graph/introspect/introspect_query.graphql", + schema_path = "src/operations/graph/introspect/introspect_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. graph_introspect_query +pub(crate) struct GraphIntrospectQuery; + +/// The main function to be used from this module. This function fetches a +/// schema from apollo studio and returns it in either sdl (default) or json format +pub fn run( + input: GraphIntrospectInput, + client: &GraphQLClient, +) -> Result { + let variables = input.clone().into(); + let response_data = client.post::(variables, &input.headers)?; + build_response(response_data) +} + +fn build_response( + response: QueryResponseData, +) -> Result { + match Schema::try_from(response) { + Ok(schema) => Ok(GraphIntrospectResponse { + schema_sdl: schema.encode(), + }), + Err(msg) => Err(RoverClientError::IntrospectionError { msg: msg.into() }), + } +} diff --git a/crates/rover-client/tests/schema.rs b/crates/rover-client/src/operations/graph/introspect/schema.rs similarity index 78% rename from crates/rover-client/tests/schema.rs rename to crates/rover-client/src/operations/graph/introspect/schema.rs index 4678ec852..a6dbbe8f0 100644 --- a/crates/rover-client/tests/schema.rs +++ b/crates/rover-client/src/operations/graph/introspect/schema.rs @@ -1,20 +1,374 @@ -use graphql_client::Response; -use rover_client::introspection::Schema; +//! Schema encoding module used to work with Introspection result. +//! +//! More information on Schema Definition language(SDL) can be found in [this +//! documentation](https://www.apollographql.com/docs/apollo-server/schema/schema/). +//! +use sdl_encoder::{ + Directive, EnumDef, EnumValue, Field, InputField, InputObjectDef, InputValue, InterfaceDef, + ObjectDef, ScalarDef, Schema as SDL, SchemaDef, Type_, UnionDef, +}; +use serde::Deserialize; use std::convert::TryFrom; -use std::fs::File; + +use crate::operations::graph::introspect::runner::graph_introspect_query; + +type FullTypeField = graph_introspect_query::FullTypeFields; +type FullTypeInputField = graph_introspect_query::FullTypeInputFields; +type FullTypeFieldArg = graph_introspect_query::FullTypeFieldsArgs; +type IntrospectionResult = graph_introspect_query::ResponseData; +type SchemaMutationType = graph_introspect_query::GraphIntrospectQuerySchemaMutationType; +type SchemaQueryType = graph_introspect_query::GraphIntrospectQuerySchemaQueryType; +type SchemaType = graph_introspect_query::GraphIntrospectQuerySchemaTypes; +type SchemaDirective = graph_introspect_query::GraphIntrospectQuerySchemaDirectives; +type SchemaSubscriptionType = graph_introspect_query::GraphIntrospectQuerySchemaSubscriptionType; +type __TypeKind = graph_introspect_query::__TypeKind; + +// Represents GraphQL types we will not be encoding to SDL. +const GRAPHQL_NAMED_TYPES: [&str; 12] = [ + "__Schema", + "__Type", + "__TypeKind", + "__Field", + "__InputValue", + "__EnumValue", + "__DirectiveLocation", + "__Directive", + "Boolean", + "String", + "Int", + "ID", +]; + +// Represents GraphQL directives we will not be encoding to SDL. +const SPECIFIED_DIRECTIVES: [&str; 3] = ["skip", "include", "deprecated"]; + +/// A representation of a GraphQL Schema. +/// +/// Contains Schema Types and Directives. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Schema { + types: Vec, + directives: Vec, + mutation_type: Option, + query_type: SchemaQueryType, + subscription_type: Option, +} + +impl Schema { + /// Encode Schema into an SDL. + pub fn encode(self) -> String { + let mut sdl = SDL::new(); + + // When we have a defined mutation and subscription, we record + // everything to Schema Definition. + // https://www.apollographql.com/docs/graphql-subscriptions/subscriptions-to-schema/ + if self.mutation_type.is_some() | self.subscription_type.is_some() { + let mut schema_def = SchemaDef::new(); + if let Some(mutation_type) = self.mutation_type { + schema_def.mutation(mutation_type.name.unwrap()); + } + if let Some(subscription_type) = self.subscription_type { + schema_def.subscription(subscription_type.name.unwrap()); + } + if let Some(name) = self.query_type.name { + schema_def.query(name); + } + sdl.schema(schema_def); + } else if let Some(name) = self.query_type.name { + // If we don't have a mutation or a subscription, but do have a + // query type, only create a Schema Definition when it's something + // other than `Query`. + if name != "Query" { + let mut schema_def = SchemaDef::new(); + schema_def.query(name); + sdl.schema(schema_def); + } + } + + // Exclude GraphQL directives like 'skip' and 'include' before encoding directives. + self.directives + .into_iter() + .filter(|directive| !SPECIFIED_DIRECTIVES.contains(&directive.name.as_str())) + .for_each(|directive| Self::encode_directives(directive, &mut sdl)); + + // Exclude GraphQL named types like __Schema before encoding full type. + self.types + .into_iter() + .filter(|type_| match type_.full_type.name.as_deref() { + Some(name) => !GRAPHQL_NAMED_TYPES.contains(&name), + None => false, + }) + .for_each(|type_| Self::encode_full_type(type_, &mut sdl)); + + sdl.finish() + } + + fn encode_directives(directive: SchemaDirective, sdl: &mut SDL) { + let mut directive_ = Directive::new(directive.name); + directive_.description(directive.description); + for location in directive.locations { + // Location is of a __DirectiveLocation enum that doesn't implement + // Display (meaning we can't just do .to_string). This next line + // just forces it into a String with format! debug. + directive_.location(format!("{:?}", location)); + } + + sdl.directive(directive_) + } + + fn encode_full_type(type_: SchemaType, sdl: &mut SDL) { + let ty = type_.full_type; + + match ty.kind { + __TypeKind::OBJECT => { + let mut object_def = ObjectDef::new(ty.name.unwrap_or_else(String::new)); + object_def.description(ty.description); + if let Some(interfaces) = ty.interfaces { + for interface in interfaces { + object_def.interface(interface.type_ref.name.unwrap_or_else(String::new)); + } + } + if let Some(field) = ty.fields { + for f in field { + let field_def = Self::encode_field(f); + object_def.field(field_def); + } + sdl.object(object_def); + } + } + __TypeKind::INPUT_OBJECT => { + let mut input_def = InputObjectDef::new(ty.name.unwrap_or_else(String::new)); + input_def.description(ty.description); + if let Some(field) = ty.input_fields { + for f in field { + let input_field_def = Self::encode_input_field(f); + input_def.field(input_field_def); + } + sdl.input(input_def); + } + } + __TypeKind::INTERFACE => { + let mut interface_def = InterfaceDef::new(ty.name.unwrap_or_else(String::new)); + interface_def.description(ty.description); + if let Some(interfaces) = ty.interfaces { + for interface in interfaces { + interface_def + .interface(interface.type_ref.name.unwrap_or_else(String::new)); + } + } + if let Some(field) = ty.fields { + for f in field { + let field_def = Self::encode_field(f); + interface_def.field(field_def); + } + sdl.interface(interface_def); + } + } + __TypeKind::SCALAR => { + let mut scalar_def = ScalarDef::new(ty.name.unwrap_or_else(String::new)); + scalar_def.description(ty.description); + sdl.scalar(scalar_def); + } + __TypeKind::UNION => { + let mut union_def = UnionDef::new(ty.name.unwrap_or_else(String::new)); + union_def.description(ty.description); + if let Some(possible_types) = ty.possible_types { + for possible_type in possible_types { + union_def.member(possible_type.type_ref.name.unwrap_or_else(String::new)); + } + } + sdl.union(union_def); + } + __TypeKind::ENUM => { + let mut enum_def = EnumDef::new(ty.name.unwrap_or_else(String::new)); + if let Some(enums) = ty.enum_values { + for enum_ in enums { + let mut enum_value = EnumValue::new(enum_.name); + enum_value.description(enum_.description); + + if enum_.is_deprecated { + enum_value.deprecated(enum_.deprecation_reason); + } + + enum_def.value(enum_value); + } + } + sdl.enum_(enum_def); + } + _ => (), + } + } + + fn encode_field(field: FullTypeField) -> Field { + let ty = Self::encode_type(field.type_.type_ref); + let mut field_def = Field::new(field.name, ty); + + for value in field.args { + let field_value = Self::encode_arg(value); + field_def.arg(field_value); + } + + if field.is_deprecated { + field_def.deprecated(field.deprecation_reason); + } + field_def.description(field.description); + field_def + } + + fn encode_input_field(field: FullTypeInputField) -> InputField { + let ty = Self::encode_type(field.input_value.type_.type_ref); + let mut field_def = InputField::new(field.input_value.name, ty); + + field_def.default(field.input_value.default_value); + field_def.description(field.input_value.description); + field_def + } + + fn encode_arg(value: FullTypeFieldArg) -> InputValue { + let ty = Self::encode_type(value.input_value.type_.type_ref); + let mut value_def = InputValue::new(value.input_value.name, ty); + + value_def.default(value.input_value.default_value); + value_def.description(value.input_value.description); + value_def + } + + fn encode_type(ty: impl OfType) -> Type_ { + use graph_introspect_query::__TypeKind::*; + match ty.kind() { + SCALAR | OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT => Type_::NamedType { + name: ty.name().unwrap().to_string(), + }, + NON_NULL => { + let ty = Self::encode_type(ty.of_type().unwrap()); + Type_::NonNull { ty: Box::new(ty) } + } + LIST => { + let ty = Self::encode_type(ty.of_type().unwrap()); + Type_::List { ty: Box::new(ty) } + } + Other(ty) => panic!("Unknown type: {}", ty), + } + } +} + +impl TryFrom for Schema { + type Error = &'static str; + + fn try_from(src: IntrospectionResult) -> Result { + match src.schema { + Some(s) => Ok(Self { + types: s.types, + directives: s.directives, + mutation_type: s.mutation_type, + query_type: s.query_type, + subscription_type: s.subscription_type, + }), + None => Err("Schema not found in Introspection Result."), + } + } +} + +/// This trait is used to be able to iterate over ofType fields in +/// IntrospectionResponse. +pub trait OfType { + type TypeRef: OfType; + + fn kind(&self) -> &__TypeKind; + fn name(&self) -> Option<&str>; + fn of_type(self) -> Option; +} + +macro_rules! impl_of_type { + ($target:ty, $assoc:ty) => { + impl OfType for $target { + type TypeRef = $assoc; + + fn kind(&self) -> &__TypeKind { + &self.kind + } + + fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + fn of_type(self) -> Option { + self.of_type + } + } + }; +} + +impl_of_type!( + graph_introspect_query::TypeRef, + graph_introspect_query::TypeRefOfType +); + +impl_of_type!( + graph_introspect_query::TypeRefOfType, + graph_introspect_query::TypeRefOfTypeOfType +); + +impl_of_type!( + graph_introspect_query::TypeRefOfTypeOfType, + graph_introspect_query::TypeRefOfTypeOfTypeOfType +); + +impl_of_type!( + graph_introspect_query::TypeRefOfTypeOfTypeOfType, + graph_introspect_query::TypeRefOfTypeOfTypeOfTypeOfType +); + +impl_of_type!( + graph_introspect_query::TypeRefOfTypeOfTypeOfTypeOfType, + graph_introspect_query::TypeRefOfTypeOfTypeOfTypeOfTypeOfType +); + +impl_of_type!( + graph_introspect_query::TypeRefOfTypeOfTypeOfTypeOfTypeOfType, + graph_introspect_query::TypeRefOfTypeOfTypeOfTypeOfTypeOfTypeOfType +); + +impl_of_type!( + graph_introspect_query::TypeRefOfTypeOfTypeOfTypeOfTypeOfTypeOfType, + graph_introspect_query::TypeRefOfTypeOfTypeOfTypeOfTypeOfTypeOfTypeOfType +); + +// NOTE(lrlna): This is a **hack**. This makes sure that the last possible +// generated ofType by graphql_client can return a None for of_type method. +impl OfType for graph_introspect_query::TypeRefOfTypeOfTypeOfTypeOfTypeOfTypeOfTypeOfType { + type TypeRef = graph_introspect_query::TypeRefOfTypeOfTypeOfTypeOfTypeOfTypeOfTypeOfType; + + fn kind(&self) -> &graph_introspect_query::__TypeKind { + &self.kind + } + + fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + fn of_type(self) -> Option { + None + } +} #[cfg(test)] mod tests { use super::*; + + use graphql_client::Response; use indoc::indoc; use pretty_assertions::assert_eq; - use rover_client::query::graph::introspect; + use std::convert::TryFrom; + use std::fs::File; + + use crate::operations::graph::introspect::types::QueryResponseData; - pub type IntrospectionResult = introspect::introspection_query::ResponseData; #[test] fn it_builds_simple_schema() { - let file = File::open("tests/fixtures/simple.json").unwrap(); - let res: Response = serde_json::from_reader(file).unwrap(); + let file = File::open("src/operations/graph/introspect/fixtures/simple.json").unwrap(); + let res: Response = serde_json::from_reader(file).unwrap(); let data = res.data.unwrap(); let schema = Schema::try_from(data).unwrap(); @@ -47,8 +401,8 @@ mod tests { #[test] fn it_builds_swapi_schema() { - let file = File::open("tests/fixtures/swapi.json").unwrap(); - let res: Response = serde_json::from_reader(file).unwrap(); + let file = File::open("src/operations/graph/introspect/fixtures/swapi.json").unwrap(); + let res: Response = serde_json::from_reader(file).unwrap(); let data = res.data.unwrap(); let schema = Schema::try_from(data).unwrap(); @@ -1010,8 +1364,8 @@ mod tests { #[test] fn it_builds_schema_with_interfaces() { - let file = File::open("tests/fixtures/interfaces.json").unwrap(); - let res: Response = serde_json::from_reader(file).unwrap(); + let file = File::open("src/operations/graph/introspect/fixtures/interfaces.json").unwrap(); + let res: Response = serde_json::from_reader(file).unwrap(); let data = res.data.unwrap(); let schema = Schema::try_from(data).unwrap(); diff --git a/crates/rover-client/src/operations/graph/introspect/types.rs b/crates/rover-client/src/operations/graph/introspect/types.rs new file mode 100644 index 000000000..73fd29965 --- /dev/null +++ b/crates/rover-client/src/operations/graph/introspect/types.rs @@ -0,0 +1,22 @@ +use std::collections::HashMap; + +use crate::operations::graph::introspect::runner::graph_introspect_query; + +pub(crate) type QueryResponseData = graph_introspect_query::ResponseData; +pub(crate) type QueryVariables = graph_introspect_query::Variables; + +#[derive(Debug, Clone, PartialEq)] +pub struct GraphIntrospectInput { + pub headers: HashMap, +} + +impl From for QueryVariables { + fn from(_input: GraphIntrospectInput) -> Self { + Self {} + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct GraphIntrospectResponse { + pub schema_sdl: String, +} diff --git a/crates/rover-client/src/query/graph/mod.rs b/crates/rover-client/src/operations/graph/mod.rs similarity index 100% rename from crates/rover-client/src/query/graph/mod.rs rename to crates/rover-client/src/operations/graph/mod.rs diff --git a/crates/rover-client/src/operations/graph/publish/mod.rs b/crates/rover-client/src/operations/graph/publish/mod.rs new file mode 100644 index 000000000..e4fe3abb2 --- /dev/null +++ b/crates/rover-client/src/operations/graph/publish/mod.rs @@ -0,0 +1,7 @@ +mod runner; +mod types; + +pub use runner::run; +pub use types::{ + ChangeSummary, FieldChanges, GraphPublishInput, GraphPublishResponse, TypeChanges, +}; diff --git a/crates/rover-client/src/query/graph/publish.graphql b/crates/rover-client/src/operations/graph/publish/publish_mutation.graphql similarity index 70% rename from crates/rover-client/src/query/graph/publish.graphql rename to crates/rover-client/src/operations/graph/publish/publish_mutation.graphql index b260df1b0..8535b5689 100644 --- a/crates/rover-client/src/query/graph/publish.graphql +++ b/crates/rover-client/src/operations/graph/publish/publish_mutation.graphql @@ -1,14 +1,14 @@ -mutation PublishSchemaMutation( +mutation GraphPublishMutation( + $graph_id: ID! $variant: String! - $graphId: ID! - $schemaDocument: String - $gitContext: GitContextInput! + $proposed_schema: String! + $git_context: GitContextInput! ) { - service(id: $graphId) { + service(id: $graph_id) { uploadSchema( tag: $variant - schemaDocument: $schemaDocument - gitContext: $gitContext + schemaDocument: $proposed_schema + gitContext: $git_context ) { code message diff --git a/crates/rover-client/src/query/graph/publish.rs b/crates/rover-client/src/operations/graph/publish/runner.rs similarity index 58% rename from crates/rover-client/src/query/graph/publish.rs rename to crates/rover-client/src/operations/graph/publish/runner.rs index 3c5955600..4ff0fbaec 100644 --- a/crates/rover-client/src/query/graph/publish.rs +++ b/crates/rover-client/src/operations/graph/publish/runner.rs @@ -1,4 +1,7 @@ use crate::blocking::StudioClient; +use crate::operations::graph::publish::types::{ChangeSummary, FieldChanges, TypeChanges}; +use crate::operations::graph::publish::{GraphPublishInput, GraphPublishResponse}; +use crate::shared::GraphRef; use crate::RoverClientError; use graphql_client::*; @@ -6,40 +9,36 @@ use graphql_client::*; // 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/graph/publish.graphql", + query_path = "src/operations/graph/publish/publish_mutation.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. stash_schema_query -pub struct PublishSchemaMutation; - -#[derive(Debug, PartialEq)] -pub struct PublishResponse { - pub schema_hash: String, - pub change_summary: String, -} +/// Snake case of this name is the mod name. i.e. graph_publish_mutation +pub(crate) struct GraphPublishMutation; /// Returns a message from apollo studio about the status of the update, and /// a sha256 hash of the schema to be used with `schema publish` pub fn run( - variables: publish_schema_mutation::Variables, + input: GraphPublishInput, client: &StudioClient, -) -> Result { - let graph = variables.graph_id.clone(); - let data = client.post::(variables)?; - let publish_response = get_publish_response_from_data(data, graph)?; +) -> Result { + let graph_ref = input.graph_ref.clone(); + let data = client.post::(input.into())?; + let publish_response = get_publish_response_from_data(data, graph_ref)?; build_response(publish_response) } fn get_publish_response_from_data( - data: publish_schema_mutation::ResponseData, - graph: String, -) -> Result { + data: graph_publish_mutation::ResponseData, + graph_ref: GraphRef, +) -> Result { // then, from the response data, get .service?.upload_schema? - let service_data = data.service.ok_or(RoverClientError::NoService { graph })?; + let service_data = data + .service + .ok_or(RoverClientError::GraphNotFound { graph_ref })?; service_data .upload_schema @@ -49,8 +48,8 @@ fn get_publish_response_from_data( } fn build_response( - publish_response: publish_schema_mutation::PublishSchemaMutationServiceUploadSchema, -) -> Result { + publish_response: graph_publish_mutation::GraphPublishMutationServiceUploadSchema, +) -> Result { if !publish_response.success { let msg = format!( "Schema upload failed with error: {}", @@ -77,40 +76,66 @@ fn build_response( // which very well may have changes. For this, we'll just look at the code // first and handle the response as if there was `None` for the diff let change_summary = if publish_response.code == "NO_CHANGES" { - build_change_summary(None) + ChangeSummary::none() } else { - build_change_summary(publish_response.tag.unwrap().diff_to_previous) + let diff = publish_response + .tag + .ok_or_else(|| RoverClientError::MalformedResponse { + null_field: "service.upload_schema.tag".to_string(), + })? + .diff_to_previous; + + if let Some(diff) = diff { + diff.into() + } else { + ChangeSummary::none() + } }; - Ok(PublishResponse { - schema_hash: hash, + Ok(GraphPublishResponse { + api_schema_hash: hash, change_summary, }) } -type ChangeDiff = - publish_schema_mutation::PublishSchemaMutationServiceUploadSchemaTagDiffToPrevious; +type QueryChangeDiff = + graph_publish_mutation::GraphPublishMutationServiceUploadSchemaTagDiffToPrevious; -/// builds a string-representation of the diff between two schemas -/// e.g. ` [Fields: +2 -1 â–³0, Types: +4 -0 â–³7]` or `[No Changes]` -fn build_change_summary(diff: Option) -> String { - match diff { - None => "[No Changes]".to_string(), - Some(diff) => { - let changes = diff.change_summary; - let fields = format!( - "Fields: +{} -{} â–³ {}", - changes.field.additions, changes.field.removals, changes.field.edits - ); - let types = format!( - "Types: +{} -{} â–³ {}", - changes.type_.additions, changes.type_.removals, changes.type_.edits - ); - format!("[{}, {}]", fields, types) +impl From for ChangeSummary { + fn from(input: QueryChangeDiff) -> Self { + Self { + field_changes: input.change_summary.field.into(), + type_changes: input.change_summary.type_.into(), } } } +type QueryFieldChanges = + graph_publish_mutation::GraphPublishMutationServiceUploadSchemaTagDiffToPreviousChangeSummaryField; + +impl From for FieldChanges { + fn from(input: QueryFieldChanges) -> Self { + Self::with_diff( + input.additions as u64, + input.removals as u64, + input.edits as u64, + ) + } +} + +type QueryTypeChanges = + graph_publish_mutation::GraphPublishMutationServiceUploadSchemaTagDiffToPreviousChangeSummaryType; + +impl From for TypeChanges { + fn from(input: QueryTypeChanges) -> Self { + Self::with_diff( + input.additions as u64, + input.removals as u64, + input.edits as u64, + ) + } +} + #[cfg(test)] mod tests { use super::*; @@ -131,25 +156,25 @@ mod tests { } } }); - let data: publish_schema_mutation::ResponseData = + let data: graph_publish_mutation::ResponseData = serde_json::from_value(json_response).unwrap(); - let output = get_publish_response_from_data(data, "mygraph".to_string()); + let output = get_publish_response_from_data(data, mock_graph_ref()); assert!(output.is_ok()); assert_eq!( output.unwrap(), - publish_schema_mutation::PublishSchemaMutationServiceUploadSchema { + graph_publish_mutation::GraphPublishMutationServiceUploadSchema { code: "IT_WERK".to_string(), message: "it really do be published".to_string(), success: true, tag: Some( - publish_schema_mutation::PublishSchemaMutationServiceUploadSchemaTag { + graph_publish_mutation::GraphPublishMutationServiceUploadSchemaTag { variant: - publish_schema_mutation::PublishSchemaMutationServiceUploadSchemaTagVariant { + graph_publish_mutation::GraphPublishMutationServiceUploadSchemaTagVariant { name: "current".to_string() }, schema: - publish_schema_mutation::PublishSchemaMutationServiceUploadSchemaTagSchema { + graph_publish_mutation::GraphPublishMutationServiceUploadSchemaTagSchema { hash: "123456".to_string() }, diff_to_previous: None, @@ -162,9 +187,9 @@ mod tests { #[test] fn get_publish_response_from_data_errs_with_no_service() { let json_response = json!({ "service": null }); - let data: publish_schema_mutation::ResponseData = + let data: graph_publish_mutation::ResponseData = serde_json::from_value(json_response).unwrap(); - let output = get_publish_response_from_data(data, "mygraph".to_string()); + let output = get_publish_response_from_data(data, mock_graph_ref()); assert!(output.is_err()); } @@ -176,9 +201,9 @@ mod tests { "uploadSchema": null } }); - let data: publish_schema_mutation::ResponseData = + let data: graph_publish_mutation::ResponseData = serde_json::from_value(json_response).unwrap(); - let output = get_publish_response_from_data(data, "mygraph".to_string()); + let output = get_publish_response_from_data(data, mock_graph_ref()); assert!(output.is_err()); } @@ -194,16 +219,16 @@ mod tests { "schema": { "hash": "123456" } } }); - let update_response: publish_schema_mutation::PublishSchemaMutationServiceUploadSchema = + let update_response: graph_publish_mutation::GraphPublishMutationServiceUploadSchema = serde_json::from_value(json_response).unwrap(); let output = build_response(update_response); assert!(output.is_ok()); assert_eq!( output.unwrap(), - PublishResponse { - schema_hash: "123456".to_string(), - change_summary: "[No Changes]".to_string(), + GraphPublishResponse { + api_schema_hash: "123456".to_string(), + change_summary: ChangeSummary::none(), } ); } @@ -216,7 +241,7 @@ mod tests { "success": false, "tag": null }); - let update_response: publish_schema_mutation::PublishSchemaMutationServiceUploadSchema = + let update_response: graph_publish_mutation::GraphPublishMutationServiceUploadSchema = serde_json::from_value(json_response).unwrap(); let output = build_response(update_response); @@ -231,7 +256,7 @@ mod tests { "success": true, "tag": null }); - let update_response: publish_schema_mutation::PublishSchemaMutationServiceUploadSchema = + let update_response: graph_publish_mutation::GraphPublishMutationServiceUploadSchema = serde_json::from_value(json_response).unwrap(); let output = build_response(update_response); @@ -254,13 +279,26 @@ mod tests { } } }); - let diff_to_previous: ChangeDiff = serde_json::from_value(json_diff).unwrap(); - let output = build_change_summary(Some(diff_to_previous)); - assert_eq!(output, "[Fields: +3 -1 â–³ 0, Types: +4 -0 â–³ 2]".to_string()) + let diff_to_previous: QueryChangeDiff = serde_json::from_value(json_diff).unwrap(); + let output: ChangeSummary = diff_to_previous.into(); + assert_eq!( + output.to_string(), + "[Fields: +3 -1 â–³ 0, Types: +4 -0 â–³ 2]".to_string() + ) } #[test] fn build_change_summary_works_with_no_changes() { - assert_eq!(build_change_summary(None), "[No Changes]".to_string()) + assert_eq!( + ChangeSummary::none().to_string(), + "[No Changes]".to_string() + ) + } + + fn mock_graph_ref() -> GraphRef { + GraphRef { + name: "mygraph".to_string(), + variant: "current".to_string(), + } } } diff --git a/crates/rover-client/src/operations/graph/publish/types.rs b/crates/rover-client/src/operations/graph/publish/types.rs new file mode 100644 index 000000000..b51b0808b --- /dev/null +++ b/crates/rover-client/src/operations/graph/publish/types.rs @@ -0,0 +1,152 @@ +use crate::operations::graph::publish::runner::graph_publish_mutation; +use crate::shared::{GitContext, GraphRef}; + +use serde::Serialize; + +use std::fmt; + +#[derive(Clone, Debug, PartialEq)] +pub struct GraphPublishInput { + pub graph_ref: GraphRef, + pub proposed_schema: String, + pub git_context: GitContext, +} + +type MutationVariables = graph_publish_mutation::Variables; +impl From for MutationVariables { + fn from(input: GraphPublishInput) -> Self { + Self { + graph_id: input.graph_ref.name, + variant: input.graph_ref.variant, + proposed_schema: input.proposed_schema, + git_context: input.git_context.into(), + } + } +} + +type GraphPublishContextInput = graph_publish_mutation::GitContextInput; +impl From for GraphPublishContextInput { + fn from(git_context: GitContext) -> GraphPublishContextInput { + GraphPublishContextInput { + branch: git_context.branch, + commit: git_context.commit, + committer: git_context.author, + remote_url: git_context.remote_url, + message: None, + } + } +} + +#[derive(Clone, Serialize, Debug, PartialEq)] +pub struct GraphPublishResponse { + pub api_schema_hash: String, + #[serde(flatten)] + pub change_summary: ChangeSummary, +} + +#[derive(Clone, Serialize, Debug, PartialEq)] +pub struct ChangeSummary { + pub field_changes: FieldChanges, + pub type_changes: TypeChanges, +} + +impl ChangeSummary { + pub(crate) fn none() -> ChangeSummary { + ChangeSummary { + field_changes: FieldChanges::none(), + type_changes: TypeChanges::none(), + } + } + + pub(crate) fn is_none(&self) -> bool { + self.field_changes.is_none() && self.type_changes.is_none() + } +} + +impl fmt::Display for ChangeSummary { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.is_none() { + write!(f, "[No Changes]") + } else { + write!(f, "[{}, {}]", &self.field_changes, &self.type_changes) + } + } +} + +#[derive(Clone, Serialize, Debug, PartialEq)] +pub struct FieldChanges { + pub additions: u64, + pub removals: u64, + pub edits: u64, +} + +impl FieldChanges { + pub(crate) fn none() -> FieldChanges { + FieldChanges { + additions: 0, + removals: 0, + edits: 0, + } + } + + pub(crate) fn with_diff(additions: u64, removals: u64, edits: u64) -> FieldChanges { + FieldChanges { + additions, + removals, + edits, + } + } + + pub(crate) fn is_none(&self) -> bool { + self.additions == 0 && self.removals == 0 && self.edits == 0 + } +} + +impl fmt::Display for FieldChanges { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Fields: +{} -{} â–³ {}", + &self.additions, &self.removals, &self.edits + ) + } +} + +#[derive(Clone, Serialize, Debug, PartialEq)] +pub struct TypeChanges { + pub additions: u64, + pub removals: u64, + pub edits: u64, +} + +impl TypeChanges { + pub(crate) fn none() -> TypeChanges { + TypeChanges { + additions: 0, + removals: 0, + edits: 0, + } + } + + pub(crate) fn with_diff(additions: u64, removals: u64, edits: u64) -> TypeChanges { + TypeChanges { + additions, + removals, + edits, + } + } + + pub(crate) fn is_none(&self) -> bool { + self.additions == 0 && self.removals == 0 && self.edits == 0 + } +} + +impl fmt::Display for TypeChanges { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Types: +{} -{} â–³ {}", + &self.additions, &self.removals, &self.edits + ) + } +} diff --git a/crates/rover-client/src/query/mod.rs b/crates/rover-client/src/operations/mod.rs similarity index 100% rename from crates/rover-client/src/query/mod.rs rename to crates/rover-client/src/operations/mod.rs diff --git a/crates/rover-client/src/query/subgraph/check.graphql b/crates/rover-client/src/operations/subgraph/check/check_mutation.graphql similarity index 63% rename from crates/rover-client/src/query/subgraph/check.graphql rename to crates/rover-client/src/operations/subgraph/check/check_mutation.graphql index 5037bd561..906dfd831 100644 --- a/crates/rover-client/src/query/subgraph/check.graphql +++ b/crates/rover-client/src/operations/subgraph/check/check_mutation.graphql @@ -1,22 +1,27 @@ - mutation CheckPartialSchemaQuery( + mutation SubgraphCheckMutation( $graph_id: ID! $variant: String! - $implementingServiceName: String! - $partialSchema: PartialSchemaInput! - $gitContext: GitContextInput! + $subgraph: String! + $proposed_schema: PartialSchemaInput! + $git_context: GitContextInput! $config: HistoricQueryParameters! ) { service(id: $graph_id) { checkPartialSchema( graphVariant: $variant - implementingServiceName: $implementingServiceName - partialSchema: $partialSchema - gitContext: $gitContext + implementingServiceName: $subgraph + partialSchema: $proposed_schema + gitContext: $git_context historicParameters: $config ) { compositionValidationResult { errors { message + code + locations { + line + column + } } } checkSchemaResult { diff --git a/crates/rover-client/src/operations/subgraph/check/mod.rs b/crates/rover-client/src/operations/subgraph/check/mod.rs new file mode 100644 index 000000000..ebbc0d6e6 --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/check/mod.rs @@ -0,0 +1,5 @@ +mod runner; +mod types; + +pub use runner::run; +pub use types::SubgraphCheckInput; diff --git a/crates/rover-client/src/operations/subgraph/check/runner.rs b/crates/rover-client/src/operations/subgraph/check/runner.rs new file mode 100644 index 000000000..4b34dbba2 --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/check/runner.rs @@ -0,0 +1,117 @@ +use super::types::*; +use crate::blocking::StudioClient; +use crate::operations::{ + config::is_federated::{self, IsFederatedInput}, + subgraph::check::types::MutationResponseData, +}; +use crate::shared::{BuildError, CheckResponse, GraphRef, SchemaChange}; +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/operations/subgraph/check/check_mutation.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. subgraph_check_mutation +pub(crate) struct SubgraphCheckMutation; + +/// The main function to be used from this module. +/// This function takes a proposed schema and validates it against a published +/// schema. +pub fn run( + input: SubgraphCheckInput, + client: &StudioClient, +) -> Result { + let graph_ref = input.graph_ref.clone(); + let subgraph = input.subgraph.clone(); + // This response is used to check whether or not the current graph is federated. + let is_federated = is_federated::run( + IsFederatedInput { + graph_ref: graph_ref.clone(), + }, + &client, + )?; + if !is_federated { + return Err(RoverClientError::ExpectedFederatedGraph { + graph_ref, + can_operation_convert: false, + }); + } + let variables = input.into(); + let data = client.post::(variables)?; + get_check_response_from_data(data, graph_ref, subgraph) +} + +fn get_check_response_from_data( + data: MutationResponseData, + graph_ref: GraphRef, + subgraph: String, +) -> Result { + let service = data.service.ok_or(RoverClientError::GraphNotFound { + graph_ref: graph_ref.clone(), + })?; + + // for some reason this is a `Vec>` + // we convert this to just `Vec` because the `None` + // errors would be useless. + let query_composition_errors: Vec = service + .check_partial_schema + .composition_validation_result + .errors; + + if query_composition_errors.is_empty() { + let check_schema_result = service.check_partial_schema.check_schema_result.ok_or( + RoverClientError::MalformedResponse { + null_field: "service.check_partial_schema.check_schema_result".to_string(), + }, + )?; + + let diff_to_previous = check_schema_result.diff_to_previous; + + let operation_check_count = + diff_to_previous.number_of_checked_operations.unwrap_or(0) as u64; + + let result = diff_to_previous.severity.into(); + + let mut changes = Vec::with_capacity(diff_to_previous.changes.len()); + for change in diff_to_previous.changes { + changes.push(SchemaChange { + code: change.code, + severity: change.severity.into(), + description: change.description, + }); + } + + CheckResponse::try_new( + check_schema_result.target_url, + operation_check_count, + changes, + result, + graph_ref, + ) + } else { + let num_failures = query_composition_errors.len(); + + let mut build_errors = Vec::with_capacity(num_failures); + for query_composition_error in query_composition_errors { + build_errors.push(BuildError::composition_error( + query_composition_error.message, + query_composition_error.code, + )); + } + Err(RoverClientError::SubgraphBuildErrors { + subgraph, + graph_ref, + source: build_errors.into(), + }) + } +} diff --git a/crates/rover-client/src/operations/subgraph/check/types.rs b/crates/rover-client/src/operations/subgraph/check/types.rs new file mode 100644 index 000000000..c8aa519ba --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/check/types.rs @@ -0,0 +1,84 @@ +use crate::operations::subgraph::check::runner::subgraph_check_mutation; +use crate::shared::{ChangeSeverity, CheckConfig, GitContext, GraphRef}; + +type MutationVariables = subgraph_check_mutation::Variables; + +pub(crate) type MutationResponseData = subgraph_check_mutation::ResponseData; +pub(crate) type MutationCompositionErrors = + subgraph_check_mutation::SubgraphCheckMutationServiceCheckPartialSchemaCompositionValidationResultErrors; + +type MutationSchema = subgraph_check_mutation::PartialSchemaInput; +type MutationConfig = subgraph_check_mutation::HistoricQueryParameters; + +pub(crate) type MutationChangeSeverity = subgraph_check_mutation::ChangeSeverity; +impl From for ChangeSeverity { + fn from(severity: MutationChangeSeverity) -> Self { + match severity { + MutationChangeSeverity::NOTICE => ChangeSeverity::PASS, + MutationChangeSeverity::FAILURE => ChangeSeverity::FAIL, + _ => ChangeSeverity::unreachable(), + } + } +} + +type MutationGitContextInput = subgraph_check_mutation::GitContextInput; +impl From for MutationGitContextInput { + fn from(git_context: GitContext) -> MutationGitContextInput { + MutationGitContextInput { + branch: git_context.branch, + commit: git_context.commit, + committer: git_context.author, + remote_url: git_context.remote_url, + message: None, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SubgraphCheckInput { + pub graph_ref: GraphRef, + pub subgraph: String, + pub proposed_schema: String, + pub git_context: GitContext, + pub config: CheckConfig, +} + +impl From for MutationVariables { + fn from(input: SubgraphCheckInput) -> Self { + Self { + graph_id: input.graph_ref.name, + variant: input.graph_ref.variant, + subgraph: input.subgraph, + proposed_schema: MutationSchema { + sdl: Some(input.proposed_schema), + hash: None, + }, + config: MutationConfig { + query_count_threshold: input.config.query_count_threshold, + query_count_threshold_percentage: input.config.query_count_threshold_percentage, + from: Some( + input + .config + .validation_period + .clone() + .unwrap_or_default() + .from + .to_string(), + ), + to: Some( + input + .config + .validation_period + .unwrap_or_default() + .to + .to_string(), + ), + // we don't support configuring these, but we can't leave them out + excluded_clients: None, + ignored_operations: None, + included_variants: None, + }, + git_context: input.git_context.into(), + } + } +} diff --git a/crates/rover-client/src/operations/subgraph/delete/delete_mutation.graphql b/crates/rover-client/src/operations/subgraph/delete/delete_mutation.graphql new file mode 100644 index 000000000..c5a15a429 --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/delete/delete_mutation.graphql @@ -0,0 +1,20 @@ +mutation SubgraphDeleteMutation( + $graph_id: ID! + $variant: String! + $subgraph: String! + $dry_run: Boolean! +) { + service(id: $graph_id) { + removeImplementingServiceAndTriggerComposition( + graphVariant: $variant + name: $subgraph + dryRun: $dry_run + ) { + errors { + message + code + } + updatedGateway + } + } +} diff --git a/crates/rover-client/src/operations/subgraph/delete/mod.rs b/crates/rover-client/src/operations/subgraph/delete/mod.rs new file mode 100644 index 000000000..db8cb0622 --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/delete/mod.rs @@ -0,0 +1,5 @@ +mod runner; +mod types; + +pub use runner::run; +pub use types::{SubgraphDeleteInput, SubgraphDeleteResponse}; diff --git a/crates/rover-client/src/operations/subgraph/delete/runner.rs b/crates/rover-client/src/operations/subgraph/delete/runner.rs new file mode 100644 index 000000000..9acfd9ed4 --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/delete/runner.rs @@ -0,0 +1,164 @@ +use crate::blocking::StudioClient; +use crate::operations::subgraph::delete::types::*; +use crate::shared::{BuildError, BuildErrors, GraphRef}; +use crate::RoverClientError; + +use graphql_client::*; + +#[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/operations/subgraph/delete/delete_mutation.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. subgraph_delete_mutation +pub(crate) struct SubgraphDeleteMutation; + +/// The main function to be used from this module. This function fetches a +/// schema from apollo studio and returns it in either sdl (default) or json format +pub fn run( + input: SubgraphDeleteInput, + client: &StudioClient, +) -> Result { + let graph_ref = input.graph_ref.clone(); + let response_data = client.post::(input.into())?; + let data = get_delete_data_from_response(response_data, graph_ref)?; + Ok(build_response(data)) +} + +fn get_delete_data_from_response( + response_data: subgraph_delete_mutation::ResponseData, + graph_ref: GraphRef, +) -> Result { + let service_data = response_data + .service + .ok_or(RoverClientError::GraphNotFound { graph_ref })?; + + Ok(service_data.remove_implementing_service_and_trigger_composition) +} + +fn build_response(response: MutationComposition) -> SubgraphDeleteResponse { + let build_errors: BuildErrors = response + .errors + .iter() + .filter_map(|error| { + error + .as_ref() + .map(|e| BuildError::composition_error(e.message.clone(), e.code.clone())) + }) + .collect(); + + SubgraphDeleteResponse { + supergraph_was_updated: response.updated_gateway, + build_errors, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn get_delete_data_from_response_works() { + let json_response = json!({ + "service": { + "removeImplementingServiceAndTriggerComposition": { + "errors": [ + { + "message": "wow", + "code": null + }, + null, + { + "message": "boo", + "code": "BOO" + } + ], + "updatedGateway": false, + } + } + }); + let data: subgraph_delete_mutation::ResponseData = + serde_json::from_value(json_response).unwrap(); + let output = get_delete_data_from_response(data, mock_graph_ref()); + + assert!(output.is_ok()); + + let expected_response = MutationComposition { + errors: vec![ + Some(MutationCompositionErrors { + message: "wow".to_string(), + code: None, + }), + None, + Some(MutationCompositionErrors { + message: "boo".to_string(), + code: Some("BOO".to_string()), + }), + ], + updated_gateway: false, + }; + assert_eq!(output.unwrap(), expected_response); + } + + #[test] + fn build_response_works_with_successful_responses() { + let response = MutationComposition { + errors: vec![ + Some(MutationCompositionErrors { + message: "wow".to_string(), + code: None, + }), + None, + Some(MutationCompositionErrors { + message: "boo".to_string(), + code: Some("BOO".to_string()), + }), + ], + updated_gateway: false, + }; + + let parsed = build_response(response); + assert_eq!( + parsed, + SubgraphDeleteResponse { + build_errors: vec![ + BuildError::composition_error("wow".to_string(), None), + BuildError::composition_error("boo".to_string(), Some("BOO".to_string())) + ] + .into(), + supergraph_was_updated: false, + } + ); + } + + #[test] + fn build_response_works_with_failure_responses() { + let response = MutationComposition { + errors: vec![], + updated_gateway: true, + }; + + let parsed = build_response(response); + assert_eq!( + parsed, + SubgraphDeleteResponse { + build_errors: BuildErrors::new(), + supergraph_was_updated: true, + } + ); + } + + fn mock_graph_ref() -> GraphRef { + GraphRef { + name: "mygraph".to_string(), + variant: "current".to_string(), + } + } +} diff --git a/crates/rover-client/src/operations/subgraph/delete/types.rs b/crates/rover-client/src/operations/subgraph/delete/types.rs new file mode 100644 index 000000000..86052896e --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/delete/types.rs @@ -0,0 +1,42 @@ +use crate::{ + operations::subgraph::delete::runner::subgraph_delete_mutation, + shared::{BuildErrors, GraphRef}, +}; + +pub(crate) type MutationComposition = subgraph_delete_mutation::SubgraphDeleteMutationServiceRemoveImplementingServiceAndTriggerComposition; +pub(crate) type MutationVariables = subgraph_delete_mutation::Variables; + +use serde::Serialize; + +#[cfg(test)] +pub(crate) type MutationCompositionErrors = subgraph_delete_mutation::SubgraphDeleteMutationServiceRemoveImplementingServiceAndTriggerCompositionErrors; + +#[derive(Debug, Clone, PartialEq)] +pub struct SubgraphDeleteInput { + pub graph_ref: GraphRef, + pub subgraph: String, + pub dry_run: bool, +} + +/// this struct contains all the info needed to print the result of the delete. +/// `updated_gateway` is true when composition succeeds and the gateway config +/// is updated for the gateway to consume. `composition_errors` is just a list +/// of strings for when there are build errors as a result of the delete. +#[derive(Debug, Clone, Serialize, PartialEq)] +pub struct SubgraphDeleteResponse { + pub supergraph_was_updated: bool, + + #[serde(skip_serializing)] + pub build_errors: BuildErrors, +} + +impl From for MutationVariables { + fn from(input: SubgraphDeleteInput) -> Self { + Self { + graph_id: input.graph_ref.name, + variant: input.graph_ref.variant, + subgraph: input.subgraph, + dry_run: input.dry_run, + } + } +} diff --git a/crates/rover-client/src/query/subgraph/fetch.graphql b/crates/rover-client/src/operations/subgraph/fetch/fetch_query.graphql similarity index 72% rename from crates/rover-client/src/query/subgraph/fetch.graphql rename to crates/rover-client/src/operations/subgraph/fetch/fetch_query.graphql index 8b4e600c0..631177641 100644 --- a/crates/rover-client/src/query/subgraph/fetch.graphql +++ b/crates/rover-client/src/operations/subgraph/fetch/fetch_query.graphql @@ -1,5 +1,5 @@ -query FetchSubgraphQuery($variant: String!, $graphID: ID!) { - service(id: $graphID) { +query SubgraphFetchQuery($graph_id: ID!, $variant: String!) { + service(id: $graph_id) { implementingServices(graphVariant: $variant) { __typename ... on FederatedImplementingServices { diff --git a/crates/rover-client/src/operations/subgraph/fetch/mod.rs b/crates/rover-client/src/operations/subgraph/fetch/mod.rs new file mode 100644 index 000000000..65ea4d534 --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/fetch/mod.rs @@ -0,0 +1,5 @@ +mod runner; +mod types; + +pub use runner::run; +pub use types::SubgraphFetchInput; diff --git a/crates/rover-client/src/query/subgraph/fetch.rs b/crates/rover-client/src/operations/subgraph/fetch/runner.rs similarity index 68% rename from crates/rover-client/src/query/subgraph/fetch.rs rename to crates/rover-client/src/operations/subgraph/fetch/runner.rs index 9509d3951..dd0a1e2ac 100644 --- a/crates/rover-client/src/query/subgraph/fetch.rs +++ b/crates/rover-client/src/operations/subgraph/fetch/runner.rs @@ -1,71 +1,85 @@ +use super::types::*; use crate::blocking::StudioClient; +use crate::shared::{FetchResponse, GraphRef, Sdl, SdlType}; use crate::RoverClientError; + use graphql_client::*; #[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/fetch.graphql", + query_path = "src/operations/subgraph/fetch/fetch_query.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. fetch_subgraph_query -pub struct FetchSubgraphQuery; +/// Snake case of this name is the mod name. i.e. subgraph_fetch_query +pub(crate) struct SubgraphFetchQuery; /// Fetches a schema from apollo studio and returns its SDL (String) pub fn run( - variables: fetch_subgraph_query::Variables, + input: SubgraphFetchInput, client: &StudioClient, - // we can't specify this as a variable in the op, so we have to filter the - // operation response by this name - subgraph: &str, -) -> Result { - let graph = variables.graph_id.clone(); - let response_data = client.post::(variables)?; - let services = get_services_from_response_data(response_data, graph)?; - get_sdl_for_service(services, subgraph) - // if we want json, we can parse & serialize it here +) -> Result { + let input_clone = input.clone(); + let response_data = client.post::(input.into())?; + get_sdl_from_response_data(input_clone, response_data) +} + +fn get_sdl_from_response_data( + input: SubgraphFetchInput, + response_data: SubgraphFetchResponseData, +) -> Result { + let graph_ref = input.graph_ref.clone(); + let service_list = get_services_from_response_data(graph_ref, response_data)?; + let sdl_contents = get_sdl_for_service(&input.subgraph, service_list)?; + Ok(FetchResponse { + sdl: Sdl { + contents: sdl_contents, + r#type: SdlType::Subgraph, + }, + }) } -type ServiceList = Vec; fn get_services_from_response_data( - response_data: fetch_subgraph_query::ResponseData, - graph: String, + graph_ref: GraphRef, + response_data: SubgraphFetchResponseData, ) -> Result { - let service_data = response_data.service.ok_or(RoverClientError::NoService { - graph: graph.clone(), - })?; + let service_data = response_data + .service + .ok_or(RoverClientError::GraphNotFound { + graph_ref: graph_ref.clone(), + })?; // 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: graph.clone(), + graph_ref: graph_ref.clone(), can_operation_convert: false, }), }?; match services { - fetch_subgraph_query::FetchSubgraphQueryServiceImplementingServices::FederatedImplementingServices (services) => { - Ok(services.services) - }, - fetch_subgraph_query::FetchSubgraphQueryServiceImplementingServices::NonFederatedImplementingService => { - Err(RoverClientError::ExpectedFederatedGraph { graph, can_operation_convert: false }) + Services::FederatedImplementingServices(services) => Ok(services.services), + Services::NonFederatedImplementingService => { + Err(RoverClientError::ExpectedFederatedGraph { + graph_ref, + can_operation_convert: false, + }) } } } -fn get_sdl_for_service(services: ServiceList, subgraph: &str) -> Result { +fn get_sdl_for_service( + subgraph_name: &str, + services: ServiceList, +) -> Result { // find the right service by name - let service = services.iter().find(|svc| svc.name == subgraph); + let service = services.iter().find(|svc| svc.name == subgraph_name); // if there is a service, get it's active sdl, otherwise, error and list // available services to fetch @@ -75,7 +89,7 @@ fn get_sdl_for_service(services: ServiceList, subgraph: &str) -> Result = services.iter().map(|svc| svc.name.clone()).collect(); Err(RoverClientError::NoSubgraphInGraph { - invalid_subgraph: subgraph.to_string(), + invalid_subgraph: subgraph_name.to_string(), valid_subgraphs, }) } @@ -109,9 +123,8 @@ mod tests { } } }); - let data: fetch_subgraph_query::ResponseData = - serde_json::from_value(json_response).unwrap(); - let output = get_services_from_response_data(data, "mygraph".to_string()); + let data: SubgraphFetchResponseData = serde_json::from_value(json_response).unwrap(); + let output = get_services_from_response_data(mock_graph_ref(), data); let expected_json = json!([ { @@ -140,9 +153,8 @@ mod tests { "implementingServices": null } }); - let data: fetch_subgraph_query::ResponseData = - serde_json::from_value(json_response).unwrap(); - let output = get_services_from_response_data(data, "mygraph".to_string()); + let data: SubgraphFetchResponseData = serde_json::from_value(json_response).unwrap(); + let output = get_services_from_response_data(mock_graph_ref(), data); assert!(output.is_err()); } @@ -163,7 +175,7 @@ mod tests { } ]); let service_list: ServiceList = serde_json::from_value(json_service_list).unwrap(); - let output = get_sdl_for_service(service_list, "accounts2"); + let output = get_sdl_for_service("accounts2", service_list); assert_eq!( output.unwrap(), "extend type User @key(fields: \"id\") {\n id: ID! @external\n age: Int\n}\n" @@ -188,7 +200,14 @@ mod tests { } ]); let service_list: ServiceList = serde_json::from_value(json_service_list).unwrap(); - let output = get_sdl_for_service(service_list, "harambe-was-an-inside-job"); + let output = get_sdl_for_service("harambe-was-an-inside-job", service_list); assert!(output.is_err()); } + + fn mock_graph_ref() -> GraphRef { + GraphRef { + name: "mygraph".to_string(), + variant: "current".to_string(), + } + } } diff --git a/crates/rover-client/src/operations/subgraph/fetch/types.rs b/crates/rover-client/src/operations/subgraph/fetch/types.rs new file mode 100644 index 000000000..68f0f1a75 --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/fetch/types.rs @@ -0,0 +1,23 @@ +use crate::shared::GraphRef; + +use super::runner::subgraph_fetch_query; + +pub(crate) type ServiceList = Vec; +pub(crate) type SubgraphFetchResponseData = subgraph_fetch_query::ResponseData; +pub(crate) type Services = subgraph_fetch_query::SubgraphFetchQueryServiceImplementingServices; +pub(crate) type QueryVariables = subgraph_fetch_query::Variables; + +#[derive(Debug, Clone, PartialEq)] +pub struct SubgraphFetchInput { + pub graph_ref: GraphRef, + pub subgraph: String, +} + +impl From for QueryVariables { + fn from(input: SubgraphFetchInput) -> Self { + Self { + graph_id: input.graph_ref.name, + variant: input.graph_ref.variant, + } + } +} diff --git a/crates/rover-client/src/operations/subgraph/introspect/introspect_query.graphql b/crates/rover-client/src/operations/subgraph/introspect/introspect_query.graphql new file mode 100644 index 000000000..a5023b264 --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/introspect/introspect_query.graphql @@ -0,0 +1,6 @@ +query SubgraphIntrospectQuery { + # eslint-disable-next-line + _service { + sdl + } +} \ No newline at end of file diff --git a/crates/rover-client/src/operations/subgraph/introspect/introspect_schema.graphql b/crates/rover-client/src/operations/subgraph/introspect/introspect_schema.graphql new file mode 100644 index 000000000..9b16951ea --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/introspect/introspect_schema.graphql @@ -0,0 +1,16 @@ +# eslint-disable-next-line +schema { + query: Query +} + +# eslint-disable-next-line +type Query { + # eslint-disable-next-line + _service: _Service +} + +# eslint-disable-next-line +type _Service { + # eslint-disable-next-line + sdl: String! +} diff --git a/crates/rover-client/src/operations/subgraph/introspect/mod.rs b/crates/rover-client/src/operations/subgraph/introspect/mod.rs new file mode 100644 index 000000000..047cf54f2 --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/introspect/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod runner; +pub(crate) mod types; + +pub use runner::run; +pub use types::{SubgraphIntrospectInput, SubgraphIntrospectResponse}; diff --git a/crates/rover-client/src/query/subgraph/introspect.rs b/crates/rover-client/src/operations/subgraph/introspect/runner.rs similarity index 57% rename from crates/rover-client/src/query/subgraph/introspect.rs rename to crates/rover-client/src/operations/subgraph/introspect/runner.rs index 8a2a0b206..9009c2942 100644 --- a/crates/rover-client/src/query/subgraph/introspect.rs +++ b/crates/rover-client/src/operations/subgraph/introspect/runner.rs @@ -1,29 +1,25 @@ use crate::blocking::GraphQLClient; +use crate::operations::subgraph::introspect::types::*; use crate::RoverClientError; + use graphql_client::*; -use std::collections::HashMap; #[derive(GraphQLQuery)] #[graphql( - query_path = "src/query/subgraph/introspect_query.graphql", - schema_path = "src/query/subgraph/introspect_schema.graphql", + query_path = "src/operations/subgraph/introspect/introspect_query.graphql", + schema_path = "src/operations/subgraph/introspect/introspect_schema.graphql", response_derives = "PartialEq, Debug, Serialize, Deserialize", deprecated = "warn" )] -pub struct IntrospectionQuery; - -#[derive(Debug, PartialEq)] -pub struct IntrospectionResponse { - pub result: String, -} +pub(crate) struct SubgraphIntrospectQuery; pub fn run( + input: SubgraphIntrospectInput, client: &GraphQLClient, - headers: &HashMap, -) -> Result { - let variables = introspection_query::Variables {}; - let response_data = client.post::(variables, headers); +) -> Result { + let response_data = + client.post::(input.clone().into(), &input.headers); match response_data { Ok(data) => build_response(data), @@ -39,14 +35,12 @@ pub fn run( } } -fn build_response( - data: introspection_query::ResponseData, -) -> Result { +fn build_response(data: QueryResponseData) -> Result { let service_data = data.service.ok_or(RoverClientError::IntrospectionError { msg: "No introspection response available.".to_string(), })?; - Ok(IntrospectionResponse { + Ok(SubgraphIntrospectResponse { result: service_data.sdl, }) } diff --git a/crates/rover-client/src/operations/subgraph/introspect/types.rs b/crates/rover-client/src/operations/subgraph/introspect/types.rs new file mode 100644 index 000000000..0b1a933c7 --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/introspect/types.rs @@ -0,0 +1,22 @@ +use crate::operations::subgraph::introspect::runner::subgraph_introspect_query; + +pub(crate) type QueryVariables = subgraph_introspect_query::Variables; +pub(crate) type QueryResponseData = subgraph_introspect_query::ResponseData; + +use std::collections::HashMap; + +#[derive(Debug, Clone, PartialEq)] +pub struct SubgraphIntrospectInput { + pub headers: HashMap, +} + +impl From for QueryVariables { + fn from(_input: SubgraphIntrospectInput) -> Self { + Self {} + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SubgraphIntrospectResponse { + pub result: String, +} diff --git a/crates/rover-client/src/query/subgraph/list.graphql b/crates/rover-client/src/operations/subgraph/list/list_query.graphql similarity index 72% rename from crates/rover-client/src/query/subgraph/list.graphql rename to crates/rover-client/src/operations/subgraph/list/list_query.graphql index d733b65aa..06a50be9a 100644 --- a/crates/rover-client/src/query/subgraph/list.graphql +++ b/crates/rover-client/src/operations/subgraph/list/list_query.graphql @@ -1,6 +1,6 @@ -query ListSubgraphsQuery($graphId: ID!, $variant: String!) { +query SubgraphListQuery($graph_id: ID!, $variant: String!) { frontendUrlRoot - service(id: $graphId) { + service(id: $graph_id) { implementingServices(graphVariant: $variant) { __typename ... on FederatedImplementingServices { diff --git a/crates/rover-client/src/operations/subgraph/list/mod.rs b/crates/rover-client/src/operations/subgraph/list/mod.rs new file mode 100644 index 000000000..dd493dd4c --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/list/mod.rs @@ -0,0 +1,5 @@ +mod runner; +mod types; + +pub use runner::run; +pub use types::{SubgraphInfo, SubgraphListInput, SubgraphListResponse, SubgraphUpdatedAt}; diff --git a/crates/rover-client/src/query/subgraph/list.rs b/crates/rover-client/src/operations/subgraph/list/runner.rs similarity index 66% rename from crates/rover-client/src/query/subgraph/list.rs rename to crates/rover-client/src/operations/subgraph/list/runner.rs index a7752d5f7..2b9912783 100644 --- a/crates/rover-client/src/query/subgraph/list.rs +++ b/crates/rover-client/src/operations/subgraph/list/runner.rs @@ -1,62 +1,50 @@ use crate::blocking::StudioClient; +use crate::operations::subgraph::list::types::*; +use crate::shared::GraphRef; 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", + query_path = "src/operations/subgraph/list/list_query.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: Option>, -} - -#[derive(Clone, PartialEq, Debug)] -pub struct ListDetails { - pub subgraphs: Vec, - pub root_url: String, - pub graph_name: String, -} +/// Snake case of this name is the mod name. i.e. subgraph_list_query +pub(crate) struct SubgraphListQuery; /// Fetches list of subgraphs for a given graph, returns name & url of each pub fn run( - variables: list_subgraphs_query::Variables, + input: SubgraphListInput, client: &StudioClient, -) -> Result { - let graph = variables.graph_id.clone(); - let response_data = client.post::(variables)?; +) -> Result { + let graph_ref = input.graph_ref.clone(); + let response_data = client.post::(input.into())?; let root_url = response_data.frontend_url_root.clone(); - let subgraphs = get_subgraphs_from_response_data(response_data, graph.clone())?; - Ok(ListDetails { + let subgraphs = get_subgraphs_from_response_data(response_data, graph_ref.clone())?; + Ok(SubgraphListResponse { subgraphs: format_subgraphs(&subgraphs), root_url, - graph_name: graph, + graph_ref, }) } -type RawSubgraphInfo = list_subgraphs_query::ListSubgraphsQueryServiceImplementingServicesOnFederatedImplementingServicesServices; fn get_subgraphs_from_response_data( - response_data: list_subgraphs_query::ResponseData, - graph: String, -) -> Result, RoverClientError> { - let service_data = response_data.service.ok_or(RoverClientError::NoService { - graph: graph.clone(), - })?; + response_data: QueryResponseData, + graph_ref: GraphRef, +) -> Result, RoverClientError> { + let service_data = response_data + .service + .ok_or(RoverClientError::GraphNotFound { + graph_ref: graph_ref.clone(), + })?; // get list of services let services = match service_data.implementing_services { @@ -72,31 +60,35 @@ fn get_subgraphs_from_response_data( // implementing_services.services match services { - list_subgraphs_query::ListSubgraphsQueryServiceImplementingServices::FederatedImplementingServices (services) => { - Ok(services.services) - }, - list_subgraphs_query::ListSubgraphsQueryServiceImplementingServices::NonFederatedImplementingService => { - Err(RoverClientError::ExpectedFederatedGraph { graph, can_operation_convert: false }) + QueryGraphType::FederatedImplementingServices(services) => Ok(services.services), + QueryGraphType::NonFederatedImplementingService => { + Err(RoverClientError::ExpectedFederatedGraph { + graph_ref, + can_operation_convert: false, + }) } } } /// 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 { +fn format_subgraphs(subgraphs: &[QuerySubgraphInfo]) -> Vec { let mut subgraphs: Vec = subgraphs .iter() .map(|subgraph| SubgraphInfo { name: subgraph.name.clone(), url: subgraph.url.clone(), - updated_at: subgraph.updated_at.clone().parse::>().ok(), + updated_at: SubgraphUpdatedAt { + local: subgraph.updated_at.clone().parse().ok(), + utc: subgraph.updated_at.clone().parse().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.sort_unstable_by(|a, b| a.updated_at.utc.cmp(&b.updated_at.utc).reverse()); subgraphs } @@ -128,9 +120,8 @@ mod tests { } } }); - let data: list_subgraphs_query::ResponseData = - serde_json::from_value(json_response).unwrap(); - let output = get_subgraphs_from_response_data(data, "mygraph".to_string()); + let data: QueryResponseData = serde_json::from_value(json_response).unwrap(); + let output = get_subgraphs_from_response_data(data, mock_graph_ref()); let expected_json = json!([ { @@ -144,7 +135,7 @@ mod tests { "updatedAt": "2020-09-16T19:22:06.420Z" } ]); - let expected_service_list: Vec = + let expected_service_list: Vec = serde_json::from_value(expected_json).unwrap(); assert!(output.is_ok()); @@ -159,9 +150,8 @@ mod tests { "implementingServices": null } }); - let data: list_subgraphs_query::ResponseData = - serde_json::from_value(json_response).unwrap(); - let output = get_subgraphs_from_response_data(data, "mygraph".to_string()); + let data: QueryResponseData = serde_json::from_value(json_response).unwrap(); + let output = get_subgraphs_from_response_data(data, mock_graph_ref()); assert!(output.is_err()); } @@ -184,10 +174,17 @@ mod tests { "updatedAt": "2020-09-16T19:22:06.420Z" } ]); - let raw_subgraph_list: Vec = + 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()); } + + fn mock_graph_ref() -> GraphRef { + GraphRef { + name: "mygraph".to_string(), + variant: "current".to_string(), + } + } } diff --git a/crates/rover-client/src/operations/subgraph/list/types.rs b/crates/rover-client/src/operations/subgraph/list/types.rs new file mode 100644 index 000000000..8e18dd9f2 --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/list/types.rs @@ -0,0 +1,48 @@ +use crate::{operations::subgraph::list::runner::subgraph_list_query, shared::GraphRef}; + +pub(crate) type QuerySubgraphInfo = subgraph_list_query::SubgraphListQueryServiceImplementingServicesOnFederatedImplementingServicesServices; +pub(crate) type QueryResponseData = subgraph_list_query::ResponseData; +pub(crate) type QueryGraphType = subgraph_list_query::SubgraphListQueryServiceImplementingServices; + +type QueryVariables = subgraph_list_query::Variables; + +use chrono::{DateTime, Local, Utc}; +use serde::Serialize; + +#[derive(Clone, PartialEq, Debug)] +pub struct SubgraphListInput { + pub graph_ref: GraphRef, +} + +impl From for QueryVariables { + fn from(input: SubgraphListInput) -> Self { + Self { + graph_id: input.graph_ref.name, + variant: input.graph_ref.variant, + } + } +} + +#[derive(Clone, Serialize, PartialEq, Debug)] +pub struct SubgraphListResponse { + pub subgraphs: Vec, + + #[serde(skip_serializing)] + pub root_url: String, + + #[serde(skip_serializing)] + pub graph_ref: GraphRef, +} + +#[derive(Clone, Serialize, PartialEq, Debug)] +pub struct SubgraphInfo { + pub name: String, + pub url: Option, // optional, and may not be a real url + pub updated_at: SubgraphUpdatedAt, +} + +#[derive(Clone, Serialize, PartialEq, Debug)] +pub struct SubgraphUpdatedAt { + pub local: Option>, + pub utc: Option>, +} diff --git a/crates/rover-client/src/query/subgraph/mod.rs b/crates/rover-client/src/operations/subgraph/mod.rs similarity index 100% rename from crates/rover-client/src/query/subgraph/mod.rs rename to crates/rover-client/src/operations/subgraph/mod.rs diff --git a/crates/rover-client/src/operations/subgraph/publish/mod.rs b/crates/rover-client/src/operations/subgraph/publish/mod.rs new file mode 100644 index 000000000..0cf673b54 --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/publish/mod.rs @@ -0,0 +1,5 @@ +mod runner; +mod types; + +pub use runner::run; +pub use types::{SubgraphPublishInput, SubgraphPublishResponse}; diff --git a/crates/rover-client/src/operations/subgraph/publish/publish_mutation.graphql b/crates/rover-client/src/operations/subgraph/publish/publish_mutation.graphql new file mode 100644 index 000000000..0777dadd5 --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/publish/publish_mutation.graphql @@ -0,0 +1,30 @@ +mutation SubgraphPublishMutation( + $graph_id: ID! + $variant: String! + $subgraph: String! + $url: String + $revision: String! + $schema: PartialSchemaInput! + $git_context: GitContextInput! +) { + service(id: $graph_id) { + upsertImplementingServiceAndTriggerComposition( + name: $subgraph + url: $url + revision: $revision + activePartialSchema: $schema + graphVariant: $variant + gitContext: $git_context + ) { + compositionConfig { + schemaHash + } + errors { + message + code + } + didUpdateGateway: updatedGateway + serviceWasCreated: wasCreated + } + } +} diff --git a/crates/rover-client/src/query/subgraph/publish.rs b/crates/rover-client/src/operations/subgraph/publish/runner.rs similarity index 50% rename from crates/rover-client/src/query/subgraph/publish.rs rename to crates/rover-client/src/operations/subgraph/publish/runner.rs index 3b08f088a..9f6df852b 100644 --- a/crates/rover-client/src/query/subgraph/publish.rs +++ b/crates/rover-client/src/operations/subgraph/publish/runner.rs @@ -1,6 +1,7 @@ -// PublishPartialSchemaMutation +use super::types::*; use crate::blocking::StudioClient; -use crate::query::config::is_federated; +use crate::operations::config::is_federated::{self, IsFederatedInput}; +use crate::shared::{BuildError, BuildErrors, GraphRef}; use crate::RoverClientError; use graphql_client::*; @@ -8,88 +9,75 @@ use graphql_client::*; // 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/publish.graphql", + query_path = "src/operations/subgraph/publish/publish_mutation.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. publish_partial_schema_mutation -pub struct PublishPartialSchemaMutation; - -#[derive(Debug, PartialEq)] -pub struct PublishPartialSchemaResponse { - pub schema_hash: Option, - pub did_update_gateway: bool, - pub service_was_created: bool, - pub composition_errors: Option>, -} +/// Snake case of this name is the mod name. i.e. subgraph_publish_mutation +pub(crate) struct SubgraphPublishMutation; pub fn run( - variables: publish_partial_schema_mutation::Variables, + input: SubgraphPublishInput, client: &StudioClient, - convert_to_federated_graph: bool, -) -> Result { - let graph = variables.graph_id.clone(); +) -> Result { + let graph_ref = input.graph_ref.clone(); + let variables: MutationVariables = input.clone().into(); // We don't want to implicitly convert non-federated graph to supergraphs. // Error here if no --convert flag is passed _and_ the current context // is non-federated. Add a suggestion to require a --convert flag. - if !convert_to_federated_graph { + if !input.convert_to_federated_graph { let is_federated = is_federated::run( - is_federated::is_federated_graph::Variables { - graph_id: variables.graph_id.clone(), - graph_variant: variables.graph_variant.clone(), + IsFederatedInput { + graph_ref: graph_ref.clone(), }, &client, )?; if !is_federated { return Err(RoverClientError::ExpectedFederatedGraph { - graph, + graph_ref, can_operation_convert: true, }); } } - let data = client.post::(variables)?; - let publish_response = get_publish_response_from_data(data, graph)?; + let data = client.post::(variables)?; + let publish_response = get_publish_response_from_data(data, graph_ref)?; Ok(build_response(publish_response)) } -// alias this return type since it's disgusting -type UpdateResponse = publish_partial_schema_mutation::PublishPartialSchemaMutationServiceUpsertImplementingServiceAndTriggerComposition; - fn get_publish_response_from_data( - data: publish_partial_schema_mutation::ResponseData, - graph: String, + data: ResponseData, + graph_ref: GraphRef, ) -> Result { - let service_data = data.service.ok_or(RoverClientError::NoService { graph })?; + let service_data = data + .service + .ok_or(RoverClientError::GraphNotFound { graph_ref })?; Ok(service_data.upsert_implementing_service_and_trigger_composition) } -fn build_response(publish_response: UpdateResponse) -> PublishPartialSchemaResponse { - let composition_errors: Vec = publish_response +fn build_response(publish_response: UpdateResponse) -> SubgraphPublishResponse { + let build_errors: BuildErrors = publish_response .errors .iter() - .filter_map(|error| error.as_ref().map(|e| e.message.clone())) + .filter_map(|error| { + error + .as_ref() + .map(|e| BuildError::composition_error(e.message.clone(), e.code.clone())) + }) .collect(); - // if there are no errors, just return None - let composition_errors = if !composition_errors.is_empty() { - Some(composition_errors) - } else { - None - }; - - PublishPartialSchemaResponse { - schema_hash: match publish_response.composition_config { + SubgraphPublishResponse { + api_schema_hash: match publish_response.composition_config { Some(config) => Some(config.schema_hash), None => None, }, - did_update_gateway: publish_response.did_update_gateway, - service_was_created: publish_response.service_was_created, - composition_errors, + supergraph_was_updated: publish_response.did_update_gateway, + subgraph_was_created: publish_response.service_was_created, + build_errors, } } @@ -102,9 +90,15 @@ mod tests { let json_response = json!({ "compositionConfig": { "schemaHash": "5gf564" }, "errors": [ - {"message": "[Accounts] User -> composition error"}, + { + "message": "[Accounts] User -> build error", + "code": null + }, null, // this is technically allowed in the types - {"message": "[Products] Product -> another one"} + { + "message": "[Products] Product -> another one", + "code": "ERROR" + } ], "didUpdateGateway": false, "serviceWasCreated": true @@ -114,14 +108,21 @@ mod tests { assert_eq!( output, - PublishPartialSchemaResponse { - schema_hash: Some("5gf564".to_string()), - composition_errors: Some(vec![ - "[Accounts] User -> composition error".to_string(), - "[Products] Product -> another one".to_string() - ]), - did_update_gateway: false, - service_was_created: true, + SubgraphPublishResponse { + api_schema_hash: Some("5gf564".to_string()), + build_errors: vec![ + BuildError::composition_error( + "[Accounts] User -> build error".to_string(), + None + ), + BuildError::composition_error( + "[Products] Product -> another one".to_string(), + Some("ERROR".to_string()) + ) + ] + .into(), + supergraph_was_updated: false, + subgraph_was_created: true, } ); } @@ -139,11 +140,11 @@ mod tests { assert_eq!( output, - PublishPartialSchemaResponse { - schema_hash: Some("5gf564".to_string()), - composition_errors: None, - did_update_gateway: true, - service_was_created: true, + SubgraphPublishResponse { + api_schema_hash: Some("5gf564".to_string()), + build_errors: BuildErrors::new(), + supergraph_was_updated: true, + subgraph_was_created: true, } ); } @@ -154,7 +155,10 @@ mod tests { fn build_response_works_with_failure_and_no_hash() { let json_response = json!({ "compositionConfig": null, - "errors": [{ "message": "[Accounts] -> Things went really wrong" }], + "errors": [{ + "message": "[Accounts] -> Things went really wrong", + "code": null + }], "didUpdateGateway": false, "serviceWasCreated": false }); @@ -163,13 +167,15 @@ mod tests { assert_eq!( output, - PublishPartialSchemaResponse { - schema_hash: None, - composition_errors: Some( - vec!["[Accounts] -> Things went really wrong".to_string()] - ), - did_update_gateway: false, - service_was_created: false, + SubgraphPublishResponse { + api_schema_hash: None, + build_errors: vec![BuildError::composition_error( + "[Accounts] -> Things went really wrong".to_string(), + None + )] + .into(), + supergraph_was_updated: false, + subgraph_was_created: false, } ); } diff --git a/crates/rover-client/src/operations/subgraph/publish/types.rs b/crates/rover-client/src/operations/subgraph/publish/types.rs new file mode 100644 index 000000000..4dd7eb182 --- /dev/null +++ b/crates/rover-client/src/operations/subgraph/publish/types.rs @@ -0,0 +1,63 @@ +use super::runner::subgraph_publish_mutation; + +use crate::shared::{BuildErrors, GitContext, GraphRef}; + +pub(crate) type ResponseData = subgraph_publish_mutation::ResponseData; +pub(crate) type MutationVariables = subgraph_publish_mutation::Variables; +pub(crate) type UpdateResponse = subgraph_publish_mutation::SubgraphPublishMutationServiceUpsertImplementingServiceAndTriggerComposition; + +type SchemaInput = subgraph_publish_mutation::PartialSchemaInput; +type GitContextInput = subgraph_publish_mutation::GitContextInput; + +use serde::Serialize; + +#[derive(Debug, Clone, PartialEq)] +pub struct SubgraphPublishInput { + pub graph_ref: GraphRef, + pub subgraph: String, + pub url: Option, + pub schema: String, + pub git_context: GitContext, + pub convert_to_federated_graph: bool, +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +pub struct SubgraphPublishResponse { + pub api_schema_hash: Option, + + pub supergraph_was_updated: bool, + + pub subgraph_was_created: bool, + + #[serde(skip_serializing)] + pub build_errors: BuildErrors, +} + +impl From for MutationVariables { + fn from(publish_input: SubgraphPublishInput) -> Self { + Self { + graph_id: publish_input.graph_ref.name, + variant: publish_input.graph_ref.variant, + subgraph: publish_input.subgraph, + url: publish_input.url, + schema: SchemaInput { + sdl: Some(publish_input.schema), + hash: None, + }, + git_context: publish_input.git_context.into(), + revision: "".to_string(), + } + } +} + +impl From for GitContextInput { + fn from(git_context: GitContext) -> GitContextInput { + GitContextInput { + branch: git_context.branch, + commit: git_context.commit, + committer: git_context.author, + remote_url: git_context.remote_url, + message: None, + } + } +} diff --git a/crates/rover-client/src/query/supergraph/fetch.graphql b/crates/rover-client/src/operations/supergraph/fetch/fetch_query.graphql similarity index 71% rename from crates/rover-client/src/query/supergraph/fetch.graphql rename to crates/rover-client/src/operations/supergraph/fetch/fetch_query.graphql index 5436b091a..0e1aebc68 100644 --- a/crates/rover-client/src/query/supergraph/fetch.graphql +++ b/crates/rover-client/src/operations/supergraph/fetch/fetch_query.graphql @@ -1,6 +1,6 @@ -query FetchSupergraphQuery($graphId: ID!, $variant: String!) { +query SupergraphFetchQuery($graph_id: ID!, $variant: String!) { frontendUrlRoot - service(id: $graphId) { + service(id: $graph_id) { variants { name } @@ -13,7 +13,8 @@ query FetchSupergraphQuery($graphId: ID!, $variant: String!) { mostRecentCompositionPublish(graphVariant: $variant) { errors { message + code } } } -} \ No newline at end of file +} diff --git a/crates/rover-client/src/operations/supergraph/fetch/mod.rs b/crates/rover-client/src/operations/supergraph/fetch/mod.rs new file mode 100644 index 000000000..d97562469 --- /dev/null +++ b/crates/rover-client/src/operations/supergraph/fetch/mod.rs @@ -0,0 +1,5 @@ +mod runner; +mod types; + +pub use runner::run; +pub use types::SupergraphFetchInput; diff --git a/crates/rover-client/src/query/supergraph/fetch.rs b/crates/rover-client/src/operations/supergraph/fetch/runner.rs similarity index 65% rename from crates/rover-client/src/query/supergraph/fetch.rs rename to crates/rover-client/src/operations/supergraph/fetch/runner.rs index dc1b7b235..8374c6d51 100644 --- a/crates/rover-client/src/query/supergraph/fetch.rs +++ b/crates/rover-client/src/operations/supergraph/fetch/runner.rs @@ -1,5 +1,8 @@ use crate::blocking::StudioClient; +use crate::operations::supergraph::fetch::SupergraphFetchInput; +use crate::shared::{BuildError, BuildErrors, FetchResponse, GraphRef, Sdl, SdlType}; use crate::RoverClientError; + use graphql_client::*; // I'm not sure where this should live long-term @@ -10,41 +13,46 @@ type GraphQLDocument = String; // 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/supergraph/fetch.graphql", + query_path = "src/operations/supergraph/fetch/fetch_query.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. fetch_supergraph_query -pub struct FetchSupergraphQuery; +/// Snake case of this name is the mod name. i.e. supergraph_fetch_query +pub(crate) struct SupergraphFetchQuery; /// The main function to be used from this module. This function fetches a /// core schema from apollo studio pub fn run( - variables: fetch_supergraph_query::Variables, + input: SupergraphFetchInput, client: &StudioClient, -) -> Result { - let graph = variables.graph_id.clone(); - let variant = variables.variant.clone(); - let response_data = client.post::(variables)?; - get_supergraph_sdl_from_response_data(response_data, graph, variant) +) -> Result { + let graph_ref = input.graph_ref.clone(); + let response_data = client.post::(input.into())?; + get_supergraph_sdl_from_response_data(response_data, graph_ref) } fn get_supergraph_sdl_from_response_data( - response_data: fetch_supergraph_query::ResponseData, - graph: String, - variant: String, -) -> Result { - let service_data = response_data.service.ok_or(RoverClientError::NoService { - graph: graph.clone(), - })?; + response_data: supergraph_fetch_query::ResponseData, + graph_ref: GraphRef, +) -> Result { + let service_data = response_data + .service + .ok_or(RoverClientError::GraphNotFound { + graph_ref: graph_ref.clone(), + })?; if let Some(schema_tag) = service_data.schema_tag { if let Some(composition_result) = schema_tag.composition_result { - if let Some(supergraph_sdl) = composition_result.supergraph_sdl { - Ok(supergraph_sdl) + if let Some(sdl_contents) = composition_result.supergraph_sdl { + Ok(FetchResponse { + sdl: Sdl { + contents: sdl_contents, + r#type: SdlType::Supergraph, + }, + }) } else { Err(RoverClientError::MalformedResponse { null_field: "supergraphSdl".to_string(), @@ -52,21 +60,21 @@ fn get_supergraph_sdl_from_response_data( } } else { Err(RoverClientError::ExpectedFederatedGraph { - graph, + graph_ref, can_operation_convert: false, }) } } else if let Some(most_recent_composition_publish) = service_data.most_recent_composition_publish { - let composition_errors = most_recent_composition_publish + let build_errors: BuildErrors = most_recent_composition_publish .errors .into_iter() - .map(|error| error.message) + .map(|error| BuildError::composition_error(error.message, error.code)) .collect(); - Err(RoverClientError::NoCompositionPublishes { - graph, - composition_errors, + Err(RoverClientError::NoSupergraphBuilds { + graph_ref, + source: build_errors, }) } else { let mut valid_variants = Vec::new(); @@ -75,16 +83,15 @@ fn get_supergraph_sdl_from_response_data( valid_variants.push(variant.name) } - if !valid_variants.contains(&variant) { + if !valid_variants.contains(&graph_ref.variant) { Err(RoverClientError::NoSchemaForVariant { - graph, - invalid_variant: variant, + graph_ref, valid_variants, frontend_url_root: response_data.frontend_url_root, }) } else { Err(RoverClientError::ExpectedFederatedGraph { - graph, + graph_ref, can_operation_convert: false, }) } @@ -112,59 +119,75 @@ mod tests { } }, }); - let data: fetch_supergraph_query::ResponseData = + let data: supergraph_fetch_query::ResponseData = serde_json::from_value(json_response).unwrap(); - let (graph, invalid_variant) = mock_vars(); - let output = get_supergraph_sdl_from_response_data(data, graph, invalid_variant); + let graph_ref = mock_graph_ref(); + let output = get_supergraph_sdl_from_response_data(data, graph_ref); assert!(output.is_ok()); - assert_eq!(output.unwrap(), "type Query { hello: String }".to_string()); + assert_eq!( + output.unwrap(), + FetchResponse { + sdl: Sdl { + contents: "type Query { hello: String }".to_string(), + r#type: SdlType::Supergraph + } + } + ); } #[test] fn get_schema_from_response_data_errs_on_no_service() { let json_response = json!({ "service": null, "frontendUrlRoot": "https://studio.apollographql.com" }); - let data: fetch_supergraph_query::ResponseData = + let data: supergraph_fetch_query::ResponseData = serde_json::from_value(json_response).unwrap(); - let (graph, invalid_variant) = mock_vars(); - let output = get_supergraph_sdl_from_response_data(data, graph.clone(), invalid_variant); - let expected_error = RoverClientError::NoService { graph }.to_string(); + let graph_ref = mock_graph_ref(); + let output = get_supergraph_sdl_from_response_data(data, graph_ref.clone()); + let expected_error = RoverClientError::GraphNotFound { graph_ref }.to_string(); let actual_error = output.unwrap_err().to_string(); assert_eq!(actual_error, expected_error); } #[test] fn get_schema_from_response_data_errs_on_no_schema_tag() { - let (graph, variant) = mock_vars(); - let composition_errors = vec![ - "Unknown type \"Unicorn\".".to_string(), - "Type Query must define one or more fields.".to_string(), + let build_errors = vec![ + BuildError::composition_error( + "Unknown type \"Unicorn\".".to_string(), + Some("UNKNOWN_TYPE".to_string()), + ), + BuildError::composition_error( + "Type Query must define one or more fields.".to_string(), + None, + ), ]; - let composition_errors_json = json!([ + let build_errors_json = json!([ { - "message": composition_errors[0] + "message": build_errors[0].get_message(), + "code": build_errors[0].get_code() }, { - "message": composition_errors[1] + "message": build_errors[1].get_message(), + "code": build_errors[1].get_code() } ]); + let graph_ref = mock_graph_ref(); let json_response = json!({ "frontendUrlRoot": "https://studio.apollographql.com/", "service": { "schemaTag": null, - "variants": [{"name": variant}], + "variants": [{"name": &graph_ref.variant}], "mostRecentCompositionPublish": { - "errors": composition_errors_json + "errors": build_errors_json, } }, }); - let data: fetch_supergraph_query::ResponseData = + let data: supergraph_fetch_query::ResponseData = serde_json::from_value(json_response).unwrap(); - let output = get_supergraph_sdl_from_response_data(data, graph.clone(), variant); - let expected_error = RoverClientError::NoCompositionPublishes { - graph, - composition_errors, + let output = get_supergraph_sdl_from_response_data(data, graph_ref.clone()); + let expected_error = RoverClientError::NoSupergraphBuilds { + graph_ref, + source: build_errors.into(), } .to_string(); let actual_error = output.unwrap_err().to_string(); @@ -173,7 +196,6 @@ mod tests { #[test] fn get_schema_from_response_data_errs_on_invalid_variant() { - let (graph, variant) = mock_vars(); let valid_variant = "cccuuurrreeennnttt".to_string(); let frontend_url_root = "https://studio.apollographql.com".to_string(); let json_response = json!({ @@ -184,12 +206,12 @@ mod tests { "mostRecentCompositionPublish": null }, }); - let data: fetch_supergraph_query::ResponseData = + let data: supergraph_fetch_query::ResponseData = serde_json::from_value(json_response).unwrap(); - let output = get_supergraph_sdl_from_response_data(data, graph.clone(), variant.clone()); + let graph_ref = mock_graph_ref(); + let output = get_supergraph_sdl_from_response_data(data, graph_ref.clone()); let expected_error = RoverClientError::NoSchemaForVariant { - graph, - invalid_variant: variant, + graph_ref, valid_variants: vec![valid_variant], frontend_url_root, } @@ -200,7 +222,6 @@ mod tests { #[test] fn get_schema_from_response_data_errs_on_no_composition_result() { - let (graph, variant) = mock_vars(); let valid_variant = "current".to_string(); let frontend_url_root = "https://studio.apollographql.com".to_string(); let json_response = json!({ @@ -213,11 +234,12 @@ mod tests { "mostRecentCompositionResult": null }, }); - let data: fetch_supergraph_query::ResponseData = + let data: supergraph_fetch_query::ResponseData = serde_json::from_value(json_response).unwrap(); - let output = get_supergraph_sdl_from_response_data(data, graph.clone(), variant.clone()); + let graph_ref = mock_graph_ref(); + let output = get_supergraph_sdl_from_response_data(data, graph_ref.clone()); let expected_error = RoverClientError::ExpectedFederatedGraph { - graph, + graph_ref, can_operation_convert: false, } .to_string(); @@ -227,7 +249,6 @@ mod tests { #[test] fn get_schema_from_response_data_errs_on_no_supergraph_sdl() { - let (graph, variant) = mock_vars(); let valid_variant = "current".to_string(); let frontend_url_root = "https://studio.apollographql.com".to_string(); let json_response = json!({ @@ -243,9 +264,10 @@ mod tests { "mostRecentCompositionPublish": null }, }); - let data: fetch_supergraph_query::ResponseData = + let data: supergraph_fetch_query::ResponseData = serde_json::from_value(json_response).unwrap(); - let output = get_supergraph_sdl_from_response_data(data, graph.clone(), variant.clone()); + let graph_ref = mock_graph_ref(); + let output = get_supergraph_sdl_from_response_data(data, graph_ref.clone()); let expected_error = RoverClientError::MalformedResponse { null_field: "supergraphSdl".to_string(), } @@ -254,7 +276,10 @@ mod tests { assert_eq!(actual_error, expected_error); } - fn mock_vars() -> (String, String) { - ("mygraph".to_string(), "current".to_string()) + fn mock_graph_ref() -> GraphRef { + GraphRef { + name: "mygraph".to_string(), + variant: "current".to_string(), + } } } diff --git a/crates/rover-client/src/operations/supergraph/fetch/types.rs b/crates/rover-client/src/operations/supergraph/fetch/types.rs new file mode 100644 index 000000000..1bcfa8193 --- /dev/null +++ b/crates/rover-client/src/operations/supergraph/fetch/types.rs @@ -0,0 +1,18 @@ +use crate::operations::supergraph::fetch::runner::supergraph_fetch_query; +use crate::shared::GraphRef; + +type QueryVariables = supergraph_fetch_query::Variables; + +#[derive(Debug, Clone, PartialEq)] +pub struct SupergraphFetchInput { + pub graph_ref: GraphRef, +} + +impl From for QueryVariables { + fn from(input: SupergraphFetchInput) -> Self { + Self { + graph_id: input.graph_ref.name, + variant: input.graph_ref.variant, + } + } +} diff --git a/crates/rover-client/src/query/supergraph/mod.rs b/crates/rover-client/src/operations/supergraph/mod.rs similarity index 100% rename from crates/rover-client/src/query/supergraph/mod.rs rename to crates/rover-client/src/operations/supergraph/mod.rs diff --git a/crates/rover-client/src/query/config/is_federated.graphql b/crates/rover-client/src/query/config/is_federated.graphql deleted file mode 100644 index 77ede9055..000000000 --- a/crates/rover-client/src/query/config/is_federated.graphql +++ /dev/null @@ -1,7 +0,0 @@ -query IsFederatedGraph($graphId: ID!, $graphVariant: String!) { - service(id: $graphId) { - implementingServices(graphVariant: $graphVariant) { - __typename - } - } -} \ No newline at end of file diff --git a/crates/rover-client/src/query/graph/check.rs b/crates/rover-client/src/query/graph/check.rs deleted file mode 100644 index c57dad4ca..000000000 --- a/crates/rover-client/src/query/graph/check.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::blocking::StudioClient; -use crate::RoverClientError; -use graphql_client::*; - -use reqwest::Url; - -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/graph/check.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. check_schema_query -pub struct CheckSchemaQuery; - -/// The main function to be used from this module. -/// This function takes a proposed schema and validates it against a published -/// schema. -pub fn run( - variables: check_schema_query::Variables, - client: &StudioClient, -) -> Result { - let graph = variables.graph_id.clone(); - let data = client.post::(variables)?; - get_check_response_from_data(data, graph) -} - -#[derive(Debug)] -pub struct CheckResponse { - pub target_url: Option, - pub number_of_checked_operations: i64, - pub change_severity: check_schema_query::ChangeSeverity, - pub changes: Vec, -} - -fn get_check_response_from_data( - data: check_schema_query::ResponseData, - graph: String, -) -> Result { - let service = data.service.ok_or(RoverClientError::NoService { graph })?; - let target_url = get_url(service.check_schema.target_url); - - let diff_to_previous = service.check_schema.diff_to_previous; - - let number_of_checked_operations = diff_to_previous.number_of_checked_operations.unwrap_or(0); - - let change_severity = diff_to_previous.severity; - let changes = diff_to_previous.changes; - - Ok(CheckResponse { - target_url, - number_of_checked_operations, - change_severity, - changes, - }) -} - -fn get_url(url: Option) -> Option { - match url { - Some(url) => { - let url = Url::parse(&url); - match url { - Ok(url) => Some(url), - // if the API returns an invalid URL, don't put it in the response - Err(_) => None, - } - } - None => None, - } -} diff --git a/crates/rover-client/src/query/graph/fetch.graphql b/crates/rover-client/src/query/graph/fetch.graphql deleted file mode 100644 index 697a17b23..000000000 --- a/crates/rover-client/src/query/graph/fetch.graphql +++ /dev/null @@ -1,11 +0,0 @@ -query FetchSchemaQuery($variant: String, $graphId: ID!, $hash: ID) { - frontendUrlRoot, - service(id: $graphId) { - schema(tag: $variant, hash: $hash) { - document - } - variants { - name - } - } -} diff --git a/crates/rover-client/src/query/graph/introspect.rs b/crates/rover-client/src/query/graph/introspect.rs deleted file mode 100644 index 1e88db228..000000000 --- a/crates/rover-client/src/query/graph/introspect.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::{collections::HashMap, convert::TryFrom}; - -use crate::blocking::GraphQLClient; -use crate::introspection::Schema; -use crate::RoverClientError; -use graphql_client::*; - -#[derive(GraphQLQuery)] -#[graphql( - query_path = "src/query/graph/introspect_query.graphql", - schema_path = "src/query/graph/introspect_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. introspection_query -pub struct IntrospectionQuery; - -#[derive(Debug, PartialEq)] -pub struct IntrospectionResponse { - pub result: String, -} - -/// The main function to be used from this module. This function fetches a -/// schema from apollo studio and returns it in either sdl (default) or json format -pub fn run( - client: &GraphQLClient, - headers: &HashMap, -) -> Result { - let variables = introspection_query::Variables {}; - let response_data = client.post::(variables, headers)?; - build_response(response_data) -} - -fn build_response( - response: introspection_query::ResponseData, -) -> Result { - match Schema::try_from(response) { - Ok(schema) => Ok(IntrospectionResponse { - result: schema.encode(), - }), - Err(msg) => Err(RoverClientError::IntrospectionError { msg: msg.into() }), - } -} - -/// This trait is used to be able to iterate over ofType fields in -/// IntrospectionResponse. -pub trait OfType { - type TypeRef: OfType; - - fn kind(&self) -> &introspection_query::__TypeKind; - fn name(&self) -> Option<&str>; - fn of_type(self) -> Option; -} - -macro_rules! impl_of_type { - ($target:ty, $assoc:ty) => { - impl OfType for $target { - type TypeRef = $assoc; - - fn kind(&self) -> &introspection_query::__TypeKind { - &self.kind - } - - fn name(&self) -> Option<&str> { - self.name.as_deref() - } - - fn of_type(self) -> Option { - self.of_type - } - } - }; -} - -impl_of_type!( - introspection_query::TypeRef, - introspection_query::TypeRefOfType -); - -impl_of_type!( - introspection_query::TypeRefOfType, - introspection_query::TypeRefOfTypeOfType -); - -impl_of_type!( - introspection_query::TypeRefOfTypeOfType, - introspection_query::TypeRefOfTypeOfTypeOfType -); - -impl_of_type!( - introspection_query::TypeRefOfTypeOfTypeOfType, - introspection_query::TypeRefOfTypeOfTypeOfTypeOfType -); - -impl_of_type!( - introspection_query::TypeRefOfTypeOfTypeOfTypeOfType, - introspection_query::TypeRefOfTypeOfTypeOfTypeOfTypeOfType -); - -impl_of_type!( - introspection_query::TypeRefOfTypeOfTypeOfTypeOfTypeOfType, - introspection_query::TypeRefOfTypeOfTypeOfTypeOfTypeOfTypeOfType -); - -impl_of_type!( - introspection_query::TypeRefOfTypeOfTypeOfTypeOfTypeOfTypeOfType, - introspection_query::TypeRefOfTypeOfTypeOfTypeOfTypeOfTypeOfTypeOfType -); - -// NOTE(lrlna): This is a **hack**. This makes sure that the last possible -// generated ofType by graphql_client can return a None for of_type method. -impl OfType for introspection_query::TypeRefOfTypeOfTypeOfTypeOfTypeOfTypeOfTypeOfType { - type TypeRef = introspection_query::TypeRefOfTypeOfTypeOfTypeOfTypeOfTypeOfTypeOfType; - - fn kind(&self) -> &introspection_query::__TypeKind { - &self.kind - } - - fn name(&self) -> Option<&str> { - self.name.as_deref() - } - - fn of_type(self) -> Option { - None - } -} diff --git a/crates/rover-client/src/query/subgraph/check.rs b/crates/rover-client/src/query/subgraph/check.rs deleted file mode 100644 index 7291bde2b..000000000 --- a/crates/rover-client/src/query/subgraph/check.rs +++ /dev/null @@ -1,119 +0,0 @@ -use crate::blocking::StudioClient; -use crate::query::config::is_federated; -use crate::RoverClientError; - -use graphql_client::*; - -use reqwest::Url; - -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/check.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. check_partial_schema_query -pub struct CheckPartialSchemaQuery; - -/// The main function to be used from this module. -/// This function takes a proposed schema and validates it against a published -/// schema. -pub fn run( - variables: check_partial_schema_query::Variables, - client: &StudioClient, -) -> Result { - let graph = variables.graph_id.clone(); - // This response is used to check whether or not the current graph is federated. - let is_federated = is_federated::run( - is_federated::is_federated_graph::Variables { - graph_id: variables.graph_id.clone(), - graph_variant: variables.variant.clone(), - }, - &client, - )?; - if !is_federated { - return Err(RoverClientError::ExpectedFederatedGraph { - graph, - can_operation_convert: false, - }); - } - let data = client.post::(variables)?; - get_check_response_from_data(data, graph) -} - -pub enum CheckResponse { - CompositionErrors(Vec), - CheckResult(CheckResult) -} - -#[derive(Debug)] -pub struct CheckResult { - pub target_url: Option, - pub number_of_checked_operations: i64, - pub change_severity: check_partial_schema_query::ChangeSeverity, - pub changes: Vec, -} - -fn get_check_response_from_data( - data: check_partial_schema_query::ResponseData, - graph: String, -) -> Result { - let service = data.service.ok_or(RoverClientError::NoService { graph })?; - - // for some reason this is a `Vec>` - // we convert this to just `Vec` because the `None` - // errors would be useless. - let composition_errors: Vec = service - .check_partial_schema - .composition_validation_result - .errors; - - if composition_errors.is_empty() { - let check_schema_result = service.check_partial_schema.check_schema_result.ok_or( - RoverClientError::MalformedResponse { - null_field: "service.check_partial_schema.check_schema_result".to_string(), - }, - )?; - - let target_url = get_url(check_schema_result.target_url); - - let diff_to_previous = check_schema_result.diff_to_previous; - - let number_of_checked_operations = - diff_to_previous.number_of_checked_operations.unwrap_or(0); - - let change_severity = diff_to_previous.severity; - let changes = diff_to_previous.changes; - - let check_result = CheckResult { - target_url, - number_of_checked_operations, - change_severity, - changes, - }; - - Ok(CheckResponse::CheckResult(check_result)) - } else { - Ok(CheckResponse::CompositionErrors(composition_errors)) - } -} - -fn get_url(url: Option) -> Option { - match url { - Some(url) => { - let url = Url::parse(&url); - match url { - Ok(url) => Some(url), - // if the API returns an invalid URL, don't put it in the response - Err(_) => None, - } - } - None => None, - } -} diff --git a/crates/rover-client/src/query/subgraph/delete.graphql b/crates/rover-client/src/query/subgraph/delete.graphql deleted file mode 100644 index 0b6cf25a5..000000000 --- a/crates/rover-client/src/query/subgraph/delete.graphql +++ /dev/null @@ -1,19 +0,0 @@ -mutation DeleteServiceMutation( - $graphId: ID! - $graphVariant: String! - $name: String! - $dryRun: Boolean! -) { - service(id: $graphId) { - removeImplementingServiceAndTriggerComposition( - graphVariant: $graphVariant - name: $name - dryRun: $dryRun - ) { - errors { - message - } - updatedGateway - } - } -} diff --git a/crates/rover-client/src/query/subgraph/delete.rs b/crates/rover-client/src/query/subgraph/delete.rs deleted file mode 100644 index 7e735b2ea..000000000 --- a/crates/rover-client/src/query/subgraph/delete.rs +++ /dev/null @@ -1,156 +0,0 @@ -use crate::blocking::StudioClient; -use crate::RoverClientError; -use graphql_client::*; - -#[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/delete.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. delete_service_mutation -pub struct DeleteServiceMutation; -type RawMutationResponse = delete_service_mutation::DeleteServiceMutationServiceRemoveImplementingServiceAndTriggerComposition; - -/// this struct contains all the info needed to print the result of the delete. -/// `updated_gateway` is true when composition succeeds and the gateway config -/// is updated for the gateway to consume. `composition_errors` is just a list -/// of strings for when there are composition errors as a result of the delete. -#[derive(Debug, PartialEq)] -pub struct DeleteServiceResponse { - pub updated_gateway: bool, - pub composition_errors: Option>, -} - -/// The main function to be used from this module. This function fetches a -/// schema from apollo studio and returns it in either sdl (default) or json format -pub fn run( - variables: delete_service_mutation::Variables, - client: &StudioClient, -) -> Result { - let graph = variables.graph_id.clone(); - let response_data = client.post::(variables)?; - let data = get_delete_data_from_response(response_data, graph)?; - Ok(build_response(data)) -} - -fn get_delete_data_from_response( - response_data: delete_service_mutation::ResponseData, - graph: String, -) -> Result { - let service_data = response_data - .service - .ok_or(RoverClientError::NoService { graph })?; - - Ok(service_data.remove_implementing_service_and_trigger_composition) -} - -fn build_response(response: RawMutationResponse) -> DeleteServiceResponse { - let composition_errors: Vec = response - .errors - .iter() - .filter_map(|error| error.as_ref().map(|e| e.message.clone())) - .collect(); - - // if there are no errors, just return None - let composition_errors = if !composition_errors.is_empty() { - Some(composition_errors) - } else { - None - }; - - DeleteServiceResponse { - updated_gateway: response.updated_gateway, - composition_errors, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - type RawCompositionErrors = delete_service_mutation::DeleteServiceMutationServiceRemoveImplementingServiceAndTriggerCompositionErrors; - - #[test] - fn get_delete_data_from_response_works() { - let json_response = json!({ - "service": { - "removeImplementingServiceAndTriggerComposition": { - "errors": [ - { "message": "wow" }, - null, - { "message": "boo" } - ], - "updatedGateway": false, - } - } - }); - let data: delete_service_mutation::ResponseData = - serde_json::from_value(json_response).unwrap(); - let output = get_delete_data_from_response(data, "mygraph".to_string()); - - assert!(output.is_ok()); - - let expected_response = RawMutationResponse { - errors: vec![ - Some(RawCompositionErrors { - message: "wow".to_string(), - }), - None, - Some(RawCompositionErrors { - message: "boo".to_string(), - }), - ], - updated_gateway: false, - }; - assert_eq!(output.unwrap(), expected_response); - } - - #[test] - fn build_response_works_with_successful_responses() { - let response = RawMutationResponse { - errors: vec![ - Some(RawCompositionErrors { - message: "wow".to_string(), - }), - None, - Some(RawCompositionErrors { - message: "boo".to_string(), - }), - ], - updated_gateway: false, - }; - - let parsed = build_response(response); - assert_eq!( - parsed, - DeleteServiceResponse { - composition_errors: Some(vec!["wow".to_string(), "boo".to_string()]), - updated_gateway: false, - } - ); - } - - #[test] - fn build_response_works_with_failure_responses() { - let response = RawMutationResponse { - errors: vec![], - updated_gateway: true, - }; - - let parsed = build_response(response); - assert_eq!( - parsed, - DeleteServiceResponse { - composition_errors: None, - updated_gateway: true, - } - ); - } -} diff --git a/crates/rover-client/src/query/subgraph/introspect_query.graphql b/crates/rover-client/src/query/subgraph/introspect_query.graphql deleted file mode 100644 index b29c43696..000000000 --- a/crates/rover-client/src/query/subgraph/introspect_query.graphql +++ /dev/null @@ -1,5 +0,0 @@ -query IntrospectionQuery{ - _service { - sdl - } -} \ No newline at end of file diff --git a/crates/rover-client/src/query/subgraph/introspect_schema.graphql b/crates/rover-client/src/query/subgraph/introspect_schema.graphql deleted file mode 100644 index 52fd06ab0..000000000 --- a/crates/rover-client/src/query/subgraph/introspect_schema.graphql +++ /dev/null @@ -1,11 +0,0 @@ -schema { - query: Query -} - -type Query { - _service: _Service -} - -type _Service { - sdl: String! -} diff --git a/crates/rover-client/src/query/subgraph/publish.graphql b/crates/rover-client/src/query/subgraph/publish.graphql deleted file mode 100644 index 6acd42bcf..000000000 --- a/crates/rover-client/src/query/subgraph/publish.graphql +++ /dev/null @@ -1,29 +0,0 @@ -mutation PublishPartialSchemaMutation( - $graphId: ID! - $graphVariant: String! - $name: String! - $url: String - $revision: String! - $activePartialSchema: PartialSchemaInput! - $gitContext: GitContextInput! -) { - service(id: $graphId) { - upsertImplementingServiceAndTriggerComposition( - name: $name - url: $url - revision: $revision - activePartialSchema: $activePartialSchema - graphVariant: $graphVariant - gitContext: $gitContext - ) { - compositionConfig { - schemaHash - } - errors { - message - } - didUpdateGateway: updatedGateway - serviceWasCreated: wasCreated - } - } -} diff --git a/crates/rover-client/src/releases.rs b/crates/rover-client/src/releases.rs index 544636899..8317c4a11 100644 --- a/crates/rover-client/src/releases.rs +++ b/crates/rover-client/src/releases.rs @@ -1,26 +1,28 @@ use crate::RoverClientError; -use regex::Regex; + use reqwest::blocking::Client; +pub use semver::Version; const LATEST_RELEASE_URL: &str = "https://github.com/apollographql/rover/releases/latest"; -/// Looks up the latest release version, and returns it as a string -pub fn get_latest_release(client: Client) -> Result { - let res = client.head(LATEST_RELEASE_URL).send()?; +/// Looks up and parses the latest release version +pub fn get_latest_release(client: Client) -> Result { + // send a request to the latest GitHub release + let response = client.head(LATEST_RELEASE_URL).send()?; + + // this will return a response with a redirect to the latest tagged release + let url_path_segments = response + .url() + .path_segments() + .ok_or(RoverClientError::BadReleaseUrl)?; - let release_url = res.url().to_string(); - let release_url_parts: Vec<&str> = release_url.split('/').collect(); + // the last section of the URL will have the latest version in `v0.1.1` format + let version_string = url_path_segments + .last() + .ok_or(RoverClientError::BadReleaseUrl)? + .to_string(); - match release_url_parts.last() { - Some(version) => { - // Parse out the semver version (ex. v1.0.0 -> 1.0.0) - let re = Regex::new(r"^v[0-9]*\.[0-9]*\.[0-9]*$").unwrap(); - if re.is_match(version) { - Ok(version.to_string().replace('v', "")) - } else { - Err(RoverClientError::UnparseableReleaseVersion) - } - } - None => Err(RoverClientError::UnparseableReleaseVersion), - } + // strip the `v` prefix from the last section of the URL before parsing + Version::parse(&version_string[1..]) + .map_err(|source| RoverClientError::UnparseableReleaseVersion { source }) } diff --git a/crates/rover-client/src/shared/build_errors.rs b/crates/rover-client/src/shared/build_errors.rs new file mode 100644 index 000000000..1e7e66c8c --- /dev/null +++ b/crates/rover-client/src/shared/build_errors.rs @@ -0,0 +1,174 @@ +use std::{ + error::Error, + fmt::{self, Display}, + iter::FromIterator, +}; + +use serde::{ser::SerializeSeq, Deserialize, Serialize, Serializer}; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct BuildError { + message: String, + code: Option, + r#type: BuildErrorType, +} + +impl BuildError { + pub fn composition_error(message: String, code: Option) -> BuildError { + BuildError { + message, + code, + r#type: BuildErrorType::Composition, + } + } + + pub fn get_message(&self) -> String { + self.message.clone() + } + + pub fn get_code(&self) -> Option { + self.code.clone() + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum BuildErrorType { + Composition, +} + +impl Display for BuildError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(code) = &self.code { + write!(f, "{}: ", code)?; + } else { + write!(f, "UNKNOWN: ")?; + } + write!(f, "{}", &self.message) + } +} + +#[derive(Debug, Deserialize, Default, Clone, PartialEq)] +pub struct BuildErrors { + build_errors: Vec, +} + +impl Serialize for BuildErrors { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut sequence = serializer.serialize_seq(Some(self.build_errors.len()))?; + for build_error in &self.build_errors { + sequence.serialize_element(build_error)?; + } + sequence.end() + } +} + +impl BuildErrors { + pub fn new() -> Self { + BuildErrors { + build_errors: Vec::new(), + } + } + + pub fn len(&self) -> usize { + self.build_errors.len() + } + + pub fn length_string(&self) -> String { + let num_failures = self.build_errors.len(); + if num_failures == 0 { + unreachable!("No build errors were encountered while composing the supergraph."); + } + + match num_failures { + 1 => "1 build error".to_string(), + _ => format!("{} build errors", num_failures), + } + } + + pub fn push(&mut self, error: BuildError) { + self.build_errors.push(error); + } + + pub fn is_empty(&self) -> bool { + self.build_errors.is_empty() + } +} + +impl Display for BuildErrors { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for build_error in &self.build_errors { + writeln!(f, "{}", build_error)?; + } + Ok(()) + } +} + +impl From> for BuildErrors { + fn from(build_errors: Vec) -> Self { + BuildErrors { build_errors } + } +} + +impl FromIterator for BuildErrors { + fn from_iter>(iter: I) -> Self { + let mut c = BuildErrors::new(); + + for i in iter { + c.push(i); + } + + c + } +} + +impl Error for BuildError {} +impl Error for BuildErrors {} + +#[cfg(test)] +mod tests { + use super::{BuildError, BuildErrors}; + + use serde_json::{json, Value}; + + #[test] + fn it_can_serialize_empty_errors() { + let build_errors = BuildErrors::new(); + assert_eq!( + serde_json::to_string(&build_errors).expect("Could not serialize build errors"), + json!([]).to_string() + ); + } + + #[test] + fn it_can_serialize_some_build_errors() { + let build_errors: BuildErrors = vec![ + BuildError::composition_error("wow".to_string(), None), + BuildError::composition_error("boo".to_string(), Some("BOO".to_string())), + ] + .into(); + + let actual_value: Value = serde_json::from_str( + &serde_json::to_string(&build_errors) + .expect("Could not convert build errors to string"), + ) + .expect("Could not convert build error string to serde_json::Value"); + + let expected_value = json!([ + { + "code": null, + "message": "wow", + "type": "composition" + }, + { + "code": "BOO", + "message": "boo", + "type": "composition" + } + ]); + assert_eq!(actual_value, expected_value); + } +} diff --git a/crates/rover-client/src/shared/check_response.rs b/crates/rover-client/src/shared/check_response.rs new file mode 100644 index 000000000..2831ab25a --- /dev/null +++ b/crates/rover-client/src/shared/check_response.rs @@ -0,0 +1,186 @@ +use std::cmp::Ordering; +use std::fmt::{self}; +use std::str::FromStr; + +use crate::shared::GraphRef; +use crate::RoverClientError; + +use prettytable::format::consts::FORMAT_BOX_CHARS; +use serde::Serialize; + +use prettytable::{cell, row, Table}; +use serde_json::{json, Value}; + +/// CheckResponse is the return type of the +/// `graph` and `subgraph` check operations +#[derive(Debug, Serialize, Clone, PartialEq)] +pub struct CheckResponse { + target_url: Option, + operation_check_count: u64, + changes: Vec, + #[serde(skip_serializing)] + result: ChangeSeverity, + failure_count: u64, +} + +impl CheckResponse { + pub fn try_new( + target_url: Option, + operation_check_count: u64, + changes: Vec, + result: ChangeSeverity, + graph_ref: GraphRef, + ) -> Result { + let mut failure_count = 0; + for change in &changes { + if let ChangeSeverity::FAIL = change.severity { + failure_count += 1; + } + } + + let check_response = CheckResponse { + target_url, + operation_check_count, + changes, + result, + failure_count, + }; + + match failure_count.cmp(&0) { + Ordering::Equal => Ok(check_response), + Ordering::Greater => Err(RoverClientError::OperationCheckFailure { + graph_ref, + check_response, + }), + Ordering::Less => unreachable!("Somehow encountered a negative number of failures."), + } + } + + pub fn get_table(&self) -> String { + let num_changes = self.changes.len(); + + let mut msg = match num_changes { + 0 => "There were no changes detected in the composed schema.".to_string(), + _ => format!( + "Compared {} schema changes against {} operations", + num_changes, self.operation_check_count + ), + }; + + msg.push('\n'); + + if !self.changes.is_empty() { + let mut table = Table::new(); + + table.set_format(*FORMAT_BOX_CHARS); + + // bc => sets top row to be bold and center + table.add_row(row![bc => "Change", "Code", "Description"]); + for check in &self.changes { + table.add_row(row![check.severity, check.code, check.description]); + } + + msg.push_str(&table.to_string()); + } + + if let Some(url) = &self.target_url { + msg.push_str(&format!("View full details at {}", url)); + } + + msg + } + + pub fn get_failure_count(&self) -> u64 { + self.failure_count + } + + pub fn get_json(&self) -> Value { + json!(self) + } +} + +/// ChangeSeverity indicates whether a proposed change +/// in a GraphQL schema passed or failed the check +#[derive(Debug, Serialize, Clone, PartialEq)] +pub enum ChangeSeverity { + /// The proposed schema has passed the checks + PASS, + + /// The proposed schema has failed the checks + FAIL, +} + +impl ChangeSeverity { + // This message should be used when matching on a + // ChangeSeverity originating from auto-generated + // types from graphql-client + // + // We want to panic in this situation so that we + // get bug reports if Rover doesn't know the proper type + pub(crate) fn unreachable() -> ! { + unreachable!("Unknown change severity") + } +} + +impl fmt::Display for ChangeSeverity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let msg = match self { + ChangeSeverity::PASS => "PASS", + ChangeSeverity::FAIL => "FAIL", + }; + write!(f, "{}", msg) + } +} + +#[derive(Debug, Serialize, Clone, PartialEq)] +pub struct SchemaChange { + /// The code associated with a given change + /// e.g. 'TYPE_REMOVED' + pub code: String, + + /// Explanation of a given change + pub description: String, + + /// The severity of a given change + pub severity: ChangeSeverity, +} + +/// CheckConfig is used as an input to check operations +#[derive(Debug, Clone, PartialEq)] +pub struct CheckConfig { + pub query_count_threshold: Option, + pub query_count_threshold_percentage: Option, + pub validation_period: Option, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Default, Clone)] +pub struct ValidationPeriod { + pub from: i64, + pub to: i64, +} + +// Validation period is parsed as human readable time. +// such as "10m 50s" +impl FromStr for ValidationPeriod { + type Err = RoverClientError; + fn from_str(period: &str) -> Result { + // attempt to parse strings like + // 15h 10m 2s into number of seconds + if period.contains("ns") || period.contains("us") || period.contains("ms") { + return Err(RoverClientError::ValidationPeriodTooGranular); + }; + let duration = humantime::parse_duration(period)?; + + let from = duration.as_secs() as i64; + let from = -from; + + let to = 0; + + Ok(ValidationPeriod { + // search "from" a negative time window + from: -from, + // search "to" now (-0) seconds + to: -to, + }) + } +} diff --git a/crates/rover-client/src/shared/fetch_response.rs b/crates/rover-client/src/shared/fetch_response.rs new file mode 100644 index 000000000..b5ddc80bb --- /dev/null +++ b/crates/rover-client/src/shared/fetch_response.rs @@ -0,0 +1,21 @@ +use serde::Serialize; + +#[derive(Debug, Clone, Serialize, PartialEq)] +pub struct FetchResponse { + pub sdl: Sdl, +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +pub struct Sdl { + pub contents: String, + #[serde(skip_serializing)] + pub r#type: SdlType, +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +#[serde(rename_all(serialize = "lowercase"))] +pub enum SdlType { + Graph, + Subgraph, + Supergraph, +} diff --git a/src/utils/git.rs b/crates/rover-client/src/shared/git_context.rs similarity index 55% rename from src/utils/git.rs rename to crates/rover-client/src/shared/git_context.rs index 48b4ffd37..6d065d800 100644 --- a/src/utils/git.rs +++ b/crates/rover-client/src/shared/git_context.rs @@ -1,95 +1,99 @@ -use crate::utils::env::{RoverEnv, RoverEnvKey}; -use crate::Result; -use rover_client::query::{graph, subgraph}; - use std::env; use git2::{Reference, Repository}; use git_url_parse::GitUrl; -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct GitContext { pub branch: Option, pub author: Option, pub commit: Option, - pub message: Option, pub remote_url: Option, } impl GitContext { - pub fn try_from_rover_env(env: &RoverEnv) -> Result { - let repo = Repository::discover(env::current_dir()?); - match repo { - Ok(repo) => { - let head = repo.head().ok(); - Ok(Self { - branch: GitContext::get_branch(env, head.as_ref())?, - commit: GitContext::get_commit(env, head.as_ref())?, - author: GitContext::get_author(env, head.as_ref())?, - remote_url: GitContext::get_remote_url(env, Some(&repo))?, - message: None, - }) + pub fn new_with_override(override_git_context: GitContext) -> Self { + let repo = GitContext::get_repo(); + + let mut remote_url = override_git_context.remote_url; + + if let Some(repo) = repo { + remote_url = remote_url.or_else(|| GitContext::get_remote_url(&repo)); + if let Ok(head) = repo.head() { + let branch = override_git_context + .branch + .or_else(|| GitContext::get_branch(&head)); + + let author = override_git_context + .author + .or_else(|| GitContext::get_author(&head)); + + let commit = override_git_context + .commit + .or_else(|| GitContext::get_commit(&head)); + + return GitContext { + branch, + author, + commit, + remote_url, + }; } - Err(_) => Ok(Self { - branch: GitContext::get_branch(env, None)?, - commit: GitContext::get_commit(env, None)?, - author: GitContext::get_author(env, None)?, - remote_url: GitContext::get_remote_url(env, None)?, - message: None, - }), + } + + GitContext { + branch: override_git_context.branch, + author: override_git_context.author, + commit: override_git_context.commit, + remote_url, } } - fn get_branch(env: &RoverEnv, head: Option<&Reference>) -> Result> { - Ok(env.get(RoverEnvKey::VcsBranch)?.or_else(|| { - let mut branch = None; - if let Some(head) = head { - branch = head.shorthand().map(|s| s.to_string()) - } - branch - })) + pub fn default() -> Self { + GitContext::new_with_override(GitContext { + author: None, + branch: None, + commit: None, + remote_url: None, + }) } - fn get_commit(env: &RoverEnv, head: Option<&Reference>) -> Result> { - Ok(env.get(RoverEnvKey::VcsCommit)?.or_else(|| { - let mut commit = None; - if let Some(head) = head { - if let Ok(head_commit) = head.peel_to_commit() { - commit = Some(head_commit.id().to_string()) - } - } - commit - })) + fn get_repo() -> Option { + env::current_dir() + .map(|d| Repository::discover(d).ok()) + .ok() + .flatten() } - fn get_author(env: &RoverEnv, head: Option<&Reference>) -> Result> { - Ok(env.get(RoverEnvKey::VcsAuthor)?.or_else(|| { - let mut author = None; - if let Some(head) = head { - if let Ok(head_commit) = head.peel_to_commit() { - author = Some(head_commit.author().to_string()) - } - } - author - })) + fn get_branch(head: &Reference) -> Option { + head.shorthand().map(|s| s.to_string()) } - fn get_remote_url(env: &RoverEnv, repo: Option<&Repository>) -> Result> { - let remote_url = env.get(RoverEnvKey::VcsRemoteUrl)?.or_else(|| { - let mut remote_url = None; - if let Some(repo) = repo { - if let Ok(remote) = repo.find_remote("origin") { - remote_url = remote.url().map(|r| r.to_string()) - } - } - remote_url - }); + fn get_commit(head: &Reference) -> Option { + if let Ok(head_commit) = head.peel_to_commit() { + Some(head_commit.id().to_string()) + } else { + None + } + } - Ok(if let Some(remote_url) = remote_url { - GitContext::sanitize_remote_url(&remote_url) + fn get_author(head: &Reference) -> Option { + if let Ok(head_commit) = head.peel_to_commit() { + Some(head_commit.author().to_string()) } else { None - }) + } + } + + fn get_remote_url(repo: &Repository) -> Option { + let remote_url = if let Ok(remote) = repo.find_remote("origin") { + remote.url().map(|r| r.to_string()) + } else { + None + }; + remote_url + .map(|r| GitContext::sanitize_remote_url(&r)) + .flatten() } // Parses and sanitizes git remote urls according to the same rules as @@ -131,63 +135,9 @@ impl GitContext { } } -type GraphPublishContextInput = graph::publish::publish_schema_mutation::GitContextInput; -impl From for GraphPublishContextInput { - fn from(git_context: GitContext) -> GraphPublishContextInput { - GraphPublishContextInput { - branch: git_context.branch, - commit: git_context.commit, - committer: git_context.author, - remote_url: git_context.remote_url, - message: git_context.message, - } - } -} - -type GraphCheckContextInput = graph::check::check_schema_query::GitContextInput; -impl From for GraphCheckContextInput { - fn from(git_context: GitContext) -> GraphCheckContextInput { - GraphCheckContextInput { - branch: git_context.branch, - commit: git_context.commit, - committer: git_context.author, - remote_url: git_context.remote_url, - message: git_context.message, - } - } -} - -type SubgraphPublishContextInput = - subgraph::publish::publish_partial_schema_mutation::GitContextInput; -impl From for SubgraphPublishContextInput { - fn from(git_context: GitContext) -> SubgraphPublishContextInput { - SubgraphPublishContextInput { - branch: git_context.branch, - commit: git_context.commit, - committer: git_context.author, - remote_url: git_context.remote_url, - message: git_context.message, - } - } -} - -type SubgraphCheckContextInput = subgraph::check::check_partial_schema_query::GitContextInput; -impl From for SubgraphCheckContextInput { - fn from(git_context: GitContext) -> SubgraphCheckContextInput { - SubgraphCheckContextInput { - branch: git_context.branch, - commit: git_context.commit, - committer: git_context.author, - remote_url: git_context.remote_url, - message: git_context.message, - } - } -} - #[cfg(test)] mod tests { use super::*; - use crate::PKG_NAME; #[test] fn removed_user_from_remote_with_only_user() { @@ -318,30 +268,21 @@ mod tests { let commit = "f84b32caddddfdd9fa87d7ce2140d56eabe805ee".to_string(); let remote_url = "git@bitbucket.org:roku/theworstremoteintheworld.git".to_string(); - let mut rover_env = RoverEnv::new(); - rover_env.insert(RoverEnvKey::VcsBranch, &branch); - rover_env.insert(RoverEnvKey::VcsAuthor, &author); - rover_env.insert(RoverEnvKey::VcsCommit, &commit); - rover_env.insert(RoverEnvKey::VcsRemoteUrl, &remote_url); - - let expected_git_context = GitContext { + let override_git_context = GitContext { branch: Some(branch), author: Some(author), commit: Some(commit), - message: None, remote_url: Some(remote_url), }; - let actual_git_context = GitContext::try_from_rover_env(&rover_env) - .expect("Could not create GitContext from RoverEnv"); + let actual_git_context = GitContext::new_with_override(override_git_context.clone()); - assert_eq!(expected_git_context, actual_git_context); + assert_eq!(override_git_context, actual_git_context); } #[test] - fn it_can_create_git_context_committ_author_remote_url() { - let git_context = - GitContext::try_from_rover_env(&RoverEnv::new()).expect("Could not create git context"); + fn it_can_create_git_context_commit_author_remote_url() { + let git_context = GitContext::default(); assert!(git_context.branch.is_some()); assert!(git_context.author.is_some()); @@ -352,10 +293,8 @@ mod tests { panic!("Could not find the commit hash"); } - assert!(git_context.message.is_none()); - if let Some(remote_url) = git_context.remote_url { - assert!(remote_url.contains(PKG_NAME)); + assert!(remote_url.contains("apollographql")); } else { panic!("GitContext could not find the remote url"); } diff --git a/crates/rover-client/src/shared/graph_ref.rs b/crates/rover-client/src/shared/graph_ref.rs new file mode 100644 index 000000000..99d0c16f1 --- /dev/null +++ b/crates/rover-client/src/shared/graph_ref.rs @@ -0,0 +1,105 @@ +use std::fmt; +use std::str::FromStr; + +use crate::RoverClientError; + +use regex::Regex; +use serde::Serialize; + +#[derive(Debug, Serialize, Clone, PartialEq)] +pub struct GraphRef { + pub name: String, + pub variant: String, +} + +impl fmt::Display for GraphRef { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}@{}", self.name, self.variant) + } +} + +impl FromStr for GraphRef { + type Err = RoverClientError; + + /// NOTE: THIS IS A TEMPORARY SOLUTION. IN THE FUTURE, ALL GRAPH ID PARSING + /// WILL HAPPEN IN THE BACKEND TO KEEP EVERYTHING CONSISTENT. THIS IS AN + /// INCOMPLETE PLACEHOLDER, AND MAY NOT COVER EVERY SINGLE VALID USE CASE + fn from_str(graph_id: &str) -> Result { + let pattern = Regex::new(r"^[a-zA-Z][a-zA-Z0-9_-]{0,63}$").unwrap(); + let variant_pattern = Regex::new(r"^([a-zA-Z][a-zA-Z0-9_-]{0,63})@(.{0,63})$").unwrap(); + + let valid_graph_name_only = pattern.is_match(graph_id); + let valid_graph_with_variant = variant_pattern.is_match(graph_id); + + if valid_graph_name_only { + Ok(GraphRef { + name: graph_id.to_string(), + variant: "current".to_string(), + }) + } else if valid_graph_with_variant { + let matches = variant_pattern.captures(graph_id).unwrap(); + let name = matches.get(1).unwrap().as_str(); + let variant = matches.get(2).unwrap().as_str(); + Ok(GraphRef { + name: name.to_string(), + variant: variant.to_string(), + }) + } else { + Err(RoverClientError::InvalidGraphRef) + } + } +} + +#[cfg(test)] +mod tests { + use super::GraphRef; + use std::str::FromStr; + + #[test] + fn from_str_works() { + assert!(GraphRef::from_str("engine#%^").is_err()); + assert!(GraphRef::from_str( + "1234567890123456789012345678901234567890123456789012345678901234567890" + ) + .is_err()); + assert!(GraphRef::from_str("1boi").is_err()); + assert!(GraphRef::from_str("_eng").is_err()); + assert!(GraphRef::from_str( + "engine@1234567890123456789012345678901234567890123456789012345678901234567890" + ) + .is_err()); + assert!(GraphRef::from_str( + "engine1234567890123456789012345678901234567890123456789012345678901234567890@prod" + ) + .is_err()); + + assert_eq!( + GraphRef::from_str("engine@okay").unwrap(), + GraphRef { + name: "engine".to_string(), + variant: "okay".to_string() + } + ); + assert_eq!( + GraphRef::from_str("studio").unwrap(), + GraphRef { + name: "studio".to_string(), + variant: "current".to_string() + } + ); + assert_eq!( + GraphRef::from_str("this_should_work").unwrap(), + GraphRef { + name: "this_should_work".to_string(), + variant: "current".to_string() + } + ); + assert_eq!( + GraphRef::from_str("it-is-cool@my-special/variant:from$hell").unwrap(), + GraphRef { + name: "it-is-cool".to_string(), + variant: "my-special/variant:from$hell".to_string() + } + ); + } +} diff --git a/crates/rover-client/src/shared/mod.rs b/crates/rover-client/src/shared/mod.rs new file mode 100644 index 000000000..e09603fb1 --- /dev/null +++ b/crates/rover-client/src/shared/mod.rs @@ -0,0 +1,13 @@ +mod build_errors; +mod check_response; +mod fetch_response; +mod git_context; +mod graph_ref; + +pub use build_errors::{BuildError, BuildErrors}; +pub use check_response::{ + ChangeSeverity, CheckConfig, CheckResponse, SchemaChange, ValidationPeriod, +}; +pub use fetch_response::{FetchResponse, Sdl, SdlType}; +pub use git_context::GitContext; +pub use graph_ref::GraphRef; diff --git a/crates/sdl-encoder/src/schema_def.rs b/crates/sdl-encoder/src/schema_def.rs index 19905380e..2d74b7c45 100644 --- a/crates/sdl-encoder/src/schema_def.rs +++ b/crates/sdl-encoder/src/schema_def.rs @@ -81,7 +81,7 @@ impl Default for SchemaDef { impl Display for SchemaDef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(description) = &self.description { - // We are determing on whether to have description formatted as + // We determine whether to have description formatted as // a multiline comment based on whether or not it already includes a // \n. match description.contains('\n') { diff --git a/docs/source/errors.md b/docs/source/errors.md index f3a4f4f15..0e258a69a 100644 --- a/docs/source/errors.md +++ b/docs/source/errors.md @@ -233,4 +233,16 @@ This error occurs when Rover could not connect to an HTTP endpoint. If you encountered this error while running introspection, you'll want to make sure that you typed the endpoint correctly, your Internet connection is stable, and that your server is responding to requests. You may wish to run the command again with `--log=debug`. +### E029 + +This error occurs when you propose a subgraph schema that could not be composed. + +There are many reasons why you may run into build errors. This error should include information about _why_ the proposed subgraph schema could not be composed. Error code references can be found [here](https://www.apollographql.com/docs/federation/errors/). + +Some build errors are part of normal workflows. For instance, you may need to publish a subgraph that does not compose if you are trying to [migrate an entity or field](https://www.apollographql.com/docs/federation/entities/#migrating-entities-and-fields-advanced). + + +### E030 + +This error occurs when an operation check fails. This means that you proposed a schema that would break operations in use by existing clients. You can configure this behavior in the Checks -> Configuration view in [Apollo Studio](https://studio.apollographql.com/), and you can read more about client checks [here](https://www.apollographql.com/docs/studio/schema-checks/). diff --git a/installers/binstall/src/error.rs b/installers/binstall/src/error.rs index 26ca51377..dc6f92353 100644 --- a/installers/binstall/src/error.rs +++ b/installers/binstall/src/error.rs @@ -2,21 +2,26 @@ use thiserror::Error; use std::io; -/// InstallerError is the type of Error that occured while installing. +/// InstallerError is the type of Error that occurred while installing. #[derive(Error, Debug)] pub enum InstallerError { + /// Something went wrong with system I/O #[error(transparent)] IoError(#[from] io::Error), + /// Couldn't find a valid install location on Unix #[error("Could not find the home directory of the current user")] NoHomeUnix, + /// Couldn't find a valid install location on Windows #[error("Could not find the user profile folder")] NoHomeWindows, + /// Something went wrong while adding the executable to zsh config #[error("Zsh setup failed")] ZshSetup, + /// A specified path was not valid UTF-8 #[error(transparent)] PathNotUtf8(#[from] camino::FromPathBufError), } diff --git a/installers/binstall/src/install.rs b/installers/binstall/src/install.rs index ecf494167..4ea6e51b9 100644 --- a/installers/binstall/src/install.rs +++ b/installers/binstall/src/install.rs @@ -15,12 +15,19 @@ pub struct Installer { } impl Installer { + /// Installs the executable and returns the location it was installed. pub fn install(&self) -> Result, InstallerError> { let install_path = self.do_install()?; Ok(install_path) } + /// Gets the location the executable will be installed to + pub fn get_bin_dir_path(&self) -> Result { + let bin_dir = self.get_base_dir_path()?.join("bin"); + Ok(bin_dir) + } + fn do_install(&self) -> Result, InstallerError> { let bin_destination = self.get_bin_path()?; @@ -52,11 +59,6 @@ impl Installer { Ok(base_dir.join(&format!(".{}", &self.binary_name))) } - pub fn get_bin_dir_path(&self) -> Result { - let bin_dir = self.get_base_dir_path()?.join("bin"); - Ok(bin_dir) - } - fn create_bin_dir(&self) -> Result<(), InstallerError> { fs::create_dir_all(self.get_bin_dir_path()?)?; Ok(()) diff --git a/src/bin/rover.rs b/src/bin/rover.rs index cb663447c..25ce697d0 100644 --- a/src/bin/rover.rs +++ b/src/bin/rover.rs @@ -1,65 +1,16 @@ -use command::RoverStdout; use robot_panic::setup_panic; -use rover::*; -use sputnik::Session; +use rover::cli::Rover; use structopt::StructOpt; -use std::{process, thread}; - fn main() { setup_panic!(Metadata { - name: PKG_NAME.into(), - version: PKG_VERSION.into(), - authors: PKG_AUTHORS.into(), - homepage: PKG_HOMEPAGE.into(), - repository: PKG_REPOSITORY.into() + name: rover::PKG_NAME.into(), + version: rover::PKG_VERSION.into(), + authors: rover::PKG_AUTHORS.into(), + homepage: rover::PKG_HOMEPAGE.into(), + repository: rover::PKG_REPOSITORY.into() }); - if let Err(error) = run() { - tracing::debug!(?error); - eprint!("{}", error); - process::exit(1) - } else { - process::exit(0) - } -} - -fn run() -> Result<()> { - let app = cli::Rover::from_args(); - timber::init(app.log_level); - tracing::trace!(command_structure = ?app); - - // attempt to create a new `Session` to capture anonymous usage data - let output: RoverStdout = match Session::new(&app) { - // if successful, report the usage data in the background - Ok(session) => { - // kicks off the reporting on a background thread - let report_thread = thread::spawn(move || { - // log + ignore errors because it is not in the critical path - let _ = session.report().map_err(|telemetry_error| { - tracing::debug!(?telemetry_error); - telemetry_error - }); - }); - - // kicks off the app on the main thread - // don't return an error with ? quite yet - // since we still want to report the usage data - let app_result = app.run(); - - // makes sure the reporting finishes in the background - // before continuing. - // ignore errors because it is not in the critical path - let _ = report_thread.join(); - - // return result of app execution - // now that we have reported our usage data - app_result - } - - // otherwise just run the app without reporting - Err(_) => app.run(), - }?; - output.print(); - Ok(()) + let app = Rover::from_args(); + app.run(); } diff --git a/src/cli.rs b/src/cli.rs index 4271bbec7..157548841 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,21 +1,25 @@ +use camino::Utf8PathBuf; use reqwest::blocking::Client; use serde::Serialize; use structopt::{clap::AppSettings, StructOpt}; -use crate::command::{self, RoverStdout}; +use crate::command::output::JsonOutput; +use crate::command::{self, RoverOutput}; use crate::utils::{ client::StudioClientConfig, env::{RoverEnv, RoverEnvKey}, - git::GitContext, - stringify::from_display, + stringify::option_from_display, version, }; -use crate::Result; +use crate::{anyhow, Result}; + use config::Config; use houston as config; +use rover_client::shared::GitContext; +use sputnik::Session; use timber::{Level, LEVELS}; -use camino::Utf8PathBuf; +use std::{process, str::FromStr, thread}; #[derive(Debug, Serialize, StructOpt)] #[structopt( @@ -51,16 +55,20 @@ You can open the full documentation for Rover by running: ")] pub struct Rover { #[structopt(subcommand)] - pub command: Command, + command: Command, /// Specify Rover's log level #[structopt(long = "log", short = "l", global = true, possible_values = &LEVELS, case_insensitive = true)] - #[serde(serialize_with = "from_display")] - pub log_level: Option, + #[serde(serialize_with = "option_from_display")] + log_level: Option, + + /// Use json output + #[structopt(long = "output", default_value = "plain", possible_values = &["json", "plain"], case_insensitive = true, global = true)] + output_type: OutputType, #[structopt(skip)] #[serde(skip_serializing)] - pub env_store: RoverEnv, + pub(crate) env_store: RoverEnv, #[structopt(skip)] #[serde(skip_serializing)] @@ -68,6 +76,96 @@ pub struct Rover { } impl Rover { + pub fn run(&self) -> ! { + timber::init(self.log_level); + tracing::trace!(command_structure = ?self); + + // attempt to create a new `Session` to capture anonymous usage data + let rover_output = match Session::new(self) { + // if successful, report the usage data in the background + Ok(session) => { + // kicks off the reporting on a background thread + let report_thread = thread::spawn(move || { + // log + ignore errors because it is not in the critical path + let _ = session.report().map_err(|telemetry_error| { + tracing::debug!(?telemetry_error); + telemetry_error + }); + }); + + // kicks off the app on the main thread + // don't return an error with ? quite yet + // since we still want to report the usage data + let app_result = self.execute_command(); + + // makes sure the reporting finishes in the background + // before continuing. + // ignore errors because it is not in the critical path + let _ = report_thread.join(); + + // return result of app execution + // now that we have reported our usage data + app_result + } + + // otherwise just run the app without reporting + Err(_) => self.execute_command(), + }; + + match rover_output { + Ok(output) => { + match self.output_type { + OutputType::Plain => output.print(), + OutputType::Json => println!("{}", JsonOutput::from(output)), + } + process::exit(0); + } + Err(error) => { + match self.output_type { + OutputType::Json => println!("{}", JsonOutput::from(error)), + OutputType::Plain => { + tracing::debug!(?error); + error.print(); + } + } + process::exit(1); + } + } + } + + pub fn execute_command(&self) -> Result { + // before running any commands, we check if rover is up to date + // this only happens once a day automatically + // we skip this check for the `rover update` commands, since they + // do their own checks + + if let Command::Update(_) = &self.command { /* skip check */ + } else { + let config = self.get_rover_config(); + if let Ok(config) = config { + let _ = version::check_for_update(config, false, self.get_reqwest_client()); + } + } + + match &self.command { + Command::Config(command) => command.run(self.get_client_config()?), + Command::Supergraph(command) => command.run(self.get_client_config()?), + Command::Docs(command) => command.run(), + Command::Graph(command) => { + command.run(self.get_client_config()?, self.get_git_context()?) + } + Command::Subgraph(command) => { + command.run(self.get_client_config()?, self.get_git_context()?) + } + Command::Update(command) => { + command.run(self.get_rover_config()?, self.get_reqwest_client()) + } + Command::Install(command) => command.run(self.get_install_override_path()?), + Command::Info(command) => command.run(), + Command::Explain(command) => command.run(), + } + } + pub(crate) fn get_rover_config(&self) -> Result { let override_home: Option = self .env_store @@ -96,7 +194,14 @@ impl Rover { pub(crate) fn get_git_context(&self) -> Result { // constructing GitContext with a set of overrides from env vars - let git_context = GitContext::try_from_rover_env(&self.env_store)?; + let override_git_context = GitContext { + branch: self.env_store.get(RoverEnvKey::VcsBranch).ok().flatten(), + commit: self.env_store.get(RoverEnvKey::VcsCommit).ok().flatten(), + author: self.env_store.get(RoverEnvKey::VcsAuthor).ok().flatten(), + remote_url: self.env_store.get(RoverEnvKey::VcsRemoteUrl).ok().flatten(), + }; + + let git_context = GitContext::new_with_override(override_git_context); tracing::debug!(?git_context); Ok(git_context) } @@ -139,37 +244,26 @@ pub enum Command { Explain(command::Explain), } -impl Rover { - pub fn run(&self) -> Result { - // before running any commands, we check if rover is up to date - // this only happens once a day automatically - // we skip this check for the `rover update` commands, since they - // do their own checks +#[derive(Debug, Serialize, Clone, PartialEq)] +pub enum OutputType { + Plain, + Json, +} - if let Command::Update(_) = &self.command { /* skip check */ - } else { - let config = self.get_rover_config(); - if let Ok(config) = config { - let _ = version::check_for_update(config, false, self.get_reqwest_client()); - } - } +impl FromStr for OutputType { + type Err = anyhow::Error; - match &self.command { - Command::Config(command) => command.run(self.get_client_config()?), - Command::Supergraph(command) => command.run(self.get_client_config()?), - Command::Docs(command) => command.run(), - Command::Graph(command) => { - command.run(self.get_client_config()?, self.get_git_context()?) - } - Command::Subgraph(command) => { - command.run(self.get_client_config()?, self.get_git_context()?) - } - Command::Update(command) => { - command.run(self.get_rover_config()?, self.get_reqwest_client()) - } - Command::Install(command) => command.run(self.get_install_override_path()?), - Command::Info(command) => command.run(), - Command::Explain(command) => command.run(), + fn from_str(input: &str) -> std::result::Result { + match input { + "plain" => Ok(Self::Plain), + "json" => Ok(Self::Json), + _ => Err(anyhow!("Invalid output type.")), } } } + +impl Default for OutputType { + fn default() -> Self { + OutputType::Plain + } +} diff --git a/src/command/config/auth.rs b/src/command/config/auth.rs index ae15cb16f..90951bcec 100644 --- a/src/command/config/auth.rs +++ b/src/command/config/auth.rs @@ -5,7 +5,7 @@ use structopt::StructOpt; use config::Profile; use houston as config; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::{anyhow, Result}; #[derive(Debug, Serialize, StructOpt)] @@ -26,13 +26,13 @@ pub struct Auth { } impl Auth { - pub fn run(&self, config: config::Config) -> Result { + pub fn run(&self, config: config::Config) -> Result { let api_key = api_key_prompt()?; Profile::set_api_key(&self.profile_name, &config, &api_key)?; Profile::get_credential(&self.profile_name, &config).map(|_| { eprintln!("Successfully saved API key."); })?; - Ok(RoverStdout::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/config/clear.rs b/src/command/config/clear.rs index a587885cb..5ebc9f214 100644 --- a/src/command/config/clear.rs +++ b/src/command/config/clear.rs @@ -1,7 +1,7 @@ use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::Result; use houston as config; @@ -13,9 +13,9 @@ use houston as config; pub struct Clear {} impl Clear { - pub fn run(&self, config: config::Config) -> Result { + pub fn run(&self, config: config::Config) -> Result { config.clear()?; eprintln!("Successfully cleared all configuration."); - Ok(RoverStdout::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/config/delete.rs b/src/command/config/delete.rs index 9c9664b4f..e282ae0bb 100644 --- a/src/command/config/delete.rs +++ b/src/command/config/delete.rs @@ -3,7 +3,7 @@ use structopt::StructOpt; use houston as config; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::Result; #[derive(Debug, Serialize, StructOpt)] @@ -20,9 +20,9 @@ pub struct Delete { } impl Delete { - pub fn run(&self, config: config::Config) -> Result { + pub fn run(&self, config: config::Config) -> Result { config::Profile::delete(&self.name, &config)?; eprintln!("Successfully deleted profile \"{}\"", &self.name); - Ok(RoverStdout::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/config/list.rs b/src/command/config/list.rs index 907bc0f9c..9bd56b892 100644 --- a/src/command/config/list.rs +++ b/src/command/config/list.rs @@ -4,15 +4,15 @@ use structopt::StructOpt; use crate::Result; use houston as config; -use crate::command::RoverStdout; +use crate::command::RoverOutput; #[derive(Serialize, Debug, StructOpt)] /// List all configuration profiles pub struct List {} impl List { - pub fn run(&self, config: config::Config) -> Result { + pub fn run(&self, config: config::Config) -> Result { let profiles = config::Profile::list(&config)?; - Ok(RoverStdout::Profiles(profiles)) + Ok(RoverOutput::Profiles(profiles)) } } diff --git a/src/command/config/mod.rs b/src/command/config/mod.rs index a9eed151a..71a1b87aa 100644 --- a/src/command/config/mod.rs +++ b/src/command/config/mod.rs @@ -7,7 +7,7 @@ mod whoami; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -36,7 +36,7 @@ pub enum Command { } impl Config { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { match &self.command { Command::Auth(command) => command.run(client_config.config), Command::List(command) => command.run(client_config.config), diff --git a/src/command/config/whoami.rs b/src/command/config/whoami.rs index 0a77288ec..c0d7f7d35 100644 --- a/src/command/config/whoami.rs +++ b/src/command/config/whoami.rs @@ -1,12 +1,12 @@ use ansi_term::Colour::Green; +use rover_client::operations::config::who_am_i::{self, Actor, ConfigWhoAmIInput}; use serde::Serialize; use structopt::StructOpt; use houston::CredentialOrigin; -use rover_client::query::config::whoami; use crate::anyhow; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::utils::env::RoverEnvKey; use crate::Result; @@ -22,11 +22,11 @@ pub struct WhoAmI { } impl WhoAmI { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; eprintln!("Checking identity of your API key against the registry."); - let identity = whoami::run(whoami::who_am_i_query::Variables {}, &client)?; + let identity = who_am_i::run(ConfigWhoAmIInput {}, &client)?; let mut message = format!( "{}: {:?}\n", @@ -35,7 +35,7 @@ impl WhoAmI { ); match identity.key_actor_type { - whoami::Actor::GRAPH => { + Actor::GRAPH => { if let Some(graph_title) = identity.graph_title { message.push_str(&format!( "{}: {}\n", @@ -50,7 +50,7 @@ impl WhoAmI { )); Ok(()) } - whoami::Actor::USER => { + Actor::USER => { message.push_str(&format!( "{}: {}\n", Green.normal().paint("User ID"), @@ -63,7 +63,7 @@ impl WhoAmI { )), }?; - let origin = match client.credential.origin { + let origin = match client.get_credential_origin() { CredentialOrigin::ConfigFile(path) => format!("--profile {}", &path), CredentialOrigin::EnvVar => format!("${}", &RoverEnvKey::Key), }; @@ -80,6 +80,6 @@ impl WhoAmI { eprintln!("{}", message); - Ok(RoverStdout::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/docs/list.rs b/src/command/docs/list.rs index 6a1190f2b..3f951f57a 100644 --- a/src/command/docs/list.rs +++ b/src/command/docs/list.rs @@ -1,4 +1,4 @@ -use crate::{command::RoverStdout, Result}; +use crate::{command::RoverOutput, Result}; use super::shortlinks; @@ -9,8 +9,8 @@ use structopt::StructOpt; pub struct List {} impl List { - pub fn run(&self) -> Result { - Ok(RoverStdout::DocsList( + pub fn run(&self) -> Result { + Ok(RoverOutput::DocsList( shortlinks::get_shortlinks_with_description(), )) } diff --git a/src/command/docs/mod.rs b/src/command/docs/mod.rs index 0950c0eb4..ce3c0f6c8 100644 --- a/src/command/docs/mod.rs +++ b/src/command/docs/mod.rs @@ -5,7 +5,7 @@ pub mod shortlinks; use serde::Serialize; use structopt::StructOpt; -use crate::{command::RoverStdout, Result}; +use crate::{command::RoverOutput, Result}; #[derive(Debug, Serialize, StructOpt)] pub struct Docs { @@ -23,7 +23,7 @@ pub enum Command { } impl Docs { - pub fn run(&self) -> Result { + pub fn run(&self) -> Result { match &self.command { Command::List(command) => command.run(), Command::Open(command) => command.run(), diff --git a/src/command/docs/open.rs b/src/command/docs/open.rs index 7e43ff6db..8f39136d3 100644 --- a/src/command/docs/open.rs +++ b/src/command/docs/open.rs @@ -1,4 +1,4 @@ -use crate::{anyhow, command::RoverStdout, Result}; +use crate::{anyhow, command::RoverOutput, Result}; use super::shortlinks; @@ -15,7 +15,7 @@ pub struct Open { } impl Open { - pub fn run(&self) -> Result { + pub fn run(&self) -> Result { let url = shortlinks::get_url_from_slug(&self.slug); let yellow_browser_var = format!("{}", Yellow.normal().paint("$BROWSER")); let cyan_url = format!("{}", Cyan.normal().paint(&url)); @@ -40,6 +40,6 @@ impl Open { Ok(()) }?; - Ok(RoverStdout::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/docs/shortlinks.rs b/src/command/docs/shortlinks.rs index 18833e758..6471824c0 100644 --- a/src/command/docs/shortlinks.rs +++ b/src/command/docs/shortlinks.rs @@ -1,9 +1,9 @@ pub const URL_BASE: &str = "https://go.apollo.dev/r"; -use std::collections::HashMap; +use std::collections::BTreeMap; -pub fn get_shortlinks_with_description() -> HashMap<&'static str, &'static str> { - let mut links = HashMap::new(); +pub fn get_shortlinks_with_description() -> BTreeMap<&'static str, &'static str> { + let mut links = BTreeMap::new(); links.insert("docs", "Rover's Documentation Homepage"); links.insert("api-keys", "Understanding Apollo's API Keys"); links.insert("contributing", "Contributing to Rover"); diff --git a/src/command/explain.rs b/src/command/explain.rs index 4c2d3a5ad..4de0510d7 100644 --- a/src/command/explain.rs +++ b/src/command/explain.rs @@ -1,4 +1,4 @@ -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::error::metadata::code::Code; use crate::Result; use serde::Serialize; @@ -12,8 +12,8 @@ pub struct Explain { } impl Explain { - pub fn run(&self) -> Result { + pub fn run(&self) -> Result { let explanation = &self.code.explain(); - Ok(RoverStdout::Markdown(explanation.clone())) + Ok(RoverOutput::ErrorExplanation(explanation.clone())) } } diff --git a/src/command/graph/check.rs b/src/command/graph/check.rs index f4720436e..d18851021 100644 --- a/src/command/graph/check.rs +++ b/src/command/graph/check.rs @@ -1,24 +1,23 @@ use serde::Serialize; use structopt::StructOpt; -use rover_client::query::graph::check; +use rover_client::operations::graph::check::{self, GraphCheckInput}; +use rover_client::shared::{CheckConfig, GitContext, GraphRef, ValidationPeriod}; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; -use crate::utils::git::GitContext; use crate::utils::loaders::load_schema_from_flag; use crate::utils::parsers::{ - parse_graph_ref, parse_query_count_threshold, parse_query_percentage_threshold, - parse_schema_source, parse_validation_period, GraphRef, SchemaSource, ValidationPeriod, + parse_query_count_threshold, parse_query_percentage_threshold, parse_schema_source, + SchemaSource, }; -use crate::utils::table::{self, cell, row}; use crate::Result; #[derive(Debug, Serialize, StructOpt)] pub struct Check { /// @ of graph in Apollo Studio to validate. /// @ may be left off, defaulting to @current - #[structopt(name = "GRAPH_REF", parse(try_from_str = parse_graph_ref))] + #[structopt(name = "GRAPH_REF")] #[serde(skip_serializing)] graph: GraphRef, @@ -45,7 +44,7 @@ pub struct Check { query_percentage_threshold: Option, /// Size of the time window with which to validate schema against (i.e "24h" or "1w 2d 5h") - #[structopt(long, parse(try_from_str = parse_validation_period))] + #[structopt(long)] validation_period: Option, } @@ -54,81 +53,29 @@ impl Check { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; - let sdl = load_schema_from_flag(&self.schema, std::io::stdin())?; + let proposed_schema = load_schema_from_flag(&self.schema, std::io::stdin())?; + + eprintln!( + "Checking the proposed schema against metrics from {}", + &self.graph + ); + let res = check::run( - check::check_schema_query::Variables { - graph_id: self.graph.name.clone(), - variant: Some(self.graph.variant.clone()), - schema: Some(sdl), - git_context: git_context.into(), - config: check::check_schema_query::HistoricQueryParameters { + GraphCheckInput { + graph_ref: self.graph.clone(), + proposed_schema, + git_context, + config: CheckConfig { query_count_threshold: self.query_count_threshold, query_count_threshold_percentage: self.query_percentage_threshold, - from: self.validation_period.clone().unwrap_or_default().from, - to: self.validation_period.clone().unwrap_or_default().to, - // we don't support configuring these, but we can't leave them out - excluded_clients: None, - ignored_operations: None, - included_variants: None, + validation_period: self.validation_period.clone(), }, }, &client, )?; - eprintln!( - "Validated the proposed subgraph against metrics from {}", - &self.graph - ); - - let num_changes = res.changes.len(); - - let msg = match num_changes { - 0 => "There is no difference between the proposed graph and the graph that already exists in the graph registry. Try making a change to your proposed graph before running this command.".to_string(), - _ => format!("Compared {} schema changes against {} operations", res.changes.len(), res.number_of_checked_operations), - }; - - eprintln!("{}", &msg); - - let num_failures = print_changes(&res.changes); - - if let Some(url) = res.target_url { - eprintln!("View full details at {}", &url); - } - - match num_failures { - 0 => Ok(RoverStdout::None), - 1 => Err(anyhow::anyhow!("Encountered 1 failure.").into()), - _ => Err(anyhow::anyhow!("Encountered {} failures.", num_failures).into()), - } - } -} - -fn print_changes( - checks: &[check::check_schema_query::CheckSchemaQueryServiceCheckSchemaDiffToPreviousChanges], -) -> u64 { - let mut num_failures = 0; - - if !checks.is_empty() { - let mut table = table::get_table(); - - // bc => sets top row to be bold and center - table.add_row(row![bc => "Change", "Code", "Description"]); - for check in checks { - let change = match check.severity { - check::check_schema_query::ChangeSeverity::NOTICE => "PASS", - check::check_schema_query::ChangeSeverity::FAILURE => { - num_failures += 1; - "FAIL" - } - _ => unreachable!("Unknown change severity"), - }; - table.add_row(row![change, check.code, check.description]); - } - - eprintln!("{}", table); + Ok(RoverOutput::CheckResponse(res)) } - - num_failures } diff --git a/src/command/graph/fetch.rs b/src/command/graph/fetch.rs index c1c147d31..478811b2d 100644 --- a/src/command/graph/fetch.rs +++ b/src/command/graph/fetch.rs @@ -2,18 +2,18 @@ use ansi_term::Colour::{Cyan, Yellow}; use serde::Serialize; use structopt::StructOpt; -use rover_client::query::graph::fetch; +use rover_client::operations::graph::fetch::{self, GraphFetchInput}; +use rover_client::shared::GraphRef; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; -use crate::utils::parsers::{parse_graph_ref, GraphRef}; use crate::Result; #[derive(Debug, Serialize, StructOpt)] pub struct Fetch { /// @ of graph in Apollo Studio to fetch from. /// @ may be left off, defaulting to @current - #[structopt(name = "GRAPH_REF", parse(try_from_str = parse_graph_ref))] + #[structopt(name = "GRAPH_REF")] #[serde(skip_serializing)] graph: GraphRef, @@ -24,7 +24,7 @@ pub struct Fetch { } impl Fetch { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let graph_ref = self.graph.to_string(); eprintln!( @@ -33,15 +33,13 @@ impl Fetch { Yellow.normal().paint(&self.profile_name) ); - let sdl = fetch::run( - fetch::fetch_schema_query::Variables { - graph_id: self.graph.name.clone(), - hash: None, - variant: Some(self.graph.variant.clone()), + let fetch_response = fetch::run( + GraphFetchInput { + graph_ref: self.graph.clone(), }, &client, )?; - Ok(RoverStdout::Sdl(sdl)) + Ok(RoverOutput::FetchResponse(fetch_response)) } } diff --git a/src/command/graph/introspect.rs b/src/command/graph/introspect.rs index a12ac60c9..da66e047b 100644 --- a/src/command/graph/introspect.rs +++ b/src/command/graph/introspect.rs @@ -5,9 +5,12 @@ use std::collections::HashMap; use structopt::StructOpt; use url::Url; -use rover_client::{blocking::GraphQLClient, query::graph::introspect}; +use rover_client::{ + blocking::GraphQLClient, + operations::graph::introspect::{self, GraphIntrospectInput}, +}; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::parsers::parse_header; #[derive(Debug, Serialize, StructOpt)] @@ -28,7 +31,7 @@ pub struct Introspect { } impl Introspect { - pub fn run(&self, client: Client) -> Result { + pub fn run(&self, client: Client) -> Result { let client = GraphQLClient::new(&self.endpoint.to_string(), client)?; // add the flag headers to a hashmap to pass along to rover-client @@ -39,8 +42,10 @@ impl Introspect { } } - let introspection_response = introspect::run(&client, &headers)?; + let introspection_response = introspect::run(GraphIntrospectInput { headers }, &client)?; - Ok(RoverStdout::Introspection(introspection_response.result)) + Ok(RoverOutput::Introspection( + introspection_response.schema_sdl, + )) } } diff --git a/src/command/graph/mod.rs b/src/command/graph/mod.rs index 13744518e..0603fb4e5 100644 --- a/src/command/graph/mod.rs +++ b/src/command/graph/mod.rs @@ -6,10 +6,12 @@ mod publish; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; -use crate::utils::{client::StudioClientConfig, git::GitContext}; +use crate::command::RoverOutput; +use crate::utils::client::StudioClientConfig; use crate::Result; +use rover_client::shared::GitContext; + #[derive(Debug, Serialize, StructOpt)] pub struct Graph { #[structopt(subcommand)] @@ -37,7 +39,7 @@ impl Graph { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { match &self.command { Command::Check(command) => command.run(client_config, git_context), Command::Fetch(command) => command.run(client_config), diff --git a/src/command/graph/publish.rs b/src/command/graph/publish.rs index 053d3b953..c6208e5df 100644 --- a/src/command/graph/publish.rs +++ b/src/command/graph/publish.rs @@ -2,20 +2,20 @@ use ansi_term::Colour::{Cyan, Yellow}; use serde::Serialize; use structopt::StructOpt; -use rover_client::query::graph::publish; +use rover_client::operations::graph::publish::{self, GraphPublishInput}; +use rover_client::shared::{GitContext, GraphRef}; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; -use crate::utils::git::GitContext; use crate::utils::loaders::load_schema_from_flag; -use crate::utils::parsers::{parse_graph_ref, parse_schema_source, GraphRef, SchemaSource}; +use crate::utils::parsers::{parse_schema_source, SchemaSource}; use crate::Result; #[derive(Debug, Serialize, StructOpt)] pub struct Publish { /// @ of graph in Apollo Studio to publish to. /// @ may be left off, defaulting to @current - #[structopt(name = "GRAPH_REF", parse(try_from_str = parse_graph_ref))] + #[structopt(name = "GRAPH_REF")] #[serde(skip_serializing)] graph: GraphRef, @@ -36,7 +36,7 @@ impl Publish { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let graph_ref = self.graph.to_string(); eprintln!( @@ -45,53 +45,22 @@ impl Publish { Yellow.normal().paint(&self.profile_name) ); - let schema_document = load_schema_from_flag(&self.schema, std::io::stdin())?; + let proposed_schema = load_schema_from_flag(&self.schema, std::io::stdin())?; - tracing::debug!("Publishing \n{}", &schema_document); + tracing::debug!("Publishing \n{}", &proposed_schema); let publish_response = publish::run( - publish::publish_schema_mutation::Variables { - graph_id: self.graph.name.clone(), - variant: self.graph.variant.clone(), - schema_document: Some(schema_document), - git_context: git_context.into(), + GraphPublishInput { + graph_ref: self.graph.clone(), + proposed_schema, + git_context, }, &client, )?; - let hash = handle_response(&self.graph, publish_response); - Ok(RoverStdout::SchemaHash(hash)) - } -} - -/// handle all output logging from operation -fn handle_response(graph: &GraphRef, response: publish::PublishResponse) -> String { - eprintln!( - "{}#{} Published successfully {}", - graph, response.schema_hash, response.change_summary - ); - - response.schema_hash -} - -#[cfg(test)] -mod tests { - use super::{handle_response, publish, GraphRef}; - - #[test] - fn handle_response_doesnt_err() { - let expected_hash = "123456".to_string(); - let graph = GraphRef { - name: "harambe".to_string(), - variant: "inside-job".to_string(), - }; - let actual_hash = handle_response( - &graph, - publish::PublishResponse { - schema_hash: expected_hash.clone(), - change_summary: "".to_string(), - }, - ); - assert_eq!(actual_hash, expected_hash); + Ok(RoverOutput::GraphPublishResponse { + graph_ref: self.graph.clone(), + publish_response, + }) } } diff --git a/src/command/info.rs b/src/command/info.rs index 55756b6e0..0f719690c 100644 --- a/src/command/info.rs +++ b/src/command/info.rs @@ -1,4 +1,4 @@ -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::Result; use crate::PKG_VERSION; use serde::Serialize; @@ -9,7 +9,7 @@ use structopt::StructOpt; pub struct Info {} impl Info { - pub fn run(&self) -> Result { + pub fn run(&self) -> Result { let os = os_info::get(); // something like "/usr/bin/zsh" or "Unknown" @@ -28,6 +28,6 @@ impl Info { PKG_VERSION, location, os, shell ); - Ok(RoverStdout::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/install/mod.rs b/src/command/install/mod.rs index ca2f34be3..293a4b941 100644 --- a/src/command/install/mod.rs +++ b/src/command/install/mod.rs @@ -5,7 +5,7 @@ use structopt::StructOpt; use binstall::Installer; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::PKG_NAME; use crate::{anyhow, Context, Result}; use crate::{command::docs::shortlinks, utils::env::RoverEnvKey}; @@ -20,7 +20,7 @@ pub struct Install { } impl Install { - pub fn run(&self, override_install_path: Option) -> Result { + pub fn run(&self, override_install_path: Option) -> Result { let binary_name = PKG_NAME.to_string(); if let Ok(executable_location) = env::current_exe() { let executable_location = Utf8PathBuf::try_from(executable_location)?; @@ -68,7 +68,7 @@ impl Install { } else { eprintln!("{} was not installed. To override the existing installation, you can pass the `--force` flag to the installer.", &binary_name); } - Ok(RoverStdout::None) + Ok(RoverOutput::EmptySuccess) } else { Err(anyhow!("Failed to get the current executable's path.").into()) } diff --git a/src/command/mod.rs b/src/command/mod.rs index dfc4d344b..7cf6e8bd3 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -4,18 +4,19 @@ mod explain; mod graph; mod info; mod install; -mod output; mod subgraph; mod supergraph; mod update; +pub(crate) mod output; + pub use config::Config; pub use docs::Docs; pub use explain::Explain; pub use graph::Graph; pub use info::Info; pub use install::Install; -pub use output::RoverStdout; +pub use output::RoverOutput; pub use subgraph::Subgraph; pub use supergraph::Supergraph; pub use update::Update; diff --git a/src/command/output.rs b/src/command/output.rs index eac986a73..146139540 100644 --- a/src/command/output.rs +++ b/src/command/output.rs @@ -1,42 +1,65 @@ -use std::fmt::Debug; -use std::{collections::HashMap, fmt::Display}; +use std::collections::BTreeMap; +use std::fmt::{self, Debug, Display}; +use crate::error::RoverError; use crate::utils::table::{self, cell, row}; -use ansi_term::{Colour::Yellow, Style}; +use ansi_term::{ + Colour::{Cyan, Red, Yellow}, + Style, +}; use atty::Stream; use crossterm::style::Attribute::Underlined; -use rover_client::query::subgraph::list::ListDetails; +use rover_client::operations::graph::publish::GraphPublishResponse; +use rover_client::operations::subgraph::delete::SubgraphDeleteResponse; +use rover_client::operations::subgraph::list::SubgraphListResponse; +use rover_client::operations::subgraph::publish::SubgraphPublishResponse; +use rover_client::shared::{CheckResponse, FetchResponse, GraphRef, SdlType}; +use rover_client::RoverClientError; +use serde::Serialize; +use serde_json::{json, Value}; use termimad::MadSkin; -/// RoverStdout defines all of the different types of data that are printed -/// to `stdout`. Every one of Rover's commands should return `anyhow::Result` +/// RoverOutput defines all of the different types of data that are printed +/// to `stdout`. Every one of Rover's commands should return `anyhow::Result` /// If the command needs to output some type of data, it should be structured -/// in this enum, and its print logic should be handled in `RoverStdout::print` +/// in this enum, and its print logic should be handled in `RoverOutput::print` /// /// Not all commands will output machine readable information, and those should -/// return `Ok(RoverStdout::None)`. If a new command is added and it needs to +/// return `Ok(RoverOutput::EmptySuccess)`. If a new command is added and it needs to /// return something that is not described well in this enum, it should be added. #[derive(Clone, PartialEq, Debug)] -pub enum RoverStdout { - DocsList(HashMap<&'static str, &'static str>), - SupergraphSdl(String), - Sdl(String), +pub enum RoverOutput { + DocsList(BTreeMap<&'static str, &'static str>), + FetchResponse(FetchResponse), CoreSchema(String), - SchemaHash(String), - SubgraphList(ListDetails), - VariantList(Vec), + SubgraphList(SubgraphListResponse), + CheckResponse(CheckResponse), + GraphPublishResponse { + graph_ref: GraphRef, + publish_response: GraphPublishResponse, + }, + SubgraphPublishResponse { + graph_ref: GraphRef, + subgraph: String, + publish_response: SubgraphPublishResponse, + }, + SubgraphDeleteResponse { + graph_ref: GraphRef, + subgraph: String, + dry_run: bool, + delete_response: SubgraphDeleteResponse, + }, Profiles(Vec), Introspection(String), - Markdown(String), - PlainText(String), - None, + ErrorExplanation(String), + EmptySuccess, } -impl RoverStdout { +impl RoverOutput { pub fn print(&self) { match self { - RoverStdout::DocsList(shortlinks) => { + RoverOutput::DocsList(shortlinks) => { eprintln!( "You can open any of these documentation pages by running {}.\n", Yellow.normal().paint("`rover docs open `") @@ -50,23 +73,108 @@ impl RoverStdout { } println!("{}", table); } - RoverStdout::SupergraphSdl(sdl) => { - print_descriptor("Supergraph SDL"); - print_content(&sdl); + RoverOutput::FetchResponse(fetch_response) => { + match fetch_response.sdl.r#type { + SdlType::Graph | SdlType::Subgraph => print_descriptor("SDL"), + SdlType::Supergraph => print_descriptor("Supergraph SDL"), + } + print_content(&fetch_response.sdl.contents); + } + RoverOutput::GraphPublishResponse { + graph_ref, + publish_response, + } => { + eprintln!( + "{}#{} Published successfully {}", + graph_ref, publish_response.api_schema_hash, publish_response.change_summary + ); + print_one_line_descriptor("Schema Hash"); + print_content(&publish_response.api_schema_hash); } - RoverStdout::Sdl(sdl) => { - print_descriptor("SDL"); - print_content(&sdl); + RoverOutput::SubgraphPublishResponse { + graph_ref, + subgraph, + publish_response, + } => { + if publish_response.subgraph_was_created { + eprintln!( + "A new subgraph called '{}' for the '{}' graph was created", + subgraph, graph_ref + ); + } else { + eprintln!( + "The '{}' subgraph for the '{}' graph was updated", + subgraph, graph_ref + ); + } + + if publish_response.supergraph_was_updated { + eprintln!("The gateway for the '{}' graph was updated with a new schema, composed from the updated '{}' subgraph", graph_ref, subgraph); + } else { + eprintln!( + "The gateway for the '{}' graph was NOT updated with a new schema", + graph_ref + ); + } + + if !publish_response.build_errors.is_empty() { + let warn_prefix = Red.normal().paint("WARN:"); + eprintln!("{} The following build errors occurred:", warn_prefix,); + eprintln!("{}", &publish_response.build_errors); + } + } + RoverOutput::SubgraphDeleteResponse { + graph_ref, + subgraph, + dry_run, + delete_response, + } => { + let warn_prefix = Red.normal().paint("WARN:"); + if *dry_run { + if !delete_response.build_errors.is_empty() { + eprintln!( + "{} Deleting the {} subgraph from {} would result in the following build errors:", + warn_prefix, + Cyan.normal().paint(subgraph), + Cyan.normal().paint(graph_ref.to_string()), + ); + + eprintln!("{}", &delete_response.build_errors); + eprintln!("{} This is only a prediction. If the graph changes before confirming, these errors could change.", warn_prefix); + } else { + eprintln!("{} At the time of checking, there would be no build errors resulting from the deletion of this subgraph.", warn_prefix); + eprintln!("{} This is only a prediction. If the graph changes before confirming, there could be build errors.", warn_prefix) + } + } else { + if delete_response.supergraph_was_updated { + eprintln!( + "The {} subgraph was removed from {}. Remaining subgraphs were composed.", + Cyan.normal().paint(subgraph), + Cyan.normal().paint(graph_ref.to_string()), + ) + } else { + eprintln!( + "{} The gateway for {} was not updated. See errors below.", + warn_prefix, + Cyan.normal().paint(graph_ref.to_string()) + ) + } + + if !delete_response.build_errors.is_empty() { + eprintln!( + "{} There were build errors as a result of deleting the subgraph:", + warn_prefix, + ); + + eprintln!("{}", &delete_response.build_errors); + } + } } - RoverStdout::CoreSchema(csdl) => { + RoverOutput::CoreSchema(csdl) => { print_descriptor("CoreSchema"); print_content(&csdl); } - RoverStdout::SchemaHash(hash) => { - print_one_line_descriptor("Schema Hash"); - print_content(&hash); - } - RoverStdout::SubgraphList(details) => { + RoverOutput::SubgraphList(details) => { let mut table = table::get_table(); // bc => sets top row to be bold and center @@ -83,7 +191,7 @@ impl RoverStdout { } else { url }; - let formatted_updated_at: String = if let Some(dt) = subgraph.updated_at { + let formatted_updated_at: String = if let Some(dt) = subgraph.updated_at.local { dt.format("%Y-%m-%d %H:%M:%S %Z").to_string() } else { "N/A".to_string() @@ -95,16 +203,14 @@ impl RoverStdout { println!("{}", table); println!( "View full details at {}/graph/{}/service-list", - details.root_url, details.graph_name + details.root_url, details.graph_ref.name ); } - RoverStdout::VariantList(variants) => { - print_descriptor("Variants"); - for variant in variants { - println!("{}", variant); - } + RoverOutput::CheckResponse(check_response) => { + print_descriptor("Check Result"); + print_content(check_response.get_table()); } - RoverStdout::Profiles(profiles) => { + RoverOutput::Profiles(profiles) => { if profiles.is_empty() { eprintln!("No profiles found."); } else { @@ -115,23 +221,101 @@ impl RoverStdout { println!("{}", profile); } } - RoverStdout::Introspection(introspection_response) => { + RoverOutput::Introspection(introspection_response) => { print_descriptor("Introspection Response"); print_content(&introspection_response); } - RoverStdout::Markdown(markdown_string) => { + RoverOutput::ErrorExplanation(explanation) => { // underline bolded md let mut skin = MadSkin::default(); skin.bold.add_attr(Underlined); - println!("{}", skin.inline(&markdown_string)); + println!("{}", skin.inline(&explanation)); } - RoverStdout::PlainText(text) => { - println!("{}", text); + RoverOutput::EmptySuccess => (), + } + } + + pub(crate) fn get_internal_data_json(&self) -> Value { + match self { + RoverOutput::DocsList(shortlinks) => { + let mut shortlink_vec = Vec::with_capacity(shortlinks.len()); + for (shortlink_slug, shortlink_description) in shortlinks { + shortlink_vec.push( + json!({"slug": shortlink_slug, "description": shortlink_description }), + ); + } + json!({ "shortlinks": shortlink_vec }) + } + RoverOutput::FetchResponse(fetch_response) => json!(fetch_response), + RoverOutput::CoreSchema(csdl) => json!({ "core_schema": csdl }), + RoverOutput::GraphPublishResponse { + graph_ref: _, + publish_response, + } => json!(publish_response), + RoverOutput::SubgraphPublishResponse { + graph_ref: _, + subgraph: _, + publish_response, + } => json!(publish_response), + RoverOutput::SubgraphDeleteResponse { + graph_ref: _, + subgraph: _, + dry_run: _, + delete_response, + } => { + json!(delete_response) } - RoverStdout::None => (), + RoverOutput::SubgraphList(list_response) => json!(list_response), + RoverOutput::CheckResponse(check_response) => check_response.get_json(), + RoverOutput::Profiles(profiles) => json!({ "profiles": profiles }), + RoverOutput::Introspection(introspection_response) => { + json!({ "introspection_response": introspection_response }) + } + RoverOutput::ErrorExplanation(explanation_markdown) => { + json!({ "explanation_markdown": explanation_markdown }) + } + RoverOutput::EmptySuccess => json!(null), } } + + pub(crate) fn get_internal_error_json(&self) -> Value { + let rover_error = match self { + RoverOutput::SubgraphPublishResponse { + graph_ref, + subgraph, + publish_response, + } => { + if !publish_response.build_errors.is_empty() { + Some(RoverError::from(RoverClientError::SubgraphBuildErrors { + subgraph: subgraph.clone(), + graph_ref: graph_ref.clone(), + source: publish_response.build_errors.clone(), + })) + } else { + None + } + } + RoverOutput::SubgraphDeleteResponse { + graph_ref, + subgraph, + dry_run: _, + delete_response, + } => { + if !delete_response.build_errors.is_empty() { + Some(RoverError::from(RoverClientError::SubgraphBuildErrors { + subgraph: subgraph.clone(), + graph_ref: graph_ref.clone(), + source: delete_response.build_errors.clone(), + })) + } else { + None + } + } + _ => None, + }; + json!(rover_error) + } } fn print_descriptor(descriptor: impl Display) { @@ -155,3 +339,756 @@ fn print_content(content: impl Display) { print!("{}", content) } } + +#[derive(Debug, Clone, Serialize)] +pub(crate) struct JsonOutput { + json_version: JsonVersion, + data: JsonData, + error: Value, +} + +impl JsonOutput { + pub(crate) fn success(data: Value, error: Value) -> JsonOutput { + JsonOutput { + json_version: JsonVersion::OneBeta, + data: JsonData::success(data), + error, + } + } + + pub(crate) fn failure(data: Value, error: Value) -> JsonOutput { + JsonOutput { + json_version: JsonVersion::OneBeta, + data: JsonData::failure(data), + error, + } + } +} + +impl fmt::Display for JsonOutput { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", json!(self)) + } +} + +impl From for JsonOutput { + fn from(error: RoverError) -> Self { + let data = error.get_internal_data_json(); + let error = error.get_internal_error_json(); + JsonOutput::failure(data, error) + } +} + +impl From for JsonOutput { + fn from(output: RoverOutput) -> Self { + let data = output.get_internal_data_json(); + let error = output.get_internal_error_json(); + JsonOutput::success(data, error) + } +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) struct JsonData { + #[serde(flatten)] + inner: Value, + success: bool, +} + +impl JsonData { + pub(crate) fn success(inner: Value) -> JsonData { + JsonData { + inner, + success: true, + } + } + + pub(crate) fn failure(inner: Value) -> JsonData { + JsonData { + inner, + success: false, + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) enum JsonVersion { + #[serde(rename = "1.beta")] + OneBeta, +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use assert_json_diff::assert_json_eq; + use chrono::{DateTime, Local, Utc}; + use rover_client::{ + operations::{ + graph::publish::{ChangeSummary, FieldChanges, TypeChanges}, + subgraph::{ + delete::SubgraphDeleteResponse, + list::{SubgraphInfo, SubgraphUpdatedAt}, + }, + }, + shared::{BuildError, BuildErrors, ChangeSeverity, SchemaChange, Sdl}, + }; + + use crate::anyhow; + + use super::*; + + #[test] + fn docs_list_json() { + let mut mock_shortlinks = BTreeMap::new(); + mock_shortlinks.insert("slug_one", "description_one"); + mock_shortlinks.insert("slug_two", "description_two"); + let actual_json: JsonOutput = RoverOutput::DocsList(mock_shortlinks).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "shortlinks": [ + { + "slug": "slug_one", + "description": "description_one" + }, + { + "slug": "slug_two", + "description": "description_two" + } + ], + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn fetch_response_json() { + let mock_fetch_response = FetchResponse { + sdl: Sdl { + contents: "sdl contents".to_string(), + r#type: SdlType::Subgraph, + }, + }; + let actual_json: JsonOutput = RoverOutput::FetchResponse(mock_fetch_response).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "sdl": { + "contents": "sdl contents", + }, + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn core_schema_json() { + let mock_core_schema = "core schema contents".to_string(); + let actual_json: JsonOutput = RoverOutput::CoreSchema(mock_core_schema).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "core_schema": "core schema contents", + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn subgraph_list_json() { + let now_utc: DateTime = Utc::now(); + let now_local: DateTime = now_utc.into(); + let mock_subgraph_list_response = SubgraphListResponse { + subgraphs: vec![ + SubgraphInfo { + name: "subgraph one".to_string(), + url: Some("http://localhost:4001".to_string()), + updated_at: SubgraphUpdatedAt { + local: Some(now_local), + utc: Some(now_utc), + }, + }, + SubgraphInfo { + name: "subgraph two".to_string(), + url: None, + updated_at: SubgraphUpdatedAt { + local: None, + utc: None, + }, + }, + ], + root_url: "https://studio.apollographql.com/".to_string(), + graph_ref: GraphRef { + name: "graph".to_string(), + variant: "current".to_string(), + }, + }; + let actual_json: JsonOutput = RoverOutput::SubgraphList(mock_subgraph_list_response).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "subgraphs": [ + { + "name": "subgraph one", + "url": "http://localhost:4001", + "updated_at": { + "local": now_local, + "utc": now_utc + } + }, + { + "name": "subgraph two", + "url": null, + "updated_at": { + "local": null, + "utc": null + } + } + ], + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn subgraph_delete_success_json() { + let mock_subgraph_delete = SubgraphDeleteResponse { + supergraph_was_updated: true, + build_errors: BuildErrors::new(), + }; + let actual_json: JsonOutput = RoverOutput::SubgraphDeleteResponse { + delete_response: mock_subgraph_delete, + subgraph: "subgraph".to_string(), + dry_run: false, + graph_ref: GraphRef { + name: "name".to_string(), + variant: "current".to_string(), + }, + } + .into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "supergraph_was_updated": true, + "success": true, + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn subgraph_delete_build_errors_json() { + let mock_subgraph_delete = SubgraphDeleteResponse { + supergraph_was_updated: false, + build_errors: vec![ + BuildError::composition_error( + "[Accounts] -> Things went really wrong".to_string(), + Some("AN_ERROR_CODE".to_string()), + ), + BuildError::composition_error( + "[Films] -> Something else also went wrong".to_string(), + None, + ), + ] + .into(), + }; + let actual_json: JsonOutput = RoverOutput::SubgraphDeleteResponse { + delete_response: mock_subgraph_delete, + subgraph: "subgraph".to_string(), + dry_run: true, + graph_ref: GraphRef { + name: "name".to_string(), + variant: "current".to_string(), + }, + } + .into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "supergraph_was_updated": false, + "success": true, + }, + "error": { + "message": "Encountered 2 build errors while trying to build subgraph \"subgraph\" into supergraph \"name@current\".", + "code": "E029", + "details": { + "build_errors": [ + { + "message": "[Accounts] -> Things went really wrong", + "code": "AN_ERROR_CODE", + "type": "composition" + }, + { + "message": "[Films] -> Something else also went wrong", + "code": null, + "type": "composition" + } + ], + } + } + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn supergraph_fetch_no_successful_publishes_json() { + let graph_ref = GraphRef { + name: "name".to_string(), + variant: "current".to_string(), + }; + let source = BuildErrors::from(vec![ + BuildError::composition_error( + "[Accounts] -> Things went really wrong".to_string(), + Some("AN_ERROR_CODE".to_string()), + ), + BuildError::composition_error( + "[Films] -> Something else also went wrong".to_string(), + None, + ), + ]); + let actual_json: JsonOutput = + RoverError::new(RoverClientError::NoSupergraphBuilds { graph_ref, source }).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "success": false + }, + "error": { + "message": "No supergraph SDL exists for \"name@current\" because its subgraphs failed to build.", + "details": { + "build_errors": [ + { + "message": "[Accounts] -> Things went really wrong", + "code": "AN_ERROR_CODE", + "type": "composition", + }, + { + "message": "[Films] -> Something else also went wrong", + "code": null, + "type": "composition" + } + ] + }, + "code": "E027" + } + }); + assert_json_eq!(actual_json, expected_json); + } + + #[test] + fn check_success_response_json() { + let graph_ref = GraphRef { + name: "name".to_string(), + variant: "current".to_string(), + }; + let mock_check_response = CheckResponse::try_new( + Some("https://studio.apollographql.com/graph/my-graph/composition/big-hash?variant=current".to_string()), + 10, + vec![ + SchemaChange { + code: "SOMETHING_HAPPENED".to_string(), + description: "beeg yoshi".to_string(), + severity: ChangeSeverity::PASS, + }, + SchemaChange { + code: "WOW".to_string(), + description: "that was so cool".to_string(), + severity: ChangeSeverity::PASS, + } + ], + ChangeSeverity::PASS, + graph_ref, + ); + if let Ok(mock_check_response) = mock_check_response { + let actual_json: JsonOutput = RoverOutput::CheckResponse(mock_check_response).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "target_url": "https://studio.apollographql.com/graph/my-graph/composition/big-hash?variant=current", + "operation_check_count": 10, + "changes": [ + { + "code": "SOMETHING_HAPPENED", + "description": "beeg yoshi", + "severity": "PASS" + }, + { + "code": "WOW", + "description": "that was so cool", + "severity": "PASS" + }, + ], + "failure_count": 0, + "success": true, + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } else { + panic!("The shape of this response should return a CheckResponse") + } + } + + #[test] + fn check_failure_response_json() { + let graph_ref = GraphRef { + name: "name".to_string(), + variant: "current".to_string(), + }; + let check_response = CheckResponse::try_new( + Some("https://studio.apollographql.com/graph/my-graph/composition/big-hash?variant=current".to_string()), + 10, + vec![ + SchemaChange { + code: "SOMETHING_HAPPENED".to_string(), + description: "beeg yoshi".to_string(), + severity: ChangeSeverity::FAIL, + }, + SchemaChange { + code: "WOW".to_string(), + description: "that was so cool".to_string(), + severity: ChangeSeverity::FAIL, + } + ], + ChangeSeverity::FAIL, graph_ref); + + if let Err(operation_check_failure) = check_response { + let actual_json: JsonOutput = RoverError::new(operation_check_failure).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "target_url": "https://studio.apollographql.com/graph/my-graph/composition/big-hash?variant=current", + "operation_check_count": 10, + "changes": [ + { + "code": "SOMETHING_HAPPENED", + "description": "beeg yoshi", + "severity": "FAIL" + }, + { + "code": "WOW", + "description": "that was so cool", + "severity": "FAIL" + }, + ], + "failure_count": 2, + "success": false, + }, + "error": { + "message": "This operation check has encountered 2 schema changes that would break operations from existing client traffic.", + "code": "E030", + } + }); + assert_json_eq!(expected_json, actual_json); + } else { + panic!("The shape of this response should return a RoverClientError") + } + } + + #[test] + fn graph_publish_response_json() { + let mock_publish_response = GraphPublishResponse { + api_schema_hash: "123456".to_string(), + change_summary: ChangeSummary { + field_changes: FieldChanges { + additions: 2, + removals: 1, + edits: 0, + }, + type_changes: TypeChanges { + additions: 4, + removals: 0, + edits: 7, + }, + }, + }; + let actual_json: JsonOutput = RoverOutput::GraphPublishResponse { + graph_ref: GraphRef { + name: "graph".to_string(), + variant: "variant".to_string(), + }, + publish_response: mock_publish_response, + } + .into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "api_schema_hash": "123456", + "field_changes": { + "additions": 2, + "removals": 1, + "edits": 0 + }, + "type_changes": { + "additions": 4, + "removals": 0, + "edits": 7 + }, + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn subgraph_publish_success_response_json() { + let mock_publish_response = SubgraphPublishResponse { + api_schema_hash: Some("123456".to_string()), + build_errors: BuildErrors::new(), + supergraph_was_updated: true, + subgraph_was_created: true, + }; + let actual_json: JsonOutput = RoverOutput::SubgraphPublishResponse { + graph_ref: GraphRef { + name: "graph".to_string(), + variant: "variant".to_string(), + }, + subgraph: "subgraph".to_string(), + publish_response: mock_publish_response, + } + .into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "api_schema_hash": "123456", + "supergraph_was_updated": true, + "subgraph_was_created": true, + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn subgraph_publish_failure_response_json() { + let mock_publish_response = SubgraphPublishResponse { + api_schema_hash: None, + + build_errors: vec![ + BuildError::composition_error( + "[Accounts] -> Things went really wrong".to_string(), + Some("AN_ERROR_CODE".to_string()), + ), + BuildError::composition_error( + "[Films] -> Something else also went wrong".to_string(), + None, + ), + ] + .into(), + supergraph_was_updated: false, + subgraph_was_created: false, + }; + let actual_json: JsonOutput = RoverOutput::SubgraphPublishResponse { + graph_ref: GraphRef { + name: "name".to_string(), + variant: "current".to_string(), + }, + subgraph: "subgraph".to_string(), + publish_response: mock_publish_response, + } + .into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "api_schema_hash": null, + "subgraph_was_created": false, + "supergraph_was_updated": false, + "success": true + }, + "error": { + "message": "Encountered 2 build errors while trying to build subgraph \"subgraph\" into supergraph \"name@current\".", + "code": "E029", + "details": { + "build_errors": [ + { + "message": "[Accounts] -> Things went really wrong", + "code": "AN_ERROR_CODE", + "type": "composition", + }, + { + "message": "[Films] -> Something else also went wrong", + "code": null, + "type": "composition" + } + ] + } + } + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn profiles_json() { + let mock_profiles = vec!["default".to_string(), "staging".to_string()]; + let actual_json: JsonOutput = RoverOutput::Profiles(mock_profiles).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "profiles": [ + "default", + "staging" + ], + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn introspection_json() { + let actual_json: JsonOutput = RoverOutput::Introspection( + "i cant believe its not a real introspection response".to_string(), + ) + .into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "introspection_response": "i cant believe its not a real introspection response", + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn error_explanation_json() { + let actual_json: JsonOutput = RoverOutput::ErrorExplanation( + "this error occurs when stuff is real complicated... I wouldn't worry about it" + .to_string(), + ) + .into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "explanation_markdown": "this error occurs when stuff is real complicated... I wouldn't worry about it", + "success": true + }, + "error": null + } + + ); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn empty_success_json() { + let actual_json: JsonOutput = RoverOutput::EmptySuccess.into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "success": true + }, + "error": null + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn base_error_message_json() { + let actual_json: JsonOutput = RoverError::new(anyhow!("Some random error")).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "success": false + }, + "error": { + "message": "Some random error", + "code": null + } + }); + assert_json_eq!(expected_json, actual_json); + } + + #[test] + fn coded_error_message_json() { + let actual_json: JsonOutput = RoverError::new(RoverClientError::NoSubgraphInGraph { + invalid_subgraph: "invalid_subgraph".to_string(), + valid_subgraphs: Vec::new(), + }) + .into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "success": false + }, + "error": { + "message": "Could not find subgraph \"invalid_subgraph\".", + "code": "E009" + } + }); + assert_json_eq!(expected_json, actual_json) + } + + #[test] + fn composition_error_message_json() { + let source = BuildErrors::from(vec![ + BuildError::composition_error( + "[Accounts] -> Things went really wrong".to_string(), + Some("AN_ERROR_CODE".to_string()), + ), + BuildError::composition_error( + "[Films] -> Something else also went wrong".to_string(), + None, + ), + ]); + let actual_json: JsonOutput = + RoverError::from(RoverClientError::BuildErrors { source }).into(); + let expected_json = json!( + { + "json_version": "1.beta", + "data": { + "success": false + }, + "error": { + "details": { + "build_errors": [ + { + "message": "[Accounts] -> Things went really wrong", + "code": "AN_ERROR_CODE", + "type": "composition" + }, + { + "message": "[Films] -> Something else also went wrong", + "code": null, + "type": "composition" + } + ], + }, + "message": "Encountered 2 build errors while trying to build a supergraph.", + "code": "E029" + } + }); + assert_json_eq!(expected_json, actual_json) + } +} diff --git a/src/command/subgraph/check.rs b/src/command/subgraph/check.rs index 9fff514ec..696ae2b2f 100644 --- a/src/command/subgraph/check.rs +++ b/src/command/subgraph/check.rs @@ -1,25 +1,23 @@ -use ansi_term::Colour::Red; use serde::Serialize; use structopt::StructOpt; -use rover_client::query::subgraph::check; +use rover_client::operations::subgraph::check::{self, SubgraphCheckInput}; +use rover_client::shared::{CheckConfig, GitContext, GraphRef, ValidationPeriod}; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; -use crate::utils::git::GitContext; use crate::utils::loaders::load_schema_from_flag; use crate::utils::parsers::{ - parse_graph_ref, parse_query_count_threshold, parse_query_percentage_threshold, - parse_schema_source, parse_validation_period, GraphRef, SchemaSource, ValidationPeriod, + parse_query_count_threshold, parse_query_percentage_threshold, parse_schema_source, + SchemaSource, }; -use crate::utils::table::{self, cell, row}; use crate::Result; #[derive(Debug, Serialize, StructOpt)] pub struct Check { /// @ of graph in Apollo Studio to validate. /// @ may be left off, defaulting to @current - #[structopt(name = "GRAPH_REF", parse(try_from_str = parse_graph_ref))] + #[structopt(name = "GRAPH_REF")] #[serde(skip_serializing)] graph: GraphRef, @@ -51,7 +49,7 @@ pub struct Check { query_percentage_threshold: Option, /// Size of the time window with which to validate schema against (i.e "24h" or "1w 2d 5h") - #[structopt(long, parse(try_from_str = parse_validation_period))] + #[structopt(long)] validation_period: Option, } @@ -60,116 +58,31 @@ impl Check { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; - let sdl = load_schema_from_flag(&self.schema, std::io::stdin())?; + let proposed_schema = load_schema_from_flag(&self.schema, std::io::stdin())?; - let partial_schema = check::check_partial_schema_query::PartialSchemaInput { - sdl: Some(sdl), - // we never need to send the hash since the back end computes it from SDL - hash: None, - }; + eprintln!( + "Checking the proposed schema for subgraph {} against {}", + &self.subgraph, &self.graph + ); let res = check::run( - check::check_partial_schema_query::Variables { - graph_id: self.graph.name.clone(), - variant: self.graph.variant.clone(), - partial_schema, - implementing_service_name: self.subgraph.clone(), - git_context: git_context.into(), - config: check::check_partial_schema_query::HistoricQueryParameters { + SubgraphCheckInput { + graph_ref: self.graph.clone(), + proposed_schema, + subgraph: self.subgraph.clone(), + git_context, + config: CheckConfig { query_count_threshold: self.query_count_threshold, query_count_threshold_percentage: self.query_percentage_threshold, - from: self.validation_period.clone().unwrap_or_default().from, - to: self.validation_period.clone().unwrap_or_default().to, - // we don't support configuring these, but we can't leave them out - excluded_clients: None, - ignored_operations: None, - included_variants: None, + validation_period: self.validation_period.clone(), }, }, &client, )?; - eprintln!("Checked the proposed subgraph against {}", &self.graph); - - match res { - check::CheckResponse::CompositionErrors(composition_errors) => { - handle_composition_errors(&composition_errors) - } - check::CheckResponse::CheckResult(check_result) => handle_checks(check_result), - } - } -} - -fn handle_checks(check_result: check::CheckResult) -> Result { - let num_changes = check_result.changes.len(); - - let msg = match num_changes { - 0 => "There were no changes detected in the composed schema.".to_string(), - _ => format!( - "Compared {} schema changes against {} operations", - check_result.changes.len(), - check_result.number_of_checked_operations - ), - }; - - eprintln!("{}", &msg); - - let mut num_failures = 0; - - if !check_result.changes.is_empty() { - let mut table = table::get_table(); - - // bc => sets top row to be bold and center - table.add_row(row![bc => "Change", "Code", "Description"]); - for check in check_result.changes { - let change = match check.severity { - check::check_partial_schema_query::ChangeSeverity::NOTICE => "PASS", - check::check_partial_schema_query::ChangeSeverity::FAILURE => { - num_failures += 1; - "FAIL" - } - _ => unreachable!("Unknown change severity"), - }; - table.add_row(row![change, check.code, check.description]); - } - - eprintln!("{}", table); - } - - if let Some(url) = check_result.target_url { - eprintln!("View full details at {}", &url); - } - - match num_failures { - 0 => Ok(RoverStdout::None), - 1 => Err(anyhow::anyhow!("Encountered 1 failure while checking your subgraph.").into()), - _ => Err(anyhow::anyhow!( - "Encountered {} failures while checking your subgraph.", - num_failures - ) - .into()), - } -} - -fn handle_composition_errors( - composition_errors: &[check::check_partial_schema_query::CheckPartialSchemaQueryServiceCheckPartialSchemaCompositionValidationResultErrors], -) -> Result { - let num_failures = composition_errors.len(); - for error in composition_errors { - eprintln!("{} {}", Red.bold().paint("error:"), &error.message); - } - match num_failures { - 0 => Ok(RoverStdout::None), - 1 => Err( - anyhow::anyhow!("Encountered 1 composition error while composing the subgraph.").into(), - ), - _ => Err(anyhow::anyhow!( - "Encountered {} composition errors while composing the subgraph.", - num_failures - ) - .into()), + Ok(RoverOutput::CheckResponse(res)) } } diff --git a/src/command/subgraph/delete.rs b/src/command/subgraph/delete.rs index d6f6ea524..4e8c3ac9e 100644 --- a/src/command/subgraph/delete.rs +++ b/src/command/subgraph/delete.rs @@ -1,19 +1,19 @@ -use ansi_term::Colour::{Cyan, Red, Yellow}; +use ansi_term::Colour::{Cyan, Yellow}; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; -use crate::utils::parsers::{parse_graph_ref, GraphRef}; use crate::Result; -use rover_client::query::subgraph::delete::{self, DeleteServiceResponse}; +use rover_client::operations::subgraph::delete::{self, SubgraphDeleteInput}; +use rover_client::shared::GraphRef; #[derive(Debug, Serialize, StructOpt)] pub struct Delete { /// @ of federated graph in Apollo Studio to delete subgraph from. /// @ may be left off, defaulting to @current - #[structopt(name = "GRAPH_REF", parse(try_from_str = parse_graph_ref))] + #[structopt(name = "GRAPH_REF")] #[serde(skip_serializing)] graph: GraphRef, @@ -28,75 +28,68 @@ pub struct Delete { subgraph: String, /// Skips the step where the command asks for user confirmation before - /// deleting the subgraph. Also skips preview of composition errors that + /// deleting the subgraph. Also skips preview of build errors that /// might occur #[structopt(long)] confirm: bool, } impl Delete { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; - let graph_ref = self.graph.to_string(); eprintln!( - "Checking for composition errors resulting from deleting subgraph {} from {} using credentials from the {} profile.", + "Checking for build errors resulting from deleting subgraph {} from {} using credentials from the {} profile.", Cyan.normal().paint(&self.subgraph), - Cyan.normal().paint(&graph_ref), + Cyan.normal().paint(self.graph.to_string()), Yellow.normal().paint(&self.profile_name) ); // this is probably the normal path -- preview a subgraph delete // and make the user confirm it manually. if !self.confirm { - // run delete with dryRun, so we can preview composition errors + let dry_run = true; + // run delete with dryRun, so we can preview build errors let delete_dry_run_response = delete::run( - delete::delete_service_mutation::Variables { - graph_id: self.graph.name.clone(), - graph_variant: self.graph.variant.clone(), - name: self.subgraph.clone(), - dry_run: true, + SubgraphDeleteInput { + graph_ref: self.graph.clone(), + subgraph: self.subgraph.clone(), + dry_run, }, &client, )?; - handle_dry_run_response(delete_dry_run_response, &self.subgraph, &graph_ref); + RoverOutput::SubgraphDeleteResponse { + graph_ref: self.graph.clone(), + subgraph: self.subgraph.clone(), + dry_run, + delete_response: delete_dry_run_response, + } + .print(); // I chose not to error here, since this is a perfectly valid path if !confirm_delete()? { eprintln!("Delete cancelled by user"); - return Ok(RoverStdout::None); + return Ok(RoverOutput::EmptySuccess); } } + let dry_run = false; + let delete_response = delete::run( - delete::delete_service_mutation::Variables { - graph_id: self.graph.name.clone(), - graph_variant: self.graph.variant.clone(), - name: self.subgraph.clone(), - dry_run: false, + SubgraphDeleteInput { + graph_ref: self.graph.clone(), + subgraph: self.subgraph.clone(), + dry_run, }, &client, )?; - handle_response(delete_response, &self.subgraph, &graph_ref); - Ok(RoverStdout::None) - } -} - -fn handle_dry_run_response(response: DeleteServiceResponse, subgraph: &str, graph_ref: &str) { - let warn_prefix = Red.normal().paint("WARN:"); - if let Some(errors) = response.composition_errors { - eprintln!( - "{} Deleting the {} subgraph from {} would result in the following composition errors: \n{}", - warn_prefix, - Cyan.normal().paint(subgraph), - Cyan.normal().paint(graph_ref), - errors.join("\n") - ); - eprintln!("{} This is only a prediction. If the graph changes before confirming, these errors could change.", warn_prefix); - } else { - eprintln!("{} At the time of checking, there would be no composition errors resulting from the deletion of this subgraph.", warn_prefix); - eprintln!("{} This is only a prediction. If the graph changes before confirming, there could be composition errors.", warn_prefix) + Ok(RoverOutput::SubgraphDeleteResponse { + graph_ref: self.graph.clone(), + subgraph: self.subgraph.clone(), + dry_run, + delete_response, + }) } } @@ -110,61 +103,3 @@ fn confirm_delete() -> Result { Ok(false) } } - -fn handle_response(response: DeleteServiceResponse, subgraph: &str, graph_ref: &str) { - let warn_prefix = Red.normal().paint("WARN:"); - if response.updated_gateway { - eprintln!( - "The {} subgraph was removed from {}. Remaining subgraphs were composed.", - Cyan.normal().paint(subgraph), - Cyan.normal().paint(graph_ref), - ) - } else { - eprintln!( - "{} The gateway for {} was not updated. See errors below.", - warn_prefix, - Cyan.normal().paint(graph_ref) - ) - } - - if let Some(errors) = response.composition_errors { - eprintln!( - "{} There were composition errors as a result of deleting the subgraph: \n{}", - warn_prefix, - errors.join("\n") - ) - } else { - eprintln!("There were no composition errors as a result of deleting the subgraph."); - } -} - -#[cfg(test)] -mod tests { - use super::{handle_response, DeleteServiceResponse}; - - #[test] - fn handle_response_doesnt_error_with_all_successes() { - let response = DeleteServiceResponse { - composition_errors: None, - updated_gateway: true, - }; - - handle_response(response, "accounts", "my-graph@current"); - } - - #[test] - fn handle_response_doesnt_error_with_all_failures() { - let response = DeleteServiceResponse { - composition_errors: Some(vec![ - "a bad thing happened".to_string(), - "another bad thing".to_string(), - ]), - updated_gateway: false, - }; - - handle_response(response, "accounts", "my-graph@prod"); - } - - // TODO: test the actual output of the logs whenever we do design work - // for the commands :) -} diff --git a/src/command/subgraph/fetch.rs b/src/command/subgraph/fetch.rs index 020dad04a..92643e71b 100644 --- a/src/command/subgraph/fetch.rs +++ b/src/command/subgraph/fetch.rs @@ -2,18 +2,18 @@ use ansi_term::Colour::{Cyan, Yellow}; use serde::Serialize; use structopt::StructOpt; -use rover_client::query::subgraph::fetch; +use rover_client::operations::subgraph::fetch::{self, SubgraphFetchInput}; +use rover_client::shared::GraphRef; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; -use crate::utils::parsers::{parse_graph_ref, GraphRef}; use crate::Result; #[derive(Debug, Serialize, StructOpt)] pub struct Fetch { /// @ of graph in Apollo Studio to fetch from. /// @ may be left off, defaulting to @current - #[structopt(name = "GRAPH_REF", parse(try_from_str = parse_graph_ref))] + #[structopt(name = "GRAPH_REF")] #[serde(skip_serializing)] graph: GraphRef, @@ -29,7 +29,7 @@ pub struct Fetch { } impl Fetch { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let graph_ref = self.graph.to_string(); eprintln!( @@ -39,15 +39,14 @@ impl Fetch { Yellow.normal().paint(&self.profile_name) ); - let sdl = fetch::run( - fetch::fetch_subgraph_query::Variables { - graph_id: self.graph.name.clone(), - variant: self.graph.variant.clone(), + let fetch_response = fetch::run( + SubgraphFetchInput { + graph_ref: self.graph.clone(), + subgraph: self.subgraph.clone(), }, &client, - &self.subgraph, )?; - Ok(RoverStdout::Sdl(sdl)) + Ok(RoverOutput::FetchResponse(fetch_response)) } } diff --git a/src/command/subgraph/introspect.rs b/src/command/subgraph/introspect.rs index cae22c14c..5b527495d 100644 --- a/src/command/subgraph/introspect.rs +++ b/src/command/subgraph/introspect.rs @@ -4,9 +4,12 @@ use std::collections::HashMap; use structopt::StructOpt; use url::Url; -use rover_client::{blocking::GraphQLClient, query::subgraph::introspect}; +use rover_client::{ + blocking::GraphQLClient, + operations::subgraph::introspect::{self, SubgraphIntrospectInput}, +}; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::parsers::parse_header; use crate::Result; @@ -33,7 +36,7 @@ pub struct Introspect { } impl Introspect { - pub fn run(&self, client: Client) -> Result { + pub fn run(&self, client: Client) -> Result { let client = GraphQLClient::new(&self.endpoint.to_string(), client)?; // add the flag headers to a hashmap to pass along to rover-client @@ -44,8 +47,8 @@ impl Introspect { } } - let introspection_response = introspect::run(&client, &headers)?; + let introspection_response = introspect::run(SubgraphIntrospectInput { headers }, &client)?; - Ok(RoverStdout::Introspection(introspection_response.result)) + Ok(RoverOutput::Introspection(introspection_response.result)) } } diff --git a/src/command/subgraph/list.rs b/src/command/subgraph/list.rs index e469b68db..c1794801e 100644 --- a/src/command/subgraph/list.rs +++ b/src/command/subgraph/list.rs @@ -2,18 +2,18 @@ use ansi_term::Colour::Cyan; use serde::Serialize; use structopt::StructOpt; -use rover_client::query::subgraph::list; +use rover_client::operations::subgraph::list::{self, SubgraphListInput}; +use rover_client::shared::GraphRef; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; -use crate::utils::parsers::{parse_graph_ref, GraphRef}; use crate::Result; #[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))] + #[structopt(name = "GRAPH_REF")] #[serde(skip_serializing)] graph: GraphRef, @@ -24,7 +24,7 @@ pub struct List { } impl List { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; eprintln!( @@ -34,13 +34,12 @@ impl List { ); let list_details = list::run( - list::list_subgraphs_query::Variables { - graph_id: self.graph.name.clone(), - variant: self.graph.variant.clone(), + SubgraphListInput { + graph_ref: self.graph.clone(), }, &client, )?; - Ok(RoverStdout::SubgraphList(list_details)) + Ok(RoverOutput::SubgraphList(list_details)) } } diff --git a/src/command/subgraph/mod.rs b/src/command/subgraph/mod.rs index 9361fcf70..08ba2adb0 100644 --- a/src/command/subgraph/mod.rs +++ b/src/command/subgraph/mod.rs @@ -8,10 +8,12 @@ mod publish; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; -use crate::utils::{client::StudioClientConfig, git::GitContext}; +use crate::command::RoverOutput; +use crate::utils::client::StudioClientConfig; use crate::Result; +use rover_client::shared::GitContext; + #[derive(Debug, Serialize, StructOpt)] pub struct Subgraph { #[structopt(subcommand)] @@ -20,7 +22,7 @@ pub struct Subgraph { #[derive(Debug, Serialize, StructOpt)] pub enum Command { - /// Check for composition errors and breaking changes caused by an updated subgraph schema + /// Check for build errors and breaking changes caused by an updated subgraph schema /// against the federated graph in the Apollo graph registry Check(check::Check), @@ -45,7 +47,7 @@ impl Subgraph { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { match &self.command { Command::Publish(command) => command.run(client_config, git_context), Command::Introspect(command) => command.run(client_config.get_reqwest_client()), diff --git a/src/command/subgraph/publish.rs b/src/command/subgraph/publish.rs index 34f68f58a..4d2f4490e 100644 --- a/src/command/subgraph/publish.rs +++ b/src/command/subgraph/publish.rs @@ -1,23 +1,23 @@ -use ansi_term::Colour::{Cyan, Red, Yellow}; +use ansi_term::Colour::{Cyan, Yellow}; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::{ client::StudioClientConfig, - git::GitContext, loaders::load_schema_from_flag, - parsers::{parse_graph_ref, parse_schema_source, GraphRef, SchemaSource}, + parsers::{parse_schema_source, SchemaSource}, }; use crate::Result; -use rover_client::query::subgraph::publish::{self, PublishPartialSchemaResponse}; +use rover_client::operations::subgraph::publish::{self, SubgraphPublishInput}; +use rover_client::shared::{GitContext, GraphRef}; #[derive(Debug, Serialize, StructOpt)] pub struct Publish { /// @ of federated graph in Apollo Studio to publish to. /// @ may be left off, defaulting to @current - #[structopt(name = "GRAPH_REF", parse(try_from_str = parse_graph_ref))] + #[structopt(name = "GRAPH_REF")] #[serde(skip_serializing)] graph: GraphRef, @@ -54,110 +54,35 @@ impl Publish { &self, client_config: StudioClientConfig, git_context: GitContext, - ) -> Result { + ) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; - let graph_ref = format!("{}:{}", &self.graph.name, &self.graph.variant); eprintln!( "Publishing SDL to {} (subgraph: {}) using credentials from the {} profile.", - Cyan.normal().paint(&graph_ref), + Cyan.normal().paint(&self.graph.to_string()), Cyan.normal().paint(&self.subgraph), Yellow.normal().paint(&self.profile_name) ); - let schema_document = load_schema_from_flag(&self.schema, std::io::stdin())?; + let schema = load_schema_from_flag(&self.schema, std::io::stdin())?; - tracing::debug!("Publishing \n{}", &schema_document); + tracing::debug!("Publishing \n{}", &schema); let publish_response = publish::run( - publish::publish_partial_schema_mutation::Variables { - graph_id: self.graph.name.clone(), - graph_variant: self.graph.variant.clone(), - name: self.subgraph.clone(), - active_partial_schema: - publish::publish_partial_schema_mutation::PartialSchemaInput { - sdl: Some(schema_document), - hash: None, - }, - revision: "".to_string(), + SubgraphPublishInput { + graph_ref: self.graph.clone(), + subgraph: self.subgraph.clone(), url: self.routing_url.clone(), - git_context: git_context.into(), + schema, + git_context, + convert_to_federated_graph: self.convert, }, &client, - self.convert, )?; - handle_publish_response(publish_response, &self.subgraph, &self.graph.name); - Ok(RoverStdout::None) + Ok(RoverOutput::SubgraphPublishResponse { + graph_ref: self.graph.clone(), + subgraph: self.subgraph.clone(), + publish_response, + }) } } - -fn handle_publish_response(response: PublishPartialSchemaResponse, subgraph: &str, graph: &str) { - if response.service_was_created { - eprintln!( - "A new subgraph called '{}' for the '{}' graph was created", - subgraph, graph - ); - } else { - eprintln!( - "The '{}' subgraph for the '{}' graph was updated", - subgraph, graph - ); - } - - if response.did_update_gateway { - eprintln!("The gateway for the '{}' graph was updated with a new schema, composed from the updated '{}' subgraph", graph, subgraph); - } else { - eprintln!( - "The gateway for the '{}' graph was NOT updated with a new schema", - graph - ); - } - - if let Some(errors) = response.composition_errors { - let warn_prefix = Red.normal().paint("WARN:"); - eprintln!( - "{} The following composition errors occurred: \n{}", - warn_prefix, - errors.join("\n") - ); - } else { - eprintln!("There were no composition errors as a result of publishing the subgraph."); - } -} - -#[cfg(test)] -mod tests { - use super::{handle_publish_response, PublishPartialSchemaResponse}; - - // this test is a bit weird, since we can't test the output. We just verify it - // doesn't error - #[test] - fn handle_response_doesnt_error_with_all_successes() { - let response = PublishPartialSchemaResponse { - schema_hash: Some("123456".to_string()), - did_update_gateway: true, - service_was_created: true, - composition_errors: None, - }; - - handle_publish_response(response, "accounts", "my-graph"); - } - - #[test] - fn handle_response_doesnt_error_with_all_failures() { - let response = PublishPartialSchemaResponse { - schema_hash: None, - did_update_gateway: false, - service_was_created: false, - composition_errors: Some(vec![ - "a bad thing happened".to_string(), - "another bad thing".to_string(), - ]), - }; - - handle_publish_response(response, "accounts", "my-graph"); - } - - // TODO: test the actual output of the logs whenever we do design work - // for the commands :) -} diff --git a/src/command/supergraph/compose/do_compose.rs b/src/command/supergraph/compose/do_compose.rs index fc9c73924..8fd80b813 100644 --- a/src/command/supergraph/compose/do_compose.rs +++ b/src/command/supergraph/compose/do_compose.rs @@ -1,19 +1,19 @@ use crate::command::supergraph::config::{self, SchemaSource, SupergraphConfig}; -use crate::utils::{client::StudioClientConfig, parsers::parse_graph_ref}; -use crate::{anyhow, command::RoverStdout, error::RoverError, Result, Suggestion}; +use crate::utils::client::StudioClientConfig; +use crate::{anyhow, command::RoverOutput, error::RoverError, Result, Suggestion}; -use ansi_term::Colour::Red; -use camino::Utf8PathBuf; +use rover_client::blocking::GraphQLClient; +use rover_client::operations::subgraph::fetch::{self, SubgraphFetchInput}; +use rover_client::operations::subgraph::introspect::{self, SubgraphIntrospectInput}; +use rover_client::shared::{BuildError, GraphRef}; +use rover_client::RoverClientError; -use rover_client::{ - blocking::GraphQLClient, - query::subgraph::{fetch, introspect}, -}; +use camino::Utf8PathBuf; +use harmonizer::ServiceDefinition as SubgraphDefinition; use serde::Serialize; -use std::{collections::HashMap, fs}; use structopt::StructOpt; -use harmonizer::ServiceDefinition as SubgraphDefinition; +use std::{collections::HashMap, fs, str::FromStr}; #[derive(Debug, Serialize, StructOpt)] pub struct Compose { @@ -29,7 +29,7 @@ pub struct Compose { } impl Compose { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let supergraph_config = config::parse_supergraph_config(&self.config_path)?; let subgraph_definitions = get_subgraph_definitions( supergraph_config, @@ -39,24 +39,20 @@ impl Compose { )?; match harmonizer::harmonize(subgraph_definitions) { - Ok(core_schema) => Ok(RoverStdout::CoreSchema(core_schema)), - Err(composition_errors) => { - let num_failures = composition_errors.len(); - for composition_error in composition_errors { - eprintln!("{} {}", Red.bold().paint("error:"), &composition_error) - } - match num_failures { - 0 => unreachable!("Composition somehow failed with no composition errors."), - 1 => Err( - anyhow!("Encountered 1 composition error while composing the graph.") - .into(), - ), - _ => Err(anyhow!( - "Encountered {} composition errors while composing the graph.", - num_failures - ) - .into()), + Ok(core_schema) => Ok(RoverOutput::CoreSchema(core_schema)), + Err(harmonizer_composition_errors) => { + let mut build_errors = Vec::with_capacity(harmonizer_composition_errors.len()); + for harmonizer_composition_error in harmonizer_composition_errors { + if let Some(message) = &harmonizer_composition_error.message { + build_errors.push(BuildError::composition_error( + message.to_string(), + Some(harmonizer_composition_error.code().to_string()), + )); + } } + Err(RoverError::new(RoverClientError::BuildErrors { + source: build_errors.into(), + })) } } } @@ -107,7 +103,12 @@ pub(crate) fn get_subgraph_definitions( client_config.get_reqwest_client(), )?; - let introspection_response = introspect::run(&client, &HashMap::new())?; + let introspection_response = introspect::run( + SubgraphIntrospectInput { + headers: HashMap::new(), + }, + &client, + )?; let schema = introspection_response.result; // We don't require a routing_url for this variant of a schema, @@ -120,18 +121,19 @@ pub(crate) fn get_subgraph_definitions( let subgraph_definition = SubgraphDefinition::new(subgraph_name, url, &schema); subgraphs.push(subgraph_definition); } - SchemaSource::Subgraph { graphref, subgraph } => { - // given a graphref and subgraph, run subgraph fetch to + SchemaSource::Subgraph { + graphref: graph_ref, + subgraph, + } => { + // given a graph_ref and subgraph, run subgraph fetch to // obtain SDL and add it to subgraph_definition. let client = client_config.get_authenticated_client(&profile_name)?; - let graphref = parse_graph_ref(graphref)?; - let schema = fetch::run( - fetch::fetch_subgraph_query::Variables { - graph_id: graphref.name.clone(), - variant: graphref.variant.clone(), + let result = fetch::run( + SubgraphFetchInput { + graph_ref: GraphRef::from_str(graph_ref)?, + subgraph: subgraph.clone(), }, &client, - subgraph, )?; // We don't require a routing_url for this variant of a schema, @@ -141,7 +143,8 @@ pub(crate) fn get_subgraph_definitions( // and use that when no routing_url is provided. let url = &subgraph_data.routing_url.clone().unwrap_or_default(); - let subgraph_definition = SubgraphDefinition::new(subgraph_name, url, &schema); + let subgraph_definition = + SubgraphDefinition::new(subgraph_name, url, &result.sdl.contents); subgraphs.push(subgraph_definition); } } diff --git a/src/command/supergraph/compose/no_compose.rs b/src/command/supergraph/compose/no_compose.rs index bc9c6904e..dea61423f 100644 --- a/src/command/supergraph/compose/no_compose.rs +++ b/src/command/supergraph/compose/no_compose.rs @@ -5,7 +5,7 @@ use structopt::StructOpt; use crate::utils::client::StudioClientConfig; use crate::{ anyhow, - command::RoverStdout, + command::RoverOutput, error::{RoverError, Suggestion}, Result, }; @@ -24,7 +24,7 @@ pub struct Compose { } impl Compose { - pub fn run(&self, _client_config: StudioClientConfig) -> Result { + pub fn run(&self, _client_config: StudioClientConfig) -> Result { let mut err = RoverError::new(anyhow!( "This version of Rover does not support this command." )); diff --git a/src/command/supergraph/fetch.rs b/src/command/supergraph/fetch.rs index 3bc84ded4..793af1f87 100644 --- a/src/command/supergraph/fetch.rs +++ b/src/command/supergraph/fetch.rs @@ -1,8 +1,8 @@ use crate::utils::client::StudioClientConfig; -use crate::utils::parsers::{parse_graph_ref, GraphRef}; -use crate::{command::RoverStdout, Result}; +use crate::{command::RoverOutput, Result}; -use rover_client::query::supergraph::fetch; +use rover_client::operations::supergraph::fetch::{self, SupergraphFetchInput}; +use rover_client::shared::GraphRef; use ansi_term::Colour::{Cyan, Yellow}; use serde::Serialize; @@ -12,7 +12,7 @@ use structopt::StructOpt; pub struct Fetch { /// @ of graph in Apollo Studio to fetch from. /// @ may be left off, defaulting to @current - #[structopt(name = "GRAPH_REF", parse(try_from_str = parse_graph_ref))] + #[structopt(name = "GRAPH_REF")] #[serde(skip_serializing)] graph: GraphRef, @@ -23,7 +23,7 @@ pub struct Fetch { } impl Fetch { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { let client = client_config.get_authenticated_client(&self.profile_name)?; let graph_ref = self.graph.to_string(); eprintln!( @@ -32,14 +32,13 @@ impl Fetch { Yellow.normal().paint(&self.profile_name) ); - let supergraph_sdl = fetch::run( - fetch::fetch_supergraph_query::Variables { - graph_id: self.graph.name.clone(), - variant: self.graph.variant.clone(), + let fetch_response = fetch::run( + SupergraphFetchInput { + graph_ref: self.graph.clone(), }, &client, )?; - Ok(RoverStdout::SupergraphSdl(supergraph_sdl)) + Ok(RoverOutput::FetchResponse(fetch_response)) } } diff --git a/src/command/supergraph/mod.rs b/src/command/supergraph/mod.rs index 8b575b5fa..51e2141a3 100644 --- a/src/command/supergraph/mod.rs +++ b/src/command/supergraph/mod.rs @@ -5,7 +5,7 @@ mod fetch; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::utils::client::StudioClientConfig; use crate::Result; @@ -26,7 +26,7 @@ pub enum Command { } impl Supergraph { - pub fn run(&self, client_config: StudioClientConfig) -> Result { + pub fn run(&self, client_config: StudioClientConfig) -> Result { match &self.command { Command::Fetch(command) => command.run(client_config), Command::Compose(command) => command.run(client_config), diff --git a/src/command/update/check.rs b/src/command/update/check.rs index f4fe31ebd..3b73ab4ad 100644 --- a/src/command/update/check.rs +++ b/src/command/update/check.rs @@ -2,7 +2,7 @@ use reqwest::blocking::Client; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::{utils::version, Result}; use houston as config; @@ -13,8 +13,8 @@ pub struct Check { } impl Check { - pub fn run(&self, config: config::Config, client: Client) -> Result { + pub fn run(&self, config: config::Config, client: Client) -> Result { version::check_for_update(config, true, client)?; - Ok(RoverStdout::None) + Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/update/mod.rs b/src/command/update/mod.rs index 9296ba172..8964ff90e 100644 --- a/src/command/update/mod.rs +++ b/src/command/update/mod.rs @@ -4,7 +4,7 @@ use reqwest::blocking::Client; use serde::Serialize; use structopt::StructOpt; -use crate::command::RoverStdout; +use crate::command::RoverOutput; use crate::Result; use houston as config; @@ -22,7 +22,7 @@ pub enum Command { } impl Update { - pub fn run(&self, config: config::Config, client: Client) -> Result { + pub fn run(&self, config: config::Config, client: Client) -> Result { match &self.command { Command::Check(command) => command.run(config, client), } diff --git a/src/error/metadata/code.rs b/src/error/metadata/code.rs index 07e09efb7..86782c40b 100644 --- a/src/error/metadata/code.rs +++ b/src/error/metadata/code.rs @@ -34,6 +34,8 @@ pub enum Code { E026, E027, E028, + E029, + E030, } impl Display for Code { @@ -75,6 +77,8 @@ impl Code { (Code::E026, include_str!("./codes/E026.md").to_string()), (Code::E027, include_str!("./codes/E027.md").to_string()), (Code::E028, include_str!("./codes/E028.md").to_string()), + (Code::E029, include_str!("./codes/E029.md").to_string()), + (Code::E030, include_str!("./codes/E030.md").to_string()), ]; contents.into_iter().collect() } @@ -84,8 +88,8 @@ impl Code { pub fn explain(&self) -> String { let all_explanations = Code::explanations(); let explanation = all_explanations.get(self); - if let Some(expl) = explanation { - format!("**{}**\n\n{}\n\n", self.to_string(), expl.clone()) + if let Some(explanation) = explanation { + format!("**{}**\n\n{}\n\n", &self, &explanation) } else { "Explanation not available".to_string() } diff --git a/src/error/metadata/codes/E028.md b/src/error/metadata/codes/E028.md index 52b1dd800..c31e8ea2f 100644 --- a/src/error/metadata/codes/E028.md +++ b/src/error/metadata/codes/E028.md @@ -1,3 +1,3 @@ This error occurs when Rover could not connect to an HTTP endpoint. -If you encountered this error while running introspection, you'll want to make sure that you typed the endpoint correctly, your Internet connection is stable, and that your server is responding to requests. You may wish to run the command again with `--log=debug`. +If you encountered this error while running introspection, you'll want to make sure that you typed the endpoint correctly, your Internet connection is stable, and that your server is responding to requests. You may wish to run the command again with `--log=debug`. \ No newline at end of file diff --git a/src/error/metadata/codes/E029.md b/src/error/metadata/codes/E029.md new file mode 100644 index 000000000..195333e5f --- /dev/null +++ b/src/error/metadata/codes/E029.md @@ -0,0 +1,5 @@ +This error occurs when you propose a subgraph schema that could not be built. + +There are many reasons why you may run into build errors. This error should include information about _why_ the proposed subgraph schema could not be composed. Error code references can be found [here](https://www.apollographql.com/docs/federation/errors/). + +Some build errors are part of normal workflows. For instance, you may need to publish a subgraph that does not compose if you are trying to [migrate an entity or field](https://www.apollographql.com/docs/federation/entities/#migrating-entities-and-fields-advanced). diff --git a/src/error/metadata/codes/E030.md b/src/error/metadata/codes/E030.md new file mode 100644 index 000000000..b761379ac --- /dev/null +++ b/src/error/metadata/codes/E030.md @@ -0,0 +1 @@ +This error occurs when an operation check fails. This means that you proposed a schema that would break operations in use by existing clients. You can configure this behavior in the Checks -> Configuration view in [Apollo Studio](https://studio.apollographql.com/), and you can read more about client checks [here](https://www.apollographql.com/docs/studio/schema-checks/). \ No newline at end of file diff --git a/src/error/metadata/mod.rs b/src/error/metadata/mod.rs index 8bc0d67ad..39887b477 100644 --- a/src/error/metadata/mod.rs +++ b/src/error/metadata/mod.rs @@ -11,15 +11,19 @@ use crate::utils::env::RoverEnvKey; use std::{env, fmt::Display}; -use ansi_term::Colour::Red; +use serde::Serialize; /// Metadata contains extra information about specific errors /// Currently this includes an optional error `Code` /// and an optional `Suggestion` -#[derive(Default, Debug)] +#[derive(Default, Serialize, Debug)] pub struct Metadata { + // skip serializing for now until we can appropriately strip color codes + #[serde(skip_serializing)] pub suggestion: Option, pub code: Option, + + #[serde(skip_serializing)] pub is_parse_error: bool, } @@ -58,11 +62,34 @@ impl From<&mut anyhow::Error> for Metadata { RoverClientError::InvalidSeverity => { (Some(Suggestion::SubmitIssue), Some(Code::E006)) } + RoverClientError::SubgraphBuildErrors { + graph_ref, + subgraph, + source: _, + } => ( + Some(Suggestion::FixSubgraphSchema { + graph_ref: graph_ref.clone(), + subgraph: subgraph.clone(), + }), + Some(Code::E029), + ), + RoverClientError::BuildErrors { .. } => { + (Some(Suggestion::FixCompositionErrors), Some(Code::E029)) + } + RoverClientError::OperationCheckFailure { + graph_ref, + check_response: _, + } => ( + Some(Suggestion::FixOperationsInSchema { + graph_ref: graph_ref.clone(), + }), + Some(Code::E030), + ), RoverClientError::SubgraphIntrospectionNotAvailable => { (Some(Suggestion::UseFederatedGraph), Some(Code::E007)) } RoverClientError::ExpectedFederatedGraph { - graph: _, + graph_ref: _, can_operation_convert, } => { if *can_operation_convert { @@ -72,14 +99,12 @@ impl From<&mut anyhow::Error> for Metadata { } } RoverClientError::NoSchemaForVariant { - graph, - invalid_variant, + graph_ref, valid_variants, frontend_url_root, } => ( Some(Suggestion::ProvideValidVariant { - graph_name: graph.clone(), - invalid_variant: invalid_variant.clone(), + graph_ref: graph_ref.clone(), valid_variants: valid_variants.clone(), frontend_url_root: frontend_url_root.clone(), }), @@ -92,30 +117,32 @@ impl From<&mut anyhow::Error> for Metadata { Some(Suggestion::ProvideValidSubgraph(valid_subgraphs.clone())), Some(Code::E009), ), - RoverClientError::NoService { graph: _ } => { + RoverClientError::GraphNotFound { .. } => { (Some(Suggestion::CheckGraphNameAndAuth), Some(Code::E010)) } - RoverClientError::GraphQl { msg: _ } => (None, None), - RoverClientError::IntrospectionError { msg: _ } => (None, Some(Code::E011)), - RoverClientError::ClientError { msg: _ } => (None, Some(Code::E012)), + RoverClientError::GraphQl { .. } => (None, None), + RoverClientError::IntrospectionError { .. } => (None, Some(Code::E011)), + RoverClientError::ClientError { .. } => (None, Some(Code::E012)), RoverClientError::InvalidKey => (Some(Suggestion::CheckKey), Some(Code::E013)), RoverClientError::MalformedKey => (Some(Suggestion::ProperKey), Some(Code::E014)), - RoverClientError::UnparseableReleaseVersion => { + RoverClientError::UnparseableReleaseVersion { source: _ } => { (Some(Suggestion::SubmitIssue), Some(Code::E015)) } - RoverClientError::NoCompositionPublishes { - graph: _, - composition_errors, - } => { - for composition_error in composition_errors { - eprintln!("{} {}", Red.bold().paint("error:"), composition_error); - } + RoverClientError::BadReleaseUrl => (Some(Suggestion::SubmitIssue), None), + RoverClientError::NoSupergraphBuilds { .. } => { (Some(Suggestion::RunComposition), Some(Code::E027)) } - RoverClientError::AdhocError { msg: _ } => (None, None), + RoverClientError::AdhocError { .. } => (None, None), RoverClientError::CouldNotConnect { .. } => { (Some(Suggestion::CheckServerConnection), Some(Code::E028)) } + RoverClientError::InvalidGraphRef { .. } => { + unreachable!("Graph ref parse errors should be caught via structopt") + } + RoverClientError::InvalidValidationPeriodDuration(_) + | RoverClientError::ValidationPeriodTooGranular => { + unreachable!("Validation period parse errors should be caught via structopt") + } }; return Metadata { suggestion, diff --git a/src/error/metadata/suggestion.rs b/src/error/metadata/suggestion.rs index 36a234eb4..506166681 100644 --- a/src/error/metadata/suggestion.rs +++ b/src/error/metadata/suggestion.rs @@ -2,11 +2,14 @@ use std::cmp::Ordering; use std::fmt::{self, Display}; use ansi_term::Colour::{Cyan, Yellow}; +use rover_client::shared::GraphRef; use crate::utils::env::RoverEnvKey; +use serde::Serialize; + /// `Suggestion` contains possible suggestions for remedying specific errors. -#[derive(Debug)] +#[derive(Serialize, Debug)] pub enum Suggestion { SubmitIssue, SetConfigHome, @@ -18,8 +21,7 @@ pub enum Suggestion { CheckGraphNameAndAuth, ProvideValidSubgraph(Vec), ProvideValidVariant { - graph_name: String, - invalid_variant: String, + graph_ref: GraphRef, valid_variants: Vec, frontend_url_root: String, }, @@ -32,6 +34,14 @@ pub enum Suggestion { CheckServerConnection, ConvertGraphToSubgraph, CheckGnuVersion, + FixSubgraphSchema { + graph_ref: GraphRef, + subgraph: String, + }, + FixCompositionErrors, + FixOperationsInSchema { + graph_ref: GraphRef, + }, } impl Display for Suggestion { @@ -65,7 +75,7 @@ impl Display for Suggestion { ) } Suggestion::RunComposition => { - format!("Try resolving the composition errors in your subgraph(s), and publish them with the {} command.", Yellow.normal().paint("`rover subgraph publish`")) + format!("Try resolving the build errors in your subgraph(s), and publish them with the {} command.", Yellow.normal().paint("`rover subgraph publish`")) } Suggestion::UseFederatedGraph => { "Try running the command on a valid federated graph, or use the appropriate `rover graph` command instead of `rover subgraph`.".to_string() @@ -79,15 +89,15 @@ impl Display for Suggestion { valid_subgraphs.join(", ") ) } - Suggestion::ProvideValidVariant { graph_name, invalid_variant, valid_variants, frontend_url_root} => { - if let Some(maybe_variant) = did_you_mean(invalid_variant, valid_variants).pop() { - format!("Did you mean \"{}@{}\"?", graph_name, maybe_variant) + Suggestion::ProvideValidVariant { graph_ref, valid_variants, frontend_url_root} => { + if let Some(maybe_variant) = did_you_mean(&graph_ref.variant, valid_variants).pop() { + format!("Did you mean \"{}@{}\"?", graph_ref.name, maybe_variant) } else { let num_valid_variants = valid_variants.len(); match num_valid_variants { - 0 => unreachable!(&format!("Graph \"{}\" exists but has no variants.", graph_name)), - 1 => format!("The only existing variant for graph \"{}\" is \"{}\".", graph_name, valid_variants[0]), - 2 => format!("The existing variants for graph \"{}\" are \"{}\" and \"{}\".", graph_name, valid_variants[0], valid_variants[1]), + 0 => unreachable!(&format!("Graph \"{}\" exists but has no variants.", graph_ref.name)), + 1 => format!("The only existing variant for graph \"{}\" is \"{}\".", graph_ref.name, valid_variants[0]), + 2 => format!("The existing variants for graph \"{}\" are \"{}\" and \"{}\".", graph_ref.name, valid_variants[0], valid_variants[1]), 3 ..= 10 => { let mut valid_variants_msg = "".to_string(); for (i, variant) in valid_variants.iter().enumerate() { @@ -99,11 +109,11 @@ impl Display for Suggestion { valid_variants_msg.push_str(", "); } } - format!("The existing variants for graph \"{}\" are {}.", graph_name, &valid_variants_msg) + format!("The existing variants for graph \"{}\" are {}.", &graph_ref.name, &valid_variants_msg) } _ => { - let graph_url = format!("{}/graph/{}/settings", &frontend_url_root, &graph_name); - format!("You can view the variants for graph \"{}\" by visiting {}", graph_name, Cyan.normal().paint(&graph_url)) + let graph_url = format!("{}/graph/{}/settings", &frontend_url_root, &graph_ref.name); + format!("You can view the variants for graph \"{}\" by visiting {}", graph_ref.name, Cyan.normal().paint(&graph_url)) } } } @@ -129,6 +139,9 @@ impl Display for Suggestion { Suggestion::CheckServerConnection => "Make sure the endpoint is accepting connections and is spelled correctly".to_string(), Suggestion::ConvertGraphToSubgraph => "If you are sure you want to convert a non-federated graph to a subgraph, you can re-run the same command with a `--convert` flag.".to_string(), Suggestion::CheckGnuVersion => "This is likely an issue with your current version of `glibc`. Try running `ldd --version`, and if the version >= 2.18, we suggest installing the Rover binary built for `x86_64-unknown-linux-gnu`".to_string(), + Suggestion::FixSubgraphSchema { graph_ref, subgraph } => format!("The changes in the schema you proposed for subgraph {} are incompatible with supergraph {}. See {} for more information on resolving build errors.", Yellow.normal().paint(subgraph.to_string()), Yellow.normal().paint(graph_ref.to_string()), Cyan.normal().paint("https://www.apollographql.com/docs/federation/errors/")), + Suggestion::FixCompositionErrors => format!("The subgraph schemas you provided are incompatible with each other. See {} for more information on resolving build errors.", Cyan.normal().paint("https://www.apollographql.com/docs/federation/errors/")), + Suggestion::FixOperationsInSchema { graph_ref } => format!("The changes in the schema you proposed are incompatible with graph {}. See {} for more information on resolving operation check errors.", Yellow.normal().paint(graph_ref.to_string()), Cyan.normal().paint("https://www.apollographql.com/docs/studio/schema-checks/")) }; write!(formatter, "{}", &suggestion) } diff --git a/src/error/mod.rs b/src/error/mod.rs index 1da93d72b..dde8b47a1 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -6,21 +6,64 @@ pub(crate) use metadata::Metadata; pub type Result = std::result::Result; use ansi_term::Colour::{Cyan, Red}; +use rover_client::RoverClientError; +use serde::ser::SerializeStruct; +use serde::{Serialize, Serializer}; +use serde_json::{json, Value}; use std::borrow::BorrowMut; +use std::error::Error; use std::fmt::{self, Debug, Display}; pub use self::metadata::Suggestion; +use rover_client::shared::BuildErrors; + /// A specialized `Error` type for Rover that wraps `anyhow` /// and provides some extra `Metadata` for end users depending /// on the specific error they encountered. -#[derive(Debug)] +#[derive(Serialize, Debug)] pub struct RoverError { + #[serde(flatten, serialize_with = "serialize_anyhow")] error: anyhow::Error, + + #[serde(flatten)] metadata: Metadata, } +#[derive(Serialize, Debug)] +#[serde(rename_all = "snake_case")] +enum RoverDetails { + BuildErrors(BuildErrors), +} + +fn serialize_anyhow(error: &anyhow::Error, serializer: S) -> std::result::Result +where + S: Serializer, +{ + let top_level_struct = "error"; + let message_field_name = "message"; + let details_struct = "details"; + + if let Some(rover_client_error) = error.downcast_ref::() { + if let Some(rover_client_error_source) = rover_client_error.source() { + if let Some(build_errors) = rover_client_error_source.downcast_ref::() { + let mut top_level_data = serializer.serialize_struct(top_level_struct, 2)?; + top_level_data.serialize_field(message_field_name, &error.to_string())?; + top_level_data.serialize_field( + details_struct, + &RoverDetails::BuildErrors(build_errors.clone()), + )?; + return top_level_data.end(); + } + } + } + + let mut data = serializer.serialize_struct(top_level_struct, 1)?; + data.serialize_field(message_field_name, &error.to_string())?; + data.end() +} + impl RoverError { pub fn new(error: E) -> Self where @@ -49,6 +92,33 @@ impl RoverError { pub fn suggestion(&mut self) -> &Option { &self.metadata.suggestion } + + pub fn print(&self) { + if let Some(RoverClientError::OperationCheckFailure { + graph_ref: _, + check_response, + }) = self.error.downcast_ref::() + { + println!("{}", check_response.get_table()); + } + + eprintln!("{}", self); + } + + pub(crate) fn get_internal_data_json(&self) -> Value { + if let Some(RoverClientError::OperationCheckFailure { + graph_ref: _, + check_response, + }) = self.error.downcast_ref::() + { + return check_response.get_json(); + } + Value::Null + } + + pub(crate) fn get_internal_error_json(&self) -> Value { + json!(self) + } } impl Display for RoverError { @@ -64,7 +134,7 @@ impl Display for RoverError { "error:".to_string() }; let error_descriptor = Red.bold().paint(&error_descriptor_message); - writeln!(formatter, "{} {}", error_descriptor, &self.error)?; + writeln!(formatter, "{} {:?}", error_descriptor, &self.error)?; error_descriptor_message }; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 989333fed..61b7c1331 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,5 @@ pub mod client; pub mod env; -pub mod git; pub mod loaders; pub mod parsers; pub mod pkg; diff --git a/src/utils/parsers.rs b/src/utils/parsers.rs index 0f6c31024..492e484be 100644 --- a/src/utils/parsers.rs +++ b/src/utils/parsers.rs @@ -1,8 +1,4 @@ use camino::Utf8PathBuf; -use regex::Regex; -use serde::Serialize; - -use std::{convert::TryInto, fmt}; use crate::{error::RoverError, Result}; @@ -25,84 +21,6 @@ pub fn parse_schema_source(loc: &str) -> Result { } } -#[derive(Debug, Clone, PartialEq)] -pub struct GraphRef { - pub name: String, - pub variant: String, -} - -impl fmt::Display for GraphRef { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}@{}", self.name, self.variant) - } -} - -/// NOTE: THIS IS A TEMPORARY SOLUTION. IN THE FUTURE, ALL GRAPH ID PARSING -/// WILL HAPPEN IN THE BACKEND TO KEEP EVERYTHING CONSISTENT. THIS IS AN -/// INCOMPLETE PLACEHOLDER, AND MAY NOT COVER EVERY SINGLE VALID USE CASE -pub fn parse_graph_ref(graph_id: &str) -> Result { - let pattern = Regex::new(r"^[a-zA-Z][a-zA-Z0-9_-]{0,63}$").unwrap(); - let variant_pattern = Regex::new(r"^([a-zA-Z][a-zA-Z0-9_-]{0,63})@(.{0,63})$").unwrap(); - - let valid_graph_name_only = pattern.is_match(graph_id); - let valid_graph_with_variant = variant_pattern.is_match(graph_id); - - if valid_graph_name_only { - Ok(GraphRef { - name: graph_id.to_string(), - variant: "current".to_string(), - }) - } else if valid_graph_with_variant { - let matches = variant_pattern.captures(graph_id).unwrap(); - let name = matches.get(1).unwrap().as_str(); - let variant = matches.get(2).unwrap().as_str(); - Ok(GraphRef { - name: name.to_string(), - variant: variant.to_string(), - }) - } else { - Err(RoverError::parse_error("Graph IDs must be in the format or @, where can only contain letters, numbers, or the characters `-` or `_`, and must be 64 characters or less. must be 64 characters or less.")) - } -} - -#[derive(Debug, Serialize, Default, Clone)] -pub struct ValidationPeriod { - // these timestamps could be represented as i64, but the API expects - // Option - pub from: Option, - pub to: Option, -} - -// Validation period is parsed as human readable time. -// such as "10m 50s" -pub fn parse_validation_period(period: &str) -> Result { - // attempt to parse strings like - // 15h 10m 2s into number of seconds - if period.contains("ns") || period.contains("us") || period.contains("ms") { - return Err(RoverError::parse_error( - "You can only specify a duration as granular as seconds.", - )); - }; - let duration = humantime::parse_duration(period).map_err(RoverError::parse_error)?; - let window: i64 = duration - .as_secs() - .try_into() - .map_err(RoverError::parse_error)?; - - if window > 0 { - Ok(ValidationPeriod { - // search "from" a negative time window - from: Some(format!("{}", -window)), - // search "to" now (-0) seconds - to: Some("-0".to_string()), - }) - } else { - Err(RoverError::parse_error( - "The number of seconds must be a positive integer.", - )) - } -} - pub fn parse_query_count_threshold(threshold: &str) -> Result { let threshold = threshold.parse::()?; if threshold < 1 { @@ -140,7 +58,7 @@ pub fn parse_header(header: &str) -> Result<(String, String)> { #[cfg(test)] mod tests { - use super::{parse_graph_ref, parse_schema_source, GraphRef, SchemaSource}; + use super::{parse_schema_source, SchemaSource}; #[test] fn it_correctly_parses_stdin_flag() { @@ -163,52 +81,4 @@ mod tests { let loc = parse_schema_source(""); assert!(loc.is_err()); } - - #[test] - fn parse_graph_ref_works() { - assert!(parse_graph_ref("engine#%^").is_err()); - assert!(parse_graph_ref( - "1234567890123456789012345678901234567890123456789012345678901234567890" - ) - .is_err()); - assert!(parse_graph_ref("1boi").is_err()); - assert!(parse_graph_ref("_eng").is_err()); - assert!(parse_graph_ref( - "engine@1234567890123456789012345678901234567890123456789012345678901234567890" - ) - .is_err()); - assert!(parse_graph_ref( - "engine1234567890123456789012345678901234567890123456789012345678901234567890@prod" - ) - .is_err()); - - assert_eq!( - parse_graph_ref("engine@okay").unwrap(), - GraphRef { - name: "engine".to_string(), - variant: "okay".to_string() - } - ); - assert_eq!( - parse_graph_ref("studio").unwrap(), - GraphRef { - name: "studio".to_string(), - variant: "current".to_string() - } - ); - assert_eq!( - parse_graph_ref("this_should_work").unwrap(), - GraphRef { - name: "this_should_work".to_string(), - variant: "current".to_string() - } - ); - assert_eq!( - parse_graph_ref("it-is-cool@my-special/variant:from$hell").unwrap(), - GraphRef { - name: "it-is-cool".to_string(), - variant: "my-special/variant:from$hell".to_string() - } - ); - } } diff --git a/src/utils/stringify.rs b/src/utils/stringify.rs index 6bd57471e..3d45e594c 100644 --- a/src/utils/stringify.rs +++ b/src/utils/stringify.rs @@ -2,20 +2,30 @@ //! a struct with the `Display`/`FromStr` implementations //! if it does not implement `Serialize`/`Deserialize` //! code taken from this: https://github.com/serde-rs/serde/issues/1316 -//! and can be used by annotating a field with -//! #[serde(serialize_with = "from_display")] +//! and can be used by annotating a field with either +//! #[serde(serialize_with = "from_display")] or +//! #[serde(serialize_with = "option_from_display")] +//! depending on if the type you're serializing is nested in an Option use std::fmt::Display; use serde::Serializer; -pub fn from_display(value: &Option, serializer: S) -> Result +pub fn option_from_display(value: &Option, serializer: S) -> Result where T: Display, S: Serializer, { if let Some(value) = value { - serializer.collect_str(value) + from_display(value, serializer) } else { serializer.serialize_none() } } + +pub fn from_display(value: &T, serializer: S) -> Result +where + T: Display, + S: Serializer, +{ + serializer.collect_str(value) +} diff --git a/src/utils/table.rs b/src/utils/table.rs index c19ad7778..1b098c1ec 100644 --- a/src/utils/table.rs +++ b/src/utils/table.rs @@ -1,9 +1,16 @@ -use prettytable::{format::consts::FORMAT_BOX_CHARS, Table}; +use prettytable::{ + format::{consts::FORMAT_BOX_CHARS, TableFormat}, + Table, +}; pub use prettytable::{cell, row}; pub fn get_table() -> Table { let mut table = Table::new(); - table.set_format(*FORMAT_BOX_CHARS); + table.set_format(get_table_format()); table } + +pub fn get_table_format() -> TableFormat { + *FORMAT_BOX_CHARS +} diff --git a/src/utils/version.rs b/src/utils/version.rs index 8ac32c7a8..9153c0b91 100644 --- a/src/utils/version.rs +++ b/src/utils/version.rs @@ -4,11 +4,10 @@ use ansi_term::Colour::{Cyan, Yellow}; use billboard::{Alignment, Billboard}; use camino::Utf8PathBuf; use reqwest::blocking::Client; -use semver::Version; use crate::{Result, PKG_VERSION}; use houston as config; -use rover_client::releases::get_latest_release; +use rover_client::releases::{get_latest_release, Version}; const ONE_HOUR: u64 = 60 * 60; const ONE_DAY: u64 = ONE_HOUR * 24; @@ -55,12 +54,11 @@ fn do_update_check( should_output_if_updated: bool, client: Client, ) -> Result<()> { - let latest = get_latest_release(client)?; - let pretty_latest = Cyan.normal().paint(format!("v{}", latest)); - let update_available = is_latest_newer(&latest, PKG_VERSION)?; - if update_available { + let latest_version = get_latest_release(client)?; + let pretty_latest = Cyan.normal().paint(format!("v{}", latest_version)); + if latest_version > Version::parse(PKG_VERSION)? { let message = format!( - "There is a newer version of Rover available: {} (currently running v{})\n\nFor instructions on how to install, run {}", + "There is a newer version of Rover available: {} (currently running v{})\n\nFor instructions on how to install, run {}", &pretty_latest, PKG_VERSION, Yellow.normal().paint("`rover docs open start`") @@ -95,9 +93,3 @@ fn get_last_checked_time_from_disk(version_file: &Utf8PathBuf) -> Option Result { - let latest = Version::parse(latest)?; - let running = Version::parse(running)?; - Ok(latest > running) -} diff --git a/xtask/src/commands/lint.rs b/xtask/src/commands/lint.rs index a82506b29..08adea188 100644 --- a/xtask/src/commands/lint.rs +++ b/xtask/src/commands/lint.rs @@ -1,7 +1,7 @@ use anyhow::Result; use structopt::StructOpt; -use crate::tools::CargoRunner; +use crate::tools::{CargoRunner, NpmRunner}; #[derive(Debug, StructOpt)] pub struct Lint {} @@ -10,6 +10,8 @@ impl Lint { pub fn run(&self, verbose: bool) -> Result<()> { let cargo_runner = CargoRunner::new(verbose)?; cargo_runner.lint()?; + let npm_runner = NpmRunner::new(verbose)?; + npm_runner.lint()?; Ok(()) } } diff --git a/xtask/src/tools/npm.rs b/xtask/src/tools/npm.rs index a5a3bb239..2d57b0ff3 100644 --- a/xtask/src/tools/npm.rs +++ b/xtask/src/tools/npm.rs @@ -10,25 +10,37 @@ use crate::{ pub(crate) struct NpmRunner { runner: Runner, - npm_package_directory: Utf8PathBuf, + npm_installer_package_directory: Utf8PathBuf, + npm_lint_directory: Utf8PathBuf, } impl NpmRunner { pub(crate) fn new(verbose: bool) -> Result { let runner = Runner::new("npm", verbose)?; - let npm_package_directory = utils::project_root()?.join("installers").join("npm"); - - if npm_package_directory.exists() { - Ok(Self { - runner, - npm_package_directory, - }) - } else { - Err(anyhow!( + let project_root = utils::project_root()?; + + let npm_lint_directory = project_root.join("crates").join("rover-client"); + let npm_installer_package_directory = project_root.join("installers").join("npm"); + + if !npm_installer_package_directory.exists() { + return Err(anyhow!( "Rover's npm installer package does not seem to be located here:\n{}", - &npm_package_directory - )) + &npm_installer_package_directory + )); + } + + if !npm_lint_directory.exists() { + return Err(anyhow!( + "Rover's GraphQL linter package does not seem to be located here:\n{}", + &npm_lint_directory + )); } + + Ok(Self { + runner, + npm_installer_package_directory, + npm_lint_directory, + }) } /// prepares our npm installer package for release @@ -51,32 +63,47 @@ impl NpmRunner { Ok(()) } + pub(crate) fn lint(&self) -> Result<()> { + self.npm_exec(&["install"], &self.npm_lint_directory)?; + self.npm_exec(&["run", "lint"], &self.npm_lint_directory)?; + Ok(()) + } + fn update_dependency_tree(&self) -> Result<()> { - self.npm_exec(&["update"])?; + self.npm_exec(&["update"], &self.npm_installer_package_directory)?; Ok(()) } fn install_dependencies(&self) -> Result<()> { // we --ignore-scripts so that we do not attempt to download and unpack a // released rover tarball - self.npm_exec(&["install", "--ignore-scripts"])?; + self.npm_exec( + &["install", "--ignore-scripts"], + &self.npm_installer_package_directory, + )?; Ok(()) } fn update_version(&self) -> Result<()> { - self.npm_exec(&["version", &PKG_VERSION, "--allow-same-version"])?; + self.npm_exec( + &["version", &PKG_VERSION, "--allow-same-version"], + &self.npm_installer_package_directory, + )?; Ok(()) } fn publish_dry_run(&self) -> Result<()> { - let command_output = self.npm_exec(&["publish", "--dry-run"])?; + let command_output = self.npm_exec( + &["publish", "--dry-run"], + &self.npm_installer_package_directory, + )?; assert_publish_includes(&command_output) .with_context(|| "There were problems with the output of 'npm publish --dry-run'.") } - fn npm_exec(&self, args: &[&str]) -> Result { - self.runner.exec(args, &self.npm_package_directory, None) + fn npm_exec(&self, args: &[&str], directory: &Utf8PathBuf) -> Result { + self.runner.exec(args, &directory, None) } }