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
51    /// create/update/delete/archive/unarchive is recorded there with the full row snapshot,
52    /// action type, timestamp, and actor.
53    #[serde(default)]
54    pub audit_log: bool,
55    /// Row-level versioning: when enabled, a `{table}_history` table is created and a snapshot
56    /// of the row is written there before every UPDATE and DELETE.
57    #[serde(default)]
58    pub versioning: Option<VersioningConfig>,
59    /// When true, this table holds data shared across all RLS tenants instead of being
60    /// tenant-isolated. Under the RLS strategy it gets asymmetric row-level-security policies:
61    /// every tenant may read all rows, but only the Platform Admin tenant
62    /// (see `tenant::platform_tenant_id`) may insert/update/delete. Has no effect under the
63    /// Database strategy (tenants are physically separate databases). Default false.
64    #[serde(default)]
65    pub global: bool,
66}
67
68/// Configuration for row-level versioning on a table.
69#[derive(Clone, Debug, Serialize, Deserialize)]
70pub struct VersioningConfig {
71    pub enabled: bool,
72    /// Maximum number of historical versions to retain per row (None = keep all).
73    /// Must be ≥ 1 when set.
74    #[serde(default)]
75    pub keep_versions: Option<i64>,
76}
77
78#[derive(Clone, Debug, Serialize, Deserialize)]
79#[serde(untagged)]
80pub enum ColumnTypeConfig {
81    Simple(String),
82    Parameterized {
83        name: String,
84        params: Option<Vec<u32>>,
85    },
86}
87
88#[derive(Clone, Debug, Serialize)]
89pub enum ColumnDefaultConfig {
90    Literal(String),
91    Expression { expression: String },
92}
93
94impl<'de> Deserialize<'de> for ColumnDefaultConfig {
95    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
96    where
97        D: Deserializer<'de>,
98    {
99        let v = serde_json::Value::deserialize(deserializer)?;
100        match v {
101            serde_json::Value::String(s) => Ok(ColumnDefaultConfig::Literal(s)),
102            serde_json::Value::Object(mut obj) => {
103                if let Some(serde_json::Value::String(s)) = obj.remove("expression") {
104                    return Ok(ColumnDefaultConfig::Expression { expression: s });
105                }
106                if let Some(serde_json::Value::String(s)) = obj.remove("value").or_else(|| obj.remove("literal")) {
107                    return Ok(ColumnDefaultConfig::Literal(s));
108                }
109                Err(serde::de::Error::custom(format!(
110                    "column default must be a string, {{ \"expression\": \"...\" }}, or {{ \"value\": \"...\" }}; got object with keys: {:?}",
111                    obj.keys().collect::<Vec<_>>()
112                )))
113            }
114            serde_json::Value::Bool(b) => Ok(ColumnDefaultConfig::Literal(b.to_string())),
115            serde_json::Value::Number(n) => Ok(ColumnDefaultConfig::Literal(n.to_string())),
116            other => Err(serde::de::Error::custom(format!(
117                "column default must be a string, boolean, number, or {{ \"expression\": \"...\" }}; got {}",
118                type_name_of_json(&other)
119            ))),
120        }
121    }
122}
123
124fn type_name_of_json(v: &serde_json::Value) -> &'static str {
125    match v {
126        serde_json::Value::Null => "null",
127        serde_json::Value::Bool(_) => "boolean",
128        serde_json::Value::Number(_) => "number",
129        serde_json::Value::String(_) => "string",
130        serde_json::Value::Array(_) => "array",
131        serde_json::Value::Object(_) => "object",
132    }
133}
134
135#[derive(Clone, Debug, Serialize, Deserialize)]
136pub struct ColumnConfig {
137    pub id: String,
138    pub table_id: String,
139    pub name: String,
140    #[serde(rename = "type")]
141    pub type_: ColumnTypeConfig,
142    #[serde(default = "default_true")]
143    pub nullable: bool,
144    #[serde(default)]
145    pub default: Option<ColumnDefaultConfig>,
146    #[serde(default)]
147    pub comment: Option<String>,
148    #[serde(default)]
149    pub asset: Option<AssetColumnConfig>,
150    /// When true, this JSON/JSONB column is an extensible "extensible fields" bag: per-tenant
151    /// field definitions are stored in the KV registry and its keys become RSQL
152    /// filterable/sortable via the `<column>.<key>` dotted syntax. Ignored (with a warning)
153    /// for non-JSON columns.
154    #[serde(default)]
155    pub extensible: bool,
156}
157
158fn default_true() -> bool {
159    true
160}
161
162#[derive(Clone, Debug, Serialize, Deserialize)]
163#[serde(untagged)]
164pub enum IndexColumnEntry {
165    Name(String),
166    Spec {
167        name: String,
168        direction: Option<String>,
169        nulls: Option<String>,
170    },
171    Expression {
172        expression: String,
173    },
174}
175
176#[derive(Clone, Debug, Serialize, Deserialize)]
177pub struct IndexConfig {
178    pub id: String,
179    #[serde(default)]
180    pub schema_id: Option<String>,
181    pub table_id: String,
182    pub name: String,
183    #[serde(default)]
184    pub method: Option<String>,
185    #[serde(default)]
186    pub unique: bool,
187    pub columns: Vec<IndexColumnEntry>,
188    #[serde(default)]
189    pub include: Vec<String>,
190    #[serde(default, rename = "where")]
191    pub where_: Option<String>,
192    #[serde(default)]
193    pub comment: Option<String>,
194}
195
196impl IndexConfig {
197    pub fn where_clause(&self) -> Option<&str> {
198        self.where_.as_deref()
199    }
200}
201
202#[derive(Clone, Debug, Serialize, Deserialize)]
203pub struct RelationshipConfig {
204    pub id: String,
205    /// Defaults to the owning package's schema when absent.
206    #[serde(default)]
207    pub from_schema_id: Option<String>,
208    pub from_table_id: String,
209    pub from_column_id: String,
210    /// When set, this relationship crosses into another installed package.
211    /// The `to_schema_id` and `to_table_id` are resolved from that package's config.
212    #[serde(default)]
213    pub to_package_id: Option<String>,
214    /// Defaults to the owning package's schema when absent (or to the target package's schema
215    /// for cross-package relationships).
216    #[serde(default)]
217    pub to_schema_id: Option<String>,
218    pub to_table_id: String,
219    pub to_column_id: String,
220    #[serde(default)]
221    pub on_update: Option<String>,
222    #[serde(default)]
223    pub on_delete: Option<String>,
224    #[serde(default)]
225    pub name: Option<String>,
226}
227
228#[derive(Clone, Debug, Default, Serialize, Deserialize)]
229pub struct ValidationRule {
230    #[serde(default)]
231    pub required: Option<bool>,
232    #[serde(default)]
233    pub format: Option<String>,
234    #[serde(default)]
235    pub max_length: Option<u32>,
236    #[serde(default)]
237    pub min_length: Option<u32>,
238    #[serde(default)]
239    pub pattern: Option<String>,
240    #[serde(default)]
241    pub allowed: Option<Vec<serde_json::Value>>,
242    #[serde(default)]
243    pub minimum: Option<f64>,
244    #[serde(default)]
245    pub maximum: Option<f64>,
246    // Asset-specific validation (only applied when the column type is "asset")
247    #[serde(default)]
248    pub allowed_mime_types: Option<Vec<String>>,
249    #[serde(default)]
250    pub allowed_extensions: Option<Vec<String>>,
251    #[serde(default)]
252    pub max_size_mb: Option<f64>,
253    #[serde(default)]
254    pub min_size_kb: Option<f64>,
255    #[serde(default)]
256    pub max_filename_length: Option<u32>,
257}
258
259#[derive(Clone, Debug, Serialize, Deserialize)]
260pub struct AssetColumnConfig {
261    /// Path prefix template. Supports {yyyy}, {mm}, {dd}, {hh}, {tenant_id}, {entity}.
262    #[serde(default)]
263    pub prefix: Option<String>,
264    /// Byte-level compression before upload: "none" | "gzip" | "zstd". Default: "none".
265    #[serde(default)]
266    pub compression: Option<String>,
267}
268
269#[derive(Clone, Debug, Serialize, Deserialize)]
270pub struct EventCondition {
271    /// Column name (snake_case) to inspect on the saved row.
272    pub field: String,
273    /// Fire when the field's new value equals this (post-update check).
274    #[serde(default)]
275    pub changed_to: Option<serde_json::Value>,
276    /// Fire when the field's current value equals this.
277    #[serde(default)]
278    pub equals: Option<serde_json::Value>,
279    /// true = fire when field is non-null; false = fire when null.
280    #[serde(default)]
281    pub not_null: Option<bool>,
282}
283
284#[derive(Clone, Debug, Serialize, Deserialize)]
285pub struct EntityEventTrigger {
286    pub id: String,
287    /// Lifecycle hook: "create" | "update" | "delete" | "archive".
288    pub on: String,
289    /// Suffix of the event type sent to decision-hub.
290    /// Defaults to "created" / "updated" / "deleted" / "archived" when omitted.
291    #[serde(default)]
292    pub event_name: Option<String>,
293    /// Only fire when this condition is satisfied against the saved row (snake_case keys).
294    #[serde(default)]
295    pub condition: Option<EventCondition>,
296    /// Related entities to expand into `context.entity`, using the same names as `?include=`.
297    /// Empty (the default) publishes the flat row. Expansion is a separate SELECT run inside the
298    /// detached publish task, so it never adds latency to the originating request.
299    #[serde(default)]
300    pub include: Vec<String>,
301}
302
303/// Configuration for exposing a selected API entity as an MCP tool.
304/// Only takes effect when the `mcp` feature is enabled.
305#[derive(Clone, Debug, Serialize, Deserialize)]
306pub struct McpEntityConfig {
307    /// Opt-in to MCP exposure. Default false.
308    #[serde(default)]
309    pub enabled: bool,
310    /// Subset of the entity's REST operations to expose as MCP tools.
311    /// Defaults to all operations on the entity when omitted.
312    /// Valid values: "list", "read", "create", "update", "delete".
313    #[serde(default)]
314    pub operations: Vec<String>,
315    /// Prefix for generated tool names. Defaults to `path_segment`.
316    #[serde(default)]
317    pub tool_prefix: Option<String>,
318    /// Human-readable description injected into each tool's MCP description.
319    #[serde(default)]
320    pub description: Option<String>,
321}
322
323#[derive(Clone, Debug, Serialize, Deserialize)]
324pub struct ApiEntityConfig {
325    pub entity_id: String,
326    pub path_segment: String,
327    pub operations: Vec<String>,
328    /// Column names that must never be exposed in API responses (e.g. password hashes, secrets).
329    #[serde(default)]
330    pub sensitive_columns: Vec<String>,
331    #[serde(default)]
332    pub validation: std::collections::HashMap<String, ValidationRule>,
333    /// Column whose null→non-null transition signals an archive. Required for on:"archive" triggers.
334    #[serde(default)]
335    pub archive_field: Option<String>,
336    /// Decision-hub event triggers for this entity.
337    #[serde(default)]
338    pub events: Vec<EntityEventTrigger>,
339    /// Column holding the human-readable natural key used to resolve `parentRef` during bulk
340    /// create (e.g. `"location_id"` for locations, `"product_id"` for products). When set, bulk
341    /// create accepts a virtual `parentRef` field; the SDK resolves it to a UUID and writes
342    /// `parent_id` in a second pass after all rows are inserted.
343    #[serde(default)]
344    pub parent_ref_column: Option<String>,
345    /// MCP tool exposure config. Only effective when the `mcp` feature is enabled.
346    #[serde(default)]
347    pub mcp: Option<McpEntityConfig>,
348}
349
350#[derive(Clone, Debug, Serialize, Deserialize)]
351pub struct KvStoreConfig {
352    pub id: String,
353    pub namespace: String,
354    #[serde(default)]
355    pub comment: Option<String>,
356}
357
358/// A single named parameter of a report. Reuses the entity [`ValidationRule`] engine (flattened
359/// into this struct) so `required`/`allowed`/`minimum`/`pattern`/`format`/etc. all apply to the
360/// value the caller supplies at run time. `default` is applied when the param is absent.
361#[derive(Clone, Debug, Serialize, Deserialize)]
362pub struct ReportParam {
363    pub name: String,
364    /// Value used when the caller omits this param. Ignored when the param is required.
365    #[serde(default)]
366    pub default: Option<serde_json::Value>,
367    /// Optional SQL cast applied to the bound value (e.g. "timestamptz", "int", "uuid").
368    /// All params bind as TEXT, so numeric/temporal params need a cast to compare correctly.
369    #[serde(default)]
370    pub db_type: Option<String>,
371    #[serde(flatten)]
372    pub rule: ValidationRule,
373}
374
375/// A read-only reporting query. The SQL is trusted (authored at deploy time, admin-gated on
376/// registration); only the declared params are runtime input. Named params (`:from`, `:to`) are
377/// translated to positional placeholders when the model is resolved. Reports carry no DDL — they
378/// are pure metadata and can be added/updated/removed at runtime without touching the schema.
379#[derive(Clone, Debug, Serialize, Deserialize)]
380pub struct ReportConfig {
381    pub id: String,
382    pub name: String,
383    #[serde(default)]
384    pub description: Option<String>,
385    /// SQL schemas the query references (used for read-only role grants and a fast pre-check).
386    #[serde(default)]
387    pub schemas: Vec<String>,
388    /// Parameterized SQL using named params, e.g. `SELECT ... WHERE created_at >= :from`.
389    pub sql: String,
390    #[serde(default)]
391    pub params: Vec<ReportParam>,
392    /// When true (the default), the SQL is validated with `EXPLAIN` at registration time so
393    /// missing tables/columns fail fast rather than at first run. Set false for reports that
394    /// reference packages installed later (lazy validation at run time).
395    #[serde(default)]
396    pub validate_on_register: Option<bool>,
397    /// Per-report result-cache TTL in seconds. Only used when result caching is enabled globally
398    /// (env `ARCHITECT_REPORT_CACHE`). Falls back to `ARCHITECT_REPORT_CACHE_TTL_SECS` when unset;
399    /// a value of 0 disables caching for this report even when the global flag is on.
400    #[serde(default)]
401    pub cache_ttl_secs: Option<i64>,
402}
403
404/// All config types in one struct for in-memory loading.
405#[derive(Clone, Debug, Default)]
406pub struct FullConfig {
407    pub schemas: Vec<SchemaConfig>,
408    pub enums: Vec<EnumConfig>,
409    pub tables: Vec<TableConfig>,
410    pub columns: Vec<ColumnConfig>,
411    pub indexes: Vec<IndexConfig>,
412    pub relationships: Vec<RelationshipConfig>,
413    pub api_entities: Vec<ApiEntityConfig>,
414    pub kv_stores: Vec<KvStoreConfig>,
415    pub reports: Vec<ReportConfig>,
416}