Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 67 additions & 2 deletions crates/cli/src/subcommands/db_arg_resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
// | subscribe | resolve_optional_database_parts | No (variable args) | No |
// | describe | resolve_database_with_optional_parts | No (optional args)| No |
//
// Commands that address a server rather than a database (`list`, `rename`, `mcp`) have no
// database argument to resolve, so they take the project's server from the config directly
// via `resolve_config_server`.
//
// "Auto-fallthrough" means: if the provided database name doesn't match any config target,
// treat it as an ad-hoc database outside the project (equivalent to --no-config for that arg).
//
Expand All @@ -21,7 +25,7 @@
// so 2+ positional args means the first must be a database. For `call`/`subscribe`/`describe`,
// the first positional could be a non-database argument, so we must error to avoid misinterpreting it.

use crate::spacetime_config::find_and_load_with_env;
use crate::spacetime_config::{find_and_load_with_env, SpacetimeConfig};
use itertools::Itertools;

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -81,6 +85,31 @@ pub(crate) fn load_config_db_targets(no_config: bool) -> anyhow::Result<Option<V
.filter(|targets| !targets.is_empty()))
}

/// Resolve the server for commands that address a *server* rather than a specific
/// database (`list` has no database argument to hang a config lookup off).
///
/// Only answers when the whole config agrees on one server: a multi-database
/// config spanning several servers has no single right answer, so we leave the
/// CLI's default server alone rather than guess.
pub(crate) fn resolve_config_server(no_config: bool) -> anyhow::Result<Option<String>> {
if no_config {
return Ok(None);
}
Ok(find_and_load_with_env(None)?.and_then(|loaded| single_config_server(&loaded.config)))
}

/// The one server a config points at, or `None` if it names zero or several.
fn single_config_server(config: &SpacetimeConfig) -> Option<String> {
config
.collect_all_targets_with_inheritance()
.iter()
.filter_map(|target| target.fields.get("server").and_then(|v| v.as_str()))
.unique()
.exactly_one()
.ok()
.map(str::to_string)
}

