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}