1use 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 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
95async 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}