feat(MR-656): inline query strings in CLI and HTTP server

CLI:
- Add -e / --query-string <STRING> to omnigraph read and omnigraph change
- Exactly one of --query, --query-string, --alias is required (3-way XOR)
- Empty --query-string is rejected with a clear error

HTTP:
- New POST /query (read-only, clean field names: query/name/params/branch/snapshot)
- Mutations on /query are rejected with 400 -- use POST /change instead
- ChangeRequest fields polished: query (alias query_source), name (alias query_name)
- POST /read and POST /change remain byte-compatible for existing clients

Tests:
- cli.rs: -e happy-path on read/change, mutex error vs --query, empty -e rejected
- system_local.rs: inline -e read and -e change exercise the local flow
- system_remote.rs: inline -e read/change over HTTP plus direct /query 200/400
- server.rs: /query 200, /query 400 on mutation, /change legacy field alias
- openapi.rs: new /query path, QueryRequest schema, ChangeRequest field-name polish

Docs: cli.md (-e examples), cli-reference.md (read/change rows), server.md (/query)
Co-Authored-By: Ragnor Comerford <ragnor.comerford@gmail.com>
This commit is contained in:
Devin AI 2026-05-22 17:54:26 +00:00
parent aadfa11ecb
commit 4152d9d5dc
14 changed files with 708 additions and 75 deletions

View file

@ -684,6 +684,73 @@
]
}
},
"/query": {
"post": {
"tags": [
"queries"
],
"summary": "Execute an inline read query (friendlier-named alternative to `POST /read`).",
"description": "Designed for ad-hoc exploration and AI-agent tool-use: short field\nnames (`query`, `name`) match the CLI `-e` flag and the GQ `query`\nkeyword. Mutations (`insert`/`update`/`delete`) are rejected with 400\n-- use `POST /change` for write queries. Otherwise behaves\nidentically to `POST /read`: same target semantics (branch xor\nsnapshot), same Cedar action (Read), same response shape.",
"operationId": "query",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/QueryRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Query results",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ReadOutput"
}
}
}
},
"400": {
"description": "Bad request - also returned when the query body contains mutations; use POST /change for write queries",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorOutput"
}
}
}
}
},
"security": [
{
"bearer_token": []
}
]
}
},
"/read": {
"post": {
"tags": [
@ -1103,7 +1170,7 @@
"ChangeRequest": {
"type": "object",
"required": [
"query_source"
"query"
],
"properties": {
"branch": {
@ -1113,19 +1180,19 @@
],
"description": "Target branch. Defaults to `main`."
},
"params": {
"description": "JSON object whose keys match the mutation's declared parameters."
},
"query_name": {
"name": {
"type": [
"string",
"null"
],
"description": "Name of the mutation to run when `query_source` declares multiple."
"description": "Name of the mutation to run when `query` declares multiple.\n\nAccepts the legacy field name `query_name` as a deserialization alias."
},
"query_source": {
"params": {
"description": "JSON object whose keys match the mutation's declared parameters."
},
"query": {
"type": "string",
"description": "GQ mutation source containing `insert`, `update`, or `delete` statements.\nMay declare multiple named mutations; pick one with `query_name`.",
"description": "GQ mutation source containing `insert`, `update`, or `delete` statements.\nMay declare multiple named mutations; pick one with `name`.\n\nAccepts the legacy field name `query_source` as a deserialization alias.",
"example": "query insert_person($name: String, $age: I32) {\n insert Person { name: $name, age: $age }\n}"
}
}
@ -1453,6 +1520,44 @@
}
}
},
"QueryRequest": {
"type": "object",
"description": "Inline read-query request for `POST /query`.\n\nFriendlier-named alternative to [`ReadRequest`] for ad-hoc reads and\nAI-agent integration. Mutations are rejected with 400 — use `POST\n/change` for write queries. Field names are deliberately short\n(`query`, `name`) to match the GQ keyword and the CLI `-e` flag.",
"required": [
"query"
],
"properties": {
"branch": {
"type": [
"string",
"null"
],
"description": "Branch to read from. Mutually exclusive with `snapshot`. Defaults to `main`."
},
"name": {
"type": [
"string",
"null"
],
"description": "Name of the query to run when `query` declares multiple. Optional when\nonly one query is declared."
},
"params": {
"description": "JSON object whose keys match the query's declared parameters."
},
"query": {
"type": "string",
"description": "GQ read-query source. May declare one or more named queries; pick one\nwith `name` when more than one is declared. Mutations\n(`insert`/`update`/`delete`) get 400 — use `POST /change` instead.",
"example": "query get_person($name: String) {\n match {\n $p: Person { name: $name }\n }\n return { $p.name, $p.age }\n}"
},
"snapshot": {
"type": [
"string",
"null"
],
"description": "Snapshot id to read from. Mutually exclusive with `branch`."
}
}
},
"ReadOutput": {
"type": "object",
"required": [