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