Skip to main content

server/
server.rs

1//! Example server: ensures _sys_* tables exist, then loads config from PACKAGE_PATH (package directory with manifest.json) or from DB (config APIs).
2//! If PACKAGE_PATH is set, config is loaded from that directory (must contain manifest.json + config JSONs) and migrations applied; otherwise config is loaded from _sys_* tables (empty until fed via config APIs or package install).
3
4use architect_sdk::{
5    apply_migrations, common_routes_with_ready, config_routes, create_pool, ensure_database_exists,
6    ensure_sys_tables, entity_routes, load_from_pool, load_registry_from_pool, report_routes,
7    resolve, AppState, FullConfig, DEFAULT_PACKAGE_ID,
8};
9use axum::Router;
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::sync::{Arc, RwLock};
13use tokio::net::TcpListener;
14use tracing_subscriber::EnvFilter;
15
16#[tokio::main]
17async fn main() -> Result<(), Box<dyn std::error::Error>> {
18    dotenvy::dotenv().ok();
19    let filter =
20        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("architect_sdk=info"));
21    tracing_subscriber::fmt().with_env_filter(filter).init();
22
23    let database_url =
24        std::env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite://architect.db".into());
25    ensure_database_exists(&database_url).await?;
26
27    // create_pool uses the compiled-in dialect automatically (sqlite by default).
28    let pool = create_pool(&database_url, 5).await?;
29
30    let dialect = architect_sdk::db::active_dialect();
31    ensure_sys_tables(&pool, dialect.as_ref()).await?;
32
33    let tenant_registry = load_registry_from_pool(&pool)
34        .await
35        .map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
36    tracing::info!("loaded tenant registry (X-Tenant-ID required for config and entity APIs)");
37
38    let (config, package_id) = match std::env::var("PACKAGE_PATH") {
39        Ok(package_path) => {
40            tracing::info!("loading config from package path: {}", package_path);
41            let (cfg, id) = load_config_from_package_path(&package_path).await?;
42            (cfg, id)
43        }
44        Err(_) => {
45            tracing::info!("PACKAGE_PATH not set; loading config from _sys_* tables (use config APIs or POST /api/v1/config/package to insert)");
46            let cfg = load_from_pool(&pool, DEFAULT_PACKAGE_ID)
47                .await
48                .map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
49            (cfg, DEFAULT_PACKAGE_ID.to_string())
50        }
51    };
52    apply_migrations(
53        &pool,
54        &config,
55        None,
56        None,
57        dialect.as_ref(),
58        &HashMap::new(),
59    )
60    .await?;
61    let model = resolve(&config)?.with_package_id(&package_id);
62    let mut package_models = HashMap::new();
63    package_models.insert(package_id.clone(), model.clone());
64    let storage = architect_sdk::init_storage_provider().await;
65    let event_client = architect_sdk::events::DecisionHubClient::from_env();
66    let authrs_client = architect_sdk::authrs::AuthrsClient::from_env();
67    let state = AppState {
68        pool: pool.clone(),
69        model: Arc::new(RwLock::new(model)),
70        package_models: Arc::new(RwLock::new(package_models)),
71        tenant_pools: Arc::new(RwLock::new(HashMap::new())),
72        tenant_registry: Arc::new(tenant_registry),
73        storage,
74        event_client,
75        authrs_client,
76        dialect,
77        extensible_cache: Default::default(),
78        cross_package_index: Arc::new(RwLock::new(None)),
79    };
80
81    let api = Router::new()
82        .merge(common_routes_with_ready(state.clone()))
83        .nest("/api/v1", config_routes(state.clone()))
84        .nest("/api/v1", report_routes(state.clone()))
85        .nest("/api/v1", entity_routes(state));
86
87    let app = Router::new().nest("/", api);
88
89    let listener = TcpListener::bind("0.0.0.0:3000").await?;
90    tracing::info!("listening on {}", listener.local_addr()?);
91    axum::serve(listener, app).await?;
92    Ok(())
93}
94
95/// Read all JSON records for a config kind from a package directory.
96/// Tries `{kind}.json` first (flat file), then scans `{kind}/*.json` (subdirectory),
97/// merging all arrays in alphabetical order. Returns an empty vec if neither exists.
98async fn read_kind_from_dir(
99    dir: &Path,
100    kind: &str,
101) -> Result<Vec<serde_json::Value>, Box<dyn std::error::Error>> {
102    let flat = dir.join(format!("{}.json", kind));
103    if flat.exists() {
104        let content = tokio::fs::read_to_string(&flat).await?;
105        return Ok(serde_json::from_str(&content)?);
106    }
107
108    let subdir = dir.join(kind);
109    if subdir.is_dir() {
110        let mut read_dir = tokio::fs::read_dir(&subdir).await?;
111        let mut files: Vec<PathBuf> = Vec::new();
112        while let Some(entry) = read_dir.next_entry().await? {
113            let path = entry.path();
114            if path.extension().and_then(|e| e.to_str()) == Some("json") {
115                files.push(path);
116            }
117        }
118        files.sort();
119        let mut merged: Vec<serde_json::Value> = Vec::new();
120        for path in files {
121            let content = tokio::fs::read_to_string(&path).await?;
122            let mut items: Vec<serde_json::Value> = serde_json::from_str(&content)?;
123            merged.append(&mut items);
124        }
125        return Ok(merged);
126    }
127
128    Ok(vec![])
129}
130
131async fn load_config_from_package_path(
132    dir: &str,
133) -> Result<(FullConfig, String), Box<dyn std::error::Error>> {
134    let dir = PathBuf::from(dir);
135    let manifest_path = dir.join("manifest.json");
136    let manifest_json = tokio::fs::read_to_string(&manifest_path)
137        .await
138        .map_err(|e| format!("package path must contain manifest.json: {}", e))?;
139    let manifest: serde_json::Value = serde_json::from_str(&manifest_json)?;
140    let manifest_obj = manifest.as_object().ok_or_else(|| {
141        std::io::Error::new(
142            std::io::ErrorKind::InvalidData,
143            "manifest.json must be an object",
144        )
145    })?;
146    let package_id = manifest_obj
147        .get("id")
148        .and_then(|v| v.as_str())
149        .ok_or_else(|| {
150            std::io::Error::new(
151                std::io::ErrorKind::InvalidData,
152                "manifest must have 'id' (string)",
153            )
154        })?
155        .to_string();
156    let _name = manifest_obj
157        .get("name")
158        .and_then(|v| v.as_str())
159        .ok_or_else(|| {
160            std::io::Error::new(
161                std::io::ErrorKind::InvalidData,
162                "manifest must have 'name' (string)",
163            )
164        })?;
165    let _version = manifest_obj
166        .get("version")
167        .and_then(|v| v.as_str())
168        .ok_or_else(|| {
169            std::io::Error::new(
170                std::io::ErrorKind::InvalidData,
171                "manifest must have 'version' (string)",
172            )
173        })?;
174    let schema_name = manifest_obj
175        .get("schema")
176        .and_then(|v| v.as_str())
177        .ok_or_else(|| {
178            std::io::Error::new(
179                std::io::ErrorKind::InvalidData,
180                "manifest must have 'schema' (string)",
181            )
182        })?;
183    tracing::info!(
184        "package manifest: id={:?} name={:?} version={:?} schema={:?}",
185        package_id,
186        _name,
187        _version,
188        schema_name
189    );
190
191    let schemas = vec![serde_json::json!({ "id": "default", "name": schema_name })];
192    let schemas: Vec<architect_sdk::config::SchemaConfig> =
193        serde_json::from_value(serde_json::Value::Array(schemas))?;
194
195    let mut enums = read_kind_from_dir(&dir, "enums").await?;
196    for o in enums.iter_mut() {
197        if let Some(obj) = o.as_object_mut() {
198            obj.entry("schema_id")
199                .or_insert_with(|| serde_json::Value::String("default".into()));
200        }
201    }
202    let enums: Vec<architect_sdk::config::EnumConfig> =
203        serde_json::from_value(serde_json::Value::Array(enums))?;
204
205    let mut tables = read_kind_from_dir(&dir, "tables").await?;
206    for o in tables.iter_mut() {
207        if let Some(obj) = o.as_object_mut() {
208            obj.entry("schema_id")
209                .or_insert_with(|| serde_json::Value::String("default".into()));
210        }
211    }
212    let tables: Vec<architect_sdk::config::TableConfig> =
213        serde_json::from_value(serde_json::Value::Array(tables))?;
214
215    let columns_raw = read_kind_from_dir(&dir, "columns").await?;
216    let columns: Vec<architect_sdk::config::ColumnConfig> =
217        serde_json::from_value(serde_json::Value::Array(columns_raw))?;
218
219    let mut indexes = read_kind_from_dir(&dir, "indexes").await?;
220    for o in indexes.iter_mut() {
221        if let Some(obj) = o.as_object_mut() {
222            obj.entry("schema_id")
223                .or_insert_with(|| serde_json::Value::String("default".into()));
224        }
225    }
226    let indexes: Vec<architect_sdk::config::IndexConfig> =
227        serde_json::from_value(serde_json::Value::Array(indexes))?;
228
229    let mut relationships = read_kind_from_dir(&dir, "relationships").await?;
230    for o in relationships.iter_mut() {
231        if let Some(obj) = o.as_object_mut() {
232            obj.entry("from_schema_id")
233                .or_insert_with(|| serde_json::Value::String("default".into()));
234            obj.entry("to_schema_id")
235                .or_insert_with(|| serde_json::Value::String("default".into()));
236        }
237    }
238    let relationships: Vec<architect_sdk::config::RelationshipConfig> =
239        serde_json::from_value(serde_json::Value::Array(relationships))?;
240
241    let api_entities_raw = read_kind_from_dir(&dir, "api_entities").await?;
242    let api_entities: Vec<architect_sdk::config::ApiEntityConfig> =
243        serde_json::from_value(serde_json::Value::Array(api_entities_raw))?;
244
245    let kv_stores_raw = read_kind_from_dir(&dir, "kv_stores").await?;
246    let kv_stores: Vec<architect_sdk::config::KvStoreConfig> =
247        serde_json::from_value(serde_json::Value::Array(kv_stores_raw))?;
248
249    let reports_raw = read_kind_from_dir(&dir, "reports").await?;
250    let reports: Vec<architect_sdk::config::ReportConfig> =
251        serde_json::from_value(serde_json::Value::Array(reports_raw))?;
252
253    Ok((
254        FullConfig {
255            schemas,
256            enums,
257            tables,
258            columns,
259            indexes,
260            relationships,
261            api_entities,
262            kv_stores,
263            reports,
264        },
265        package_id,
266    ))
267}