feat(api-explorer): add api graphql command group for GraphQL-backed domains - #200
Conversation
There was a problem hiding this comment.
Pull request overview
Adds first-class support for GraphQL-backed API domains in the API Explorer by introducing a dedicated api graphql command group, enriching the embedded API catalog with GraphQL operation/type metadata + SDL, and redirecting REST-shaped commands when given GraphQL operation IDs.
Changes:
- Introduces
gddy api graphql(get,call,type get,sdl get) for inspecting and executing GraphQL operations. - Extends catalog generation/summary to include GraphQL operation descriptions, named type details, and raw SDL text.
- Updates
api operation list/api searchto surface per-field GraphQL query/mutation rows (tagged withkind) and adds redirects from REST commands to GraphQL equivalents.
Reviewed changes
Copilot reviewed 18 out of 21 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| rust/tools/generate-api-catalog/src/openapi.rs | Simplifies GraphQL schema caching/cloning when attaching schema metadata to endpoints. |
| rust/tools/generate-api-catalog/src/graphql.rs | Switches GraphQL parsing to async-graphql-parser and emits operations + types + SDL. |
| rust/tools/generate-api-catalog/Cargo.toml | Replaces graphql-parser with async-graphql-parser/async-graphql-value. |
| rust/src/api_explorer/summary.rs | Includes GraphQL descriptions and type details in JSON summaries (+ tests). |
| rust/src/api_explorer/search.rs | Adds GraphQL sub-operation rows to search output and suggests GraphQL follow-ups. |
| rust/src/api_explorer/parameter.rs | Redirects parameter commands when given a GraphQL operation id. |
| rust/src/api_explorer/operation.rs | Lists GraphQL sub-operations per domain and redirects operation get for GraphQL ids. |
| rust/src/api_explorer/mod.rs | Registers the new api graphql command group. |
| rust/src/api_explorer/http.rs | Factors shared request/response handling into reusable helpers for REST + GraphQL calls. |
| rust/src/api_explorer/guides/api-explorer.md | Documents GraphQL operation workflows and the new command surface. |
| rust/src/api_explorer/graphql/type_cmd.rs | Adds api graphql type get to inspect named object/input/enum types. |
| rust/src/api_explorer/graphql/sdl.rs | Adds api graphql sdl get to output the raw SDL for a GraphQL wrapper operation. |
| rust/src/api_explorer/graphql/mod.rs | Wires the api graphql group and help text. |
| rust/src/api_explorer/graphql/get.rs | Implements api graphql get to show operation details + return type “shape”. |
| rust/src/api_explorer/graphql/call.rs | Implements api graphql call with --arg synthesis and --select selection building. |
| rust/src/api_explorer/catalog.rs | Adds GraphQL operation-id parsing/resolution, type lookup helpers, and schema fields. |
| rust/src/api_explorer/call.rs | Redirects api call when a GraphQL operation id is provided instead of a path. |
| rust/schemas/api/taxes.json | Regenerates embedded taxes schema with SDL + GraphQL type metadata. |
| rust/Cargo.toml | Bumps cli-engine to 0.8.4. |
| rust/Cargo.lock | Locks new GraphQL parser deps and the cli-engine bump. |
Suppressed comments (1)
rust/src/api_explorer/graphql/mod.rs:25
- There’s an extra space in the help text ("api operation get"), which will show up in
--helpoutput.
`api graphql call <id>` to execute it — `api operation get`/\
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 21 changed files in this pull request and generated no new comments.
Suppressed comments (1)
rust/src/api_explorer/http.rs:81
parsed_extra_headerswill accept inputs like ":" / " :value" becausesplit_headeronly checks for a colon. That then fails later duringreqwest::RequestBuilder::build()with a less-actionable "invalid header name" style error instead of the intended "expected KEY:VALUE" validation. Reject empty header keys up front so the CLI returns the consistent, user-focused validation error.
/// Parses repeatable `--header KEY:VALUE` flags into a validated list.
pub(super) fn parsed_extra_headers(
raw: &[String],
) -> Result<Vec<(String, String)>, cli_engine::CliCoreError> {
raw.iter()
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 21 changed files in this pull request and generated no new comments.
Suppressed comments (2)
rust/src/api_explorer/http.rs:97
send_and_reportcentralizes most of the HTTP status/GraphQL-error/403/non-2xx behavior, but there are no tests that exercise these branches (current tests in this module only cover header parsing / body parsing). This makes regressions in the shared error-handling logic harder to catch.
Consider adding a small set of tests using a local mock HTTP server (or factoring the response-handling portion into a pure helper that can be unit-tested) to cover: GraphQL errors in a 200, 403 with required scopes, and generic non-2xx with body truncation.
#[allow(clippy::too_many_arguments)]
pub(super) async fn send_and_report(
client: &reqwest::Client,
parsed_method: reqwest::Method,
method: &str,
rust/src/api_explorer/graphql/call.rs:279
- For list-typed GraphQL variables, this branch claims the value is “always JSON-parsed”, but on parse failure it silently falls back to a JSON string. That contradicts the comment and can lead to confusing server-side type errors (especially for list-of-object inputs), instead of an actionable CLI validation error.
Either make list inputs strict (fail fast with a message like “expected JSON array for list type”) or update the comment/behavior to clearly document the accepted non-JSON forms.
fn coerce_graphql_arg(raw: &str, gql_type: &str) -> Value {
if gql_type.contains('[') {
return serde_json::from_str(raw).unwrap_or_else(|_| json!(raw));
}
match graphql_base_type_name(gql_type) {
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 21 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
rust/src/api_explorer/catalog.rs:521
find_graphql_typereturns the first matching type name across all GraphQL schemas. Type names are not globally unique (e.g. bothtaxes.jsonandcatalog-products.jsoncontainPageInfoandOrderByDirectionEnum), soapi graphql type get <TypeName>can return a type from the wrong domain/schema and mislead--selectdrilling.
/// Looks up a named GraphQL type (object/input/enum) by its bare name across
/// every loaded GraphQL schema — the standalone counterpart to
/// [`graphql_resolve_type`] for `api graphql type get`, which has no
/// operation to scope the search to. First match wins; in practice GraphQL
/// type names are already distinct per-subgraph.
pub(super) fn find_graphql_type<'a>(catalog: &'a [Domain], name: &str) -> Option<&'a GraphqlType> {
catalog.iter().find_map(|d| {
d.endpoints
.iter()
.find_map(|ep| ep.graphql.as_ref()?.types.iter().find(|t| t.name == name))
})
rust/src/api_explorer/graphql/call.rs:285
- For list-typed GraphQL variables,
coerce_graphql_argparses JSON but doesn’t actually enforce that the parsed value is an array. Values like--arg ids='{"a":1}'will be accepted even though the error message says “expected a JSON array”, and the server will fail later with a less actionable type error.
if gql_type.contains('[') {
return serde_json::from_str(raw).map_err(|e| {
crate::error::GddyError::validation(format!(
"invalid --arg {name}={raw}: expected a JSON array for list type {gql_type} ({e})"
))
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 21 changed files in this pull request and generated no new comments.
Suppressed comments (2)
rust/src/api_explorer/graphql/call.rs:148
--argparsing accepts an empty name (e.g.--arg =value) and then reports it as an "unknown --arg ''" instead of a clear validation error. This is confusing and makes it harder to diagnose bad input.
let eq = raw.find('=').ok_or_else(|| {
crate::error::GddyError::validation(format!(
"invalid --arg '{raw}': expected name=value"
))
.into_cli_error()
})?;
let name = &raw[..eq];
let value = raw[eq + 1..].to_owned();
rust/src/api_explorer/guides/api-explorer.md:187
- The quick reference labels
api operation get/api parameter .../api response ...as REST-only, but earlier in this guide you explicitly call out that GraphQL wrapper operations (e.g.postTaxGraphql) still work under these commands. The table wording should be broadened to avoid contradicting the surrounding text.
| `api operation get <operationId>` | Full detail for one REST operation |
| `api parameter list/get --operation <id>` | A REST operation's parameters |
| `api response list/get --operation <id>` | A REST operation's responses |
| `api schema get <id>` | Full structure of a schema |
| `api call <path> --method <method>` | Make an authenticated REST request |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 21 changed files in this pull request and generated no new comments.
Suppressed comments (2)
rust/src/api_explorer/graphql/call.rs:347
validate_selection_exprtreats empty comma segments as valid by filtering them out (filter(|field| !field.is_empty())). This means values like--select id,or--select ,pass validation but produce an empty selection set, yielding a server-side GraphQL error for object return types (missing sub-selection) instead of a clear CLI validation error.
let all_valid = raw
.split(',')
.map(str::trim)
.filter(|field| !field.is_empty())
.all(|field| field.split('.').all(is_valid_name));
rust/src/api_explorer/catalog.rs:120
GraphqlSchemanow embeds the full SDL text (sdl: String). Since the API catalog is compiled into the binary viainclude_str!(seeDOMAIN_FILESbelow in this file), this can significantly increase the CLI binary size and memory footprint at startup for any GraphQL-backed domains, even when users never invokeapi graphql sdl get. Consider storing SDL in a more size-efficient form (e.g. compressed) and only decoding on demand, or making raw SDL embedding opt-in via a feature flag/build-time switch.
#[serde(default)]
pub(super) types: Vec<GraphqlType>,
/// The raw GraphQL SDL source verbatim — see `api graphql sdl get`.
#[serde(default)]
pub(super) sdl: String,
…d domains Some API domains (taxes, catalog-products) are backed by GraphQL rather than plain REST. `api operation list`/`api search` now tag each addressable query/mutation field with a `kind`, and `api graphql get`/`call`/`type`/`sdl` give GraphQL operations their own command surface since REST-shaped commands (`api operation get`, `api call`) don't fit a GraphQL operation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…roup Removed doubled spaces in the `api graphql` group's --help text and fixed subject-verb agreement in the GraphQL operations guide section. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A --header value like ":value" split on the first colon to an empty key, which parsed_extra_headers accepted and passed through to reqwest — which then failed with a less-actionable "invalid header name" error instead of the intended KEY:VALUE validation message. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…erage coerce_graphql_arg silently fell back to sending a JSON string for a list-typed --arg that failed to parse as JSON, trading a clear CLI validation error for a confusing server-side GraphQL type error. It's now fail-fast for list types. Also added send_and_report tests covering a GraphQL errors array on a 200, a 403 missing-scope response, and non-2xx body truncation — none of that shared status/error-handling logic had test coverage. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
--select was concatenated directly into the synthesized GraphQL query string with no validation, so a value containing braces/colons/quotes could inject arbitrary selections or break out of the intended selection set entirely. Each comma/dot-separated segment must now be a syntactically valid GraphQL field name, rejecting anything else with a validation error before the query is built. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
api call has always required a concrete path, not an operationId (pre-existing validation, unrelated to this PR) — the guide's `api call postTaxGraphql --body ...` example never worked. Point at the wrapper's actual fullPath instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
find_graphql_type silently returned the first cross-domain match for a GraphQL type name, but a type name is only unique within its own domain's schema — taxes and catalog-products each declare their own, differently-shaped ReferenceValueFilter input type, and the lookup always returned catalog-products' version regardless of which domain the caller actually meant. api graphql type get now collects every domain defining the name and, absent --domain, errors out listing all of them instead of guessing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
build_graphql_call ran after the mutation dry-run short-circuit, so a dry-run call with missing/invalid --arg or --select returned a successful "would execute" preview even though the real call would have been rejected. Validation now runs unconditionally, matching how api call's method/endpoint checks already behave under --dry-run. Also fixed a guide example missing the required storeId/x-store-id wrapper args, which made copy-pasting it fail validation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
taxes,catalog-products) are backed by GraphQL rather than plain REST.api operation list --domain <domain>andapi searchnow tag each addressable query/mutation field with akind.api graphqlcommand group gives GraphQL operations their own surface:get,call,type, andsdlsubcommands — since REST-shaped commands (api operation get,api call) don't fit a GraphQL operation.api operation get/api callredirect to the new group when pointed at a GraphQL operation id.catalog-productsandtaxesAPI catalog schemas to include GraphQL operation metadata.Refined during review
--selectonapi graphql callis now validated as comma/dot-separated GraphQL field names before being concatenated into the synthesized query — an unvalidated value could otherwise inject arbitrary GraphQL syntax into the request.--argvalue that isn't valid JSON is now a validation error instead of silently being sent as a JSON string (which the server would reject with a confusing type error).--header KEY:VALUEnow rejects an empty key (e.g.:value) up front with the CLI's own validation error, rather than letting reqwest fail later with a less-actionable message.send_and_report.Test plan
cargo checkcargo clippy -- -D warningscargo test(577 passed)cargo fmt --check./rust/scripts/check-module-size.sh--selectinjection payloads and empty header keys are rejected with clear validation errors (manual + regression tests)gddy api graphql get/call/type/sdlagainst a live GraphQL-backed domain🤖 Generated with Claude Code