Skip to main content

tablo_core/tenancy/
tenant_id.rs

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