graphql_codegen_rust/generator/
sea_orm.rs1use 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 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 output.push_str("//! Sea-ORM entities generated from GraphQL schema\n\n");
35
36 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 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 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 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 if schema.types.is_empty() && schema.enums.is_empty() {
78 return Ok(entities);
79 }
80
81 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 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 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 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 output.push_str("use sea_orm::entity::prelude::*;\n");
145 output.push_str("use serde::{Deserialize, Serialize};\n\n");
146
147 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 output.push_str("#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]\n");
167 output.push_str("pub enum Relation {}\n\n");
168
169 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 output.push_str("#[derive(Copy, Clone, Debug, EnumIter)]\n");
180 output.push_str("pub enum PrimaryKey {\n");
181 output.push_str(" Id,\n");
183 output.push_str("}\n\n");
184
185 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", 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 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 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 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}