Skip to main content

architect_sdk/config/
resolved.rs

1//! Resolved entity model: config validated and flattened for runtime use.
2
3use crate::config::types::{
4    AssetColumnConfig, EntityEventTrigger, McpEntityConfig, VersioningConfig,
5};
6use crate::config::ValidationRule;
7use std::collections::{HashMap, HashSet};
8
9/// Direction of a related-include: to_one (we have FK to them) or to_many (they have FK to us).
10#[derive(Clone, Debug)]
11pub enum IncludeDirection {
12    ToOne,
13    ToMany,
14}
15
16/// Spec for including a related entity in list/read responses. Name is the related entity's path_segment (e.g. "orders", "users").
17#[derive(Clone, Debug)]
18pub struct IncludeSpec {
19    /// API name for the include (path_segment of the related entity).
20    pub name: String,
21    pub direction: IncludeDirection,
22    /// Path segment of the related entity (for lookup in model).
23    pub related_path_segment: String,
24    /// Our column used in the join (our FK for to_one; our PK for to_many).
25    pub our_key_column: String,
26    /// Their column used in the join (their PK for to_one; their FK for to_many).
27    pub their_key_column: String,
28}
29
30/// Primary key type for parsing path/body ids.
31#[derive(Clone, Debug)]
32pub enum PkType {
33    Uuid,
34    BigInt,
35    Int,
36    Text,
37}
38
39#[derive(Clone, Debug)]
40pub struct ColumnInfo {
41    pub name: String,
42    pub pk_type: Option<PkType>,
43    pub nullable: bool,
44    /// Whether the column has a DB default (e.g. gen_random_uuid(), NOW()).
45    pub has_default: bool,
46    /// PostgreSQL type name for SQL casts (e.g. "timestamptz") when binding string values.
47    pub pg_type: Option<String>,
48    /// True when the column was declared with type "asset" or "asset[]".
49    pub is_asset: bool,
50    /// True when the column was declared with type "asset[]" (stores a JSONB array of paths).
51    pub asset_is_array: bool,
52    /// Storage config for asset columns (prefix template, compression).
53    pub asset_config: Option<AssetColumnConfig>,
54}
55
56#[derive(Clone, Debug)]
57pub struct ResolvedEntity {
58    pub table_id: String,
59    pub schema_name: String,
60    pub table_name: String,
61    pub path_segment: String,
62    pub pk_columns: Vec<String>,
63    pub pk_type: PkType,
64    pub columns: Vec<ColumnInfo>,
65    pub operations: Vec<String>,
66    /// Column names to strip from all API responses (sensitive data).
67    pub sensitive_columns: HashSet<String>,
68    /// Available includes (related entities) for ?include= name1,name2. Built from relationships.
69    pub includes: Vec<IncludeSpec>,
70    pub validation: HashMap<String, ValidationRule>,
71    /// Decision-hub event triggers. Empty when no events are configured.
72    pub events: Vec<EntityEventTrigger>,
73    /// Column whose null→non-null transition signals an archive (for on:"archive" triggers).
74    pub archive_field: Option<String>,
75    /// Package id this entity belongs to. Set via ResolvedModel::with_package_id().
76    pub package_id: String,
77    /// When true, a companion `{table}_audit` table exists and every write is journaled there.
78    pub audit_log: bool,
79    /// When true, this entity's table is shared across all RLS tenants: every tenant may read it,
80    /// but only the Platform Admin tenant may write. Carried from `TableConfig.global`. Writes by
81    /// non-admin tenants are rejected with 403 in handlers (and blocked by RLS at the DB level).
82    pub global: bool,
83    /// Natural-key column used to resolve `parentRef` in bulk create (e.g. `"location_id"`).
84    pub parent_ref_column: Option<String>,
85    /// Row-level versioning config, carried from TableConfig.
86    pub versioning: Option<VersioningConfig>,
87    /// MCP exposure config, carried from ApiEntityConfig. None when not set.
88    pub mcp: Option<McpEntityConfig>,
89    /// Names of JSON/JSONB columns flagged `extensible: true`. Each is a extensible-fields bag
90    /// whose per-tenant field definitions live in the KV registry and whose keys are
91    /// RSQL-filterable/sortable via the `<column>.<key>` syntax. Empty when none configured.
92    pub extensible_columns: Vec<String>,
93}
94
95/// A report whose named params have been translated to positional placeholders and whose
96/// validation rules/defaults are indexed by param name. Built from [`crate::config::types::ReportConfig`]
97/// during `resolve()`. Reports produce no [`ResolvedEntity`] — they are data-plane only, looked up
98/// by id when a run request arrives.
99#[derive(Clone, Debug)]
100pub struct ResolvedReport {
101    pub id: String,
102    pub name: String,
103    pub description: Option<String>,
104    /// Package id this report belongs to. Set via ResolvedModel::with_package_id().
105    pub package_id: String,
106    /// SQL schemas the query references.
107    pub schemas: Vec<String>,
108    /// SQL with named params already translated to positional placeholders (`$1`, `$2`, …).
109    pub sql: String,
110    /// Param names in positional order (index 0 → `$1`). Repeated named params are deduplicated
111    /// and share a single placeholder.
112    pub param_order: Vec<String>,
113    /// Validation rules per param name (reuses the entity ValidationRule engine).
114    pub rules: HashMap<String, ValidationRule>,
115    /// Default values per param name, applied when the param is absent from the request.
116    pub defaults: HashMap<String, serde_json::Value>,
117    /// Optional SQL cast per param name (e.g. "timestamptz").
118    pub casts: HashMap<String, String>,
119    /// Whether to EXPLAIN-validate the SQL at registration time.
120    pub validate_on_register: bool,
121    /// Per-report result-cache TTL override (seconds). `None` = use the global default TTL;
122    /// `Some(0)` = never cache this report.
123    pub cache_ttl_secs: Option<i64>,
124}
125
126#[derive(Clone, Debug)]
127pub struct ResolvedModel {
128    pub entities: Vec<ResolvedEntity>,
129    pub entity_by_path: HashMap<String, ResolvedEntity>,
130    /// Reports available for execution, keyed by report id.
131    pub reports: HashMap<String, ResolvedReport>,
132}
133
134impl ResolvedModel {
135    pub fn entity_by_path(&self, path: &str) -> Option<&ResolvedEntity> {
136        self.entity_by_path.get(path)
137    }
138
139    /// Look up a report by id.
140    pub fn report(&self, id: &str) -> Option<&ResolvedReport> {
141        self.reports.get(id)
142    }
143
144    /// Backfill `package_id` on all contained entities and reports. Call this after `resolve()`
145    /// when the package id is known (e.g. from manifest.id or the route parameter).
146    pub fn with_package_id(mut self, package_id: &str) -> Self {
147        for e in &mut self.entities {
148            e.package_id = package_id.to_string();
149        }
150        for e in self.entity_by_path.values_mut() {
151            e.package_id = package_id.to_string();
152        }
153        for r in self.reports.values_mut() {
154            r.package_id = package_id.to_string();
155        }
156        self
157    }
158}