fix(MR-656): address Devin Review findings on /query and /change

Two issues raised by Devin Review on PR #110:

1. `POST /query` mutation-rejection error pointed at the deprecated
   `/change` endpoint instead of the canonical `/mutate`. Fixed in
   three places: the runtime error message in `server_query`, the
   utoipa 400-response description, and the handler doc comment. The
   `QueryRequest` schema docstrings in `api.rs` got the same update so
   the openapi.json bodies match. Server and openapi tests updated.

2. `execute_change_remote` serialized `ChangeRequest` directly, which
   emits the new canonical field names `query` / `name` on the wire.
   `#[serde(alias = "query_source")]` only affects deserialization, so
   a newer CLI talking to an older server would have its `/change`
   POST body fail with "missing field: query_source". Fixed by
   extracting a `legacy_change_request_body` helper that hand-rolls
   the JSON with the legacy keys (`query_source` / `query_name`), the
   same byte-stable contract `execute_read_remote` already uses
   against `/read`. Added two unit tests on the helper to lock the
   wire shape in.

Co-Authored-By: Ragnor Comerford <ragnor.comerford@gmail.com>
This commit is contained in:
Devin AI 2026-05-23 13:48:21 +00:00
parent a3e1b27a63
commit 0949f28794
6 changed files with 103 additions and 25 deletions

View file

@ -252,13 +252,15 @@ pub struct ReadRequest {
///
/// Friendlier-named alternative to [`ReadRequest`] for ad-hoc reads and
/// AI-agent integration. Mutations are rejected with 400 — use `POST
/// /change` for write queries. Field names are deliberately short
/// (`query`, `name`) to match the GQ keyword and the CLI `-e` flag.
/// /mutate` (or its deprecated alias `POST /change`) for write queries.
/// Field names are deliberately short (`query`, `name`) to match the GQ
/// keyword and the CLI `-e` flag.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct QueryRequest {
/// GQ read-query source. May declare one or more named queries; pick one
/// with `name` when more than one is declared. Mutations
/// (`insert`/`update`/`delete`) get 400 — use `POST /change` instead.
/// (`insert`/`update`/`delete`) get 400 — use `POST /mutate` (or its
/// deprecated alias `POST /change`) instead.
#[schema(example = "query get_person($name: String) {\n match {\n $p: Person { name: $name }\n }\n return { $p.name, $p.age }\n}")]
pub query: String,
/// Name of the query to run when `query` declares multiple. Optional when

View file

@ -1026,7 +1026,7 @@ async fn server_read(
request_body = QueryRequest,
responses(
(status = 200, description = "Query results", body = ReadOutput),
(status = 400, description = "Bad request - also returned when the query body contains mutations; use POST /change for write queries", body = ErrorOutput),
(status = 400, description = "Bad request - also returned when the query body contains mutations; use POST /mutate (or its deprecated alias POST /change) for write queries", body = ErrorOutput),
(status = 401, description = "Unauthorized", body = ErrorOutput),
(status = 403, description = "Forbidden", body = ErrorOutput),
),
@ -1037,9 +1037,10 @@ async fn server_read(
/// Designed for ad-hoc exploration and AI-agent tool-use: short field
/// names (`query`, `name`) match the CLI `-e` flag and the GQ `query`
/// keyword. Mutations (`insert`/`update`/`delete`) are rejected with 400
/// -- use `POST /change` for write queries. Otherwise behaves
/// identically to `POST /read`: same target semantics (branch xor
/// snapshot), same Cedar action (Read), same response shape.
/// -- use `POST /mutate` (or its deprecated alias `POST /change`) for
/// write queries. Otherwise behaves identically to `POST /read`: same
/// target semantics (branch xor snapshot), same Cedar action (Read),
/// same response shape.
async fn server_query(
State(state): State<AppState>,
actor: Option<Extension<AuthenticatedActor>>,
@ -1080,7 +1081,7 @@ async fn server_query(
.map_err(|err| ApiError::bad_request(err.to_string()))?;
if !query_decl.mutations.is_empty() {
return Err(ApiError::bad_request(format!(
"query '{}' contains mutations (insert/update/delete); use POST /change for write queries",
"query '{}' contains mutations (insert/update/delete); use POST /mutate for write queries",
query_decl.name
)));
}