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