mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-12 01:45:14 +02:00
Add POST /queries/{name} stored-query invocation handler
Invoke a curated server-side stored query by name: source + name come from
the per-graph queries: registry, the client sends only runtime inputs
(params, branch, snapshot). Gated by the invoke_query Cedar action at the
boundary; the handler delegates to the existing run_query/run_mutate, whose
inner Read/Change enforce still runs — so a stored mutation is double-gated
(invoke_query to reach the tool, change for the write).
- InvokeStoredQueryRequest + an untagged InvokeStoredQueryResponse
{ Read(ReadOutput), Change(ChangeOutput) } → one Json<_> return type and a
oneOf 200 schema (a correct contract, not a wrong-but-simple one).
- Route lives in per_graph_protected → single-mode /queries/{name} and
multi-mode /graphs/{id}/queries/{name} for free.
- Deny == unknown: an invoke_query denial and a missing query both return the
same 404, so the catalog can't be probed by an unauthorized caller.
- OpenAPI regenerated; tests cover read, mutation double-gate (403 vs 200),
bad-param 400, and the identical-404 deny path.
Completes the MR-969 V1 invocation slice (registry + /queries/{name} + invoke_query).
This commit is contained in:
parent
6983608f4f
commit
d67b10fa6e
5 changed files with 462 additions and 1 deletions
142
openapi.json
142
openapi.json
|
|
@ -829,6 +829,114 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/queries/{name}": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"queries"
|
||||
],
|
||||
"summary": "Invoke a curated, server-side stored query by name.",
|
||||
"description": "The query source comes from the graph's `queries:` registry, not the\nrequest body — callers send only runtime inputs (`params`, `branch`,\n`snapshot`). Gated by the `invoke_query` Cedar action at the boundary;\na stored *mutation* additionally passes the engine's `change` gate\n(double-gated). A denied actor and an unknown query both return the\nsame 404, so the catalog can't be probed.",
|
||||
"operationId": "invoke_query",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Stored query name (the registry key)",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvokeStoredQueryRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Read envelope (ReadOutput) or mutation envelope (ChangeOutput), serialized untagged",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvokeStoredQueryResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request (param type error; snapshot on a stored mutation)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden (the inner `change` gate for a stored mutation)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Unknown stored query, or `invoke_query` denied (indistinguishable)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Merge conflict",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "Per-actor admission cap exceeded; honor `Retry-After` header",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer_token": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/query": {
|
||||
"post": {
|
||||
"tags": [
|
||||
|
|
@ -1628,6 +1736,40 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"InvokeStoredQueryRequest": {
|
||||
"type": "object",
|
||||
"description": "Body for `POST /queries/{name}` — invokes the server-side stored query\nnamed in the path. The query source and name come from the registry,\nnever the body; only the runtime inputs are supplied here.",
|
||||
"properties": {
|
||||
"branch": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "Branch to run against. Defaults to `main`; for a stored mutation the\nwrite targets this branch."
|
||||
},
|
||||
"params": {
|
||||
"description": "JSON object whose keys match the stored query's declared parameters."
|
||||
},
|
||||
"snapshot": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "Snapshot id to read from (read queries only — rejected for a stored\nmutation). Mutually exclusive with `branch`."
|
||||
}
|
||||
}
|
||||
},
|
||||
"InvokeStoredQueryResponse": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ReadOutput"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/ChangeOutput"
|
||||
}
|
||||
],
|
||||
"description": "Response for `POST /queries/{name}`: the read envelope for a stored\nread, or the mutation envelope for a stored mutation. Serialized\n**untagged**, so the wire shape is exactly [`ReadOutput`] or\n[`ChangeOutput`] — classification follows the stored query, not a\nwrapper field."
|
||||
},
|
||||
"LoadMode": {
|
||||
"type": "string",
|
||||
"description": "Shadow enum for documenting [`LoadMode`] in the OpenAPI schema.",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue