1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct TableDefinition {
5 pub name: String,
6 pub columns: Vec<ColumnDefinition>,
7 #[serde(default, skip_serializing_if = "Vec::is_empty")]
8 pub indexes: Vec<IndexDefinition>,
9 #[serde(default)]
10 pub soft_delete: bool,
11 #[serde(default, skip_serializing_if = "Option::is_none")]
14 pub owner_field: Option<String>,
15 #[serde(default)]
18 pub auth_required: bool,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub permission_area: Option<String>,
24 #[serde(default)]
32 pub owner_self_service: bool,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub hooks: Option<TableHooks>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub scoped_by: Option<String>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ColumnDefinition {
46 pub name: String,
47 pub column_type: ColumnType,
48 #[serde(default)]
49 pub primary_key: bool,
50 #[serde(default)]
51 pub nullable: bool,
52 #[serde(default)]
53 pub auto_generate: bool,
54 pub default_value: Option<String>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub references: Option<String>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub on_delete: Option<ForeignKeyAction>,
61 #[serde(default)]
63 pub unique: bool,
64 #[serde(default, skip_serializing_if = "Vec::is_empty")]
66 pub validations: Vec<ValidationRule>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
70#[serde(tag = "rule", rename_all = "snake_case")]
71pub enum ValidationRule {
73 Regex { pattern: String },
75 Min { value: f64 },
77 Max { value: f64 },
79 MinLength { value: usize },
81 MaxLength { value: usize },
83 OneOf { values: Vec<String> },
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
88pub struct IndexDefinition {
89 pub name: String,
90 pub columns: Vec<String>,
91 #[serde(default)]
92 pub unique: bool,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
96#[serde(rename_all = "snake_case")]
97pub enum ForeignKeyAction {
98 Cascade,
99 SetNull,
100 Restrict,
101 NoAction,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
105#[serde(rename_all = "lowercase")]
106pub enum ColumnType {
107 Uuid,
108 Text,
109 Integer,
110 BigInteger,
111 Float,
112 Double,
113 Boolean,
114 Timestamp,
115 Date,
116 Jsonb,
117}
118
119impl ColumnType {
120 pub fn to_sql(&self) -> &str {
121 match self {
122 ColumnType::Uuid => "UUID",
123 ColumnType::Text => "TEXT",
124 ColumnType::Integer => "INTEGER",
125 ColumnType::BigInteger => "BIGINT",
126 ColumnType::Float => "REAL",
127 ColumnType::Double => "DOUBLE PRECISION",
128 ColumnType::Boolean => "BOOLEAN",
129 ColumnType::Timestamp => "TIMESTAMPTZ",
130 ColumnType::Date => "DATE",
131 ColumnType::Jsonb => "JSONB",
132 }
133 }
134
135 pub fn filter_type(&self) -> &str {
137 match self {
138 ColumnType::Uuid => "uuid",
139 ColumnType::Text => "text",
140 ColumnType::Integer | ColumnType::BigInteger => "integer",
141 ColumnType::Float | ColumnType::Double => "double",
142 ColumnType::Boolean => "boolean",
143 ColumnType::Timestamp => "timestamp",
144 ColumnType::Date => "date",
145 ColumnType::Jsonb => "jsonb",
146 }
147 }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, Default)]
152pub struct TableHooks {
153 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub before_create: Option<String>,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub after_create: Option<String>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub before_update: Option<String>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub after_update: Option<String>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub before_delete: Option<String>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub after_delete: Option<String>,
165}
166
167impl TableDefinition {
168 pub fn primary_key(&self) -> Option<&ColumnDefinition> {
170 self.columns.iter().find(|c| c.primary_key)
171 }
172
173 pub fn writable_columns(&self) -> Vec<&ColumnDefinition> {
175 self.columns
176 .iter()
177 .filter(|c| !c.auto_generate || !c.primary_key)
178 .collect()
179 }
180
181 pub fn derived_features(&self) -> impl Iterator<Item = &'static str> {
183 self.scoped_by
184 .is_some()
185 .then_some(crate::features::SCOPED_PERMISSIONS)
186 .into_iter()
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use crate::manifest::fixtures::sample_manifest;
194
195 #[test]
196 fn test_primary_key() {
197 let manifest = sample_manifest();
198 let table = manifest.get_table("todos").unwrap();
199 let pk = table.primary_key().unwrap();
200 assert_eq!(pk.name, "id");
201 assert!(pk.auto_generate);
202 }
203
204 #[test]
205 fn test_column_type_to_sql() {
206 assert_eq!(ColumnType::Uuid.to_sql(), "UUID");
207 assert_eq!(ColumnType::Text.to_sql(), "TEXT");
208 assert_eq!(ColumnType::Integer.to_sql(), "INTEGER");
209 assert_eq!(ColumnType::BigInteger.to_sql(), "BIGINT");
210 assert_eq!(ColumnType::Boolean.to_sql(), "BOOLEAN");
211 assert_eq!(ColumnType::Timestamp.to_sql(), "TIMESTAMPTZ");
212 assert_eq!(ColumnType::Date.to_sql(), "DATE");
213 assert_eq!(ColumnType::Jsonb.to_sql(), "JSONB");
214 }
215
216 #[test]
217 fn test_fk_not_serialized_when_none() {
218 let col = ColumnDefinition {
219 name: "title".into(),
220 column_type: ColumnType::Text,
221 primary_key: false,
222 nullable: false,
223 auto_generate: false,
224 default_value: None,
225 references: None,
226 on_delete: None,
227 unique: false,
228 validations: vec![],
229 };
230 let json = serde_json::to_string(&col).unwrap();
231 assert!(!json.contains("references"));
232 assert!(!json.contains("on_delete"));
233 }
234
235 #[test]
236 fn test_unique_column_serialization() {
237 let col = ColumnDefinition {
238 name: "email".into(),
239 column_type: ColumnType::Text,
240 primary_key: false,
241 nullable: false,
242 auto_generate: false,
243 default_value: None,
244 references: None,
245 on_delete: None,
246 unique: true,
247 validations: vec![],
248 };
249 let json = serde_json::to_string(&col).unwrap();
250 assert!(json.contains("\"unique\":true"));
251 let parsed: ColumnDefinition = serde_json::from_str(&json).unwrap();
252 assert!(parsed.unique);
253 }
254
255 #[test]
256 fn test_index_serialization() {
257 let table = TableDefinition {
258 name: "users".into(),
259 columns: vec![],
260 indexes: vec![IndexDefinition {
261 name: "idx_users_email".into(),
262 columns: vec!["email".into()],
263 unique: true,
264 }],
265 soft_delete: false,
266 owner_field: None,
267 auth_required: false,
268 permission_area: None,
269 owner_self_service: false,
270 hooks: None,
271 scoped_by: None,
272 };
273 let json = serde_json::to_string(&table).unwrap();
274 assert!(json.contains("idx_users_email"));
275 let parsed: TableDefinition = serde_json::from_str(&json).unwrap();
276 assert_eq!(parsed.indexes.len(), 1);
277 assert!(parsed.indexes[0].unique);
278 }
279
280 #[test]
281 fn test_indexes_not_serialized_when_empty() {
282 let table = TableDefinition {
283 name: "users".into(),
284 columns: vec![],
285 indexes: vec![],
286 soft_delete: false,
287 owner_field: None,
288 auth_required: false,
289 permission_area: None,
290 owner_self_service: false,
291 hooks: None,
292 scoped_by: None,
293 };
294 let json = serde_json::to_string(&table).unwrap();
295 assert!(!json.contains("indexes"));
296 }
297
298 #[test]
299 fn test_owner_field_serialization() {
300 let table = TableDefinition {
301 name: "notes".into(),
302 columns: vec![],
303 indexes: vec![],
304 soft_delete: false,
305 owner_field: Some("user_id".into()),
306 auth_required: false,
307 permission_area: None,
308 owner_self_service: false,
309 hooks: None,
310 scoped_by: None,
311 };
312 let json = serde_json::to_string(&table).unwrap();
313 assert!(json.contains("\"owner_field\":\"user_id\""));
314 let parsed: TableDefinition = serde_json::from_str(&json).unwrap();
315 assert_eq!(parsed.owner_field.as_deref(), Some("user_id"));
316 }
317
318 #[test]
319 fn test_owner_field_not_serialized_when_none() {
320 let table = TableDefinition {
321 name: "notes".into(),
322 columns: vec![],
323 indexes: vec![],
324 soft_delete: false,
325 owner_field: None,
326 auth_required: false,
327 permission_area: None,
328 owner_self_service: false,
329 hooks: None,
330 scoped_by: None,
331 };
332 let json = serde_json::to_string(&table).unwrap();
333 assert!(!json.contains("owner_field"));
334 }
335
336 #[test]
337 fn test_owner_field_defaults_to_none() {
338 let json = r#"{"name":"notes","columns":[]}"#;
339 let table: TableDefinition = serde_json::from_str(json).unwrap();
340 assert!(table.owner_field.is_none());
341 }
342
343 #[test]
344 fn test_auth_required_serialization() {
345 let table = TableDefinition {
346 name: "orders".into(),
347 columns: vec![],
348 indexes: vec![],
349 soft_delete: false,
350 owner_field: None,
351 auth_required: true,
352 permission_area: None,
353 owner_self_service: false,
354 hooks: None,
355 scoped_by: None,
356 };
357 let json = serde_json::to_string(&table).unwrap();
358 assert!(json.contains("\"auth_required\":true"));
359 let parsed: TableDefinition = serde_json::from_str(&json).unwrap();
360 assert!(parsed.auth_required);
361 }
362
363 #[test]
364 fn test_auth_required_defaults_to_false() {
365 let json = r#"{"name":"orders","columns":[]}"#;
366 let table: TableDefinition = serde_json::from_str(json).unwrap();
367 assert!(!table.auth_required);
368 }
369
370 #[test]
371 fn scoped_by_is_omitted_when_absent_and_derives_its_feature() {
372 let table: TableDefinition =
373 serde_json::from_str(r#"{"name":"leads","columns":[],"owner_field":"user_id"}"#)
374 .unwrap();
375 assert!(table.scoped_by.is_none());
376 assert!(!serde_json::to_string(&table).unwrap().contains("scoped_by"));
377 assert_eq!(table.derived_features().count(), 0);
378
379 let scoped = TableDefinition {
380 scoped_by: Some("leads".into()),
381 ..table
382 };
383 let json = serde_json::to_string(&scoped).unwrap();
384 assert!(json.contains(r#""scoped_by":"leads""#), "{json}");
385 assert_eq!(
386 scoped.derived_features().collect::<Vec<_>>(),
387 [crate::features::SCOPED_PERMISSIONS]
388 );
389 }
390}