pub(crate) fn resolve_optional_database_parts(
raw_parts: &[String],
config_targets: Option<&[ConfigDbTarget]>,
Expand Down Expand Up @@ -262,8 +291,44 @@ pub(crate) fn resolve_database_with_optional_parts(
#[cfg(test)]
mod tests {
use super::{
resolve_database_arg, resolve_database_with_optional_parts, resolve_optional_database_parts, ConfigDbTarget,
resolve_database_arg, resolve_database_with_optional_parts, resolve_optional_database_parts,
single_config_server, ConfigDbTarget,
};
use crate::spacetime_config::SpacetimeConfig;

fn parse_config(json: &str) -> SpacetimeConfig {
serde_json::from_str(json).unwrap()
}

#[test]
fn root_server_is_used_when_config_has_no_database() {
let config = parse_config(r#"{ "server": "local", "module-path": "./spacetimedb" }"#);
assert_eq!(single_config_server(&config).as_deref(), Some("local"));
}

#[test]
fn children_inherit_the_root_server() {
let config =
parse_config(r#"{ "server": "local", "children": [{ "database": "foo" }, { "database": "bar" }] }"#);
assert_eq!(single_config_server(&config).as_deref(), Some("local"));
}

#[test]
fn no_server_when_config_omits_it() {
let config = parse_config(r#"{ "database": "foo" }"#);
assert_eq!(single_config_server(&config), None);
}

#[test]
fn no_server_when_children_disagree() {
let config = parse_config(
r#"{ "children": [
{ "database": "foo", "server": "local" },
{ "database": "bar", "server": "maincloud" }
] }"#,
);
assert_eq!(single_config_server(&config), None);
}

#[test]
fn single_db_infers_database() {
Expand Down
17 changes: 16 additions & 1 deletion crates/cli/src/subcommands/dns.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::common_args;
use crate::config::Config;
use crate::subcommands::db_arg_resolution::resolve_config_server;
use crate::util::{add_auth_header_opt, get_auth_header, ResponseExt};
use clap::ArgMatches;
use clap::{Arg, Command};
Expand All @@ -22,14 +23,28 @@ pub fn cli() -> Command {
)
.arg(common_args::server().help("The nickname, host name or URL of the server on which to set the name"))
.arg(common_args::yes())
.arg(
Arg::new("no_config")
.long("no-config")
.action(clap::ArgAction::SetTrue)
.help("Ignore spacetime.json configuration"),
)
.after_help("Run `spacetime rename --help` for more detailed information.\n")
}

pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
let domain = args.get_one::<String>("new-name").unwrap();
let database_identity = args.get_one::<String>("database-identity").unwrap();
let server = args.get_one::<String>("server").map(|s| s.as_ref());
let force = args.get_flag("force");
let no_config = args.get_flag("no_config");

// `rename` addresses the database by identity, so there is no database name to
// resolve against the config -- take the project's server directly.
let config_server = resolve_config_server(no_config)?;
let server = args
.get_one::<String>("server")
.map(|s| s.as_str())
.or(config_server.as_deref());
let auth_header = get_auth_header(&mut config, false, server, !force).await?;

let domain: DomainName = domain.parse()?;
Expand Down
20 changes: 18 additions & 2 deletions crates/cli/src/subcommands/list.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use crate::common_args;
use crate::subcommands::db_arg_resolution::resolve_config_server;
use crate::util;
use crate::util::get_login_token_or_log_in;
use crate::util::ResponseExt;
use crate::util::UNSTABLE_WARNING;
use crate::Config;
use anyhow::Context;
use clap::{ArgMatches, Command};
use clap::{Arg, ArgMatches, Command};
use futures::future::join_all;
use serde::Deserialize;
use spacetimedb_lib::Identity;
Expand All @@ -21,6 +22,12 @@ pub fn cli() -> Command {
))
.arg(common_args::server().help("The nickname, host name or URL of the server from which to list databases"))
.arg(common_args::yes())
.arg(
Arg::new("no_config")
.long("no-config")
.action(clap::ArgAction::SetTrue)
.help("Ignore spacetime.json configuration"),
)
}

#[derive(Deserialize)]
Expand All @@ -39,8 +46,17 @@ struct DatabaseRow {
pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
eprintln!("{UNSTABLE_WARNING}\n");

let server = args.get_one::<String>("server").map(|s| s.as_ref());
let force = args.get_flag("force");
let no_config = args.get_flag("no_config");

// `list` has no database argument, so unlike `call`/`logs`/`sql` it picks up the
// project's server directly from spacetime.json rather than from a database target.
let config_server = resolve_config_server(no_config)?;
let server = args
.get_one::<String>("server")
.map(|s| s.as_str())
.or(config_server.as_deref());

let token = get_login_token_or_log_in(&mut config, server, !force).await?;
let identity = util::decode_identity(&token)?;

Expand Down
17 changes: 16 additions & 1 deletion crates/cli/src/subcommands/mcp.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::api::{build_client, Connection};
use crate::common_args;
use crate::config::Config;
use crate::subcommands::db_arg_resolution::resolve_config_server;
use crate::util::{auth_header_from_saved_token, database_identity, ResponseExt, UNSTABLE_WARNING};
use anyhow::Context;
use clap::{Arg, ArgMatches};
Expand All @@ -17,15 +18,29 @@ pub fn cli() -> clap::Command {
))
.arg(common_args::server().help("The nickname, host name or URL of the server hosting the database"))
.arg(common_args::anonymous())
.arg(
Arg::new("no_config")
.long("no-config")
.action(clap::ArgAction::SetTrue)
.help("Ignore spacetime.json configuration"),
)
.after_help("Run `spacetime help mcp` for more detailed information.\n")
}

pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error> {
eprintln!("{UNSTABLE_WARNING}\n");

let database = args.get_one::<String>("database");
let server = args.get_one::<String>("server").map(|s| s.as_ref());
let anon_identity = args.get_flag("anon_identity");
let no_config = args.get_flag("no_config");

// The database arg stays as given (omitting it deliberately serves the whole
// server), but the server it is served from follows the project's config.
let config_server = resolve_config_server(no_config)?;
let server = args
.get_one::<String>("server")
.map(|s| s.as_str())
.or(config_server.as_deref());

let conn = Connection {
host: config.get_host_url(server)?,
Expand Down
Loading