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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions crates/client-api/src/routes/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<Option<Database>> {
Expand Down Expand Up @@ -732,6 +732,33 @@ pub async fn set_names<S: ControlStateDelegate>(
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<S: ControlStateDelegate>(
State(worker_ctx): State<S>,
Path(TimestampParams { name_or_identity }): Path<TimestampParams>,
) -> axum::response::Result<impl IntoResponse> {
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<S> {
/// POST /database
Expand Down Expand Up @@ -760,6 +787,9 @@ pub struct DatabaseRoutes<S> {
pub logs_get: MethodRouter<S>,
/// POST: /database/:name_or_identity/sql
pub sql_post: MethodRouter<S>,

/// GET: /database/: name_or_identity/unstable/timestamp
pub timestamp_get: MethodRouter<S>,
}

impl<S> Default for DatabaseRoutes<S>
Expand All @@ -782,6 +812,7 @@ where
schema_get: get(schema::<S>),
logs_get: get(logs::<S>),
sql_post: post(sql::<S>),
timestamp_get: get(get_timestamp::<S>),
}
}
}
Expand All @@ -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)
Expand Down
2 changes: 0 additions & 2 deletions crates/client-api/src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -41,5 +40,4 @@ where
axum::Router::new()
.nest("/v1", router.layer(cors))
.nest("/internal", internal::router())
.nest("/unstable", unstable::router())
}
21 changes: 0 additions & 21 deletions crates/client-api/src/routes/unstable.rs

This file was deleted.

6 changes: 5 additions & 1 deletion smoketests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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


Expand Down
20 changes: 18 additions & 2 deletions smoketests/tests/timestamp_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading