cratefield_core/tenant_conn.rs
1//! [`TenantConn`]: the only handle a module has on a database, and one it
2//! cannot have obtained for a tenant other than the request's
3//! (TENANT-ROUTING.md §5, issue #32).
4//!
5//! It is fair to say this is `ports.db` with a private constructor and a
6//! [`Tenant`] welded to it. That is the whole idea. `ModuleContext` holds
7//! `Ports`, `Ports` holds one `Arc<dyn Database>`, and the context is
8//! built once and captured in handler state — so the handle a module
9//! reaches today is the same one for every request, whoever asked. The
10//! isolation argument is not that modules are careful; it is that the
11//! careless thing stops being expressible.
12//!
13//! Core cannot name a sqlx type — `adapter-postgres` never builds for
14//! wasm — so this holds an `Arc<dyn Database>` that the resolution layer
15//! already resolved, never a checkout.
16
17use std::sync::Arc;
18
19use axum::extract::FromRequestParts;
20use axum::http::request::Parts;
21
22use crate::ports::Database;
23use crate::problem::Problem;
24use crate::tenant::Tenant;
25
26/// What the resolution layer puts in the request's extensions: the
27/// tenant, and the handle on *its* database.
28///
29/// Not `pub`: a module cannot name it, so a module cannot insert one.
30#[derive(Clone)]
31pub(crate) struct ResolvedTenant {
32 pub(crate) tenant: Tenant,
33 pub(crate) db: Arc<dyn Database>,
34}
35
36/// A database handle bound to the request's tenant.
37///
38/// Obtained only as an axum extractor, and only on a route the resolution
39/// layer covers. There is no constructor:
40///
41/// ```compile_fail
42/// use cratefield_core::TenantConn;
43/// # use std::sync::Arc;
44/// # fn db() -> Arc<dyn cratefield_core::Database> { unimplemented!() }
45/// // No public constructor: a handler cannot build a handle for a
46/// // tenant it was not given.
47/// let _conn = TenantConn { tenant: todo!(), db: db() };
48/// ```
49///
50/// Naming it is fine, which is what keeps a handler signature writable:
51///
52/// ```
53/// use cratefield_core::TenantConn;
54/// async fn handler(_db: TenantConn) {}
55/// ```
56#[derive(Clone)]
57pub struct TenantConn {
58 tenant: Tenant,
59 db: Arc<dyn Database>,
60}
61
62impl TenantConn {
63 /// The tenant this handle belongs to.
64 #[must_use]
65 pub fn tenant(&self) -> &Tenant {
66 &self.tenant
67 }
68}
69
70impl std::fmt::Debug for TenantConn {
71 /// Names the tenant and never the handle. `Display for DbError`
72 /// already scrubs a DSN out of a *message* (#135), but `Debug`
73 /// derives raw, and a database handle has no business being printed
74 /// at all — so this does not derive.
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.debug_struct("TenantConn")
77 .field("tenant", &self.tenant.id())
78 .finish_non_exhaustive()
79 }
80}
81
82#[async_trait::async_trait]
83impl Database for TenantConn {
84 async fn execute(&self, stmt: &crate::ports::Statement) -> Result<u64, crate::ports::DbError> {
85 self.db.execute(stmt).await
86 }
87
88 async fn query(
89 &self,
90 stmt: &crate::ports::Statement,
91 ) -> Result<crate::ports::Rows, crate::ports::DbError> {
92 self.db.query(stmt).await
93 }
94
95 async fn batch_atomic(
96 &self,
97 stmts: &[crate::ports::Statement],
98 ) -> Result<(), crate::ports::DbError> {
99 self.db.batch_atomic(stmts).await
100 }
101}
102
103impl<S> FromRequestParts<S> for TenantConn
104where
105 S: Send + Sync,
106{
107 type Rejection = Problem;
108
109 fn from_request_parts(
110 parts: &mut Parts,
111 _state: &S,
112 ) -> impl std::future::Future<Output = Result<Self, Self::Rejection>> + Send {
113 // A 500 and not a 404: reaching a handler that asks for a
114 // `TenantConn` on a route the resolution layer does not cover is
115 // a wiring mistake in the harness, not something the caller did.
116 // Answering 404 would make it look like the caller's problem and
117 // hide it.
118 std::future::ready(
119 parts
120 .extensions
121 .get::<ResolvedTenant>()
122 .map(|resolved| Self {
123 tenant: resolved.tenant.clone(),
124 db: Arc::clone(&resolved.db),
125 })
126 .ok_or_else(Problem::internal),
127 )
128 }
129}