mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-05-19 06:03:34 -04:00
Merge branch 'master' into tyler/error-callback
This commit is contained in:
Generated
+5
@@ -4884,6 +4884,7 @@ dependencies = [
|
||||
"is-terminal",
|
||||
"itertools 0.12.1",
|
||||
"mimalloc",
|
||||
"percent-encoding",
|
||||
"regex",
|
||||
"reqwest 0.12.9",
|
||||
"rustyline",
|
||||
@@ -4959,6 +4960,9 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-tungstenite",
|
||||
"tower-http",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
@@ -4979,6 +4983,7 @@ dependencies = [
|
||||
"proptest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"smallvec",
|
||||
"spacetimedb-lib",
|
||||
"spacetimedb-primitives",
|
||||
|
||||
@@ -192,6 +192,7 @@ nohash-hasher = "0.2"
|
||||
once_cell = "1.16"
|
||||
parking_lot = { version = "0.12.1", features = ["send_guard", "arc_lock"] }
|
||||
paste = "1.0"
|
||||
percent-encoding = "2.3"
|
||||
petgraph = { version = "0.6.5", default-features = false }
|
||||
pin-project-lite = "0.2.9"
|
||||
postgres-types = "0.2.5"
|
||||
@@ -249,6 +250,8 @@ tokio-util = { version = "0.7.4", features = ["time"] }
|
||||
toml = "0.8"
|
||||
toml_edit = "0.22.22"
|
||||
tower-http = { version = "0.5", features = ["cors"] }
|
||||
tower-layer = "0.3"
|
||||
tower-service = "0.3"
|
||||
tracing = "0.1.37"
|
||||
tracing-appender = "0.2.2"
|
||||
tracing-core = "0.1.31"
|
||||
|
||||
@@ -47,6 +47,7 @@ itertools.workspace = true
|
||||
indicatif.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
mimalloc.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
regex.workspace = true
|
||||
reqwest.workspace = true
|
||||
rustyline.workspace = true
|
||||
|
||||
@@ -7,7 +7,7 @@ use spacetimedb_lib::de::serde::DeserializeWrapper;
|
||||
use spacetimedb_lib::sats::ProductType;
|
||||
use spacetimedb_lib::Identity;
|
||||
|
||||
use crate::util::AuthHeader;
|
||||
use crate::util::{AuthHeader, ResponseExt};
|
||||
|
||||
static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
|
||||
|
||||
@@ -23,10 +23,10 @@ impl Connection {
|
||||
pub fn db_uri(&self, endpoint: &str) -> String {
|
||||
[
|
||||
&self.host,
|
||||
"/database/",
|
||||
endpoint,
|
||||
"/",
|
||||
"/v1/database/",
|
||||
&self.database_identity.to_hex(),
|
||||
"/",
|
||||
endpoint,
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
@@ -66,9 +66,8 @@ impl ClientApi {
|
||||
.get(self.con.db_uri("schema"))
|
||||
.query(&[("version", "9")])
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
let DeserializeWrapper(module_def) = res.json().await?;
|
||||
.await?;
|
||||
let DeserializeWrapper(module_def) = res.json_or_error().await?;
|
||||
Ok(module_def)
|
||||
}
|
||||
|
||||
|
||||
@@ -163,15 +163,15 @@ impl RawConfig {
|
||||
nickname: Some("local".to_string()),
|
||||
ecdsa_public_key: None,
|
||||
};
|
||||
let testnet = ServerConfig {
|
||||
host: "testnet.spacetimedb.com".to_string(),
|
||||
let maincloud = ServerConfig {
|
||||
host: "maincloud.spacetimedb.com".to_string(),
|
||||
protocol: "https".to_string(),
|
||||
nickname: Some("testnet".to_string()),
|
||||
nickname: Some("maincloud".to_string()),
|
||||
ecdsa_public_key: None,
|
||||
};
|
||||
RawConfig {
|
||||
default_server: local.nickname.clone(),
|
||||
server_configs: vec![local, testnet],
|
||||
server_configs: vec![local, maincloud],
|
||||
web_session_token: None,
|
||||
spacetimedb_token: None,
|
||||
}
|
||||
|
||||
@@ -1,22 +1,7 @@
|
||||
use reqwest::{Response, StatusCode};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RequestSource {
|
||||
Client,
|
||||
Server,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CliError {
|
||||
#[error("HTTP status {kind:?} error ({status}): {msg}")]
|
||||
Request {
|
||||
msg: String,
|
||||
kind: RequestSource,
|
||||
status: StatusCode,
|
||||
},
|
||||
#[error(transparent)]
|
||||
ReqWest(#[from] reqwest::Error),
|
||||
#[error("Config error: The option `{key}` not found")]
|
||||
Config { key: String },
|
||||
#[error("Config error: The option `{key}` is not a `{kind}`, found: `{type}: {value}`",
|
||||
@@ -29,19 +14,3 @@ pub enum CliError {
|
||||
found: Box<toml_edit::Item>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Turn a response into an error if the server returned an error.
|
||||
pub async fn error_for_status(response: Response) -> Result<Response, CliError> {
|
||||
let status = response.status();
|
||||
if let Some(kind) = status
|
||||
.is_client_error()
|
||||
.then_some(RequestSource::Client)
|
||||
// Anything that is not a success is an error for the client, even a redirect that is not followed.
|
||||
.or_else(|| (!status.is_success()).then_some(RequestSource::Server))
|
||||
{
|
||||
let msg = response.text().await?;
|
||||
return Err(CliError::Request { kind, msg, status });
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
|
||||
|
||||
let identity = database_identity(&config, database, server).await?;
|
||||
|
||||
let builder = reqwest::Client::new().post(format!("{}/database/delete/{}", config.get_host_url(server)?, identity));
|
||||
let builder = reqwest::Client::new().delete(format!("{}/v1/database/{}", config.get_host_url(server)?, identity));
|
||||
let auth_header = get_auth_header(&mut config, false, server, !force).await?;
|
||||
let builder = add_auth_header_opt(builder, &auth_header);
|
||||
builder.send().await?.error_for_status()?;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
use crate::common_args;
|
||||
use crate::config::Config;
|
||||
use crate::util::{
|
||||
add_auth_header_opt, decode_identity, get_auth_header, get_login_token_or_log_in, spacetime_register_tld,
|
||||
};
|
||||
use crate::util::{add_auth_header_opt, decode_identity, get_auth_header, get_login_token_or_log_in, ResponseExt};
|
||||
use clap::ArgMatches;
|
||||
use clap::{Arg, Command};
|
||||
use reqwest::Url;
|
||||
|
||||
use spacetimedb_client_api_messages::name::{InsertDomainResult, RegisterTldResult};
|
||||
use spacetimedb_client_api_messages::name::{DomainName, InsertDomainResult};
|
||||
|
||||
pub fn cli() -> Command {
|
||||
Command::new("rename")
|
||||
@@ -37,32 +34,17 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
|
||||
let identity = decode_identity(&token)?;
|
||||
let auth_header = get_auth_header(&mut config, false, server, !force).await?;
|
||||
|
||||
match spacetime_register_tld(&mut config, domain, server, !force).await? {
|
||||
RegisterTldResult::Success { domain } => {
|
||||
println!("Registered domain: {}", domain);
|
||||
}
|
||||
RegisterTldResult::Unauthorized { domain } => {
|
||||
return Err(anyhow::anyhow!("Domain is already registered by another: {}", domain));
|
||||
}
|
||||
RegisterTldResult::AlreadyRegistered { domain } => {
|
||||
println!("Domain is already registered by the identity you provided: {}", domain);
|
||||
}
|
||||
}
|
||||
let domain: DomainName = domain.parse()?;
|
||||
|
||||
let builder = reqwest::Client::new().get(Url::parse_with_params(
|
||||
format!("{}/database/set_name", config.get_host_url(server)?).as_str(),
|
||||
[
|
||||
("domain", domain.clone()),
|
||||
("database_identity", database_identity.clone()),
|
||||
("register_tld", "true".to_string()),
|
||||
],
|
||||
)?);
|
||||
let builder = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"{}/v1/database/{database_identity}/names",
|
||||
config.get_host_url(server)?
|
||||
))
|
||||
.body(String::from(domain));
|
||||
let builder = add_auth_header_opt(builder, &auth_header);
|
||||
|
||||
let res = builder.send().await?.error_for_status()?;
|
||||
let bytes = res.bytes().await.unwrap();
|
||||
println!("{}", String::from_utf8_lossy(&bytes[..]));
|
||||
let result: InsertDomainResult = serde_json::from_slice(&bytes[..]).unwrap();
|
||||
let result = builder.send().await?.json_or_error().await?;
|
||||
match result {
|
||||
InsertDomainResult::Success {
|
||||
domain,
|
||||
|
||||
@@ -60,7 +60,7 @@ async fn exec_status(mut config: Config, args: &ArgMatches) -> Result<(), anyhow
|
||||
};
|
||||
|
||||
let status = reqwest::Client::new()
|
||||
.get(format!("{}/energy/{}", config.get_host_url(server)?, identity))
|
||||
.get(format!("{}/v1/energy/{}", config.get_host_url(server)?, identity))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
|
||||
@@ -348,23 +348,23 @@ removeOnUpdate = (cb: (ctx: EventContext, onRow: {row_type}, newRow: {row_type})
|
||||
writeln!(out, "}},");
|
||||
writeln!(
|
||||
out,
|
||||
"// Constructors which are used by the DBConnectionImpl to
|
||||
"// Constructors which are used by the DbConnectionImpl to
|
||||
// extract type information from the generated RemoteModule.
|
||||
//
|
||||
// NOTE: This is not strictly necessary for `eventContextConstructor` because
|
||||
// all we do is build a TypeScript object which we could have done inside the
|
||||
// SDK, but if in the future we wanted to create a class this would be
|
||||
// necessary because classes have methods, so we'll keep it.
|
||||
eventContextConstructor: (imp: DBConnectionImpl, event: Event<Reducer>) => {{
|
||||
eventContextConstructor: (imp: DbConnectionImpl, event: Event<Reducer>) => {{
|
||||
return {{
|
||||
...(imp as DBConnection),
|
||||
...(imp as DbConnection),
|
||||
event
|
||||
}}
|
||||
}},
|
||||
dbViewConstructor: (imp: DBConnectionImpl) => {{
|
||||
dbViewConstructor: (imp: DbConnectionImpl) => {{
|
||||
return new RemoteTables(imp);
|
||||
}},
|
||||
reducersConstructor: (imp: DBConnectionImpl, setReducerFlags: SetReducerFlags) => {{
|
||||
reducersConstructor: (imp: DbConnectionImpl, setReducerFlags: SetReducerFlags) => {{
|
||||
return new RemoteReducers(imp, setReducerFlags);
|
||||
}},
|
||||
setReducerFlagsConstructor: () => {{
|
||||
@@ -433,7 +433,7 @@ fn print_remote_reducers(module: &ModuleDef, out: &mut Indenter) {
|
||||
out.indent(1);
|
||||
writeln!(
|
||||
out,
|
||||
"constructor(private connection: DBConnectionImpl, private setCallReducerFlags: SetReducerFlags) {{}}"
|
||||
"constructor(private connection: DbConnectionImpl, private setCallReducerFlags: SetReducerFlags) {{}}"
|
||||
);
|
||||
out.newline();
|
||||
|
||||
@@ -537,7 +537,7 @@ fn print_set_reducer_flags(module: &ModuleDef, out: &mut Indenter) {
|
||||
fn print_remote_tables(module: &ModuleDef, out: &mut Indenter) {
|
||||
writeln!(out, "export class RemoteTables {{");
|
||||
out.indent(1);
|
||||
writeln!(out, "constructor(private connection: DBConnectionImpl) {{}}");
|
||||
writeln!(out, "constructor(private connection: DbConnectionImpl) {{}}");
|
||||
|
||||
for table in iter_tables(module) {
|
||||
writeln!(out);
|
||||
@@ -571,12 +571,12 @@ fn print_subscription_builder(_module: &ModuleDef, out: &mut Indenter) {
|
||||
fn print_db_connection(_module: &ModuleDef, out: &mut Indenter) {
|
||||
writeln!(
|
||||
out,
|
||||
"export class DBConnection extends DBConnectionImpl<RemoteTables, RemoteReducers, SetReducerFlags> {{"
|
||||
"export class DbConnection extends DbConnectionImpl<RemoteTables, RemoteReducers, SetReducerFlags> {{"
|
||||
);
|
||||
out.indent(1);
|
||||
writeln!(
|
||||
out,
|
||||
"static builder = (): DBConnectionBuilder<DBConnection, ErrorContext, SubscriptionEventContext> => {{"
|
||||
"static builder = (): DbConnectionBuilder<DbConnection, ErrorContext, SubscriptionEventContext> => {{"
|
||||
);
|
||||
out.indent(1);
|
||||
writeln!(
|
||||
@@ -620,7 +620,7 @@ fn print_spacetimedb_imports(out: &mut Indenter) {
|
||||
"ConnectionId",
|
||||
"Timestamp",
|
||||
"TimeDuration",
|
||||
"DBConnectionBuilder",
|
||||
"DbConnectionBuilder",
|
||||
"TableCache",
|
||||
"BinaryWriter",
|
||||
"CallReducerFlags",
|
||||
@@ -630,8 +630,8 @@ fn print_spacetimedb_imports(out: &mut Indenter) {
|
||||
"ErrorContextInterface",
|
||||
"SubscriptionBuilderImpl",
|
||||
"BinaryReader",
|
||||
"DBConnectionImpl",
|
||||
"DBContext",
|
||||
"DbConnectionImpl",
|
||||
"DbContext",
|
||||
"Event",
|
||||
"deepEqual",
|
||||
];
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::common_args;
|
||||
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 reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use spacetimedb::Identity;
|
||||
use tabled::{
|
||||
@@ -44,7 +45,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
|
||||
let client = reqwest::Client::new();
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/identity/{}/databases",
|
||||
"{}/v1/identity/{}/databases",
|
||||
config.get_host_url(server)?,
|
||||
identity
|
||||
))
|
||||
@@ -52,14 +53,10 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if res.status() != StatusCode::OK {
|
||||
return Err(anyhow::anyhow!(format!(
|
||||
"Unable to retrieve databases for identity: {}",
|
||||
res.status()
|
||||
)));
|
||||
}
|
||||
|
||||
let result: DatabasesResult = res.json().await?;
|
||||
let result: DatabasesResult = res
|
||||
.json_or_error()
|
||||
.await
|
||||
.context("unable to retrieve databases for identity")?;
|
||||
|
||||
if !result.identities.is_empty() {
|
||||
let mut table = Table::new(result.identities);
|
||||
|
||||
@@ -273,6 +273,12 @@ struct LocalLoginResponse {
|
||||
|
||||
async fn spacetimedb_direct_login(host: &Url) -> Result<String, anyhow::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
let response: LocalLoginResponse = client.post(host.join("identity")?).send().await?.json().await?;
|
||||
let response: LocalLoginResponse = client
|
||||
.post(host.join("/v1/identity")?)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
Ok(response.token)
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
|
||||
|
||||
let host_url = config.get_host_url(server)?;
|
||||
|
||||
let builder = reqwest::Client::new().get(format!("{}/database/logs/{}", host_url, database_identity));
|
||||
let builder = reqwest::Client::new().get(format!("{}/v1/database/{}/logs", host_url, database_identity));
|
||||
let builder = add_auth_header_opt(builder, &auth_header);
|
||||
let mut res = builder.query(&query_parms).send().await?;
|
||||
let status = res.status();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use anyhow::bail;
|
||||
use clap::Arg;
|
||||
use clap::ArgAction::{Set, SetTrue};
|
||||
use clap::ArgMatches;
|
||||
@@ -9,7 +8,7 @@ use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::util::{add_auth_header_opt, get_auth_header};
|
||||
use crate::util::{add_auth_header_opt, get_auth_header, ResponseExt};
|
||||
use crate::util::{decode_identity, unauth_error_context, y_or_n};
|
||||
use crate::{build, common_args};
|
||||
|
||||
@@ -82,18 +81,19 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
|
||||
// easily create a new identity with an email
|
||||
let auth_header = get_auth_header(&mut config, anon_identity, server, !force).await?;
|
||||
|
||||
let mut query_params = Vec::<(&str, &str)>::new();
|
||||
query_params.push(("host_type", "wasm"));
|
||||
query_params.push(("register_tld", "true"));
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// If a domain or identity was provided, we should locally make sure it looks correct and
|
||||
// append it as a query parameter
|
||||
if let Some(name_or_identity) = name_or_identity {
|
||||
let mut builder = if let Some(name_or_identity) = name_or_identity {
|
||||
if !is_identity(name_or_identity) {
|
||||
parse_domain_name(name_or_identity)?;
|
||||
}
|
||||
query_params.push(("name_or_identity", name_or_identity.as_str()));
|
||||
}
|
||||
let encode_set = const { &percent_encoding::NON_ALPHANUMERIC.remove(b'_').remove(b'-') };
|
||||
let domain = percent_encoding::percent_encode(name_or_identity.as_bytes(), encode_set);
|
||||
client.put(format!("{database_host}/v1/database/{domain}"))
|
||||
} else {
|
||||
client.post(format!("{database_host}/v1/database"))
|
||||
};
|
||||
|
||||
if !path_to_project.exists() {
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -145,16 +145,11 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
|
||||
println!("Aborting");
|
||||
return Ok(());
|
||||
}
|
||||
query_params.push(("clear", "true"));
|
||||
builder = builder.query(&[("clear", true)]);
|
||||
}
|
||||
|
||||
println!("Publishing module...");
|
||||
|
||||
let mut builder = reqwest::Client::new().post(Url::parse_with_params(
|
||||
format!("{}/database/publish", database_host).as_str(),
|
||||
query_params,
|
||||
)?);
|
||||
|
||||
builder = add_auth_header_opt(builder, &auth_header);
|
||||
|
||||
let res = builder.body(program_bytes).send().await?;
|
||||
@@ -169,13 +164,8 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
|
||||
config.server_nick_or_host(server)?,
|
||||
);
|
||||
}
|
||||
if res.status().is_client_error() || res.status().is_server_error() {
|
||||
let err = res.text().await?;
|
||||
bail!(err)
|
||||
}
|
||||
let bytes = res.bytes().await.unwrap();
|
||||
|
||||
let response: PublishResult = serde_json::from_slice(&bytes[..]).unwrap();
|
||||
let response: PublishResult = res.json_or_error().await?;
|
||||
match response {
|
||||
PublishResult::Success {
|
||||
domain,
|
||||
@@ -192,20 +182,9 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
|
||||
println!("{} database with identity: {}", op, database_identity);
|
||||
}
|
||||
}
|
||||
PublishResult::TldNotRegistered { domain } => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"The top level domain that you provided is not registered.\n\
|
||||
This tld is not yet registered to any identity: {}",
|
||||
domain.tld()
|
||||
));
|
||||
}
|
||||
PublishResult::PermissionDenied { domain } => {
|
||||
PublishResult::PermissionDenied { name } => {
|
||||
if anon_identity {
|
||||
anyhow::bail!(
|
||||
"You need to be logged in as the owner of {} to publish to {}",
|
||||
domain.tld(),
|
||||
domain.tld()
|
||||
);
|
||||
anyhow::bail!("You need to be logged in as the owner of {name} to publish to {name}",);
|
||||
}
|
||||
// If we're not in the `anon_identity` case, then we have already forced the user to log in above (using `get_auth_header`), so this should be safe to unwrap.
|
||||
let token = config.spacetimedb_token().unwrap();
|
||||
@@ -213,24 +192,11 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
|
||||
//TODO(jdetter): Have a nice name generator here, instead of using some abstract characters
|
||||
// we should perhaps generate fun names like 'green-fire-dragon' instead
|
||||
let suggested_tld: String = identity.chars().take(12).collect();
|
||||
if let Some(sub_domain) = domain.sub_domain() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"The top level domain {} is not registered to the identity you provided.\n\
|
||||
We suggest you publish to a domain that starts with a TLD owned by you, or publish to a new domain like:\n\
|
||||
\tspacetime publish {}/{}\n",
|
||||
domain.tld(),
|
||||
suggested_tld,
|
||||
sub_domain
|
||||
));
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"The top level domain {} is not registered to the identity you provided.\n\
|
||||
return Err(anyhow::anyhow!(
|
||||
"The database {name} is not registered to the identity you provided.\n\
|
||||
We suggest you push to either a domain owned by you, or a new domain like:\n\
|
||||
\tspacetime publish {}\n",
|
||||
domain.tld(),
|
||||
suggested_tld
|
||||
));
|
||||
}
|
||||
\tspacetime publish {suggested_tld}\n",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -292,7 +292,7 @@ pub async fn exec_ping(config: Config, args: &ArgMatches) -> Result<(), anyhow::
|
||||
let server = args.get_one::<String>("server").unwrap().as_str();
|
||||
let url = config.get_host_url(Some(server))?;
|
||||
|
||||
let builder = reqwest::Client::new().get(format!("{}/database/ping", url).as_str());
|
||||
let builder = reqwest::Client::new().get(format!("{}/v1/ping", url).as_str());
|
||||
let response = builder.send().await?;
|
||||
|
||||
match response.status() {
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::time::Instant;
|
||||
|
||||
use crate::api::{from_json_seed, ClientApi, Connection, StmtResultJson};
|
||||
use crate::common_args;
|
||||
use anyhow::Context;
|
||||
use clap::{Arg, ArgAction, ArgMatches};
|
||||
use itertools::Itertools;
|
||||
use reqwest::RequestBuilder;
|
||||
@@ -10,8 +11,7 @@ use spacetimedb_lib::sats::{satn, Typespace};
|
||||
use tabled::settings::Style;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::errors::error_for_status;
|
||||
use crate::util::{database_identity, get_auth_header, UNSTABLE_WARNING};
|
||||
use crate::util::{database_identity, get_auth_header, ResponseExt, UNSTABLE_WARNING};
|
||||
|
||||
pub fn cli() -> clap::Command {
|
||||
clap::Command::new("sql")
|
||||
@@ -67,12 +67,16 @@ fn print_timings(now: Instant) {
|
||||
pub(crate) async fn run_sql(builder: RequestBuilder, sql: &str, with_stats: bool) -> Result<(), anyhow::Error> {
|
||||
let now = Instant::now();
|
||||
|
||||
let json = error_for_status(builder.body(sql.to_owned()).send().await?)
|
||||
let json = builder
|
||||
.body(sql.to_owned())
|
||||
.send()
|
||||
.await?
|
||||
.ensure_content_type("application/json")
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
let stmt_result_json: Vec<StmtResultJson> = serde_json::from_str(&json)?;
|
||||
let stmt_result_json: Vec<StmtResultJson> = serde_json::from_str(&json).context("malformed sql response")?;
|
||||
|
||||
// Print only `OK for empty tables as it's likely a command like `INSERT`.
|
||||
if stmt_result_json.is_empty() {
|
||||
|
||||
+96
-36
@@ -2,7 +2,7 @@ use anyhow::Context;
|
||||
use base64::{engine::general_purpose::STANDARD_NO_PAD as BASE_64_STD_NO_PAD, Engine as _};
|
||||
use reqwest::{RequestBuilder, Url};
|
||||
use spacetimedb::auth::identity::{IncomingClaims, SpacetimeIdentityClaims};
|
||||
use spacetimedb_client_api_messages::name::{DnsLookupResponse, RegisterTldResult, ReverseDNSResponse};
|
||||
use spacetimedb_client_api_messages::name::GetNamesResponse;
|
||||
use spacetimedb_lib::Identity;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
@@ -21,9 +21,88 @@ pub async fn database_identity(
|
||||
if let Ok(identity) = Identity::from_hex(name_or_identity) {
|
||||
return Ok(identity);
|
||||
}
|
||||
match spacetime_dns(config, name_or_identity, server).await? {
|
||||
DnsLookupResponse::Success { domain: _, identity } => Ok(identity),
|
||||
DnsLookupResponse::Failure { domain } => Err(anyhow::anyhow!("The dns resolution of `{}` failed.", domain)),
|
||||
spacetime_dns(config, name_or_identity, server)
|
||||
.await?
|
||||
.with_context(|| format!("the dns resolution of `{name_or_identity}` failed."))
|
||||
}
|
||||
|
||||
pub(crate) trait ResponseExt: Sized {
|
||||
/// Ensure that this response has the given content-type, especially if it's
|
||||
/// a success response.
|
||||
///
|
||||
/// This checks the response status for you, so you shouldn't call
|
||||
/// `error_for_status()` beforehand.
|
||||
///
|
||||
/// If the response does not have the given content type, assume it's an error message
|
||||
/// and return it as such. Success responses with the wrong content type are treated
|
||||
/// as a bug in the API implementation, since that makes it harder to tell what's
|
||||
/// meant to be a structured response and what's a plain-text error message.
|
||||
async fn ensure_content_type(self, content_type: &str) -> anyhow::Result<Self>;
|
||||
|
||||
/// Like [`reqwest::Response::json()`], but handles non-JSON error messages gracefully.
|
||||
async fn json_or_error<T: serde::de::DeserializeOwned>(self) -> anyhow::Result<T>;
|
||||
|
||||
/// Transforms a status of `NOT_FOUND` into `None`.
|
||||
fn found(self) -> Option<Self>;
|
||||
}
|
||||
|
||||
fn err_status_desc(status: http::StatusCode) -> Option<&'static str> {
|
||||
if status.is_success() {
|
||||
None
|
||||
} else if status.is_client_error() {
|
||||
Some("HTTP status client error")
|
||||
} else if status.is_server_error() {
|
||||
Some("HTTP status server error")
|
||||
} else {
|
||||
Some("unexpected HTTP status code")
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseExt for reqwest::Response {
|
||||
async fn ensure_content_type(self, content_type: &str) -> anyhow::Result<Self> {
|
||||
let status = self.status();
|
||||
if self
|
||||
.headers()
|
||||
.get(http::header::CONTENT_TYPE)
|
||||
.is_some_and(|ty| ty == content_type)
|
||||
{
|
||||
return Ok(self);
|
||||
}
|
||||
let url = self.url();
|
||||
let Some(status_desc) = err_status_desc(status) else {
|
||||
anyhow::bail!("HTTP response from url ({url}) was success but did not have content-type: {content_type}");
|
||||
};
|
||||
let url = url.to_string();
|
||||
let status_err = match self.error_for_status_ref() {
|
||||
Err(e) => anyhow::Error::from(e),
|
||||
Ok(_) => anyhow::anyhow!("{status_desc} ({status}) from url ({url})"),
|
||||
};
|
||||
let err = match self.text().await {
|
||||
Ok(text) => status_err.context(text),
|
||||
Err(err) => anyhow::Error::from(err)
|
||||
.context(format!("{status_desc} ({status})"))
|
||||
.context("failed to get response text"),
|
||||
};
|
||||
Err(err)
|
||||
}
|
||||
|
||||
async fn json_or_error<T: serde::de::DeserializeOwned>(self) -> anyhow::Result<T> {
|
||||
let status = self.status();
|
||||
self.ensure_content_type("application/json")
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
let mut err = anyhow::Error::from(err);
|
||||
if let Some(desc) = err_status_desc(status) {
|
||||
err = err.context(format!("malformed json payload for {desc} ({status})"))
|
||||
}
|
||||
err
|
||||
})
|
||||
}
|
||||
|
||||
fn found(self) -> Option<Self> {
|
||||
(self.status() != http::StatusCode::NOT_FOUND).then_some(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,37 +111,20 @@ pub async fn spacetime_dns(
|
||||
config: &Config,
|
||||
domain: &str,
|
||||
server: Option<&str>,
|
||||
) -> Result<DnsLookupResponse, anyhow::Error> {
|
||||
) -> Result<Option<Identity>, anyhow::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!("{}/database/dns/{}", config.get_host_url(server)?, domain);
|
||||
let res = client.get(url).send().await?.error_for_status()?;
|
||||
let bytes = res.bytes().await.unwrap();
|
||||
Ok(serde_json::from_slice(&bytes[..]).unwrap())
|
||||
}
|
||||
|
||||
/// Registers the given top level domain to the given identity. If None is passed in as identity, the default
|
||||
/// identity will be looked up in the config and it will be used instead. Returns Ok() if the
|
||||
/// domain is successfully registered, returns Err otherwise.
|
||||
pub async fn spacetime_register_tld(
|
||||
config: &mut Config,
|
||||
tld: &str,
|
||||
server: Option<&str>,
|
||||
interactive: bool,
|
||||
) -> Result<RegisterTldResult, anyhow::Error> {
|
||||
let auth_header = get_auth_header(config, false, server, interactive).await?;
|
||||
|
||||
// TODO(jdetter): Fix URL encoding on specifying this domain
|
||||
let builder = reqwest::Client::new()
|
||||
.get(format!("{}/database/register_tld?tld={}", config.get_host_url(server)?, tld).as_str());
|
||||
let builder = add_auth_header_opt(builder, &auth_header);
|
||||
|
||||
let res = builder.send().await?.error_for_status()?;
|
||||
let bytes = res.bytes().await.unwrap();
|
||||
Ok(serde_json::from_slice(&bytes[..]).unwrap())
|
||||
let url = format!("{}/v1/database/{}/identity", config.get_host_url(server)?, domain);
|
||||
let Some(res) = client.get(url).send().await?.found() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let text = res.error_for_status()?.text().await?;
|
||||
text.parse()
|
||||
.map(Some)
|
||||
.context("identity endpoint did not return an identity")
|
||||
}
|
||||
|
||||
pub async fn spacetime_server_fingerprint(url: &str) -> anyhow::Result<String> {
|
||||
let builder = reqwest::Client::new().get(format!("{}/identity/public-key", url).as_str());
|
||||
let builder = reqwest::Client::new().get(format!("{}/v1/identity/public-key", url).as_str());
|
||||
let res = builder.send().await?.error_for_status()?;
|
||||
let fingerprint = res.text().await?;
|
||||
Ok(fingerprint)
|
||||
@@ -73,12 +135,10 @@ pub async fn spacetime_reverse_dns(
|
||||
config: &Config,
|
||||
identity: &str,
|
||||
server: Option<&str>,
|
||||
) -> Result<ReverseDNSResponse, anyhow::Error> {
|
||||
) -> Result<GetNamesResponse, anyhow::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!("{}/database/reverse_dns/{}", config.get_host_url(server)?, identity);
|
||||
let res = client.get(url).send().await?.error_for_status()?;
|
||||
let bytes = res.bytes().await.unwrap();
|
||||
Ok(serde_json::from_slice(&bytes[..]).unwrap())
|
||||
let url = format!("{}/v1/database/{}/names", config.get_host_url(server)?, identity);
|
||||
client.get(url).send().await?.json_or_error().await
|
||||
}
|
||||
|
||||
/// Add an authorization header, if provided, to the request `builder`.
|
||||
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -79,9 +79,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -142,9 +142,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -207,9 +207,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -267,9 +267,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -330,9 +330,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -390,9 +390,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -453,9 +453,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -516,9 +516,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -594,9 +594,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -672,9 +672,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -737,9 +737,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -962,23 +962,23 @@ const REMOTE_MODULE = {
|
||||
argsType: TestBtreeIndexArgs.getTypeScriptAlgebraicType(),
|
||||
},
|
||||
},
|
||||
// Constructors which are used by the DBConnectionImpl to
|
||||
// Constructors which are used by the DbConnectionImpl to
|
||||
// extract type information from the generated RemoteModule.
|
||||
//
|
||||
// NOTE: This is not strictly necessary for `eventContextConstructor` because
|
||||
// all we do is build a TypeScript object which we could have done inside the
|
||||
// SDK, but if in the future we wanted to create a class this would be
|
||||
// necessary because classes have methods, so we'll keep it.
|
||||
eventContextConstructor: (imp: DBConnectionImpl, event: Event<Reducer>) => {
|
||||
eventContextConstructor: (imp: DbConnectionImpl, event: Event<Reducer>) => {
|
||||
return {
|
||||
...(imp as DBConnection),
|
||||
...(imp as DbConnection),
|
||||
event
|
||||
}
|
||||
},
|
||||
dbViewConstructor: (imp: DBConnectionImpl) => {
|
||||
dbViewConstructor: (imp: DbConnectionImpl) => {
|
||||
return new RemoteTables(imp);
|
||||
},
|
||||
reducersConstructor: (imp: DBConnectionImpl, setReducerFlags: SetReducerFlags) => {
|
||||
reducersConstructor: (imp: DbConnectionImpl, setReducerFlags: SetReducerFlags) => {
|
||||
return new RemoteReducers(imp, setReducerFlags);
|
||||
},
|
||||
setReducerFlagsConstructor: () => {
|
||||
@@ -1005,7 +1005,7 @@ export type Reducer = never
|
||||
;
|
||||
|
||||
export class RemoteReducers {
|
||||
constructor(private connection: DBConnectionImpl, private setCallReducerFlags: SetReducerFlags) {}
|
||||
constructor(private connection: DbConnectionImpl, private setCallReducerFlags: SetReducerFlags) {}
|
||||
|
||||
add(name: string, age: number) {
|
||||
const __args = { name, age };
|
||||
@@ -1274,7 +1274,7 @@ export class SetReducerFlags {
|
||||
}
|
||||
|
||||
export class RemoteTables {
|
||||
constructor(private connection: DBConnectionImpl) {}
|
||||
constructor(private connection: DbConnectionImpl) {}
|
||||
|
||||
get hasSpecialStuff(): HasSpecialStuffTableHandle {
|
||||
return new HasSpecialStuffTableHandle(this.connection.clientCache.getOrCreateTable<HasSpecialStuff>(REMOTE_MODULE.tables.has_special_stuff));
|
||||
@@ -1327,8 +1327,8 @@ export class RemoteTables {
|
||||
|
||||
export class SubscriptionBuilder extends SubscriptionBuilderImpl<RemoteTables, RemoteReducers, SetReducerFlags> { }
|
||||
|
||||
export class DBConnection extends DBConnectionImpl<RemoteTables, RemoteReducers, SetReducerFlags> {
|
||||
static builder = (): DBConnectionBuilder<DBConnection, ErrorContext, SubscriptionEventContext> => {
|
||||
export class DbConnection extends DbConnectionImpl<RemoteTables, RemoteReducers, SetReducerFlags> {
|
||||
static builder = (): DbConnectionBuilder<DbConnection, ErrorContext, SubscriptionEventContext> => {
|
||||
return new DBConnectionBuilder<DBConnection, ErrorContext, SubscriptionEventContext>(REMOTE_MODULE, (imp: DBConnectionImpl) => imp as DBConnection);
|
||||
}
|
||||
subscriptionBuilder = (): SubscriptionBuilder => {
|
||||
@@ -1355,9 +1355,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -1418,9 +1418,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -1478,9 +1478,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -1630,9 +1630,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -1703,9 +1703,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -1779,9 +1779,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -1887,9 +1887,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -1954,9 +1954,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -2084,9 +2084,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -2149,9 +2149,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -2301,9 +2301,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -2368,9 +2368,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -2433,9 +2433,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -2511,9 +2511,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -2589,9 +2589,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -2652,9 +2652,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -2712,9 +2712,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -2820,9 +2820,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -2887,9 +2887,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -2952,9 +2952,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -3012,9 +3012,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -3090,9 +3090,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -3157,9 +3157,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -3220,9 +3220,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -3280,9 +3280,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -3360,9 +3360,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -3425,9 +3425,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -3533,9 +3533,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -3598,9 +3598,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -3678,9 +3678,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
@@ -3743,9 +3743,9 @@ import {
|
||||
BinaryWriter,
|
||||
CallReducerFlags,
|
||||
ConnectionId,
|
||||
DBConnectionBuilder,
|
||||
DBConnectionImpl,
|
||||
DBContext,
|
||||
DbConnectionBuilder,
|
||||
DbConnectionImpl,
|
||||
DbContext,
|
||||
ErrorContextInterface,
|
||||
Event,
|
||||
EventContextInterface,
|
||||
|
||||
@@ -18,6 +18,7 @@ enum-as-inner.workspace = true
|
||||
flate2.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
serde_with.workspace = true
|
||||
smallvec.workspace = true
|
||||
strum.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -50,7 +50,7 @@ pub enum PublishResult {
|
||||
/// In other words, this echoes back a domain name if one was given. If
|
||||
/// the database name given was in fact a database identity, this will be
|
||||
/// `None`.
|
||||
domain: Option<String>,
|
||||
domain: Option<DatabaseName>,
|
||||
/// The identity of the published database.
|
||||
///
|
||||
/// Always set, regardless of whether publish resolved a domain name first
|
||||
@@ -59,14 +59,6 @@ pub enum PublishResult {
|
||||
op: PublishOp,
|
||||
},
|
||||
|
||||
// TODO: below variants are obsolete with control db module
|
||||
/// The top level domain for the database name is not registered. For example:
|
||||
///
|
||||
/// - `clockworklabs/bitcraft`
|
||||
///
|
||||
/// if `clockworklabs` is not registered, this error is returned.
|
||||
TldNotRegistered { domain: DomainName },
|
||||
|
||||
/// The top level domain for the database name is registered, but the identity that you provided does
|
||||
/// not have permission to insert the given database name. For example:
|
||||
///
|
||||
@@ -75,7 +67,7 @@ pub enum PublishResult {
|
||||
/// If you were trying to insert this database name, but the tld `clockworklabs` is
|
||||
/// owned by an identity other than the identity that you provided, then you will receive
|
||||
/// this error.
|
||||
PermissionDenied { domain: DomainName },
|
||||
PermissionDenied { name: DatabaseName },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
@@ -118,6 +110,106 @@ pub enum SetDefaultDomainResult {
|
||||
},
|
||||
}
|
||||
|
||||
/// A simplified version of [`DomainName`] that allows a limited set of characters.
|
||||
///
|
||||
/// Must match the regex `^[a-z0-9]+(-[a-z0-9]+)*$`
|
||||
#[derive(Clone, Debug, serde_with::DeserializeFromStr, serde_with::SerializeDisplay)]
|
||||
pub struct DatabaseName(String);
|
||||
|
||||
impl AsRef<str> for DatabaseName {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DatabaseName> for String {
|
||||
fn from(name: DatabaseName) -> Self {
|
||||
name.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DatabaseName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Clone, Copy, Debug)]
|
||||
pub enum DatabaseNameError {
|
||||
#[error("database names cannot be identities")]
|
||||
Identity,
|
||||
#[error("database names cannot be empty")]
|
||||
Empty,
|
||||
#[error("invalid hyphen in database name")]
|
||||
Hyphen,
|
||||
#[error("invalid characters in database name")]
|
||||
Invalid,
|
||||
}
|
||||
|
||||
fn parse_database_name(s: &str) -> Result<&str, DatabaseNameError> {
|
||||
use DatabaseNameError::*;
|
||||
|
||||
if is_identity(s) {
|
||||
return Err(Identity);
|
||||
}
|
||||
|
||||
let mut chrs = s.chars();
|
||||
let mut next = || chrs.next();
|
||||
|
||||
let is_az09 = |c: char| matches!(c, 'a'..='z' | '0'..='9');
|
||||
|
||||
let c = next().ok_or(Empty)?;
|
||||
if c == '-' {
|
||||
return Err(Hyphen);
|
||||
} else if !is_az09(c) {
|
||||
return Err(Invalid);
|
||||
}
|
||||
|
||||
while let Some(c) = next() {
|
||||
if c == '-' {
|
||||
// can't have a hyphen at the end
|
||||
let c = next().ok_or(Hyphen)?;
|
||||
// can't have 2 hyphens in a row
|
||||
if !is_az09(c) {
|
||||
return Err(Hyphen);
|
||||
}
|
||||
} else if !is_az09(c) {
|
||||
return Err(Invalid);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
impl TryFrom<String> for DatabaseName {
|
||||
type Error = DatabaseNameError;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
parse_database_name(&s)?;
|
||||
Ok(Self(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for DatabaseName {
|
||||
type Err = DatabaseNameError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self(parse_database_name(s)?.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DatabaseName> for Tld {
|
||||
fn from(name: DatabaseName) -> Self {
|
||||
Tld(name.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DatabaseName> for DomainName {
|
||||
fn from(name: DatabaseName) -> Self {
|
||||
Tld::from(name).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// The top level domain part of a [`DomainName`].
|
||||
///
|
||||
/// This newtype witnesses that the TLD is well-formed as per the parsing rules
|
||||
@@ -317,6 +409,12 @@ impl AsRef<str> for DomainName {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomainName> for String {
|
||||
fn from(name: DomainName) -> Self {
|
||||
name.domain_name
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DomainName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(&self.domain_name)
|
||||
@@ -420,8 +518,8 @@ mod serde_impls {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ReverseDNSResponse {
|
||||
pub names: Vec<DomainName>,
|
||||
pub struct GetNamesResponse {
|
||||
pub names: Vec<DatabaseName>,
|
||||
}
|
||||
|
||||
/// Returns whether a hex string is a valid identity.
|
||||
|
||||
@@ -34,6 +34,9 @@ http.workspace = true
|
||||
headers.workspace = true
|
||||
mime = "0.3.17"
|
||||
tokio-stream = { version = "0.1.12", features = ["sync"] }
|
||||
tower-layer.workspace = true
|
||||
tower-service.workspace = true
|
||||
tower-http.workspace = true
|
||||
futures = "0.3"
|
||||
bytes = "1"
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -185,7 +185,7 @@ pub trait ControlStateReadAccess {
|
||||
fn get_energy_balance(&self, identity: &Identity) -> anyhow::Result<Option<EnergyBalance>>;
|
||||
|
||||
// DNS
|
||||
fn lookup_identity(&self, domain: &DomainName) -> anyhow::Result<Option<Identity>>;
|
||||
fn lookup_identity(&self, domain: &str) -> anyhow::Result<Option<Identity>>;
|
||||
fn reverse_lookup(&self, database_identity: &Identity) -> anyhow::Result<Vec<DomainName>>;
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ impl<T: ControlStateReadAccess + ?Sized> ControlStateReadAccess for Arc<T> {
|
||||
}
|
||||
|
||||
// DNS
|
||||
fn lookup_identity(&self, domain: &DomainName) -> anyhow::Result<Option<Identity>> {
|
||||
fn lookup_identity(&self, domain: &str) -> anyhow::Result<Option<Identity>> {
|
||||
(**self).lookup_identity(domain)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use crate::auth::{
|
||||
anon_auth_middleware, SpacetimeAuth, SpacetimeAuthHeader, SpacetimeEnergyUsed, SpacetimeExecutionDurationMicros,
|
||||
SpacetimeIdentity, SpacetimeIdentityToken,
|
||||
anon_auth_middleware, SpacetimeAuth, SpacetimeEnergyUsed, SpacetimeExecutionDurationMicros, SpacetimeIdentity,
|
||||
SpacetimeIdentityToken,
|
||||
};
|
||||
use crate::routes::subscribe::generate_random_connection_id;
|
||||
use crate::util::{ByteStringBody, NameOrIdentity};
|
||||
use crate::{log_and_500, ControlStateDelegate, DatabaseDef, NodeDelegate};
|
||||
use axum::body::{Body, Bytes};
|
||||
use axum::extract::{DefaultBodyLimit, Path, Query, State};
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::response::{ErrorResponse, IntoResponse};
|
||||
use axum::routing::MethodRouter;
|
||||
use axum::Extension;
|
||||
use axum_extra::TypedHeader;
|
||||
use futures::StreamExt;
|
||||
@@ -20,20 +21,12 @@ use spacetimedb::host::ReducerOutcome;
|
||||
use spacetimedb::host::UpdateDatabaseResult;
|
||||
use spacetimedb::identity::Identity;
|
||||
use spacetimedb::messages::control_db::{Database, HostType};
|
||||
use spacetimedb_client_api_messages::name::{self, DnsLookupResponse, DomainName, PublishOp, PublishResult};
|
||||
use spacetimedb_client_api_messages::name::{self, DatabaseName, PublishOp, PublishResult};
|
||||
use spacetimedb_lib::db::raw_def::v9::RawModuleDefV9;
|
||||
use spacetimedb_lib::identity::AuthCtx;
|
||||
use spacetimedb_lib::sats;
|
||||
|
||||
use super::identity::IdentityForUrl;
|
||||
|
||||
pub(crate) struct DomainParsingRejection;
|
||||
|
||||
impl IntoResponse for DomainParsingRejection {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
(StatusCode::BAD_REQUEST, "Unable to parse domain name").into_response()
|
||||
}
|
||||
}
|
||||
use super::subscribe::handle_websocket;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CallParams {
|
||||
@@ -48,13 +41,17 @@ pub async fn call<S: ControlStateDelegate + NodeDelegate>(
|
||||
name_or_identity,
|
||||
reducer,
|
||||
}): Path<CallParams>,
|
||||
TypedHeader(content_type): TypedHeader<headers::ContentType>,
|
||||
ByteStringBody(body): ByteStringBody,
|
||||
) -> axum::response::Result<impl IntoResponse> {
|
||||
if content_type != headers::ContentType::json() {
|
||||
return Err(axum::extract::rejection::MissingJsonContentType::default().into());
|
||||
}
|
||||
let caller_identity = auth.identity;
|
||||
|
||||
let args = ReducerArgs::Json(body);
|
||||
|
||||
let db_identity = name_or_identity.resolve(&worker_ctx).await?.into();
|
||||
let db_identity = name_or_identity.resolve(&worker_ctx).await?;
|
||||
let database = worker_ctx_find_database(&worker_ctx, &db_identity)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
@@ -180,7 +177,7 @@ pub async fn schema<S>(
|
||||
where
|
||||
S: ControlStateDelegate + NodeDelegate,
|
||||
{
|
||||
let db_identity = name_or_identity.resolve(&worker_ctx).await?.into();
|
||||
let db_identity = name_or_identity.resolve(&worker_ctx).await?;
|
||||
let database = worker_ctx_find_database(&worker_ctx, &db_identity)
|
||||
.await?
|
||||
.ok_or((StatusCode::NOT_FOUND, "No such database."))?;
|
||||
@@ -236,7 +233,7 @@ pub async fn db_info<S: ControlStateDelegate>(
|
||||
Path(DatabaseParam { name_or_identity }): Path<DatabaseParam>,
|
||||
) -> axum::response::Result<impl IntoResponse> {
|
||||
log::trace!("Trying to resolve database identity: {:?}", name_or_identity);
|
||||
let database_identity = name_or_identity.resolve(&worker_ctx).await?.into();
|
||||
let database_identity = name_or_identity.resolve(&worker_ctx).await?;
|
||||
log::trace!("Resolved identity to: {database_identity:?}");
|
||||
let database = worker_ctx_find_database(&worker_ctx, &database_identity)
|
||||
.await?
|
||||
@@ -271,7 +268,7 @@ where
|
||||
// You should not be able to read the logs from a database that you do not own
|
||||
// so, unless you are the owner, this will fail.
|
||||
|
||||
let database_identity: Identity = name_or_identity.resolve(&worker_ctx).await?.into();
|
||||
let database_identity: Identity = name_or_identity.resolve(&worker_ctx).await?;
|
||||
let database = worker_ctx_find_database(&worker_ctx, &database_identity)
|
||||
.await?
|
||||
.ok_or((StatusCode::NOT_FOUND, "No such database."))?;
|
||||
@@ -373,7 +370,7 @@ where
|
||||
// Anyone is authorized to execute SQL queries. The SQL engine will determine
|
||||
// which queries this identity is allowed to execute against the database.
|
||||
|
||||
let db_identity = name_or_identity.resolve(&worker_ctx).await?.into();
|
||||
let db_identity = name_or_identity.resolve(&worker_ctx).await?;
|
||||
let database = worker_ctx_find_database(&worker_ctx, &db_identity)
|
||||
.await?
|
||||
.ok_or((StatusCode::NOT_FOUND, "No such database."))?;
|
||||
@@ -393,109 +390,97 @@ where
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DNSParams {
|
||||
database_name: String,
|
||||
name_or_identity: NameOrIdentity,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ReverseDNSParams {
|
||||
database_identity: IdentityForUrl,
|
||||
name_or_identity: NameOrIdentity,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DNSQueryParams {}
|
||||
|
||||
pub async fn dns<S: ControlStateDelegate>(
|
||||
pub async fn get_identity<S: ControlStateDelegate>(
|
||||
State(ctx): State<S>,
|
||||
Path(DNSParams { database_name }): Path<DNSParams>,
|
||||
Path(DNSParams { name_or_identity }): Path<DNSParams>,
|
||||
Query(DNSQueryParams {}): Query<DNSQueryParams>,
|
||||
) -> axum::response::Result<impl IntoResponse> {
|
||||
let domain = database_name.parse().map_err(|_| DomainParsingRejection)?;
|
||||
let db_identity = ctx.lookup_identity(&domain).map_err(log_and_500)?;
|
||||
let response = if let Some(db_identity) = db_identity {
|
||||
DnsLookupResponse::Success {
|
||||
domain,
|
||||
identity: db_identity,
|
||||
}
|
||||
} else {
|
||||
DnsLookupResponse::Failure { domain }
|
||||
};
|
||||
|
||||
Ok(axum::Json(response))
|
||||
let identity = name_or_identity.resolve(&ctx).await?;
|
||||
Ok(identity.to_string())
|
||||
}
|
||||
|
||||
pub async fn reverse_dns<S: ControlStateDelegate>(
|
||||
pub async fn get_names<S: ControlStateDelegate>(
|
||||
State(ctx): State<S>,
|
||||
Path(ReverseDNSParams { database_identity }): Path<ReverseDNSParams>,
|
||||
Path(ReverseDNSParams { name_or_identity }): Path<ReverseDNSParams>,
|
||||
) -> axum::response::Result<impl IntoResponse> {
|
||||
let database_identity = Identity::from(database_identity);
|
||||
let database_identity = name_or_identity.resolve(&ctx).await?;
|
||||
|
||||
let names = ctx.reverse_lookup(&database_identity).map_err(log_and_500)?;
|
||||
let names = ctx
|
||||
.reverse_lookup(&database_identity)
|
||||
.map_err(log_and_500)?
|
||||
.into_iter()
|
||||
.filter_map(|x| String::from(x).try_into().ok())
|
||||
.collect();
|
||||
|
||||
let response = name::ReverseDNSResponse { names };
|
||||
let response = name::GetNamesResponse { names };
|
||||
Ok(axum::Json(response))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RegisterTldParams {
|
||||
tld: String,
|
||||
pub struct PublishDatabaseParams {
|
||||
name_or_identity: Option<NameOrIdentity>,
|
||||
}
|
||||
|
||||
pub async fn register_tld<S: ControlStateDelegate>(
|
||||
State(ctx): State<S>,
|
||||
Query(RegisterTldParams { tld }): Query<RegisterTldParams>,
|
||||
Extension(auth): Extension<SpacetimeAuth>,
|
||||
) -> axum::response::Result<impl IntoResponse> {
|
||||
// You should not be able to publish to a database that you do not own
|
||||
// so, unless you are the owner, this will fail, hence not using get_or_create
|
||||
|
||||
let tld = tld.parse::<DomainName>().map_err(|_| DomainParsingRejection)?.into();
|
||||
let result = ctx.register_tld(&auth.identity, tld).await.map_err(log_and_500)?;
|
||||
Ok(axum::Json(result))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PublishDatabaseParams {}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PublishDatabaseQueryParams {
|
||||
#[serde(default)]
|
||||
clear: bool,
|
||||
name_or_identity: Option<NameOrIdentity>,
|
||||
}
|
||||
|
||||
impl PublishDatabaseQueryParams {
|
||||
pub fn name_or_identity(&self) -> Option<&NameOrIdentity> {
|
||||
self.name_or_identity.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
|
||||
State(ctx): State<S>,
|
||||
Path(PublishDatabaseParams {}): Path<PublishDatabaseParams>,
|
||||
Query(query_params): Query<PublishDatabaseQueryParams>,
|
||||
Path(PublishDatabaseParams { name_or_identity }): Path<PublishDatabaseParams>,
|
||||
Query(PublishDatabaseQueryParams { clear }): Query<PublishDatabaseQueryParams>,
|
||||
Extension(auth): Extension<SpacetimeAuth>,
|
||||
body: Bytes,
|
||||
) -> axum::response::Result<axum::Json<PublishResult>> {
|
||||
let PublishDatabaseQueryParams {
|
||||
name_or_identity,
|
||||
clear,
|
||||
} = query_params;
|
||||
|
||||
// You should not be able to publish to a database that you do not own
|
||||
// so, unless you are the owner, this will fail.
|
||||
|
||||
let (database_identity, db_name) = match name_or_identity {
|
||||
let (database_identity, db_name) = match &name_or_identity {
|
||||
Some(noa) => match noa.try_resolve(&ctx).await? {
|
||||
Ok(resolved) => resolved.into(),
|
||||
Err(domain) => {
|
||||
Ok(resolved) => (resolved, noa.name()),
|
||||
Err(name) => {
|
||||
// `name_or_identity` was a `NameOrIdentity::Name`, but no record
|
||||
// exists yet. Create it now with a fresh identity.
|
||||
let database_auth = SpacetimeAuth::alloc(&ctx).await?;
|
||||
let database_identity = database_auth.identity;
|
||||
ctx.create_dns_record(&auth.identity, &domain, &database_identity)
|
||||
let tld: name::Tld = name.clone().into();
|
||||
let tld = match ctx.register_tld(&auth.identity, tld).await.map_err(log_and_500)? {
|
||||
name::RegisterTldResult::Success { domain }
|
||||
| name::RegisterTldResult::AlreadyRegistered { domain } => domain,
|
||||
name::RegisterTldResult::Unauthorized { .. } => {
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
axum::Json(PublishResult::PermissionDenied { name: name.clone() }),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
let res = ctx
|
||||
.create_dns_record(&auth.identity, &tld.into(), &database_identity)
|
||||
.await
|
||||
.map_err(log_and_500)?;
|
||||
(database_identity, Some(domain))
|
||||
match res {
|
||||
name::InsertDomainResult::Success { .. } => {}
|
||||
name::InsertDomainResult::TldNotRegistered { .. }
|
||||
| name::InsertDomainResult::PermissionDenied { .. } => {
|
||||
return Err(log_and_500("impossible: we just registered the tld"))
|
||||
}
|
||||
name::InsertDomainResult::OtherError(e) => return Err(log_and_500(e)),
|
||||
}
|
||||
(database_identity, Some(name))
|
||||
}
|
||||
},
|
||||
None => {
|
||||
@@ -556,7 +541,7 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
|
||||
}
|
||||
|
||||
Ok(axum::Json(PublishResult::Success {
|
||||
domain: db_name.as_ref().map(ToString::to_string),
|
||||
domain: db_name.cloned(),
|
||||
database_identity,
|
||||
op,
|
||||
}))
|
||||
@@ -564,15 +549,15 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct DeleteDatabaseParams {
|
||||
database_identity: IdentityForUrl,
|
||||
name_or_identity: NameOrIdentity,
|
||||
}
|
||||
|
||||
pub async fn delete_database<S: ControlStateDelegate>(
|
||||
State(ctx): State<S>,
|
||||
Path(DeleteDatabaseParams { database_identity }): Path<DeleteDatabaseParams>,
|
||||
Path(DeleteDatabaseParams { name_or_identity }): Path<DeleteDatabaseParams>,
|
||||
Extension(auth): Extension<SpacetimeAuth>,
|
||||
) -> axum::response::Result<impl IntoResponse> {
|
||||
let database_identity = Identity::from(database_identity);
|
||||
let database_identity = name_or_identity.resolve(&ctx).await?;
|
||||
|
||||
ctx.delete_database(&auth.identity, &database_identity)
|
||||
.await
|
||||
@@ -582,76 +567,107 @@ pub async fn delete_database<S: ControlStateDelegate>(
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SetNameQueryParams {
|
||||
domain: String,
|
||||
database_identity: IdentityForUrl,
|
||||
pub struct AddNameParams {
|
||||
name_or_identity: NameOrIdentity,
|
||||
}
|
||||
|
||||
pub async fn set_name<S: ControlStateDelegate>(
|
||||
pub async fn add_name<S: ControlStateDelegate>(
|
||||
State(ctx): State<S>,
|
||||
Query(SetNameQueryParams {
|
||||
domain,
|
||||
database_identity,
|
||||
}): Query<SetNameQueryParams>,
|
||||
Path(AddNameParams { name_or_identity }): Path<AddNameParams>,
|
||||
Extension(auth): Extension<SpacetimeAuth>,
|
||||
name: String,
|
||||
) -> axum::response::Result<impl IntoResponse> {
|
||||
let database_identity = Identity::from(database_identity);
|
||||
let name = DatabaseName::try_from(name).map_err(|err| (StatusCode::BAD_REQUEST, err.to_string()))?;
|
||||
let database_identity = name_or_identity.resolve(&ctx).await?;
|
||||
|
||||
let database = ctx
|
||||
.get_database_by_identity(&database_identity)
|
||||
.map_err(log_and_500)?
|
||||
.ok_or((StatusCode::NOT_FOUND, "No such database."))?;
|
||||
|
||||
if database.owner_identity != auth.identity {
|
||||
return Err((StatusCode::UNAUTHORIZED, "Identity does not own database.").into());
|
||||
}
|
||||
|
||||
let domain = domain.parse().map_err(|_| DomainParsingRejection)?;
|
||||
let response = ctx
|
||||
.create_dns_record(&auth.identity, &domain, &database_identity)
|
||||
.create_dns_record(&auth.identity, &name.into(), &database_identity)
|
||||
.await
|
||||
// TODO: better error code handling
|
||||
.map_err(log_and_500)?;
|
||||
|
||||
Ok(axum::Json(response))
|
||||
let code = match response {
|
||||
name::InsertDomainResult::Success { .. } => StatusCode::OK,
|
||||
name::InsertDomainResult::TldNotRegistered { .. } => StatusCode::BAD_REQUEST,
|
||||
name::InsertDomainResult::PermissionDenied { .. } => StatusCode::UNAUTHORIZED,
|
||||
name::InsertDomainResult::OtherError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
Ok((code, axum::Json(response)))
|
||||
}
|
||||
|
||||
/// This API call is just designed to allow clients to determine whether or not they can
|
||||
/// establish a connection to SpacetimeDB. This API call doesn't actually do anything.
|
||||
pub async fn ping<S>(State(_ctx): State<S>, _auth: SpacetimeAuthHeader) -> axum::response::Result<impl IntoResponse> {
|
||||
Ok(())
|
||||
/// This struct allows the edition to customize `/database` routes more meticulously.
|
||||
pub struct DatabaseRoutes<S> {
|
||||
/// POST /database
|
||||
pub root_post: MethodRouter<S>,
|
||||
/// PUT: /database/:name_or_identity
|
||||
pub db_put: MethodRouter<S>,
|
||||
/// GET: /database/:name_or_identity
|
||||
pub db_get: MethodRouter<S>,
|
||||
/// DELETE: /database/:name_or_identity
|
||||
pub db_delete: MethodRouter<S>,
|
||||
/// GET: /database/:name_or_identity/names
|
||||
pub names_get: MethodRouter<S>,
|
||||
/// POST: /database/:name_or_identity/names
|
||||
pub names_post: MethodRouter<S>,
|
||||
/// GET: /database/:name_or_identity/identity
|
||||
pub identity_get: MethodRouter<S>,
|
||||
/// GET: /database/:name_or_identity/subscribe
|
||||
pub subscribe_get: MethodRouter<S>,
|
||||
/// POST: /database/:name_or_identity/call/:reducer
|
||||
pub call_reducer_post: MethodRouter<S>,
|
||||
/// GET: /database/:name_or_identity/schema
|
||||
pub schema_get: MethodRouter<S>,
|
||||
/// GET: /database/:name_or_identity/logs
|
||||
pub logs_get: MethodRouter<S>,
|
||||
/// POST: /database/:name_or_identity/sql
|
||||
pub sql_post: MethodRouter<S>,
|
||||
}
|
||||
|
||||
pub fn control_routes<S>(ctx: S) -> axum::Router<S>
|
||||
impl<S> Default for DatabaseRoutes<S>
|
||||
where
|
||||
S: NodeDelegate + ControlStateDelegate + Clone + 'static,
|
||||
{
|
||||
use axum::routing::{get, post};
|
||||
axum::Router::new()
|
||||
.route("/dns/:database_name", get(dns::<S>))
|
||||
.route("/reverse_dns/:database_identity", get(reverse_dns::<S>))
|
||||
.route("/set_name", get(set_name::<S>))
|
||||
.route("/ping", get(ping::<S>))
|
||||
.route("/register_tld", get(register_tld::<S>))
|
||||
.route("/publish", post(publish::<S>).layer(DefaultBodyLimit::disable()))
|
||||
.route("/delete/:database_identity", post(delete_database::<S>))
|
||||
.route_layer(axum::middleware::from_fn_with_state(ctx, anon_auth_middleware::<S>))
|
||||
fn default() -> Self {
|
||||
use axum::routing::{delete, get, post, put};
|
||||
Self {
|
||||
root_post: post(publish::<S>),
|
||||
db_put: put(publish::<S>),
|
||||
db_get: get(db_info::<S>),
|
||||
db_delete: delete(delete_database::<S>),
|
||||
names_get: get(get_names::<S>),
|
||||
names_post: post(add_name::<S>),
|
||||
identity_get: get(get_identity::<S>),
|
||||
subscribe_get: get(handle_websocket::<S>),
|
||||
call_reducer_post: post(call::<S>),
|
||||
schema_get: get(schema::<S>),
|
||||
logs_get: get(logs::<S>),
|
||||
sql_post: post(sql::<S>),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn worker_routes<S>(ctx: S) -> axum::Router<S>
|
||||
impl<S> DatabaseRoutes<S>
|
||||
where
|
||||
S: NodeDelegate + ControlStateDelegate + Clone + 'static,
|
||||
{
|
||||
use axum::routing::{get, post};
|
||||
axum::Router::new()
|
||||
.route("/:name_or_identity", get(db_info::<S>))
|
||||
.route(
|
||||
"/subscribe/:name_or_identity",
|
||||
get(super::subscribe::handle_websocket::<S>),
|
||||
)
|
||||
.route("/call/:name_or_identity/:reducer", post(call::<S>))
|
||||
.route("/schema/:name_or_identity", get(schema::<S>))
|
||||
.route("/logs/:name_or_identity", get(logs::<S>))
|
||||
.route("/sql/:name_or_identity", post(sql::<S>))
|
||||
.route_layer(axum::middleware::from_fn_with_state(ctx, anon_auth_middleware::<S>))
|
||||
pub fn into_router(self, ctx: S) -> axum::Router<S> {
|
||||
let db_router = axum::Router::<S>::new()
|
||||
.route("/", self.db_put)
|
||||
.route("/", self.db_get)
|
||||
.route("/", self.db_delete)
|
||||
.route("/names", self.names_get)
|
||||
.route("/names", self.names_post)
|
||||
.route("/identity", self.identity_get)
|
||||
.route("/subscribe", self.subscribe_get)
|
||||
.route("/call/:reducer", self.call_reducer_post)
|
||||
.route("/schema", self.schema_get)
|
||||
.route("/logs", self.logs_get)
|
||||
.route("/sql", self.sql_post);
|
||||
|
||||
axum::Router::new()
|
||||
.route("/", self.root_post)
|
||||
.nest("/:name_or_identity", db_router)
|
||||
.route_layer(axum::middleware::from_fn_with_state(ctx, anon_auth_middleware::<S>))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,11 +124,10 @@ where
|
||||
S: NodeDelegate + ControlStateDelegate + Clone + 'static,
|
||||
{
|
||||
use axum::routing::get;
|
||||
// TODO: rework this. probably no path param.
|
||||
axum::Router::new().route(
|
||||
"/:identity",
|
||||
get(get_energy_balance::<S>)
|
||||
.post(set_energy_balance::<S>)
|
||||
.put(add_energy::<S>),
|
||||
.put(set_energy_balance::<S>)
|
||||
.post(add_energy::<S>),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,13 +29,12 @@ pub async fn health<S: ControlStateDelegate + NodeDelegate>(
|
||||
.map(|n| n.unschedulable)
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
Ok(axum::Json(serde_json::json!({
|
||||
"package_name": PACKAGE_NAME,
|
||||
"version": VERSION,
|
||||
"nodes": nodes,
|
||||
"schedulable": schedulable,
|
||||
})
|
||||
.to_string())
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn router<S>() -> axum::Router<S>
|
||||
|
||||
@@ -135,7 +135,7 @@ pub async fn get_public_key<S: NodeDelegate>(State(ctx): State<S>) -> axum::resp
|
||||
))
|
||||
}
|
||||
|
||||
pub fn router<S>(_: S) -> axum::Router<S>
|
||||
pub fn router<S>() -> axum::Router<S>
|
||||
where
|
||||
S: NodeDelegate + ControlStateDelegate + Clone + 'static,
|
||||
{
|
||||
@@ -143,7 +143,7 @@ where
|
||||
axum::Router::new()
|
||||
.route("/", post(create_identity::<S>))
|
||||
.route("/public-key", get(get_public_key::<S>))
|
||||
.route("/websocket_token", post(create_websocket_token::<S>))
|
||||
.route("/websocket-token", post(create_websocket_token::<S>))
|
||||
.route("/:identity/verify", get(validate_token))
|
||||
.route("/:identity/databases", get(get_databases::<S>))
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
use database::DatabaseRoutes;
|
||||
use http::header;
|
||||
use tower_http::cors;
|
||||
|
||||
use crate::{ControlStateDelegate, NodeDelegate};
|
||||
|
||||
pub mod database;
|
||||
pub mod energy;
|
||||
pub mod health;
|
||||
@@ -5,3 +11,30 @@ pub mod identity;
|
||||
pub mod metrics;
|
||||
pub mod prometheus;
|
||||
pub mod subscribe;
|
||||
|
||||
/// This API call is just designed to allow clients to determine whether or not they can
|
||||
/// establish a connection to SpacetimeDB. This API call doesn't actually do anything.
|
||||
pub async fn ping(_auth: crate::auth::SpacetimeAuthHeader) {}
|
||||
|
||||
#[allow(clippy::let_and_return)]
|
||||
pub fn router<S>(ctx: &S, database_routes: DatabaseRoutes<S>, extra: axum::Router<S>) -> axum::Router<S>
|
||||
where
|
||||
S: NodeDelegate + ControlStateDelegate + Clone + 'static,
|
||||
{
|
||||
use axum::routing::get;
|
||||
let router = axum::Router::new()
|
||||
.nest("/database", database_routes.into_router(ctx.clone()))
|
||||
.nest("/identity", identity::router())
|
||||
.nest("/energy", energy::router())
|
||||
.nest("/prometheus", prometheus::router())
|
||||
.nest("/metrics", metrics::router())
|
||||
.route("/ping", get(ping))
|
||||
.merge(extra);
|
||||
|
||||
let cors = cors::CorsLayer::new()
|
||||
.allow_headers([header::AUTHORIZATION, header::ACCEPT, header::CONTENT_TYPE])
|
||||
.allow_methods(cors::Any)
|
||||
.allow_origin(cors::Any);
|
||||
|
||||
axum::Router::new().nest("/v1", router.layer(cors))
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ where
|
||||
))?;
|
||||
}
|
||||
|
||||
let db_identity = name_or_identity.resolve(&ctx).await?.into();
|
||||
let db_identity = name_or_identity.resolve(&ctx).await?;
|
||||
|
||||
let (res, ws_upgrade, protocol) =
|
||||
ws.select_protocol([(BIN_PROTOCOL, Protocol::Binary), (TEXT_PROTOCOL, Protocol::Text)]);
|
||||
|
||||
@@ -8,12 +8,13 @@ use axum::body::Bytes;
|
||||
use axum::extract::{FromRequest, Request};
|
||||
use axum::response::IntoResponse;
|
||||
use bytestring::ByteString;
|
||||
use futures::TryStreamExt;
|
||||
use http::{HeaderName, HeaderValue, StatusCode};
|
||||
|
||||
use hyper::body::Body;
|
||||
use spacetimedb::Identity;
|
||||
use spacetimedb_client_api_messages::name::DomainName;
|
||||
use spacetimedb_client_api_messages::name::DatabaseName;
|
||||
|
||||
use crate::routes::database::DomainParsingRejection;
|
||||
use crate::routes::identity::IdentityForUrl;
|
||||
use crate::{log_and_500, ControlStateReadAccess};
|
||||
|
||||
@@ -60,62 +61,52 @@ impl headers::Header for XForwardedFor {
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum NameOrIdentity {
|
||||
Identity(IdentityForUrl),
|
||||
Name(String),
|
||||
Name(DatabaseName),
|
||||
}
|
||||
|
||||
impl NameOrIdentity {
|
||||
pub fn into_string(self) -> String {
|
||||
match self {
|
||||
NameOrIdentity::Identity(addr) => Identity::from(addr).to_hex().to_string(),
|
||||
NameOrIdentity::Name(name) => name,
|
||||
NameOrIdentity::Name(name) => name.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Option<&DatabaseName> {
|
||||
if let Self::Name(name) = self {
|
||||
Some(name)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve this [`NameOrIdentity`].
|
||||
///
|
||||
/// If `self` is a [`NameOrIdentity::Identity`], the inner [`Identity`] is
|
||||
/// returned in a [`ResolvedIdentity`] without a [`DomainName`].
|
||||
/// returned directly.
|
||||
///
|
||||
/// Otherwise, if `self` is a [`NameOrIdentity::Name`], the [`Identity`] is
|
||||
/// looked up by that name in the SpacetimeDB DNS and returned in a
|
||||
/// [`ResolvedIdentity`] alongside `Some` [`DomainName`].
|
||||
/// looked up by that name in the SpacetimeDB DNS and returned.
|
||||
///
|
||||
/// Errors are returned if [`NameOrIdentity::Name`] cannot be parsed into a
|
||||
/// [`DomainName`], or the DNS lookup fails.
|
||||
/// Errors are returned if [`NameOrIdentity::Name`] the DNS lookup fails.
|
||||
///
|
||||
/// An `Ok` result is itself a [`Result`], which is `Err(DomainName)` if the
|
||||
/// An `Ok` result is itself a [`Result`], which is `Err(DatabaseName)` if the
|
||||
/// given [`NameOrIdentity::Name`] is not registered in the SpacetimeDB DNS,
|
||||
/// i.e. no corresponding [`Identity`] exists.
|
||||
pub async fn try_resolve(
|
||||
&self,
|
||||
ctx: &(impl ControlStateReadAccess + ?Sized),
|
||||
) -> axum::response::Result<Result<ResolvedIdentity, DomainName>> {
|
||||
) -> axum::response::Result<Result<Identity, &DatabaseName>> {
|
||||
Ok(match self {
|
||||
Self::Identity(identity) => Ok(ResolvedIdentity {
|
||||
identity: Identity::from(*identity),
|
||||
domain: None,
|
||||
}),
|
||||
Self::Name(name) => {
|
||||
let domain = name.parse().map_err(|_| DomainParsingRejection)?;
|
||||
let identity = ctx.lookup_identity(&domain).map_err(log_and_500)?;
|
||||
match identity {
|
||||
Some(identity) => Ok(ResolvedIdentity {
|
||||
identity,
|
||||
domain: Some(domain),
|
||||
}),
|
||||
None => Err(domain),
|
||||
}
|
||||
}
|
||||
Self::Identity(identity) => Ok(Identity::from(*identity)),
|
||||
Self::Name(name) => ctx.lookup_identity(name.as_ref()).map_err(log_and_500)?.ok_or(name),
|
||||
})
|
||||
}
|
||||
|
||||
/// A variant of [`Self::try_resolve()`] which maps to a 404 (Not Found)
|
||||
/// response if `self` is a [`NameOrIdentity::Name`] for which no
|
||||
/// corresponding [`Identity`] is found in the SpacetimeDB DNS.
|
||||
pub async fn resolve(
|
||||
&self,
|
||||
ctx: &(impl ControlStateReadAccess + ?Sized),
|
||||
) -> axum::response::Result<ResolvedIdentity> {
|
||||
pub async fn resolve(&self, ctx: &(impl ControlStateReadAccess + ?Sized)) -> axum::response::Result<Identity> {
|
||||
self.try_resolve(ctx).await?.map_err(|_| StatusCode::NOT_FOUND.into())
|
||||
}
|
||||
}
|
||||
@@ -125,13 +116,13 @@ impl<'de> serde::Deserialize<'de> for NameOrIdentity {
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
String::deserialize(deserializer).map(|s| {
|
||||
if let Ok(addr) = Identity::from_hex(&s) {
|
||||
NameOrIdentity::Identity(IdentityForUrl::from(addr))
|
||||
} else {
|
||||
NameOrIdentity::Name(s)
|
||||
}
|
||||
})
|
||||
let s = String::deserialize(deserializer)?;
|
||||
if let Ok(addr) = Identity::from_hex(&s) {
|
||||
Ok(NameOrIdentity::Identity(IdentityForUrl::from(addr)))
|
||||
} else {
|
||||
let name: DatabaseName = s.try_into().map_err(serde::de::Error::custom)?;
|
||||
Ok(NameOrIdentity::Name(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,37 +130,30 @@ impl fmt::Display for NameOrIdentity {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Identity(addr) => f.write_str(addr.into_inner().to_hex().as_str()),
|
||||
Self::Name(name) => f.write_str(name),
|
||||
Self::Name(name) => f.write_str(name.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A resolved [`NameOrIdentity`].
|
||||
///
|
||||
/// Constructed by [`NameOrIdentity::try_resolve()`].
|
||||
pub struct ResolvedIdentity {
|
||||
identity: Identity,
|
||||
domain: Option<DomainName>,
|
||||
}
|
||||
pub struct EmptyBody;
|
||||
|
||||
impl ResolvedIdentity {
|
||||
pub fn identity(&self) -> &Identity {
|
||||
&self.identity
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl<S> FromRequest<S> for EmptyBody {
|
||||
type Rejection = axum::response::Response;
|
||||
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
let body = req.into_body();
|
||||
if body.is_end_stream() {
|
||||
return Ok(Self);
|
||||
}
|
||||
|
||||
pub fn domain(&self) -> Option<&DomainName> {
|
||||
self.domain.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResolvedIdentity> for Identity {
|
||||
fn from(value: ResolvedIdentity) -> Self {
|
||||
value.identity
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResolvedIdentity> for (Identity, Option<DomainName>) {
|
||||
fn from(ResolvedIdentity { identity, domain }: ResolvedIdentity) -> Self {
|
||||
(identity, domain)
|
||||
if body
|
||||
.into_data_stream()
|
||||
.try_any(|data| futures::future::ready(!data.is_empty()))
|
||||
.await
|
||||
.map_err(|_| (StatusCode::BAD_REQUEST, "Failed to buffer the request body").into_response())?
|
||||
{
|
||||
return Err((StatusCode::BAD_REQUEST, "body must be empty").into_response());
|
||||
}
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,8 +123,9 @@ fn make_uri(host: Uri, db_name: &str, connection_id: ConnectionId, params: WsPar
|
||||
path.push('/');
|
||||
}
|
||||
|
||||
path.push_str("database/subscribe/");
|
||||
path.push_str("v1/database/");
|
||||
path.push_str(db_name);
|
||||
path.push_str("/subscribe");
|
||||
|
||||
// Provide the connection ID.
|
||||
path.push_str("?connection_id=");
|
||||
|
||||
@@ -69,52 +69,52 @@ fn main() {
|
||||
.expect("Pass a test name as a command-line argument to the test client");
|
||||
|
||||
match &*test {
|
||||
"insert_primitive" => exec_insert_primitive(),
|
||||
"subscribe_and_cancel" => exec_subscribe_and_cancel(),
|
||||
"subscribe_and_unsubscribe" => exec_subscribe_and_unsubscribe(),
|
||||
"subscription_error_smoke_test" => exec_subscription_error_smoke_test(),
|
||||
"delete_primitive" => exec_delete_primitive(),
|
||||
"update_primitive" => exec_update_primitive(),
|
||||
"insert-primitive" => exec_insert_primitive(),
|
||||
"subscribe-and-cancel" => exec_subscribe_and_cancel(),
|
||||
"subscribe-and-unsubscribe" => exec_subscribe_and_unsubscribe(),
|
||||
"subscription-error-smoke-test" => exec_subscription_error_smoke_test(),
|
||||
"delete-primitive" => exec_delete_primitive(),
|
||||
"update-primitive" => exec_update_primitive(),
|
||||
|
||||
"insert_identity" => exec_insert_identity(),
|
||||
"insert_caller_identity" => exec_insert_caller_identity(),
|
||||
"delete_identity" => exec_delete_identity(),
|
||||
"update_identity" => exec_update_identity(),
|
||||
"insert-identity" => exec_insert_identity(),
|
||||
"insert-caller-identity" => exec_insert_caller_identity(),
|
||||
"delete-identity" => exec_delete_identity(),
|
||||
"update-identity" => exec_update_identity(),
|
||||
|
||||
"insert_connection_id" => exec_insert_connection_id(),
|
||||
"insert_caller_connection_id" => exec_insert_caller_connection_id(),
|
||||
"delete_connection_id" => exec_delete_connection_id(),
|
||||
"update_connection_id" => exec_update_connection_id(),
|
||||
"insert-connection-id" => exec_insert_connection_id(),
|
||||
"insert-caller-connection-id" => exec_insert_caller_connection_id(),
|
||||
"delete-connection-id" => exec_delete_connection_id(),
|
||||
"update-connection-id" => exec_update_connection_id(),
|
||||
|
||||
"insert_timestamp" => exec_insert_timestamp(),
|
||||
"insert_call_timestamp" => exec_insert_call_timestamp(),
|
||||
"insert-timestamp" => exec_insert_timestamp(),
|
||||
"insert-call-timestamp" => exec_insert_call_timestamp(),
|
||||
|
||||
"on_reducer" => exec_on_reducer(),
|
||||
"fail_reducer" => exec_fail_reducer(),
|
||||
"on-reducer" => exec_on_reducer(),
|
||||
"fail-reducer" => exec_fail_reducer(),
|
||||
|
||||
"insert_vec" => exec_insert_vec(),
|
||||
"insert_option_some" => exec_insert_option_some(),
|
||||
"insert_option_none" => exec_insert_option_none(),
|
||||
"insert_struct" => exec_insert_struct(),
|
||||
"insert_simple_enum" => exec_insert_simple_enum(),
|
||||
"insert_enum_with_payload" => exec_insert_enum_with_payload(),
|
||||
"insert-vec" => exec_insert_vec(),
|
||||
"insert-option-some" => exec_insert_option_some(),
|
||||
"insert-option-none" => exec_insert_option_none(),
|
||||
"insert-struct" => exec_insert_struct(),
|
||||
"insert-simple-enum" => exec_insert_simple_enum(),
|
||||
"insert-enum-with-payload" => exec_insert_enum_with_payload(),
|
||||
|
||||
"insert_delete_large_table" => exec_insert_delete_large_table(),
|
||||
"insert-delete-large-table" => exec_insert_delete_large_table(),
|
||||
|
||||
"insert_primitives_as_strings" => exec_insert_primitives_as_strings(),
|
||||
"insert-primitives-as-strings" => exec_insert_primitives_as_strings(),
|
||||
|
||||
// "resubscribe" => exec_resubscribe(),
|
||||
//
|
||||
"reauth_part_1" => exec_reauth_part_1(),
|
||||
"reauth_part_2" => exec_reauth_part_2(),
|
||||
"reauth-part-1" => exec_reauth_part_1(),
|
||||
"reauth-part-2" => exec_reauth_part_2(),
|
||||
|
||||
"should_fail" => exec_should_fail(),
|
||||
"should-fail" => exec_should_fail(),
|
||||
|
||||
"reconnect_same_connection_id" => exec_reconnect_same_connection_id(),
|
||||
"caller_always_notified" => exec_caller_always_notified(),
|
||||
"reconnect-same-connection-id" => exec_reconnect_same_connection_id(),
|
||||
"caller-always-notified" => exec_caller_always_notified(),
|
||||
|
||||
"subscribe_all_select_star" => exec_subscribe_all_select_star(),
|
||||
"caller_alice_receives_reducer_callback_but_not_bob" => {
|
||||
"subscribe-all-select-star" => exec_subscribe_all_select_star(),
|
||||
"caller-alice-receives-reducer-callback-but-not-bob" => {
|
||||
exec_caller_alice_receives_reducer_callback_but_not_bob()
|
||||
}
|
||||
_ => panic!("Unknown test: {}", test),
|
||||
|
||||
+34
-34
@@ -20,131 +20,131 @@ macro_rules! declare_tests_with_suffix {
|
||||
|
||||
#[test]
|
||||
fn insert_primitive() {
|
||||
make_test("insert_primitive").run();
|
||||
make_test("insert-primitive").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subscribe_and_cancel() {
|
||||
make_test("subscribe_and_cancel").run();
|
||||
make_test("subscribe-and-cancel").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subscribe_and_unsubscribe() {
|
||||
make_test("subscribe_and_unsubscribe").run();
|
||||
make_test("subscribe-and-unsubscribe").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subscription_error_smoke_test() {
|
||||
make_test("subscription_error_smoke_test").run();
|
||||
make_test("subscription-error-smoke-test").run();
|
||||
}
|
||||
#[test]
|
||||
fn delete_primitive() {
|
||||
make_test("delete_primitive").run();
|
||||
make_test("delete-primitive").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_primitive() {
|
||||
make_test("update_primitive").run();
|
||||
make_test("update-primitive").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_identity() {
|
||||
make_test("insert_identity").run();
|
||||
make_test("insert-identity").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_caller_identity() {
|
||||
make_test("insert_caller_identity").run();
|
||||
make_test("insert-caller-identity").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_identity() {
|
||||
make_test("delete_identity").run();
|
||||
make_test("delete-identity").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_identity() {
|
||||
make_test("delete_identity").run();
|
||||
make_test("delete-identity").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_connection_id() {
|
||||
make_test("insert_connection_id").run();
|
||||
make_test("insert-connection-id").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_caller_connection_id() {
|
||||
make_test("insert_caller_connection_id").run();
|
||||
make_test("insert-caller-connection-id").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_connection_id() {
|
||||
make_test("delete_connection_id").run();
|
||||
make_test("delete-connection-id").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_connection_id() {
|
||||
make_test("delete_connection_id").run();
|
||||
make_test("delete-connection-id").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_timestamp() {
|
||||
make_test("insert_timestamp").run();
|
||||
make_test("insert-timestamp").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_call_timestamp() {
|
||||
make_test("insert_call_timestamp").run();
|
||||
make_test("insert-call-timestamp").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_reducer() {
|
||||
make_test("on_reducer").run();
|
||||
make_test("on-reducer").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fail_reducer() {
|
||||
make_test("fail_reducer").run();
|
||||
make_test("fail-reducer").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_vec() {
|
||||
make_test("insert_vec").run();
|
||||
make_test("insert-vec").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_option_some() {
|
||||
make_test("insert_option_some").run();
|
||||
make_test("insert-option-some").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_option_none() {
|
||||
make_test("insert_option_none").run();
|
||||
make_test("insert-option-none").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_struct() {
|
||||
make_test("insert_struct").run();
|
||||
make_test("insert-struct").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_simple_enum() {
|
||||
make_test("insert_simple_enum").run();
|
||||
make_test("insert-simple-enum").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_enum_with_payload() {
|
||||
make_test("insert_enum_with_payload").run();
|
||||
make_test("insert-enum-with-payload").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_delete_large_table() {
|
||||
make_test("insert_delete_large_table").run();
|
||||
make_test("insert-delete-large-table").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_primitives_as_strings() {
|
||||
make_test("insert_primitives_as_strings").run();
|
||||
make_test("insert-primitives-as-strings").run();
|
||||
}
|
||||
|
||||
// #[test]
|
||||
@@ -155,24 +155,24 @@ macro_rules! declare_tests_with_suffix {
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn should_fail() {
|
||||
make_test("should_fail").run();
|
||||
make_test("should-fail").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reauth() {
|
||||
make_test("reauth_part_1").run();
|
||||
make_test("reauth_part_2").run();
|
||||
make_test("reauth-part-1").run();
|
||||
make_test("reauth-part-2").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconnect_same_connection_id() {
|
||||
make_test("reconnect_same_connection_id").run();
|
||||
make_test("reconnect-same-connection-id").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connect_disconnect_callbacks() {
|
||||
Test::builder()
|
||||
.with_name(concat!("connect_disconnect_callback_", stringify!($lang)))
|
||||
.with_name(concat!("connect-disconnect-callback-", stringify!($lang)))
|
||||
.with_module(concat!("sdk-test-connect-disconnect", $suffix))
|
||||
.with_client(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
@@ -188,17 +188,17 @@ macro_rules! declare_tests_with_suffix {
|
||||
|
||||
#[test]
|
||||
fn caller_always_notified() {
|
||||
make_test("caller_always_notified").run();
|
||||
make_test("caller-always-notified").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subscribe_all_select_star() {
|
||||
make_test("subscribe_all_select_star").run();
|
||||
make_test("subscribe-all-select-star").run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caller_alice_receives_reducer_callback_but_not_bob() {
|
||||
make_test("caller_alice_receives_reducer_callback_but_not_bob").run();
|
||||
make_test("caller-alice-receives-reducer-callback-but-not-bob").run();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -78,7 +78,7 @@ impl ControlDb {
|
||||
}
|
||||
|
||||
impl ControlDb {
|
||||
pub fn spacetime_dns(&self, domain: &DomainName) -> Result<Option<Identity>> {
|
||||
pub fn spacetime_dns(&self, domain: &str) -> Result<Option<Identity>> {
|
||||
let tree = self.db.open_tree("dns")?;
|
||||
let value = tree.get(domain.to_lowercase().as_bytes())?;
|
||||
if let Some(value) = value {
|
||||
@@ -118,7 +118,7 @@ impl ControlDb {
|
||||
try_register_tld: bool,
|
||||
) -> Result<InsertDomainResult> {
|
||||
let database_identity = *database_identity;
|
||||
if self.spacetime_dns(&domain)?.is_some() {
|
||||
if self.spacetime_dns(domain.as_ref())?.is_some() {
|
||||
return Err(Error::RecordAlreadyExists(domain));
|
||||
}
|
||||
let tld = domain.tld();
|
||||
|
||||
@@ -67,11 +67,11 @@ fn test_domain() -> anyhow::Result<()> {
|
||||
let tld_owner = cdb.spacetime_lookup_tld(domain.tld())?;
|
||||
assert_eq!(tld_owner, Some(*ALICE));
|
||||
|
||||
let registered_addr = cdb.spacetime_dns(&domain)?;
|
||||
let registered_addr = cdb.spacetime_dns(domain.as_ref())?;
|
||||
assert_eq!(registered_addr, Some(addr));
|
||||
|
||||
// Try lowercase, too
|
||||
let registered_addr = cdb.spacetime_dns(&domain_lower)?;
|
||||
let registered_addr = cdb.spacetime_dns(domain_lower.as_ref())?;
|
||||
assert_eq!(registered_addr, Some(addr));
|
||||
|
||||
// Reverse should yield the original domain (in mixed-case)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
mod control_db;
|
||||
mod energy_monitor;
|
||||
pub mod routes;
|
||||
pub mod subcommands;
|
||||
pub mod util;
|
||||
pub mod version;
|
||||
@@ -208,7 +207,7 @@ impl spacetimedb_client_api::ControlStateReadAccess for StandaloneEnv {
|
||||
}
|
||||
|
||||
// DNS
|
||||
fn lookup_identity(&self, domain: &DomainName) -> anyhow::Result<Option<Identity>> {
|
||||
fn lookup_identity(&self, domain: &str) -> anyhow::Result<Option<Identity>> {
|
||||
Ok(self.control_db.spacetime_dns(domain)?)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::routes::router;
|
||||
use crate::StandaloneEnv;
|
||||
use anyhow::Context;
|
||||
use axum::extract::DefaultBodyLimit;
|
||||
use clap::ArgAction::SetTrue;
|
||||
use clap::{Arg, ArgMatches};
|
||||
use spacetimedb::config::{CertificateAuthority, ConfigFile};
|
||||
use spacetimedb::db::{Config, Storage};
|
||||
use spacetimedb::startup::{self, TracingOptions};
|
||||
use spacetimedb_client_api::routes::database::DatabaseRoutes;
|
||||
use spacetimedb_client_api::routes::router;
|
||||
use spacetimedb_paths::cli::{PrivKeyPath, PubKeyPath};
|
||||
use spacetimedb_paths::server::ServerDataDir;
|
||||
use tokio::net::TcpListener;
|
||||
@@ -132,7 +134,11 @@ pub async fn exec(args: &ArgMatches) -> anyhow::Result<()> {
|
||||
let data_dir = Arc::new(data_dir.clone());
|
||||
let ctx = StandaloneEnv::init(db_config, &certs, data_dir).await?;
|
||||
|
||||
let service = router(ctx);
|
||||
let mut db_routes = DatabaseRoutes::default();
|
||||
db_routes.root_post = db_routes.root_post.layer(DefaultBodyLimit::disable());
|
||||
db_routes.db_put = db_routes.db_put.layer(DefaultBodyLimit::disable());
|
||||
let extra = axum::Router::new().nest("/health", spacetimedb_client_api::routes::health::router());
|
||||
let service = router(&ctx, db_routes, extra).with_state(ctx);
|
||||
|
||||
let tcp = TcpListener::bind(listen_addr).await?;
|
||||
socket2::SockRef::from(&tcp).set_nodelay(true)?;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use duct::cmd;
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use rand::seq::IteratorRandom;
|
||||
use spacetimedb_data_structures::map::HashMap;
|
||||
use spacetimedb_paths::{RootDir, SpacetimePaths};
|
||||
use std::fs::create_dir_all;
|
||||
@@ -136,7 +136,10 @@ fn status_ok_or_panic(output: std::process::Output, command: &str, test_name: &s
|
||||
}
|
||||
|
||||
fn random_module_name() -> String {
|
||||
Alphanumeric.sample_string(&mut rand::thread_rng(), 16)
|
||||
let mut rng = rand::thread_rng();
|
||||
std::iter::repeat_with(|| ('a'..='z').chain('0'..='9').choose(&mut rng).unwrap())
|
||||
.take(16)
|
||||
.collect()
|
||||
}
|
||||
|
||||
macro_rules! memoized {
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ services:
|
||||
--jwt-pub-key-path=/etc/spacetimedb/id_ecdsa.pub
|
||||
--jwt-priv-key-path=/etc/spacetimedb/id_ecdsa'
|
||||
healthcheck:
|
||||
test: curl -f http://localhost/database/ping || exit 1
|
||||
test: curl -f http://localhost/ping || exit 1
|
||||
privileged: true
|
||||
environment:
|
||||
SPACETIMEDB_FLAMEGRAPH_PATH: ../../../../flamegraphs/flamegraph.folded
|
||||
|
||||
@@ -77,7 +77,7 @@ def requires_docker(item):
|
||||
return unittest.skip("docker not available")(item)
|
||||
|
||||
def random_string(k=20):
|
||||
return ''.join(random.choices(string.ascii_letters, k=k))
|
||||
return ''.join(random.choices(string.ascii_lowercase, k=k))
|
||||
|
||||
def extract_fields(cmd_output, field_name):
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
default_server = "127.0.0.1:3000"
|
||||
spacetimedb_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJoZXhfaWRlbnRpdHkiOiJjMjAwODIzOTA3M2M5MDgyNWQxZWY0MWVjNGJlYzg1MmNkNWIzOTdiMzBjZTVhZjUzNmZlZGExOTE3OWM5ZTJjIiwic3ViIjoiMzVmMjQ5ZTYtZjI0NC00ZDE1LWIzYmUtM2Q5NWZjMjA4MTFmIiwiaXNzIjoibG9jYWxob3N0IiwiYXVkIjpbInNwYWNldGltZWRiIl0sImlhdCI6MTczMjYzODI4NywiZXhwIjpudWxsfQ.oVIaYaH7w8ZiuowAflzKo4BrUeGk_1WqlaySMCYqIrkzB96SxVjCQuR0PYM8dOs7WhsiXvYH7dgVxbSbVV4PGg"
|
||||
spacetimedb_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJoZXhfaWRlbnRpdHkiOiJjMjAwMTk4MDViYzU2ZDczMmUwN2ZjZjlkYWJjYTJkY2I3ZGIwMzMwYjQyOGJjMTI0Y2E0YmMzNWQxNTgzZTkxIiwic3ViIjoiMDMwYmM2NjAtMDdlZC00ZDg0LThkMTYtMDRmMTUzMTgzZjZhIiwiaXNzIjoibG9jYWxob3N0IiwiYXVkIjpbInNwYWNldGltZWRiIl0sImlhdCI6MTczODk1MjY3OCwiZXhwIjpudWxsfQ.TInoGjszYrkVw6vqk_vfj_zgirJcf2-8aNQZHr8e0dJRYK9sCMh2RAjJ_odTbgXRmEIaW416vOZjFGVzuUH6Sg"
|
||||
|
||||
[[server_configs]]
|
||||
nickname = "localhost"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from .. import Smoketest, random_string
|
||||
import unittest
|
||||
|
||||
class Domains(Smoketest):
|
||||
AUTOPUBLISH = False
|
||||
@@ -19,6 +20,7 @@ class Domains(Smoketest):
|
||||
# Now we're essentially just testing that it *doesn't* throw an exception
|
||||
self.spacetime("logs", rand_name)
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_subdomain_behavior(self):
|
||||
"""Test how we treat the / character in published names"""
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ def ping():
|
||||
tries += 1
|
||||
try:
|
||||
print(f"Ping Server at {host}")
|
||||
urlopen(f"http://{host}/database/ping")
|
||||
urlopen(f"http://{host}/v1/ping")
|
||||
print("Server up")
|
||||
break
|
||||
except Exception:
|
||||
|
||||
Reference in New Issue
Block a user