Skip to main content

jamjet_state/
tenant.rs

1//! Multi-tenant isolation types and constants.
2//!
3//! All tenant-scoped data is partitioned by `TenantId`. The default
4//! tenant (`"default"`) ensures backward compatibility with single-tenant
5//! deployments.
6
7use serde::{Deserialize, Serialize};
8
9/// The default tenant for single-tenant and backward-compatible deployments.
10pub const DEFAULT_TENANT: &str = "default";
11
12/// Strongly typed tenant identifier.
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct TenantId(pub String);
15
16impl TenantId {
17    pub fn default_tenant() -> Self {
18        Self(DEFAULT_TENANT.to_string())
19    }
20
21    pub fn as_str(&self) -> &str {
22        &self.0
23    }
24}
25
26impl Default for TenantId {
27    fn default() -> Self {
28        Self::default_tenant()
29    }
30}
31
32impl std::fmt::Display for TenantId {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        write!(f, "{}", self.0)
35    }
36}
37
38impl From<&str> for TenantId {
39    fn from(s: &str) -> Self {
40        Self(s.to_string())
41    }
42}
43
44impl From<String> for TenantId {
45    fn from(s: String) -> Self {
46        Self(s)
47    }
48}
49
50/// Tenant metadata stored in the `tenants` table.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct Tenant {
53    pub id: TenantId,
54    pub name: String,
55    pub status: TenantStatus,
56    /// Per-tenant policy set (JSON, applied between global and workflow policies).
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub policy: Option<serde_json::Value>,
59    /// Per-tenant resource limits.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub limits: Option<TenantLimits>,
62    pub created_at: chrono::DateTime<chrono::Utc>,
63    pub updated_at: chrono::DateTime<chrono::Utc>,
64}
65
66/// Tenant lifecycle status.
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum TenantStatus {
70    Active,
71    Suspended,
72    Archived,
73}
74
75impl TenantStatus {
76    pub fn as_str(&self) -> &'static str {
77        match self {
78            Self::Active => "active",
79            Self::Suspended => "suspended",
80            Self::Archived => "archived",
81        }
82    }
83
84    pub fn parse(s: &str) -> Self {
85        match s {
86            "suspended" => Self::Suspended,
87            "archived" => Self::Archived,
88            _ => Self::Active,
89        }
90    }
91}
92
93impl Tenant {
94    /// Deserialize the JSON policy field into a `PolicySetIr`.
95    ///
96    /// Returns `None` if no policy is set or if deserialization fails.
97    pub fn policy_set(&self) -> Option<jamjet_ir::workflow::PolicySetIr> {
98        self.policy
99            .as_ref()
100            .and_then(|v| serde_json::from_value(v.clone()).ok())
101    }
102}
103
104/// Per-tenant resource and cost limits.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct TenantLimits {
107    /// Maximum concurrent workflow executions for this tenant.
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub max_concurrent_executions: Option<u32>,
110    /// Maximum workflow definitions this tenant can create.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub max_workflows: Option<u32>,
113    /// Monthly token budget.
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub max_tokens_per_month: Option<u64>,
116    /// Monthly cost budget in USD.
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub max_cost_per_month_usd: Option<f64>,
119}