Skip to main content

architect_sdk/config/
types.rs

1//! Raw config types matching the JSON schema (postgres-config-schema + api_entities).
2
3use serde::{Deserialize, Deserializer, Serialize};
4
5#[derive(Clone, Debug, Serialize, Deserialize)]
6pub struct SchemaConfig {
7    pub id: String,
8    pub name: String,
9    #[serde(default)]
10    pub comment: Option<String>,
11}
12
13#[derive(Clone, Debug, Serialize, Deserialize)]
14pub struct EnumConfig {
15    pub id: String,
16    #[serde(default)]
17    pub schema_id: Option<String>,
18    pub name: String,
19    pub values: Vec<String>,
20    #[serde(default)]
21    pub comment: Option<String>,
22}
23
24#[derive(Clone, Debug, Serialize, Deserialize)]
25pub struct TableCheck {
26    pub name: String,
27    pub expression: String,
28}
29
30#[derive(Clone, Debug, Serialize, Deserialize)]
31#[serde(untagged)]
32pub enum PrimaryKeyConfig {
33    Single(String),
34    Composite(Vec<String>),
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize)]
38pub struct TableConfig {
39    pub id: String,
40    #[serde(default)]
41    pub schema_id: Option<String>,
42    pub name: String,
43    #[serde(default)]
44    pub comment: Option<String>,
45    pub primary_key: PrimaryKeyConfig,
46    #[serde(default)]
47    pub unique: Vec<Vec<String>>,
48    #[serde(default)]
49    pub check: Vec<TableCheck>,
50    /// When true, a companion `{table}_audit` table is created and every create/update/delete
51    /// is recorded there with the full row snapshot, action type, timestamp, and actor.
52    #[serde(default)]
53    pub audit_log: bool,
54}
55
56#[derive(Clone, Debug, Serialize, Deserialize)]
57#[serde(untagged)]
58pub enum ColumnTypeConfig {
59    Simple(String),
60    Parameterized {
61        name: String,
62        params: Option<Vec<u32>>,
63    },
64}
65
66#[derive(Clone, Debug, Serialize)]
67pub enum ColumnDefaultConfig {
68    Literal(String),
69    Expression { expression: String },
70}
71
72impl<'de> Deserialize<'de> for ColumnDefaultConfig {
73    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
74    where
75        D: Deserializer<'de>,
76    {
77        let v = serde_json::Value::deserialize(deserializer)?;
78        match v {
79            serde_json::Value::String(s) => Ok(ColumnDefaultConfig::Literal(s)),
80            serde_json::Value::Object(mut obj) => {
81                if let Some(serde_json::Value::String(s)) = obj.remove("expression") {
82                    return Ok(ColumnDefaultConfig::Expression { expression: s });
83                }
84                if let Some(serde_json::Value::String(s)) = obj.remove("value").or_else(|| obj.remove("literal")) {
85                    return Ok(ColumnDefaultConfig::Literal(s));
86                }
87                Err(serde::de::Error::custom(format!(
88                    "column default must be a string, {{ \"expression\": \"...\" }}, or {{ \"value\": \"...\" }}; got object with keys: {:?}",
89                    obj.keys().collect::<Vec<_>>()
90                )))
91            }
92            serde_json::Value::Bool(b) => Ok(ColumnDefaultConfig::Literal(b.to_string())),
93            serde_json::Value::Number(n) => Ok(ColumnDefaultConfig::Literal(n.to_string())),
94            other => Err(serde::de::Error::custom(format!(
95                "column default must be a string, boolean, number, or {{ \"expression\": \"...\" }}; got {}",
96                type_name_of_json(&other)
97            ))),
98        }
99    }
100}
101
102fn type_name_of_json(v: &serde_json::Value) -> &'static str {
103    match v {
104        serde_json::Value::Null => "null",
105        serde_json::Value::Bool(_) => "boolean",
106        serde_json::Value::Number(_) => "number",
107        serde_json::Value::String(_) => "string",
108        serde_json::Value::Array(_) => "array",
109        serde_json::Value::Object(_) => "object",
110    }
111}
112
113#[derive(Clone, Debug, Serialize, Deserialize)]
114pub struct ColumnConfig {
115    pub id: String,
116    pub table_id: String,
117    pub name: String,
118    #[serde(rename = "type")]
119    pub type_: ColumnTypeConfig,
120    #[serde(default = "default_true")]
121    pub nullable: bool,
122    #[serde(default)]
123    pub default: Option<ColumnDefaultConfig>,
124    #[serde(default)]
125    pub comment: Option<String>,
126    #[serde(default)]
127    pub asset: Option<AssetColumnConfig>,
128}
129
130fn default_true() -> bool {
131    true
132}
133
134#[derive(Clone, Debug, Serialize, Deserialize)]
135#[serde(untagged)]
136pub enum IndexColumnEntry {
137    Name(String),
138    Spec {
139        name: String,
140        direction: Option<String>,
141        nulls: Option<String>,
142    },
143    Expression {
144        expression: String,
145    },
146}
147
148#[derive(Clone, Debug, Serialize, Deserialize)]
149pub struct IndexConfig {
150    pub id: String,
151    #[serde(default)]
152    pub schema_id: Option<String>,
153    pub table_id: String,
154    pub name: String,
155    #[serde(default)]
156    pub method: Option<String>,
157    #[serde(default)]
158    pub unique: bool,
159    pub columns: Vec<IndexColumnEntry>,
160    #[serde(default)]
161    pub include: Vec<String>,
162    #[serde(default, rename = "where")]
163    pub where_: Option<String>,
164    #[serde(default)]
165    pub comment: Option<String>,
166}
167
168impl IndexConfig {
169    pub fn where_clause(&self) -> Option<&str> {
170        self.where_.as_deref()
171    }
172}
173
174#[derive(Clone, Debug, Serialize, Deserialize)]
175pub struct RelationshipConfig {
176    pub id: String,
177    pub from_schema_id: String,
178    pub from_table_id: String,
179    pub from_column_id: String,
180    pub to_schema_id: String,
181    pub to_table_id: String,
182    pub to_column_id: String,
183    #[serde(default)]
184    pub on_update: Option<String>,
185    #[serde(default)]
186    pub on_delete: Option<String>,
187    #[serde(default)]
188    pub name: Option<String>,
189}
190
191#[derive(Clone, Debug, Default, Serialize, Deserialize)]
192pub struct ValidationRule {
193    #[serde(default)]
194    pub required: Option<bool>,
195    #[serde(default)]
196    pub format: Option<String>,
197    #[serde(default)]
198    pub max_length: Option<u32>,
199    #[serde(default)]
200    pub min_length: Option<u32>,
201    #[serde(default)]
202    pub pattern: Option<String>,
203    #[serde(default)]
204    pub allowed: Option<Vec<serde_json::Value>>,
205    #[serde(default)]
206    pub minimum: Option<f64>,
207    #[serde(default)]
208    pub maximum: Option<f64>,
209    // Asset-specific validation (only applied when the column type is "asset")
210    #[serde(default)]
211    pub allowed_mime_types: Option<Vec<String>>,
212    #[serde(default)]
213    pub allowed_extensions: Option<Vec<String>>,
214    #[serde(default)]
215    pub max_size_mb: Option<f64>,
216    #[serde(default)]
217    pub min_size_kb: Option<f64>,
218    #[serde(default)]
219    pub max_filename_length: Option<u32>,
220}
221
222#[derive(Clone, Debug, Serialize, Deserialize)]
223pub struct AssetColumnConfig {
224    /// Path prefix template. Supports {yyyy}, {mm}, {dd}, {hh}, {tenant_id}, {entity}.
225    #[serde(default)]
226    pub prefix: Option<String>,
227    /// Byte-level compression before upload: "none" | "gzip" | "zstd". Default: "none".
228    #[serde(default)]
229    pub compression: Option<String>,
230}
231
232#[derive(Clone, Debug, Serialize, Deserialize)]
233pub struct EventCondition {
234    /// Column name (snake_case) to inspect on the saved row.
235    pub field: String,
236    /// Fire when the field's new value equals this (post-update check).
237    #[serde(default)]
238    pub changed_to: Option<serde_json::Value>,
239    /// Fire when the field's current value equals this.
240    #[serde(default)]
241    pub equals: Option<serde_json::Value>,
242    /// true = fire when field is non-null; false = fire when null.
243    #[serde(default)]
244    pub not_null: Option<bool>,
245}
246
247#[derive(Clone, Debug, Serialize, Deserialize)]
248pub struct EntityEventTrigger {
249    pub id: String,
250    /// Lifecycle hook: "create" | "update" | "delete" | "archive".
251    pub on: String,
252    /// Suffix of the event type sent to decision-hub.
253    /// Defaults to "created" / "updated" / "deleted" / "archived" when omitted.
254    #[serde(default)]
255    pub event_name: Option<String>,
256    /// Only fire when this condition is satisfied against the saved row (snake_case keys).
257    #[serde(default)]
258    pub condition: Option<EventCondition>,
259}
260
261#[derive(Clone, Debug, Serialize, Deserialize)]
262pub struct ApiEntityConfig {
263    pub entity_id: String,
264    pub path_segment: String,
265    pub operations: Vec<String>,
266    /// Column names that must never be exposed in API responses (e.g. password hashes, secrets).
267    #[serde(default)]
268    pub sensitive_columns: Vec<String>,
269    #[serde(default)]
270    pub validation: std::collections::HashMap<String, ValidationRule>,
271    /// Column whose null→non-null transition signals an archive. Required for on:"archive" triggers.
272    #[serde(default)]
273    pub archive_field: Option<String>,
274    /// Decision-hub event triggers for this entity.
275    #[serde(default)]
276    pub events: Vec<EntityEventTrigger>,
277    /// Column holding the human-readable natural key used to resolve `parentRef` during bulk
278    /// create (e.g. `"location_id"` for locations, `"product_id"` for products). When set, bulk
279    /// create accepts a virtual `parentRef` field; the SDK resolves it to a UUID and writes
280    /// `parent_id` in a second pass after all rows are inserted.
281    #[serde(default)]
282    pub parent_ref_column: Option<String>,
283}
284
285#[derive(Clone, Debug, Serialize, Deserialize)]
286pub struct KvStoreConfig {
287    pub id: String,
288    pub namespace: String,
289    #[serde(default)]
290    pub comment: Option<String>,
291}
292
293/// All config types in one struct for in-memory loading.
294#[derive(Clone, Debug, Default)]
295pub struct FullConfig {
296    pub schemas: Vec<SchemaConfig>,
297    pub enums: Vec<EnumConfig>,
298    pub tables: Vec<TableConfig>,
299    pub columns: Vec<ColumnConfig>,
300    pub indexes: Vec<IndexConfig>,
301    pub relationships: Vec<RelationshipConfig>,
302    pub api_entities: Vec<ApiEntityConfig>,
303    pub kv_stores: Vec<KvStoreConfig>,
304}