Skip to main content

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}