graphql_codegen_rust/generator/
sea_orm.rs

1use std::collections::HashMap;
2
3use crate::cli::DatabaseType;
4use crate::config::Config;
5use crate::generator::{
6    CodeGenerator, MigrationFile, rust_type_for_field, sql_type_for_field, to_snake_case,
7};
8use crate::parser::{ParsedEnum, ParsedSchema, ParsedType};
9
10pub struct SeaOrmGenerator;
11
12impl SeaOrmGenerator {
13    pub fn new() -> Self {
14        Self
15    }
16}
17
18impl Default for SeaOrmGenerator {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl CodeGenerator for SeaOrmGenerator {
25    fn generate_schema(&self, schema: &ParsedSchema, _config: &Config) -> anyhow::Result<String> {
26        // Handle empty schemas gracefully
27        if schema.types.is_empty() && schema.enums.is_empty() {
28            return Ok("// No GraphQL types or enums found in schema\n".to_string());
29        }
30
31        let mut output = String::new();
32
33        // Add header comment
34        output.push_str("//! Sea-ORM entities generated from GraphQL schema\n\n");
35
36        // Generate module declarations for all entities
37        for type_name in schema.types.keys() {
38            let module_name = to_snake_case(type_name);
39            output.push_str(&format!("pub mod {};\n", module_name));
40        }
41
42        // Generate module declarations for enums
43        for enum_name in schema.enums.keys() {
44            let module_name = to_snake_case(enum_name);
45            output.push_str(&format!("pub mod {};\n", module_name));
46        }
47
48        output.push('\n');
49
50        // Generate re-exports for convenience
51        output.push_str("// Re-exports for convenience\n");
52        for type_name in schema.types.keys() {
53            let module_name = to_snake_case(type_name);
54            output.push_str(&format!("pub use {}::Entity;\n", module_name));
55            output.push_str(&format!("pub use {}::Model;\n", module_name));
56            output.push_str(&format!("pub use {}::ActiveModel;\n", module_name));
57            output.push_str(&format!("pub use {}::Column;\n", module_name));
58        }
59
60        // Re-export enums
61        for enum_name in schema.enums.keys() {
62            let module_name = to_snake_case(enum_name);
63            output.push_str(&format!("pub use {}::{};\n", module_name, enum_name));
64        }
65
66        Ok(output)
67    }
68
69    fn generate_entities(
70        &self,
71        schema: &ParsedSchema,
72        config: &Config,
73    ) -> anyhow::Result<HashMap<String, String>> {
74        let mut entities = HashMap::new();
75
76        // Handle empty schemas gracefully
77        if schema.types.is_empty() && schema.enums.is_empty() {
78            return Ok(entities);
79        }
80
81        // Generate entities for Object types (not interfaces or unions)
82        for (type_name, parsed_type) in &schema.types {
83            if matches!(parsed_type.kind, crate::parser::TypeKind::Object) {
84                let entity_code = self
85                    .generate_entity_struct(type_name, parsed_type, config)
86                    .map_err(|e| {
87                        anyhow::anyhow!(
88                            "Failed to generate Sea-ORM entity for type '{}': {}",
89                            type_name,
90                            e
91                        )
92                    })?;
93                entities.insert(format!("{}.rs", to_snake_case(type_name)), entity_code);
94            }
95        }
96
97        // Generate enums
98        for (enum_name, parsed_enum) in &schema.enums {
99            let enum_code = self
100                .generate_enum_type(enum_name, parsed_enum)
101                .map_err(|e| {
102                    anyhow::anyhow!("Failed to generate Sea-ORM enum '{}': {}", enum_name, e)
103                })?;
104            entities.insert(format!("{}.rs", to_snake_case(enum_name)), enum_code);
105        }
106
107        // Handle empty schemas gracefully - no error for empty schemas
108
109        Ok(entities)
110    }
111
112    fn generate_migrations(
113        &self,
114        schema: &ParsedSchema,
115        config: &Config,
116    ) -> anyhow::Result<Vec<MigrationFile>> {
117        let mut migrations = Vec::new();
118
119        // Only generate migrations for Object types (not interfaces or unions)
120        for (type_name, parsed_type) in &schema.types {
121            if matches!(parsed_type.kind, crate::parser::TypeKind::Object) {
122                let migration = self.generate_table_migration(type_name, parsed_type, config)?;
123                migrations.push(migration);
124            }
125        }
126
127        Ok(migrations)
128    }
129}
130
131impl SeaOrmGenerator {
132    fn generate_entity_struct(
133        &self,
134        type_name: &str,
135        parsed_type: &ParsedType,
136        config: &Config,
137    ) -> anyhow::Result<String> {
138        let _struct_name = type_name.to_string();
139        let table_name = to_snake_case(type_name);
140
141        let mut output = String::new();
142
143        // Add imports
144        output.push_str("use sea_orm::entity::prelude::*;\n");
145        output.push_str("use serde::{Deserialize, Serialize};\n\n");
146
147        // Generate the entity struct
148        output.push_str(
149            "#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)]\n",
150        );
151        output.push_str(&format!("#[sea_orm(table_name = \"{}\")]\n", table_name));
152        output.push_str("pub struct Model {\n");
153
154        for field in &parsed_type.fields {
155            let field_name = to_snake_case(&field.name);
156            let field_type = rust_type_for_field(field, &config.db, &config.type_mappings);
157            let column_attr = format!("#[sea_orm(column_name = \"{}\")]", field_name);
158
159            output.push_str(&format!("    {}\n", column_attr));
160            output.push_str(&format!("    pub {}: {},\n", field_name, field_type));
161        }
162
163        output.push_str("}\n\n");
164
165        // Generate relation enum (empty for now)
166        output.push_str("#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]\n");
167        output.push_str("pub enum Relation {}\n\n");
168
169        // Generate ActiveModel
170        output.push_str("#[derive(Copy, Clone, Debug, EnumIter, DeriveCustomColumn)]\n");
171        output.push_str("pub enum Column {\n");
172        for field in &parsed_type.fields {
173            let field_name = to_snake_case(&field.name);
174            output.push_str(&format!("    {},\n", field_name));
175        }
176        output.push_str("}\n\n");
177
178        // Generate PrimaryKey
179        output.push_str("#[derive(Copy, Clone, Debug, EnumIter)]\n");
180        output.push_str("pub enum PrimaryKey {\n");
181        // Assume id is primary key
182        output.push_str("    Id,\n");
183        output.push_str("}\n\n");
184
185        // Determine the ID type based on database
186        let id_type = match config.db {
187            DatabaseType::Sqlite => "i32",
188            DatabaseType::Postgres => "uuid::Uuid",
189            DatabaseType::Mysql => "u32",
190        };
191
192        let auto_increment = match config.db {
193            DatabaseType::Sqlite => "true",
194            DatabaseType::Postgres => "false", // UUIDs don't auto-increment
195            DatabaseType::Mysql => "true",
196        };
197
198        output.push_str("impl PrimaryKeyTrait for PrimaryKey {\n");
199        output.push_str(&format!("    type ValueType = {};\n", id_type));
200        output.push_str("    fn auto_increment() -> bool {\n");
201        output.push_str(&format!("        {}\n", auto_increment));
202        output.push_str("    }\n");
203        output.push_str("}\n\n");
204
205        output.push_str("impl ActiveModelBehavior for ActiveModel {}\n\n");
206
207        // Generate Entity constant (Sea-ORM convention)
208        output.push_str("pub struct Entity;\n\n");
209        output.push_str("impl EntityName for Entity {\n");
210        output.push_str("    fn table_name(&self) -> &str {\n");
211        output.push_str(&format!("        \"{}\"\n", table_name));
212        output.push_str("    }\n");
213        output.push_str("}\n\n");
214
215        // Generate relationships based on detected foreign keys
216        // For Sea-ORM, we can use derive macros and relationship definitions
217        let mut has_relationships = false;
218
219        for field in &parsed_type.fields {
220            if field.name.ends_with("Id") && field.name.len() > 2 {
221                let related_type = &field.name[..field.name.len() - 2];
222                if related_type
223                    .chars()
224                    .next()
225                    .is_some_and(|c| c.is_uppercase())
226                {
227                    if !has_relationships {
228                        output.push_str("// Relationships\n");
229                        has_relationships = true;
230                    }
231                    let _relation_name = to_snake_case(&field.name[..field.name.len() - 2]);
232                    output.push_str("#[derive(Clone, Debug, PartialEq, DeriveRelation)]\n");
233                    output.push_str(&format!("#[sea_orm(table_name = \"{}\")]\n", table_name));
234                    output.push_str("pub enum Relation {\n");
235                    output.push_str("    #[sea_orm(\n");
236                    output.push_str(&format!(
237                        "        belongs_to = \"super::{}::Entity\",\n",
238                        related_type
239                    ));
240                    output.push_str(&format!("        from = \"Column::{}\",\n", field.name));
241                    output.push_str(&format!(
242                        "        to = \"super::{}::Column::Id\",\n",
243                        related_type
244                    ));
245                    output.push_str("        on_update = \"Cascade\",\n");
246                    output.push_str("        on_delete = \"Cascade\"\n");
247                    output.push_str("    )]\n");
248                    output.push_str(&format!("    {},\n", related_type));
249                    output.push_str("}\n\n");
250                }
251            }
252        }
253
254        Ok(output)
255    }
256
257    fn generate_enum_type(
258        &self,
259        enum_name: &str,
260        parsed_enum: &ParsedEnum,
261    ) -> anyhow::Result<String> {
262        let mut output = String::new();
263
264        if let Some(description) = &parsed_enum.description {
265            output.push_str(&format!("/// {}\n", description));
266        }
267
268        output.push_str("#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)]\n");
269        output.push_str("#[sea_orm(rs_type = \"String\", db_type = \"String(Some(1))\")]\n");
270        output.push_str(&format!("pub enum {} {{\n", enum_name));
271
272        for value in &parsed_enum.values {
273            output.push_str(&format!("    #[sea_orm(string_value = \"{}\")]\n", value));
274            output.push_str(&format!("    {},\n", value));
275        }
276
277        output.push_str("}\n");
278
279        Ok(output)
280    }
281
282    fn generate_table_migration(
283        &self,
284        type_name: &str,
285        parsed_type: &ParsedType,
286        config: &Config,
287    ) -> anyhow::Result<MigrationFile> {
288        let table_name = to_snake_case(type_name);
289        let migration_name = format!(
290            "m{}_create_{}_table",
291            chrono::Utc::now().timestamp(),
292            table_name
293        );
294
295        let mut up_sql = format!("CREATE TABLE {} (\n", table_name);
296
297        let mut columns = Vec::new();
298
299        // Add id column if not present
300        let has_id = parsed_type.fields.iter().any(|f| f.name == "id");
301        if !has_id {
302            let id_type = match config.db {
303                DatabaseType::Sqlite => "INTEGER PRIMARY KEY AUTOINCREMENT",
304                DatabaseType::Postgres => "UUID PRIMARY KEY DEFAULT gen_random_uuid()",
305                DatabaseType::Mysql => "INT UNSIGNED PRIMARY KEY AUTO_INCREMENT",
306            };
307            columns.push(format!("    id {}", id_type));
308        }
309
310        for field in &parsed_type.fields {
311            let column_name = to_snake_case(&field.name);
312            let sql_type = sql_type_for_field(field, &config.db, &config.type_mappings);
313
314            let nullable = if field.is_nullable { "" } else { " NOT NULL" };
315            let primary_key = if field.name == "id" {
316                " PRIMARY KEY"
317            } else {
318                ""
319            };
320
321            columns.push(format!(
322                "    {} {}{}{}",
323                column_name, sql_type, nullable, primary_key
324            ));
325        }
326
327        up_sql.push_str(&columns.join(",\n"));
328        up_sql.push_str("\n);");
329
330        let down_sql = format!("DROP TABLE {};", table_name);
331
332        Ok(MigrationFile {
333            name: migration_name,
334            up_sql,
335            down_sql,
336        })
337    }
338}