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    /// Row-level versioning: when enabled, a `{table}_history` table is created and a snapshot
55    /// of the row is written there before every UPDATE and DELETE.
56    #[serde(default)]
57    pub versioning: Option<VersioningConfig>,
58}
59
60/// Configuration for row-level versioning on a table.
61#[derive(Clone, Debug, Serialize, Deserialize)]
62pub struct VersioningConfig {
63    pub enabled: bool,
64    /// Maximum number of historical versions to retain per row (None = keep all).
65    /// Must be ≥ 1 when set.
66    #[serde(default)]
67    pub keep_versions: Option<i64>,
68}
69
70#[derive(Clone, Debug, Serialize, Deserialize)]
71#[serde(untagged)]
72pub enum ColumnTypeConfig {
73    Simple(String),
74    Parameterized {
75        name: String,
76        params: Option<Vec<u32>>,
77    },
78}
79
80#[derive(Clone, Debug, Serialize)]
81pub enum ColumnDefaultConfig {
82    Literal(String),
83    Expression { expression: String },
84}
85
86impl<'de> Deserialize<'de> for ColumnDefaultConfig {
87    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
88    where
89        D: Deserializer<'de>,
90    {
91        let v = serde_json::Value::deserialize(deserializer)?;
92        match v {
93            serde_json::Value::String(s) => Ok(ColumnDefaultConfig::Literal(s)),
94            serde_json::Value::Object(mut obj) => {
95                if let Some(serde_json::Value::String(s)) = obj.remove("expression") {
96                    return Ok(ColumnDefaultConfig::Expression { expression: s });
97                }
98                if let Some(serde_json::Value::String(s)) = obj.remove("value").or_else(|| obj.remove("literal")) {
99                    return Ok(ColumnDefaultConfig::Literal(s));
100                }
101                Err(serde::de::Error::custom(format!(
102                    "column default must be a string, {{ \"expression\": \"...\" }}, or {{ \"value\": \"...\" }}; got object with keys: {:?}",
103                    obj.keys().collect::<Vec<_>>()
104                )))
105            }
106            serde_json::Value::Bool(b) => Ok(ColumnDefaultConfig::Literal(b.to_string())),
107            serde_json::Value::Number(n) => Ok(ColumnDefaultConfig::Literal(n.to_string())),
108            other => Err(serde::de::Error::custom(format!(
109                "column default must be a string, boolean, number, or {{ \"expression\": \"...\" }}; got {}",
110                type_name_of_json(&other)
111            ))),
112        }
113    }
114}
115
116fn type_name_of_json(v: &serde_json::Value) -> &'static str {
117    match v {
118        serde_json::Value::Null => "null",
119        serde_json::Value::Bool(_) => "boolean",
120        serde_json::Value::Number(_) => "number",
121        serde_json::Value::String(_) => "string",
122        serde_json::Value::Array(_) => "array",
123        serde_json::Value::Object(_) => "object",
124    }
125}
126
127#[derive(Clone, Debug, Serialize, Deserialize)]
128pub struct ColumnConfig {
129    pub id: String,
130    pub table_id: String,
131    pub name: String,
132    #[serde(rename = "type")]
133    pub type_: ColumnTypeConfig,
134    #[serde(default = "default_true")]
135    pub nullable: bool,
136    #[serde(default)]
137    pub default: Option<ColumnDefaultConfig>,
138    #[serde(default)]
139    pub comment: Option<String>,
140    #[serde(default)]
141    pub asset: Option<AssetColumnConfig>,
142    /// When true, this JSON/JSONB column is an extensible "extensible fields" bag: per-tenant
143    /// field definitions are stored in the KV registry and its keys become RSQL
144    /// filterable/sortable via the `<column>.<key>` dotted syntax. Ignored (with a warning)
145    /// for non-JSON columns.
146    #[serde(default)]
147    pub extensible: bool,
148}
149
150fn default_true() -> bool {
151    true
152}
153
154#[derive(Clone, Debug, Serialize, Deserialize)]
155#[serde(untagged)]
156pub enum IndexColumnEntry {
157    Name(String),
158    Spec {
159        name: String,
160        direction: Option<String>,
161        nulls: Option<String>,
162    },
163    Expression {
164        expression: String,
165    },
166}
167
168#[derive(Clone, Debug, Serialize, Deserialize)]
169pub struct IndexConfig {
170    pub id: String,
171    #[serde(default)]
172    pub schema_id: Option<String>,
173    pub table_id: String,
174    pub name: String,
175    #[serde(default)]
176    pub method: Option<String>,
177    #[serde(default)]
178    pub unique: bool,
179    pub columns: Vec<IndexColumnEntry>,
180    #[serde(default)]
181    pub include: Vec<String>,
182    #[serde(default, rename = "where")]
183    pub where_: Option<String>,
184    #[serde(default)]
185    pub comment: Option<String>,
186}
187
188impl IndexConfig {
189    pub fn where_clause(&self) -> Option<&str> {
190        self.where_.as_deref()
191    }
192}
193
194#[derive(Clone, Debug, Serialize, Deserialize)]
195pub struct RelationshipConfig {
196    pub id: String,
197    /// Defaults to the owning package's schema when absent.
198    #[serde(default)]
199    pub from_schema_id: Option<String>,
200    pub from_table_id: String,
201    pub from_column_id: String,
202    /// When set, this relationship crosses into another installed package.
203    /// The `to_schema_id` and `to_table_id` are resolved from that package's config.
204    #[serde(default)]
205    pub to_package_id: Option<String>,
206    /// Defaults to the owning package's schema when absent (or to the target package's schema
207    /// for cross-package relationships).
208    #[serde(default)]
209    pub to_schema_id: Option<String>,
210    pub to_table_id: String,
211    pub to_column_id: String,
212    #[serde(default)]
213    pub on_update: Option<String>,
214    #[serde(default)]
215    pub on_delete: Option<String>,
216    #[serde(default)]
217    pub name: Option<String>,
218}
219
220#[derive(Clone, Debug, Default, Serialize, Deserialize)]
221pub struct ValidationRule {
222    #[serde(default)]
223    pub required: Option<bool>,
224    #[serde(default)]
225    pub format: Option<String>,
226    #[serde(default)]
227    pub max_length: Option<u32>,
228    #[serde(default)]
229    pub min_length: Option<u32>,
230    #[serde(default)]
231    pub pattern: Option<String>,
232    #[serde(default)]
233    pub allowed: Option<Vec<serde_json::Value>>,
234    #[serde(default)]
235    pub minimum: Option<f64>,
236    #[serde(default)]
237    pub maximum: Option<f64>,
238    // Asset-specific validation (only applied when the column type is "asset")
239    #[serde(default)]
240    pub allowed_mime_types: Option<Vec<String>>,
241    #[serde(default)]
242    pub allowed_extensions: Option<Vec<String>>,
243    #[serde(default)]
244    pub max_size_mb: Option<f64>,
245    #[serde(default)]
246    pub min_size_kb: Option<f64>,
247    #[serde(default)]
248    pub max_filename_length: Option<u32>,
249}
250
251#[derive(Clone, Debug, Serialize, Deserialize)]
252pub struct AssetColumnConfig {
253    /// Path prefix template. Supports {yyyy}, {mm}, {dd}, {hh}, {tenant_id}, {entity}.
254    #[serde(default)]
255    pub prefix: Option<String>,
256    /// Byte-level compression before upload: "none" | "gzip" | "zstd". Default: "none".
257    #[serde(default)]
258    pub compression: Option<String>,
259}
260
261#[derive(Clone, Debug, Serialize, Deserialize)]
262pub struct EventCondition {
263    /// Column name (snake_case) to inspect on the saved row.
264    pub field: String,
265    /// Fire when the field's new value equals this (post-update check).
266    #[serde(default)]
267    pub changed_to: Option<serde_json::Value>,
268    /// Fire when the field's current value equals this.
269    #[serde(default)]
270    pub equals: Option<serde_json::Value>,
271    /// true = fire when field is non-null; false = fire when null.
272    #[serde(default)]
273    pub not_null: Option<bool>,
274}
275
276#[derive(Clone, Debug, Serialize, Deserialize)]
277pub struct EntityEventTrigger {
278    pub id: String,
279    /// Lifecycle hook: "create" | "update" | "delete" | "archive".
280    pub on: String,
281    /// Suffix of the event type sent to decision-hub.
282    /// Defaults to "created" / "updated" / "deleted" / "archived" when omitted.
283    #[serde(default)]
284    pub event_name: Option<String>,
285    /// Only fire when this condition is satisfied against the saved row (snake_case keys).
286    #[serde(default)]
287    pub condition: Option<EventCondition>,
288}
289
290/// Configuration for exposing a selected API entity as an MCP tool.
291/// Only takes effect when the `mcp` feature is enabled.
292#[derive(Clone, Debug, Serialize, Deserialize)]
293pub struct McpEntityConfig {
294    /// Opt-in to MCP exposure. Default false.
295    #[serde(default)]
296    pub enabled: bool,
297    /// Subset of the entity's REST operations to expose as MCP tools.
298    /// Defaults to all operations on the entity when omitted.
299    /// Valid values: "list", "read", "create", "update", "delete".
300    #[serde(default)]
301    pub operations: Vec<String>,
302    /// Prefix for generated tool names. Defaults to `path_segment`.
303    #[serde(default)]
304    pub tool_prefix: Option<String>,
305    /// Human-readable description injected into each tool's MCP description.
306    #[serde(default)]
307    pub description: Option<String>,
308}
309
310#[derive(Clone, Debug, Serialize, Deserialize)]
311pub struct ApiEntityConfig {
312    pub entity_id: String,
313    pub path_segment: String,
314    pub operations: Vec<String>,
315    /// Column names that must never be exposed in API responses (e.g. password hashes, secrets).
316    #[serde(default)]
317    pub sensitive_columns: Vec<String>,
318    #[serde(default)]
319    pub validation: std::collections::HashMap<String, ValidationRule>,
320    /// Column whose null→non-null transition signals an archive. Required for on:"archive" triggers.
321    #[serde(default)]
322    pub archive_field: Option<String>,
323    /// Decision-hub event triggers for this entity.
324    #[serde(default)]
325    pub events: Vec<EntityEventTrigger>,
326    /// Column holding the human-readable natural key used to resolve `parentRef` during bulk
327    /// create (e.g. `"location_id"` for locations, `"product_id"` for products). When set, bulk
328    /// create accepts a virtual `parentRef` field; the SDK resolves it to a UUID and writes
329    /// `parent_id` in a second pass after all rows are inserted.
330    #[serde(default)]
331    pub parent_ref_column: Option<String>,
332    /// MCP tool exposure config. Only effective when the `mcp` feature is enabled.
333    #[serde(default)]
334    pub mcp: Option<McpEntityConfig>,
335}
336
337#[derive(Clone, Debug, Serialize, Deserialize)]
338pub struct KvStoreConfig {
339    pub id: String,
340    pub namespace: String,
341    #[serde(default)]
342    pub comment: Option<String>,
343}
344
345/// All config types in one struct for in-memory loading.
346#[derive(Clone, Debug, Default)]
347pub struct FullConfig {
348    pub schemas: Vec<SchemaConfig>,
349    pub enums: Vec<EnumConfig>,
350    pub tables: Vec<TableConfig>,
351    pub columns: Vec<ColumnConfig>,
352    pub indexes: Vec<IndexConfig>,
353    pub relationships: Vec<RelationshipConfig>,
354    pub api_entities: Vec<ApiEntityConfig>,
355    pub kv_stores: Vec<KvStoreConfig>,
356}