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#[derive(Clone, Debug)]
96pub struct ResolvedModel {
97 pub entities: Vec<ResolvedEntity>,
98 pub entity_by_path: HashMap<String, ResolvedEntity>,
99}
100
101impl ResolvedModel {
102 pub fn entity_by_path(&self, path: &str) -> Option<&ResolvedEntity> {
103 self.entity_by_path.get(path)
104 }
105
106 /// Backfill `package_id` on all contained entities. Call this after `resolve()` when the
107 /// package id is known (e.g. from manifest.id or the route parameter).
108 pub fn with_package_id(mut self, package_id: &str) -> Self {
109 for e in &mut self.entities {
110 e.package_id = package_id.to_string();
111 }
112 for e in self.entity_by_path.values_mut() {
113 e.package_id = package_id.to_string();
114 }
115 self
116 }
117}