diff --git a/crates/client-api/src/routes/database.rs b/crates/client-api/src/routes/database.rs index ea7e37627c4..00f67783952 100644 --- a/crates/client-api/src/routes/database.rs +++ b/crates/client-api/src/routes/database.rs @@ -29,7 +29,7 @@ use spacetimedb::messages::control_db::{Database, HostType}; use spacetimedb_client_api_messages::name::{self, DatabaseName, DomainName, PublishOp, PublishResult}; use spacetimedb_lib::db::raw_def::v9::RawModuleDefV9; use spacetimedb_lib::identity::AuthCtx; -use spacetimedb_lib::sats; +use spacetimedb_lib::{sats, Timestamp}; use super::subscribe::handle_websocket; @@ -371,7 +371,7 @@ fn mime_ndjson() -> mime::Mime { "application/x-ndjson".parse().unwrap() } -async fn worker_ctx_find_database( +pub(crate) async fn worker_ctx_find_database( worker_ctx: &(impl ControlStateDelegate + ?Sized), database_identity: &Identity, ) -> axum::response::Result> { @@ -732,6 +732,33 @@ pub async fn set_names( Ok((status, axum::Json(response))) } +#[derive(serde::Deserialize)] +pub struct TimestampParams { + name_or_identity: NameOrIdentity, +} + +/// Returns the database's view of the current time, +/// as a SATS-JSON encoded [`Timestamp`]. +/// +/// Takes a particular database's [`NameOrIdentity`] as an argument +/// because in a clusterized SpacetimeDB-cloud deployment, +/// this request will be routed to the node running the requested database. +async fn get_timestamp( + State(worker_ctx): State, + Path(TimestampParams { name_or_identity }): Path, +) -> axum::response::Result { + let db_identity = name_or_identity.resolve(&worker_ctx).await?; + + let _database = worker_ctx_find_database(&worker_ctx, &db_identity) + .await? + .ok_or_else(|| { + log::error!("Could not find database: {}", db_identity.to_hex()); + NO_SUCH_DATABASE + })?; + + Ok(axum::Json(sats::serde::SerdeWrapper(Timestamp::now())).into_response()) +} + /// This struct allows the edition to customize `/database` routes more meticulously. pub struct DatabaseRoutes { /// POST /database @@ -760,6 +787,9 @@ pub struct DatabaseRoutes { pub logs_get: MethodRouter, /// POST: /database/:name_or_identity/sql pub sql_post: MethodRouter, + + /// GET: /database/: name_or_identity/unstable/timestamp + pub timestamp_get: MethodRouter, } impl Default for DatabaseRoutes @@ -782,6 +812,7 @@ where schema_get: get(schema::), logs_get: get(logs::), sql_post: post(sql::), + timestamp_get: get(get_timestamp::), } } } @@ -803,7 +834,8 @@ where .route("/call/:reducer", self.call_reducer_post) .route("/schema", self.schema_get) .route("/logs", self.logs_get) - .route("/sql", self.sql_post); + .route("/sql", self.sql_post) + .route("/unstable/timestamp", self.timestamp_get); axum::Router::new() .route("/", self.root_post) diff --git a/crates/client-api/src/routes/mod.rs b/crates/client-api/src/routes/mod.rs index 16fe55022e7..f0930eefb4c 100644 --- a/crates/client-api/src/routes/mod.rs +++ b/crates/client-api/src/routes/mod.rs @@ -12,7 +12,6 @@ mod internal; pub mod metrics; pub mod prometheus; pub mod subscribe; -pub mod unstable; /// 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. @@ -41,5 +40,4 @@ where axum::Router::new() .nest("/v1", router.layer(cors)) .nest("/internal", internal::router()) - .nest("/unstable", unstable::router()) } diff --git a/crates/client-api/src/routes/unstable.rs b/crates/client-api/src/routes/unstable.rs deleted file mode 100644 index 0b0b86fc274..00000000000 --- a/crates/client-api/src/routes/unstable.rs +++ /dev/null @@ -1,21 +0,0 @@ -use axum::response::IntoResponse; -use spacetimedb_lib::{sats, Timestamp}; - -use crate::NodeDelegate; - -/// Returns the database's view of the current time, -/// as a SATS-JSON encoded [`Timestamp`]. -async fn get_timestamp() -> impl IntoResponse { - axum::Json(sats::serde::SerdeWrapper(Timestamp::now())).into_response() -} - -/// The internal router is for routes which are early in design, -/// and may incompatibly change or be removed without a major version bump. -pub fn router() -> axum::Router -where - S: NodeDelegate + Clone + 'static, -{ - use axum::routing::get; - - axum::Router::new().route("/timestamp", get(get_timestamp)) -} diff --git a/smoketests/__init__.py b/smoketests/__init__.py index ba924d5da00..290ca2bb1bc 100644 --- a/smoketests/__init__.py +++ b/smoketests/__init__.py @@ -253,6 +253,10 @@ def run(): # and **not raise any exceptions to the caller**. return ReturnThread(run).join + # Make an HTTP call with `method` to `path`. + # + # If the response is 200, return the body. + # Otherwise, throw an `Exception` constructed with two arguments, the response object and the body. def api_call(self, method, path, body = None, headers = {}): with open(self.config_path, "rb") as f: config = tomllib.load(f) @@ -267,7 +271,7 @@ def api_call(self, method, path, body = None, headers = {}): body = resp.read() logging.debug(f"{resp.status} {body}") if resp.status != 200: - raise resp + raise Exception(resp, body) return body diff --git a/smoketests/tests/timestamp_route.py b/smoketests/tests/timestamp_route.py index 1fc1fe759c6..7a84cce85b7 100644 --- a/smoketests/tests/timestamp_route.py +++ b/smoketests/tests/timestamp_route.py @@ -6,13 +6,29 @@ TIMESTAMP_TAG = "__timestamp_micros_since_unix_epoch__" class TimestampRoute(Smoketest): - AUTOPUBLISH = False + AUTO_PUBLISH = False def test_timestamp_route(self): + name = random_string() + + # A request for the timestamp at a non-existent database is an error... + with self.assertRaises(Exception) as err: + self.api_call( + "GET", + f"/v1/database/{name}/unstable/timestamp", + ) + # ... with code 404. + self.assertEqual(err.exception.args[0].status, 404) + + self.publish_module(name) + + # A request for the timestamp at an extant database is a success... resp = self.api_call( "GET", - "/unstable/timestamp", + f"/v1/database/{name}/unstable/timestamp", ) + + # ... and the response body is a SATS-JSON encoded `Timestamp`. timestamp = json.load(io.BytesIO(resp)) self.assertIsInstance(timestamp, dict) self.assertIn(TIMESTAMP_TAG, timestamp)