use anyhow::{bail, Context, Result};
use serde_json::{json, Value};
use crate::api::client::ApiClient;
use crate::api::error::ApiError;
use crate::config::credentials::resolve_api_key;
use crate::config::manager::ConfigManager;
use crate::model::loader::Models;
use super::dispatch::{print_output_with_opts, OutputOpts};
const RESOURCE_DESC: &str = "Manage external collections (refresh jobs).";
const REFRESH_DESC: &str = "Manage refresh jobs (trigger / describe / list).";
pub async fn run_from_args(
_models: &Models,
config_mgr: &ConfigManager,
raw_args: &[String],
output_opts: &OutputOpts<'_>,
) -> Result<()> {
let op = raw_args.first().map(|s| s.as_str()).unwrap_or("");
if op.is_empty() || op == "-h" || op == "--help" {
print_resource_help();
return Ok(());
}
if op != "refresh" {
bail!(
"Unknown external-collection operation '{}'. Available: refresh",
op
);
}
let action = raw_args.get(1).map(|s| s.as_str()).unwrap_or("");
if action.is_empty() || action == "-h" || action == "--help" {
print_refresh_help();
return Ok(());
}
let rest: Vec<String> = if raw_args.len() > 2 {
raw_args[2..].to_vec()
} else {
vec![]
};
match action {
"trigger" => trigger(config_mgr, &rest, output_opts).await,
"describe" => describe(config_mgr, &rest, output_opts).await,
"list" => list(config_mgr, &rest, output_opts).await,
other => bail!(
"Unknown refresh action '{}'. Available: trigger, describe, list",
other
),
}
}
fn print_resource_help() {
print!(
"{}\n\n\
Usage: zilliz external-collection <OPERATION> [OPTIONS]\n\n\
Operations:\n\
\x20 {:21}{}\n",
RESOURCE_DESC, "refresh", REFRESH_DESC,
);
}
fn print_refresh_help() {
print!(
"{}\n\n\
Usage: zilliz external-collection refresh <ACTION> [OPTIONS]\n\n\
Actions:\n\
\x20 {:11}Trigger a refresh job for an external collection. Returns the jobId.\n\
\x20 {:11}Get the status of a single refresh job.\n\
\x20 {:11}List refresh jobs (optionally filtered by collection).\n",
REFRESH_DESC, "trigger", "describe", "list",
);
}
fn print_trigger_help() {
print!(
"Trigger a refresh job for an external collection. Returns the jobId.\n\n\
Usage: zilliz external-collection refresh trigger [OPTIONS]\n\n\
Options:\n\
\x20 {:24}{:18}External collection name (required)\n\
\x20 {:24}{:18}Database name\n\
\x20 {:24}{:18}Override external source (optional)\n\
\x20 {:24}{:18}Override external spec (optional)\n\n\
Examples:\n\
\x20 zilliz external-collection refresh trigger --name my_external_coll\n\
\x20 zilliz external-collection refresh trigger --name my_external_coll --database my_db\n",
"--name", "<string>", "--database", "<string>", "--external-source", "<string>",
"--external-spec", "<string>",
);
}
fn print_describe_help() {
print!(
"Get the status of a single external-collection refresh job.\n\n\
Usage: zilliz external-collection refresh describe [OPTIONS]\n\n\
Options:\n\
\x20 {:24}{:18}Refresh job id, returned by trigger (required)\n\n\
Examples:\n\
\x20 zilliz external-collection refresh describe --job-id 123456\n",
"--job-id", "<integer>",
);
}
fn print_list_help() {
print!(
"List external-collection refresh jobs (optionally filtered by collection).\n\n\
Usage: zilliz external-collection refresh list [OPTIONS]\n\n\
Options:\n\
\x20 {:24}{:18}Filter by external collection name\n\
\x20 {:24}{:18}Database name\n\n\
Examples:\n\
\x20 zilliz external-collection refresh list\n\
\x20 zilliz external-collection refresh list --name my_external_coll\n",
"--name", "<string>", "--database", "<string>",
);
}
fn is_help(args: &[String]) -> bool {
args.iter().any(|a| a == "-h" || a == "--help")
}
fn next_val<'a>(args: &'a [String], i: &mut usize) -> Option<&'a str> {
let next_i = *i + 1;
let val = args
.get(next_i)
.map(|s| s.as_str())
.filter(|s| !s.starts_with("--"));
if val.is_some() {
*i = next_i;
}
val
}
fn make_data_plane_client(
config_mgr: &ConfigManager,
api_key_override: Option<&str>,
) -> Result<ApiClient> {
let api_key =
resolve_api_key(api_key_override, config_mgr).ok_or_else(|| ApiError::NoApiKey)?;
let ctx = config_mgr.get_context();
let base_url = ctx
.endpoint
.context("No cluster context set. Run: zilliz context set --cluster-id <id>")?;
let mut client = ApiClient::new(api_key, base_url);
if let Some(cid) = ctx.cluster_id {
client = client.with_extra_query([("cluster_id", cid)]);
}
Ok(client)
}
async fn trigger(
config_mgr: &ConfigManager,
raw_args: &[String],
output_opts: &OutputOpts<'_>,
) -> Result<()> {
if is_help(raw_args) {
print_trigger_help();
return Ok(());
}
let mut name: Option<String> = None;
let mut db_name: Option<String> = None;
let mut external_source: Option<String> = None;
let mut external_spec: Option<String> = None;
let mut i = 0;
while i < raw_args.len() {
match raw_args[i].as_str() {
"--name" => name = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--database" => db_name = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--external-source" => {
external_source = next_val(raw_args, &mut i).map(|s| s.to_string())
}
"--external-spec" => external_spec = next_val(raw_args, &mut i).map(|s| s.to_string()),
other => bail!(
"Unknown flag '{}'. Available: --name, --database, --external-source, --external-spec",
other
),
}
i += 1;
}
let name = name.context("Missing required option: --name")?;
let mut body = json!({ "collectionName": name });
if let Some(db) = db_name {
body["dbName"] = json!(db);
}
if let Some(src) = external_source {
body["externalSource"] = json!(src);
}
if let Some(spec) = external_spec {
body["externalSpec"] = json!(spec);
}
let client = make_data_plane_client(config_mgr, output_opts.api_key)?;
let result: Value = client
.call(
"POST",
"/v2/vectordb/jobs/external_collection/refresh",
None,
Some(&body),
)
.await?;
print_output_with_opts(&result, output_opts, None);
Ok(())
}
async fn describe(
config_mgr: &ConfigManager,
raw_args: &[String],
output_opts: &OutputOpts<'_>,
) -> Result<()> {
if is_help(raw_args) {
print_describe_help();
return Ok(());
}
let mut job_id: Option<i64> = None;
let mut i = 0;
while i < raw_args.len() {
match raw_args[i].as_str() {
"--job-id" => {
job_id = next_val(raw_args, &mut i)
.map(|s| s.parse::<i64>().context("--job-id must be an integer"))
.transpose()?;
}
other => bail!("Unknown flag '{}'. Available: --job-id", other),
}
i += 1;
}
let job_id = job_id.context("Missing required option: --job-id")?;
let body = json!({ "jobId": job_id });
let client = make_data_plane_client(config_mgr, output_opts.api_key)?;
let result: Value = client
.call(
"POST",
"/v2/vectordb/jobs/external_collection/describe",
None,
Some(&body),
)
.await?;
print_output_with_opts(&result, output_opts, None);
Ok(())
}
async fn list(
config_mgr: &ConfigManager,
raw_args: &[String],
output_opts: &OutputOpts<'_>,
) -> Result<()> {
if is_help(raw_args) {
print_list_help();
return Ok(());
}
let mut name: Option<String> = None;
let mut db_name: Option<String> = None;
let mut i = 0;
while i < raw_args.len() {
match raw_args[i].as_str() {
"--name" => name = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--database" => db_name = next_val(raw_args, &mut i).map(|s| s.to_string()),
other => bail!("Unknown flag '{}'. Available: --name, --database", other),
}
i += 1;
}
let mut body = json!({});
let ctx_db = config_mgr.get_context().database;
let effective_db = db_name.or(if ctx_db.is_empty() {
None
} else {
Some(ctx_db)
});
if let Some(db) = effective_db {
body["dbName"] = json!(db);
}
if let Some(n) = name {
body["collectionName"] = json!(n);
}
let client = make_data_plane_client(config_mgr, output_opts.api_key)?;
let result: Value = client
.call(
"POST",
"/v2/vectordb/jobs/external_collection/list",
None,
Some(&body),
)
.await?;
print_output_with_opts(&result, output_opts, None);
Ok(())
}