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::{Tenancy, 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) tenancy: Tenancy,
34    pub(crate) db: Arc<dyn Database>,
35}
36
37/// A database handle bound to the request's tenant.
38///
39/// Obtained only as an axum extractor, and only on a route the resolution
40/// layer covers. There is no constructor:
41///
42/// ```compile_fail
43/// use cratefield_core::TenantConn;
44/// # use std::sync::Arc;
45/// # fn db() -> Arc<dyn cratefield_core::Database> { unimplemented!() }
46/// // No public constructor: a handler cannot build a handle for a
47/// // tenant it was not given.
48/// let _conn = TenantConn { tenant: todo!(), db: db() };
49/// ```
50///
51/// Naming it is fine, which is what keeps a handler signature writable:
52///
53/// ```
54/// use cratefield_core::TenantConn;
55/// async fn handler(_db: TenantConn) {}
56/// ```
57#[derive(Clone)]
58pub struct TenantConn {
59    tenant: Tenant,
60    tenancy: Tenancy,
61    db: Arc<dyn Database>,
62}
63
64impl TenantConn {
65    /// The tenant this handle belongs to.
66    #[must_use]
67    pub fn tenant(&self) -> &Tenant {
68        &self.tenant
69    }
70
71    /// Which tenancy shape served this request: a registry resolved it,
72    /// or the deployment has no registry and this host is the one
73    /// implicit tenant.
74    ///
75    /// A routing fact, not a membership fact (#385):
76    /// [`Tenancy::FromRegistry`] says the registry named this tenant for
77    /// this host, and nothing about who the caller is to it. A rule that
78    /// needs membership reads this to fail closed under
79    /// [`Tenancy::FromRegistry`] — where "any verified caller" and "a
80    /// member of this tenant" are different sets — and to serve under
81    /// [`Tenancy::Sole`], where they are the same set.
82    #[must_use]
83    pub fn tenancy(&self) -> Tenancy {
84        self.tenancy
85    }
86}
87
88impl std::fmt::Debug for TenantConn {
89    /// Names the tenant and never the handle. `Display for DbError`
90    /// already scrubs a DSN out of a *message* (#135), but `Debug`
91    /// derives raw, and a database handle has no business being printed
92    /// at all — so this does not derive. The tenancy bit is shown: it is
93    /// a routing fact with nothing secret in it, and it is the first
94    /// question anyone debugging a refused request asks — did a registry
95    /// even resolve this tenant (#385)?
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        f.debug_struct("TenantConn")
98            .field("tenant", &self.tenant.id())
99            .field("tenancy", &self.tenancy)
100            .finish_non_exhaustive()
101    }
102}
103
104#[async_trait::async_trait]
105impl Database for TenantConn {
106    async fn execute(&self, stmt: &crate::ports::Statement) -> Result<u64, crate::ports::DbError> {
107        self.db.execute(stmt).await
108    }
109
110    async fn query(
111        &self,
112        stmt: &crate::ports::Statement,
113    ) -> Result<crate::ports::Rows, crate::ports::DbError> {
114        self.db.query(stmt).await
115    }
116
117    async fn batch_atomic(
118        &self,
119        stmts: &[crate::ports::Statement],
120    ) -> Result<(), crate::ports::DbError> {
121        self.db.batch_atomic(stmts).await
122    }
123}
124
125impl<S> FromRequestParts<S> for TenantConn
126where
127    S: Send + Sync,
128{
129    type Rejection = Problem;
130
131    fn from_request_parts(
132        parts: &mut Parts,
133        _state: &S,
134    ) -> impl std::future::Future<Output = Result<Self, Self::Rejection>> + Send {
135        // A 500 and not a 404: reaching a handler that asks for a
136        // `TenantConn` on a route the resolution layer does not cover is
137        // a wiring mistake in the harness, not something the caller did.
138        // Answering 404 would make it look like the caller's problem and
139        // hide it.
140        std::future::ready(
141            parts
142                .extensions
143                .get::<ResolvedTenant>()
144                .map(|resolved| Self {
145                    tenant: resolved.tenant.clone(),
146                    tenancy: resolved.tenancy,
147                    db: Arc::clone(&resolved.db),
148                })
149                .ok_or_else(Problem::internal),
150        )
151    }
152}