Skip to main content

tablo_core/tenancy/
tenant_id.rs

1//! The tenant column type.
2
3use toasty::{
4    codegen_support::index::IndexableField,
5    schema::{Field, Load},
6    stmt::{Assign, Assignment, Expr, IntoExpr, List, Path},
7};
8use toasty_core::{
9    Error,
10    stmt::{self, Assignments, Projection, Value},
11};
12
13/// One tenant's id, and the field type of a resource's tenant column.
14///
15/// A model declares its tenant column with this type:
16///
17/// ```
18/// # use tablo_core::TenantId;
19/// #[derive(Debug, Clone, toasty::Model)]
20/// pub struct Note {
21///     #[key]
22///     #[auto]
23///     id: uuid::Uuid,
24///     #[index]
25///     tenant_id: TenantId,
26///     body: String,
27/// }
28/// ```
29///
30/// [`Tenancy::column`](crate::Tenancy::column) and
31/// [`Tenancy::via`](crate::Tenancy::via) accept only a [`TenantColumn`], so a lens naming any
32/// other `Uuid` column — an author, a parent row, the key — does not compile. The column stores a
33/// UUID, so the database column is unchanged.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
35pub struct TenantId(uuid::Uuid);
36
37impl TenantId {
38    /// The id, as [`tenant_id`](crate::tenant_id) answers it and a session carries it.
39    pub fn get(self) -> uuid::Uuid {
40        self.0
41    }
42}
43
44impl From<uuid::Uuid> for TenantId {
45    fn from(id: uuid::Uuid) -> Self {
46        Self(id)
47    }
48}
49
50impl std::fmt::Display for TenantId {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        self.0.fmt(f)
53    }
54}
55
56impl std::str::FromStr for TenantId {
57    type Err = uuid::Error;
58
59    fn from_str(text: &str) -> Result<Self, Self::Err> {
60        uuid::Uuid::from_str(text).map(Self)
61    }
62}
63
64impl Load for TenantId {
65    type Output = Self;
66
67    fn ty() -> stmt::Type {
68        stmt::Type::Uuid
69    }
70
71    fn load(value: stmt::Value) -> Result<Self::Output, Error> {
72        match value {
73            stmt::Value::Uuid(id) => Ok(Self(id)),
74            _ => Err(Error::type_conversion(value, "TenantId")),
75        }
76    }
77
78    fn reload(target: &mut Self::Output, value: stmt::Value) -> Result<(), Error> {
79        *target = Self::load(value)?;
80        Ok(())
81    }
82}
83
84impl Field for TenantId {
85    type ExprTarget = Self;
86    type Path<Origin> = Path<Origin, Self>;
87    type ListPath<Origin> = Path<Origin, List<Self::ExprTarget>>;
88    type Update<'a> = ();
89    type Inner = Self;
90
91    fn new_path<Origin>(path: Path<Origin, Self>) -> Self::Path<Origin> {
92        path
93    }
94
95    fn new_list_path<Origin>(path: Path<Origin, List<Self::ExprTarget>>) -> Self::ListPath<Origin> {
96        path
97    }
98
99    fn new_update<'a>(
100        _assignments: &'a mut Assignments,
101        _projection: Projection,
102    ) -> Self::Update<'a> {
103    }
104
105    fn key_constraint<Origin>(&self, target: Path<Origin, Self::Inner>) -> Expr<bool> {
106        target.eq(*self)
107    }
108}
109
110impl IndexableField for TenantId {}
111
112impl IntoExpr<TenantId> for TenantId {
113    fn into_expr(self) -> Expr<TenantId> {
114        tenant_expr(self.0)
115    }
116
117    fn by_ref(&self) -> Expr<TenantId> {
118        tenant_expr(self.0)
119    }
120}
121
122impl Assign<TenantId> for TenantId {
123    fn into_assignment(self) -> Assignment<TenantId> {
124        toasty::stmt::set(IntoExpr::<TenantId>::into_expr(self))
125    }
126}
127
128fn tenant_expr(id: uuid::Uuid) -> Expr<TenantId> {
129    Expr::from_untyped(stmt::Expr::Value(Value::Uuid(id)))
130}
131
132/// A field type a tenancy names: [`TenantId`], or `Option<TenantId>` for a
133/// nullable column.
134///
135/// Implement it for an app's own newtype to name that instead. `uuid::Uuid` cannot implement it:
136/// neither the trait nor the type belongs to the app, so an unmarked UUID column can never be
137/// named as a tenant column.
138///
139/// ```compile_fail,E0277
140/// # use tablo_core::{Tenancy, TenantId};
141/// #[derive(Debug, Clone, toasty::Model)]
142/// struct Post {
143///     #[key]
144///     #[auto]
145///     id: uuid::Uuid,
146///     #[index]
147///     tenant_id: TenantId,
148///     author_id: uuid::Uuid,
149/// }
150///
151/// // `author_id` is a `Uuid`: the lens names no tenant column.
152/// let _ = Tenancy::column(Post::fields().author_id());
153/// ```
154#[diagnostic::on_unimplemented(
155    message = "`{Self}` is not a tenant column",
156    label = "not a tenant column",
157    note = "type the model's tenant field as `TenantId`, or `Option<TenantId>` for a nullable one"
158)]
159pub trait TenantColumn: Sized + Field + IntoExpr<Self> + Send + Sync + 'static {
160    /// The column's value for the request tenant `id`.
161    fn from_tenant(id: uuid::Uuid) -> Self;
162}
163
164impl TenantColumn for TenantId {
165    fn from_tenant(id: uuid::Uuid) -> Self {
166        Self(id)
167    }
168}
169
170impl TenantColumn for Option<TenantId> {
171    fn from_tenant(id: uuid::Uuid) -> Self {
172        Some(TenantId(id))
173    }
174}