mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-21 02:28:07 +02:00
feat(cli)!: query/mutate invoke stored queries by name; server kind-assert (RFC-011 D3) (#247)
omnigraph query <name> / mutate <name> invoke a stored query by name from the served catalog (served-only). The verb asserts kind via a new expect_mutation on POST /queries/{name} (400 on mismatch). -e/--query + --store is the ad-hoc lane; the positional selects within the source (replacing --name). The bare positional graph URI, --uri, and --name are removed from query/mutate.
This commit is contained in:
parent
1bc0ea6b51
commit
9ef5f90991
13 changed files with 331 additions and 120 deletions
|
|
@ -92,20 +92,18 @@ pub(crate) enum Command {
|
|||
/// when used. Pairs with `omnigraph mutate` on the write side.
|
||||
#[command(visible_alias = "read")]
|
||||
Query {
|
||||
/// Graph URI
|
||||
#[arg(long)]
|
||||
uri: Option<String>,
|
||||
#[arg(hide = true)]
|
||||
legacy_uri: Option<String>,
|
||||
/// Query name. With no `--query`/`-e`, the stored query to invoke from
|
||||
/// the catalog (served — addressed via --server/--profile). With
|
||||
/// `--query`/`-e`, selects which query in that ad-hoc source to run.
|
||||
name: Option<String>,
|
||||
#[arg(long)]
|
||||
config: Option<PathBuf>,
|
||||
/// Ad-hoc query file (a `.gq` you're authoring / break-glass).
|
||||
#[arg(long, conflicts_with = "query_string")]
|
||||
query: Option<PathBuf>,
|
||||
/// Inline GQ source — alternative to `--query <path>`.
|
||||
/// Inline ad-hoc GQ source — alternative to `--query <path>`.
|
||||
#[arg(short = 'e', long = "query-string", value_name = "GQ", conflicts_with = "query")]
|
||||
query_string: Option<String>,
|
||||
#[arg(long)]
|
||||
name: Option<String>,
|
||||
#[command(flatten)]
|
||||
params: ParamsArgs,
|
||||
#[arg(long, conflicts_with = "snapshot")]
|
||||
|
|
@ -124,20 +122,18 @@ pub(crate) enum Command {
|
|||
/// warning when used. Pairs with `omnigraph query` on the read side.
|
||||
#[command(visible_alias = "change")]
|
||||
Mutate {
|
||||
/// Graph URI
|
||||
#[arg(long)]
|
||||
uri: Option<String>,
|
||||
#[arg(hide = true)]
|
||||
legacy_uri: Option<String>,
|
||||
/// Query name. With no `--query`/`-e`, the stored mutation to invoke
|
||||
/// from the catalog (served — addressed via --server/--profile). With
|
||||
/// `--query`/`-e`, selects which query in that ad-hoc source to run.
|
||||
name: Option<String>,
|
||||
#[arg(long)]
|
||||
config: Option<PathBuf>,
|
||||
/// Ad-hoc mutation file (a `.gq` you're authoring / break-glass).
|
||||
#[arg(long, conflicts_with = "query_string")]
|
||||
query: Option<PathBuf>,
|
||||
/// Inline GQ source — alternative to `--query <path>`.
|
||||
/// Inline ad-hoc GQ source — alternative to `--query <path>`.
|
||||
#[arg(short = 'e', long = "query-string", value_name = "GQ", conflicts_with = "query")]
|
||||
query_string: Option<String>,
|
||||
#[arg(long)]
|
||||
name: Option<String>,
|
||||
#[command(flatten)]
|
||||
params: ParamsArgs,
|
||||
#[arg(long)]
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ use omnigraph::db::{Omnigraph, ReadTarget};
|
|||
use omnigraph_api_types::{
|
||||
BranchCreateOutput, BranchCreateRequest, BranchDeleteOutput, BranchListOutput,
|
||||
BranchMergeOutput, BranchMergeRequest, ChangeOutput, CommitListOutput, CommitOutput,
|
||||
ErrorOutput, ExportRequest, GraphListResponse, IngestOutput, IngestRequest, ReadOutput,
|
||||
ErrorOutput, ExportRequest, GraphListResponse, IngestOutput, IngestRequest,
|
||||
InvokeStoredQueryRequest, ReadOutput,
|
||||
ReadRequest, SchemaApplyOutput, SchemaApplyRequest, SchemaOutput, SnapshotOutput, commit_output,
|
||||
ingest_output, read_output, schema_apply_output, snapshot_payload,
|
||||
};
|
||||
|
|
@ -563,6 +564,50 @@ impl GraphClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// `invoke_named` — run a stored query **by catalog name** (RFC-011 D3).
|
||||
/// Served-only: the catalog is server-owned, so a `--store` (embedded)
|
||||
/// scope has nothing to resolve the name against. `expect_mutation` carries
|
||||
/// the verb's asserted kind; the server rejects a mismatch (400) before
|
||||
/// running, so the response is exactly the expected envelope — the caller
|
||||
/// deserializes it as the concrete `T` (`ReadOutput` for `query`,
|
||||
/// `ChangeOutput` for `mutate`), sidestepping the untagged wire enum.
|
||||
pub(crate) async fn invoke_named<T: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
name: &str,
|
||||
expect_mutation: bool,
|
||||
params_json: Option<&Value>,
|
||||
branch: Option<String>,
|
||||
snapshot: Option<String>,
|
||||
) -> Result<T> {
|
||||
match self {
|
||||
GraphClient::Remote {
|
||||
http,
|
||||
base_url,
|
||||
token,
|
||||
} => {
|
||||
let body = InvokeStoredQueryRequest {
|
||||
params: params_json.cloned(),
|
||||
branch,
|
||||
snapshot,
|
||||
expect_mutation: Some(expect_mutation),
|
||||
};
|
||||
remote_json(
|
||||
http,
|
||||
Method::POST,
|
||||
remote_url(base_url, &["queries", name], &[])?,
|
||||
Some(serde_json::to_value(body)?),
|
||||
token.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
GraphClient::Embedded { .. } => bail!(
|
||||
"by-name invocation needs a server (the stored-query catalog is \
|
||||
server-owned); use -e '<gq>' or --query <file> for an ad-hoc query \
|
||||
against --store, or address a server with --server / --profile"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn branch_create_from(
|
||||
&self,
|
||||
from: &str,
|
||||
|
|
|
|||
|
|
@ -578,75 +578,96 @@ async fn main() -> Result<()> {
|
|||
.await?;
|
||||
}
|
||||
Command::Query {
|
||||
uri,
|
||||
legacy_uri,
|
||||
name,
|
||||
config,
|
||||
query,
|
||||
query_string,
|
||||
name,
|
||||
params,
|
||||
branch,
|
||||
snapshot,
|
||||
format,
|
||||
json,
|
||||
} => {
|
||||
if query.is_none() && query_string.is_none() {
|
||||
bail!("provide a query: --query <file> or -e '<inline gq>'");
|
||||
}
|
||||
|
||||
let config = load_cli_config(config.as_ref())?;
|
||||
let client = client::GraphClient::resolve(
|
||||
&config,
|
||||
cli.server.as_deref(),
|
||||
cli.graph.as_deref(),
|
||||
uri.or(legacy_uri),
|
||||
None,
|
||||
cli.profile.as_deref(),
|
||||
cli.store.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
let query_source =
|
||||
resolve_query_source(&config, query.as_ref(), query_string.as_deref(), None)?;
|
||||
let params_json = load_params_json(¶ms)?;
|
||||
let target = resolve_read_target(&config, branch, snapshot, None)?;
|
||||
let output = client
|
||||
.query(target, &query_source, name.as_deref(), params_json.as_ref())
|
||||
.await?;
|
||||
let output: ReadOutput = if query.is_some() || query_string.is_some() {
|
||||
// Ad-hoc lane: run the source; the positional `name` selects
|
||||
// within it when it holds more than one query.
|
||||
let query_source =
|
||||
resolve_query_source(&config, query.as_ref(), query_string.as_deref(), None)?;
|
||||
client
|
||||
.query(target, &query_source, name.as_deref(), params_json.as_ref())
|
||||
.await?
|
||||
} else {
|
||||
// Catalog lane (served-only): invoke the stored query by name.
|
||||
let Some(name) = name else {
|
||||
bail!(
|
||||
"provide a query name to invoke from the catalog, or -e '<gq>' / \
|
||||
--query <file> for an ad-hoc query"
|
||||
);
|
||||
};
|
||||
let (branch, snapshot) = match &target {
|
||||
ReadTarget::Branch(b) => (Some(b.clone()), None),
|
||||
ReadTarget::Snapshot(s) => (None, Some(s.as_str().to_string())),
|
||||
};
|
||||
client
|
||||
.invoke_named(&name, false, params_json.as_ref(), branch, snapshot)
|
||||
.await?
|
||||
};
|
||||
let format = resolve_read_format(&config, format, json, None);
|
||||
print_read_output(&output, format, &config)?;
|
||||
}
|
||||
Command::Mutate {
|
||||
uri,
|
||||
legacy_uri,
|
||||
name,
|
||||
config,
|
||||
query,
|
||||
query_string,
|
||||
name,
|
||||
params,
|
||||
branch,
|
||||
json,
|
||||
} => {
|
||||
if query.is_none() && query_string.is_none() {
|
||||
bail!("provide a mutation query: --query <file> or -e '<inline gq>'");
|
||||
}
|
||||
|
||||
let config = load_cli_config(config.as_ref())?;
|
||||
let client = client::GraphClient::resolve_with_policy(
|
||||
&config,
|
||||
cli.server.as_deref(),
|
||||
cli.graph.as_deref(),
|
||||
uri.or(legacy_uri),
|
||||
None,
|
||||
cli.as_actor.as_deref(),
|
||||
cli.profile.as_deref(),
|
||||
cli.store.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
let query_source =
|
||||
resolve_query_source(&config, query.as_ref(), query_string.as_deref(), None)?;
|
||||
let params_json = load_params_json(¶ms)?;
|
||||
let branch = resolve_branch(&config, branch, None, "main");
|
||||
let output = client
|
||||
.mutate(&branch, &query_source, name.as_deref(), params_json.as_ref())
|
||||
.await?;
|
||||
let output: ChangeOutput = if query.is_some() || query_string.is_some() {
|
||||
// Ad-hoc lane: run the source; positional `name` selects within it.
|
||||
let query_source =
|
||||
resolve_query_source(&config, query.as_ref(), query_string.as_deref(), None)?;
|
||||
client
|
||||
.mutate(&branch, &query_source, name.as_deref(), params_json.as_ref())
|
||||
.await?
|
||||
} else {
|
||||
// Catalog lane (served-only): invoke the stored mutation by name.
|
||||
let Some(name) = name else {
|
||||
bail!(
|
||||
"provide a mutation name to invoke from the catalog, or -e '<gq>' / \
|
||||
--query <file> for an ad-hoc mutation"
|
||||
);
|
||||
};
|
||||
client
|
||||
.invoke_named(&name, true, params_json.as_ref(), Some(branch), None)
|
||||
.await?
|
||||
};
|
||||
if json {
|
||||
print_json(&output)?;
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue