1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub enum ScalarType {
13 String,
14 Number,
15 Integer,
16 Decimal,
17 Boolean,
18 Datetime,
19 Date,
20 Json,
21 Enum,
22 Uuid,
23}
24
25impl ScalarType {
26 pub fn to_ts_type(&self, values: Option<&[String]>) -> String {
28 match self {
29 ScalarType::String => "string".to_owned(),
30 ScalarType::Number | ScalarType::Decimal => "number".to_owned(),
31 ScalarType::Integer => "number".to_owned(),
32 ScalarType::Boolean => "boolean".to_owned(),
33 ScalarType::Datetime | ScalarType::Date => "string".to_owned(),
34 ScalarType::Json => "unknown".to_owned(),
35 ScalarType::Uuid => "string".to_owned(),
36 ScalarType::Enum => {
37 if let Some(vs) = values {
38 if vs.is_empty() {
39 "string".to_owned()
40 } else {
41 vs.iter()
42 .map(|v| format!("\"{}\"", v))
43 .collect::<Vec<_>>()
44 .join(" | ")
45 }
46 } else {
47 "string".to_owned()
48 }
49 }
50 }
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct IrField {
58 pub name: String,
59 #[serde(rename = "type")]
60 pub field_type: ScalarType,
61 #[serde(default)]
62 pub primary: bool,
63 #[serde(default)]
64 pub required: bool,
65 #[serde(default)]
66 pub unique: bool,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub default: Option<serde_json::Value>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub values: Option<Vec<String>>,
71 #[serde(default)]
72 pub auto: bool,
73}
74
75impl IrField {
76 pub fn ts_type_string(&self) -> String {
78 self.field_type.to_ts_type(self.values.as_deref())
79 }
80
81 pub fn is_optional(&self) -> bool {
83 !self.required && !self.primary
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub enum RelationKind {
92 BelongsTo,
93 HasMany,
94 ManyToMany,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct IrRelation {
99 pub name: String,
100 #[serde(rename = "type")]
101 pub kind: RelationKind,
102 pub target: String,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub foreign_key: Option<String>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub through: Option<String>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct IrEntity {
113 pub name: String,
114 pub table: String,
115 pub primary_key: String,
116 pub fields: Vec<IrField>,
117 pub relations: Vec<IrRelation>,
118}
119
120#[derive(Debug, Clone, Default, Serialize, Deserialize)]
123pub struct LocalFirstConfig {
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub engine: Option<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub sync: Option<String>,
128}
129
130#[derive(Debug, Clone, Default, Serialize, Deserialize)]
131pub struct AiConfig {
132 #[serde(default)]
133 pub mcp: bool,
134 #[serde(default)]
135 pub a2a: bool,
136}
137
138#[derive(Debug, Clone, Default, Serialize, Deserialize)]
139pub struct SdlConfig {
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub local_first: Option<LocalFirstConfig>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub ai: Option<AiConfig>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct EntityGraphIR {
150 pub version: String,
151 pub entities: Vec<IrEntity>,
152 #[serde(default)]
153 pub config: SdlConfig,
154}
155
156#[cfg(test)]
159mod tests {
160 use super::*;
161 use serde_json::json;
162
163 fn sample_ir_json() -> serde_json::Value {
164 json!({
165 "version": "1.0",
166 "entities": [
167 {
168 "name": "user",
169 "table": "user",
170 "primary_key": "id",
171 "fields": [
172 { "name": "id", "type": "uuid", "primary": true, "required": true, "unique": true, "auto": false },
173 { "name": "name", "type": "string", "primary": false, "required": true, "unique": false, "auto": false },
174 { "name": "role", "type": "enum", "primary": false, "required": false, "unique": false, "auto": false,
175 "values": ["admin", "member"] }
176 ],
177 "relations": [
178 { "name": "orders", "type": "hasMany", "target": "order", "foreign_key": "userId" }
179 ]
180 },
181 {
182 "name": "order",
183 "table": "order",
184 "primary_key": "id",
185 "fields": [
186 { "name": "id", "type": "uuid", "primary": true, "required": true, "unique": true, "auto": false },
187 { "name": "userId", "type": "uuid", "primary": false, "required": true, "unique": false, "auto": false }
188 ],
189 "relations": [
190 { "name": "user", "type": "belongsTo", "target": "user", "foreign_key": "userId" }
191 ]
192 }
193 ],
194 "config": {}
195 })
196 }
197
198 #[test]
199 fn deserialize_full_ir() {
200 let ir: EntityGraphIR = serde_json::from_value(sample_ir_json()).unwrap();
201 assert_eq!(ir.version, "1.0");
202 assert_eq!(ir.entities.len(), 2);
203 let user = &ir.entities[0];
204 assert_eq!(user.name, "user");
205 assert_eq!(user.primary_key, "id");
206 assert_eq!(user.fields.len(), 3);
207 assert_eq!(user.relations.len(), 1);
208 }
209
210 #[test]
211 fn scalar_type_to_ts() {
212 assert_eq!(ScalarType::String.to_ts_type(None), "string");
213 assert_eq!(ScalarType::Integer.to_ts_type(None), "number");
214 assert_eq!(ScalarType::Boolean.to_ts_type(None), "boolean");
215 assert_eq!(ScalarType::Uuid.to_ts_type(None), "string");
216 let e = ScalarType::Enum.to_ts_type(Some(&["a".into(), "b".into()]));
217 assert_eq!(e, "\"a\" | \"b\"");
218 }
219
220 #[test]
221 fn field_optionality() {
222 let required_field = IrField {
223 name: "name".into(),
224 field_type: ScalarType::String,
225 primary: false,
226 required: true,
227 unique: false,
228 default: None,
229 values: None,
230 auto: false,
231 };
232 assert!(!required_field.is_optional());
233
234 let optional_field = IrField {
235 name: "bio".into(),
236 field_type: ScalarType::String,
237 primary: false,
238 required: false,
239 unique: false,
240 default: None,
241 values: None,
242 auto: false,
243 };
244 assert!(optional_field.is_optional());
245 }
246}