1use std::collections::HashMap;
6
7use super::{to_camel_case, to_pascal_case, GraphQLScalar, RelationType};
8
9#[derive(Debug, Clone)]
11pub struct GraphQLSchema {
12 pub types: Vec<GraphQLType>,
14 pub queries: Vec<QueryDefinition>,
16 pub mutations: Vec<MutationDefinition>,
18 pub relationships: Vec<Relationship>,
20 pub input_types: Vec<GraphQLInputType>,
22 pub enum_types: Vec<GraphQLEnumType>,
24}
25
26impl GraphQLSchema {
27 pub fn new() -> Self {
29 Self {
30 types: Vec::new(),
31 queries: Vec::new(),
32 mutations: Vec::new(),
33 relationships: Vec::new(),
34 input_types: Vec::new(),
35 enum_types: Vec::new(),
36 }
37 }
38
39 pub fn add_type(&mut self, type_def: GraphQLType) {
41 self.types.push(type_def);
42 }
43
44 pub fn add_query(&mut self, query: QueryDefinition) {
46 self.queries.push(query);
47 }
48
49 pub fn add_mutation(&mut self, mutation: MutationDefinition) {
51 self.mutations.push(mutation);
52 }
53
54 pub fn add_relationship(&mut self, relationship: Relationship) {
56 self.relationships.push(relationship);
57 }
58
59 pub fn get_type(&self, name: &str) -> Option<&GraphQLType> {
61 self.types.iter().find(|t| t.name == name)
62 }
63
64 pub fn get_relationships_for(&self, type_name: &str) -> Vec<&Relationship> {
66 self.relationships
67 .iter()
68 .filter(|r| r.from_type == type_name)
69 .collect()
70 }
71
72 pub fn to_sdl(&self) -> String {
74 let mut sdl = String::new();
75
76 sdl.push_str("# Custom Scalars\n");
78 sdl.push_str("scalar DateTime\n");
79 sdl.push_str("scalar Date\n");
80 sdl.push_str("scalar Time\n");
81 sdl.push_str("scalar JSON\n");
82 sdl.push_str("scalar Decimal\n");
83 sdl.push_str("scalar BigInt\n");
84 sdl.push('\n');
85
86 for enum_type in &self.enum_types {
88 sdl.push_str(&format!("enum {} {{\n", enum_type.name));
89 for value in &enum_type.values {
90 sdl.push_str(&format!(" {}\n", value));
91 }
92 sdl.push_str("}\n\n");
93 }
94
95 for type_def in &self.types {
97 if let Some(ref desc) = type_def.description {
98 sdl.push_str(&format!("\"\"\"{}\"\"\"\n", desc));
99 }
100 sdl.push_str(&format!("type {} {{\n", type_def.name));
101
102 for field in &type_def.fields {
103 if let Some(ref desc) = field.description {
104 sdl.push_str(&format!(" \"\"\"{}\"\"\"\n", desc));
105 }
106
107 let type_str = if field.nullable {
108 field.graphql_type.to_string()
109 } else {
110 format!("{}!", field.graphql_type)
111 };
112
113 sdl.push_str(&format!(" {}: {}\n", field.name, type_str));
114 }
115
116 for rel in self.get_relationships_for(&type_def.name) {
118 let type_str = if rel.relation_type.is_list() {
119 format!("[{}!]!", rel.to_type)
120 } else {
121 format!("{}!", rel.to_type)
122 };
123 sdl.push_str(&format!(" {}: {}\n", rel.field_name, type_str));
124 }
125
126 sdl.push_str("}\n\n");
127 }
128
129 for input_type in &self.input_types {
131 sdl.push_str(&format!("input {} {{\n", input_type.name));
132 for field in &input_type.fields {
133 let type_str = if field.nullable {
134 field.graphql_type.to_string()
135 } else {
136 format!("{}!", field.graphql_type)
137 };
138 sdl.push_str(&format!(" {}: {}\n", field.name, type_str));
139 }
140 sdl.push_str("}\n\n");
141 }
142
143 sdl.push_str("type Query {\n");
145 for query in &self.queries {
146 let args: Vec<String> = query
147 .arguments
148 .iter()
149 .map(|a| {
150 let type_str = if a.nullable {
151 a.graphql_type.to_string()
152 } else {
153 format!("{}!", a.graphql_type)
154 };
155 format!("{}: {}", a.name, type_str)
156 })
157 .collect();
158
159 let args_str = if args.is_empty() {
160 String::new()
161 } else {
162 format!("({})", args.join(", "))
163 };
164
165 let return_type = if query.returns_list {
166 format!("[{}!]!", query.return_type)
167 } else {
168 query.return_type.clone()
169 };
170
171 sdl.push_str(&format!(" {}{}: {}\n", query.name, args_str, return_type));
172 }
173 sdl.push_str("}\n\n");
174
175 if !self.mutations.is_empty() {
177 sdl.push_str("type Mutation {\n");
178 for mutation in &self.mutations {
179 let args: Vec<String> = mutation
180 .arguments
181 .iter()
182 .map(|a| {
183 let type_str = if a.nullable {
184 a.graphql_type.to_string()
185 } else {
186 format!("{}!", a.graphql_type)
187 };
188 format!("{}: {}", a.name, type_str)
189 })
190 .collect();
191
192 let args_str = if args.is_empty() {
193 String::new()
194 } else {
195 format!("({})", args.join(", "))
196 };
197
198 sdl.push_str(&format!(
199 " {}{}: {}\n",
200 mutation.name, args_str, mutation.return_type
201 ));
202 }
203 sdl.push_str("}\n");
204 }
205
206 sdl
207 }
208}
209
210impl Default for GraphQLSchema {
211 fn default() -> Self {
212 Self::new()
213 }
214}
215
216#[derive(Debug, Clone)]
218pub struct GraphQLType {
219 pub name: String,
221 pub fields: Vec<GraphQLField>,
223 pub description: Option<String>,
225 pub table_name: Option<String>,
227}
228
229impl GraphQLType {
230 pub fn new(name: impl Into<String>) -> Self {
232 Self {
233 name: name.into(),
234 fields: Vec::new(),
235 description: None,
236 table_name: None,
237 }
238 }
239
240 pub fn add_field(&mut self, field: GraphQLField) {
242 self.fields.push(field);
243 }
244
245 pub fn with_description(mut self, description: impl Into<String>) -> Self {
247 self.description = Some(description.into());
248 self
249 }
250
251 pub fn from_table(mut self, table_name: impl Into<String>) -> Self {
253 self.table_name = Some(table_name.into());
254 self
255 }
256
257 pub fn get_field(&self, name: &str) -> Option<&GraphQLField> {
259 self.fields.iter().find(|f| f.name == name)
260 }
261}
262
263#[derive(Debug, Clone)]
265pub struct GraphQLField {
266 pub name: String,
268 pub graphql_type: FieldType,
270 pub nullable: bool,
272 pub description: Option<String>,
274 pub column_name: Option<String>,
276 pub deprecated: bool,
278 pub deprecation_reason: Option<String>,
280}
281
282impl GraphQLField {
283 pub fn new(name: impl Into<String>, graphql_type: FieldType) -> Self {
285 Self {
286 name: name.into(),
287 graphql_type,
288 nullable: true,
289 description: None,
290 column_name: None,
291 deprecated: false,
292 deprecation_reason: None,
293 }
294 }
295
296 pub fn nullable(mut self, nullable: bool) -> Self {
298 self.nullable = nullable;
299 self
300 }
301
302 pub fn with_description(mut self, description: impl Into<String>) -> Self {
304 self.description = Some(description.into());
305 self
306 }
307
308 pub fn from_column(mut self, column_name: impl Into<String>) -> Self {
310 self.column_name = Some(column_name.into());
311 self
312 }
313
314 pub fn deprecated(mut self, reason: impl Into<String>) -> Self {
316 self.deprecated = true;
317 self.deprecation_reason = Some(reason.into());
318 self
319 }
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
324pub enum FieldType {
325 Scalar(GraphQLScalar),
327 Object(String),
329 List(Box<FieldType>),
331 NonNull(Box<FieldType>),
333}
334
335impl FieldType {
336 pub fn scalar(scalar: GraphQLScalar) -> Self {
338 FieldType::Scalar(scalar)
339 }
340
341 pub fn object(name: impl Into<String>) -> Self {
343 FieldType::Object(name.into())
344 }
345
346 pub fn list(inner: FieldType) -> Self {
348 FieldType::List(Box::new(inner))
349 }
350
351 pub fn non_null(inner: FieldType) -> Self {
353 FieldType::NonNull(Box::new(inner))
354 }
355}
356
357impl std::fmt::Display for FieldType {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 match self {
360 FieldType::Scalar(s) => write!(f, "{}", s.to_sdl()),
361 FieldType::Object(name) => write!(f, "{}", name),
362 FieldType::List(inner) => write!(f, "[{}]", inner),
363 FieldType::NonNull(inner) => write!(f, "{}!", inner),
364 }
365 }
366}
367
368#[derive(Debug, Clone)]
370pub struct GraphQLInputType {
371 pub name: String,
373 pub fields: Vec<GraphQLField>,
375}
376
377#[derive(Debug, Clone)]
379pub struct GraphQLEnumType {
380 pub name: String,
382 pub values: Vec<String>,
384}
385
386#[derive(Debug, Clone)]
388pub struct QueryDefinition {
389 pub name: String,
391 pub arguments: Vec<ArgumentDefinition>,
393 pub return_type: String,
395 pub returns_list: bool,
397 pub table_name: Option<String>,
399}
400
401impl QueryDefinition {
402 pub fn new(name: impl Into<String>, return_type: impl Into<String>) -> Self {
404 Self {
405 name: name.into(),
406 arguments: Vec::new(),
407 return_type: return_type.into(),
408 returns_list: false,
409 table_name: None,
410 }
411 }
412
413 pub fn arg(mut self, arg: ArgumentDefinition) -> Self {
415 self.arguments.push(arg);
416 self
417 }
418
419 pub fn returns_list(mut self, list: bool) -> Self {
421 self.returns_list = list;
422 self
423 }
424
425 pub fn from_table(mut self, table: impl Into<String>) -> Self {
427 self.table_name = Some(table.into());
428 self
429 }
430}
431
432#[derive(Debug, Clone)]
434pub struct MutationDefinition {
435 pub name: String,
437 pub arguments: Vec<ArgumentDefinition>,
439 pub return_type: String,
441 pub table_name: Option<String>,
443 pub kind: MutationKind,
445}
446
447impl MutationDefinition {
448 pub fn new(
450 name: impl Into<String>,
451 return_type: impl Into<String>,
452 kind: MutationKind,
453 ) -> Self {
454 Self {
455 name: name.into(),
456 arguments: Vec::new(),
457 return_type: return_type.into(),
458 table_name: None,
459 kind,
460 }
461 }
462
463 pub fn arg(mut self, arg: ArgumentDefinition) -> Self {
465 self.arguments.push(arg);
466 self
467 }
468
469 pub fn from_table(mut self, table: impl Into<String>) -> Self {
471 self.table_name = Some(table.into());
472 self
473 }
474}
475
476#[derive(Debug, Clone, Copy, PartialEq, Eq)]
478pub enum MutationKind {
479 Create,
481 Update,
483 Delete,
485}
486
487#[derive(Debug, Clone)]
489pub struct ArgumentDefinition {
490 pub name: String,
492 pub graphql_type: FieldType,
494 pub nullable: bool,
496 pub default_value: Option<serde_json::Value>,
498}
499
500impl ArgumentDefinition {
501 pub fn new(name: impl Into<String>, graphql_type: FieldType) -> Self {
503 Self {
504 name: name.into(),
505 graphql_type,
506 nullable: true,
507 default_value: None,
508 }
509 }
510
511 pub fn required(mut self) -> Self {
513 self.nullable = false;
514 self
515 }
516
517 pub fn default(mut self, value: serde_json::Value) -> Self {
519 self.default_value = Some(value);
520 self
521 }
522}
523
524#[derive(Debug, Clone)]
526pub struct Relationship {
527 pub name: String,
529 pub from_type: String,
531 pub to_type: String,
533 pub from_column: String,
535 pub to_column: String,
537 pub relation_type: RelationType,
539 pub field_name: String,
541}
542
543impl Relationship {
544 pub fn new(
546 name: impl Into<String>,
547 from_type: impl Into<String>,
548 to_type: impl Into<String>,
549 relation_type: RelationType,
550 ) -> Self {
551 let name = name.into();
552 let field_name = to_camel_case(&name);
553
554 Self {
555 name: name.clone(),
556 from_type: from_type.into(),
557 to_type: to_type.into(),
558 from_column: "id".to_string(),
559 to_column: "id".to_string(),
560 relation_type,
561 field_name,
562 }
563 }
564
565 pub fn columns(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
567 self.from_column = from.into();
568 self.to_column = to.into();
569 self
570 }
571
572 pub fn field(mut self, name: impl Into<String>) -> Self {
574 self.field_name = name.into();
575 self
576 }
577}
578
579#[derive(Debug)]
581pub struct SchemaIntrospector {
582 excluded_tables: Vec<String>,
584 excluded_columns: HashMap<String, Vec<String>>,
586 type_names: HashMap<String, String>,
588}
589
590impl SchemaIntrospector {
591 pub fn new() -> Self {
593 Self {
594 excluded_tables: vec!["pg_catalog".to_string(), "information_schema".to_string()],
595 excluded_columns: HashMap::new(),
596 type_names: HashMap::new(),
597 }
598 }
599
600 pub fn exclude_table(&mut self, table: impl Into<String>) {
602 self.excluded_tables.push(table.into());
603 }
604
605 pub fn exclude_column(&mut self, table: impl Into<String>, column: impl Into<String>) {
607 self.excluded_columns
608 .entry(table.into())
609 .or_default()
610 .push(column.into());
611 }
612
613 pub fn set_type_name(&mut self, table: impl Into<String>, type_name: impl Into<String>) {
615 self.type_names.insert(table.into(), type_name.into());
616 }
617
618 pub fn build_schema(&self, tables: &[TableDefinition]) -> GraphQLSchema {
620 let mut schema = GraphQLSchema::new();
621
622 for table in tables {
623 if self.excluded_tables.contains(&table.name) {
624 continue;
625 }
626
627 let type_def = self.generate_type(table);
629 let type_name = type_def.name.clone();
630 schema.add_type(type_def);
631
632 schema.add_query(
634 QueryDefinition::new(to_camel_case(&table.name), &type_name)
635 .arg(
636 ArgumentDefinition::new("id", FieldType::scalar(GraphQLScalar::ID))
637 .required(),
638 )
639 .from_table(&table.name),
640 );
641
642 schema.add_query(
643 QueryDefinition::new(format!("{}s", to_camel_case(&table.name)), &type_name)
644 .arg(ArgumentDefinition::new(
645 "limit",
646 FieldType::scalar(GraphQLScalar::Int),
647 ))
648 .arg(ArgumentDefinition::new(
649 "offset",
650 FieldType::scalar(GraphQLScalar::Int),
651 ))
652 .arg(ArgumentDefinition::new(
653 "where",
654 FieldType::object(format!("{}Filter", type_name)),
655 ))
656 .returns_list(true)
657 .from_table(&table.name),
658 );
659
660 schema.add_mutation(
662 MutationDefinition::new(
663 format!("create{}", type_name),
664 &type_name,
665 MutationKind::Create,
666 )
667 .arg(
668 ArgumentDefinition::new(
669 "input",
670 FieldType::object(format!("Create{}Input", type_name)),
671 )
672 .required(),
673 )
674 .from_table(&table.name),
675 );
676
677 schema.add_mutation(
678 MutationDefinition::new(
679 format!("update{}", type_name),
680 &type_name,
681 MutationKind::Update,
682 )
683 .arg(ArgumentDefinition::new("id", FieldType::scalar(GraphQLScalar::ID)).required())
684 .arg(
685 ArgumentDefinition::new(
686 "input",
687 FieldType::object(format!("Update{}Input", type_name)),
688 )
689 .required(),
690 )
691 .from_table(&table.name),
692 );
693
694 schema.add_mutation(
695 MutationDefinition::new(
696 format!("delete{}", type_name),
697 "Boolean".to_string(),
698 MutationKind::Delete,
699 )
700 .arg(ArgumentDefinition::new("id", FieldType::scalar(GraphQLScalar::ID)).required())
701 .from_table(&table.name),
702 );
703
704 let filter_type = self.generate_filter_type(table);
706 schema.input_types.push(filter_type);
707
708 let create_input = self.generate_create_input(table, &type_name);
710 schema.input_types.push(create_input);
711
712 let update_input = self.generate_update_input(table, &type_name);
714 schema.input_types.push(update_input);
715 }
716
717 for table in tables {
719 for fk in &table.foreign_keys {
720 let from_type = self.get_type_name(&table.name);
721 let to_type = self.get_type_name(&fk.referenced_table);
722
723 schema.add_relationship(
725 Relationship::new(&fk.name, &from_type, &to_type, RelationType::ManyToOne)
726 .columns(&fk.column, &fk.referenced_column)
727 .field(to_camel_case(&fk.name)),
728 );
729
730 let reverse_name = format!("{}s", to_camel_case(&table.name));
732 schema.add_relationship(
733 Relationship::new(&reverse_name, &to_type, &from_type, RelationType::OneToMany)
734 .columns(&fk.referenced_column, &fk.column)
735 .field(&reverse_name),
736 );
737 }
738 }
739
740 schema
741 }
742
743 fn generate_type(&self, table: &TableDefinition) -> GraphQLType {
745 let type_name = self.get_type_name(&table.name);
746 let mut type_def = GraphQLType::new(&type_name).from_table(&table.name);
747
748 let excluded = self.excluded_columns.get(&table.name);
749
750 for column in &table.columns {
751 if let Some(excluded) = excluded {
752 if excluded.contains(&column.name) {
753 continue;
754 }
755 }
756
757 let scalar = GraphQLScalar::from_sql_type(&column.data_type);
758 let field = GraphQLField::new(to_camel_case(&column.name), FieldType::scalar(scalar))
759 .nullable(column.nullable)
760 .from_column(&column.name);
761
762 type_def.add_field(field);
763 }
764
765 type_def
766 }
767
768 fn generate_filter_type(&self, table: &TableDefinition) -> GraphQLInputType {
770 let type_name = self.get_type_name(&table.name);
771 let mut input = GraphQLInputType {
772 name: format!("{}Filter", type_name),
773 fields: Vec::new(),
774 };
775
776 for column in &table.columns {
777 let scalar = GraphQLScalar::from_sql_type(&column.data_type);
778 let filter_type_name = format!("{}Filter", scalar.to_sdl());
779
780 input.fields.push(GraphQLField::new(
781 to_camel_case(&column.name),
782 FieldType::object(filter_type_name),
783 ));
784 }
785
786 input.fields.push(GraphQLField::new(
788 "AND",
789 FieldType::list(FieldType::object(format!("{}Filter", type_name))),
790 ));
791 input.fields.push(GraphQLField::new(
792 "OR",
793 FieldType::list(FieldType::object(format!("{}Filter", type_name))),
794 ));
795
796 input
797 }
798
799 fn generate_create_input(&self, table: &TableDefinition, type_name: &str) -> GraphQLInputType {
801 let mut input = GraphQLInputType {
802 name: format!("Create{}Input", type_name),
803 fields: Vec::new(),
804 };
805
806 for column in &table.columns {
807 if column.is_primary_key && column.data_type.to_lowercase().contains("serial") {
809 continue;
810 }
811
812 let scalar = GraphQLScalar::from_sql_type(&column.data_type);
813 input.fields.push(
814 GraphQLField::new(to_camel_case(&column.name), FieldType::scalar(scalar))
815 .nullable(column.nullable || column.has_default),
816 );
817 }
818
819 input
820 }
821
822 fn generate_update_input(&self, table: &TableDefinition, type_name: &str) -> GraphQLInputType {
824 let mut input = GraphQLInputType {
825 name: format!("Update{}Input", type_name),
826 fields: Vec::new(),
827 };
828
829 for column in &table.columns {
830 if column.is_primary_key {
832 continue;
833 }
834
835 let scalar = GraphQLScalar::from_sql_type(&column.data_type);
836 input.fields.push(GraphQLField::new(
837 to_camel_case(&column.name),
838 FieldType::scalar(scalar),
839 ));
840 }
841
842 input
843 }
844
845 fn get_type_name(&self, table_name: &str) -> String {
847 self.type_names
848 .get(table_name)
849 .cloned()
850 .unwrap_or_else(|| to_pascal_case(table_name))
851 }
852}
853
854impl Default for SchemaIntrospector {
855 fn default() -> Self {
856 Self::new()
857 }
858}
859
860#[derive(Debug, Clone)]
862pub struct TableDefinition {
863 pub name: String,
865 pub schema: String,
867 pub columns: Vec<ColumnDefinition>,
869 pub foreign_keys: Vec<ForeignKeyDefinition>,
871}
872
873impl TableDefinition {
874 pub fn new(name: impl Into<String>) -> Self {
876 Self {
877 name: name.into(),
878 schema: "public".to_string(),
879 columns: Vec::new(),
880 foreign_keys: Vec::new(),
881 }
882 }
883
884 pub fn column(mut self, column: ColumnDefinition) -> Self {
886 self.columns.push(column);
887 self
888 }
889
890 pub fn foreign_key(mut self, fk: ForeignKeyDefinition) -> Self {
892 self.foreign_keys.push(fk);
893 self
894 }
895}
896
897#[derive(Debug, Clone)]
899pub struct ColumnDefinition {
900 pub name: String,
902 pub data_type: String,
904 pub nullable: bool,
906 pub is_primary_key: bool,
908 pub has_default: bool,
910}
911
912impl ColumnDefinition {
913 pub fn new(name: impl Into<String>, data_type: impl Into<String>) -> Self {
915 Self {
916 name: name.into(),
917 data_type: data_type.into(),
918 nullable: true,
919 is_primary_key: false,
920 has_default: false,
921 }
922 }
923
924 pub fn nullable(mut self, nullable: bool) -> Self {
926 self.nullable = nullable;
927 self
928 }
929
930 pub fn primary_key(mut self) -> Self {
932 self.is_primary_key = true;
933 self.nullable = false;
934 self
935 }
936
937 pub fn with_default(mut self) -> Self {
939 self.has_default = true;
940 self
941 }
942}
943
944#[derive(Debug, Clone)]
946pub struct ForeignKeyDefinition {
947 pub name: String,
949 pub column: String,
951 pub referenced_table: String,
953 pub referenced_column: String,
955}
956
957impl ForeignKeyDefinition {
958 pub fn new(
960 name: impl Into<String>,
961 column: impl Into<String>,
962 referenced_table: impl Into<String>,
963 referenced_column: impl Into<String>,
964 ) -> Self {
965 Self {
966 name: name.into(),
967 column: column.into(),
968 referenced_table: referenced_table.into(),
969 referenced_column: referenced_column.into(),
970 }
971 }
972}
973
974#[cfg(test)]
975mod tests {
976 use super::*;
977
978 fn create_test_tables() -> Vec<TableDefinition> {
979 vec![
980 TableDefinition::new("users")
981 .column(ColumnDefinition::new("id", "serial").primary_key())
982 .column(ColumnDefinition::new("name", "varchar(255)").nullable(false))
983 .column(ColumnDefinition::new("email", "varchar(255)").nullable(false))
984 .column(ColumnDefinition::new("created_at", "timestamp").with_default()),
985 TableDefinition::new("posts")
986 .column(ColumnDefinition::new("id", "serial").primary_key())
987 .column(ColumnDefinition::new("title", "varchar(255)").nullable(false))
988 .column(ColumnDefinition::new("content", "text"))
989 .column(ColumnDefinition::new("user_id", "integer").nullable(false))
990 .foreign_key(ForeignKeyDefinition::new(
991 "author", "user_id", "users", "id",
992 )),
993 ]
994 }
995
996 #[test]
997 fn test_introspector_build_schema() {
998 let introspector = SchemaIntrospector::new();
999 let tables = create_test_tables();
1000 let schema = introspector.build_schema(&tables);
1001
1002 assert_eq!(schema.types.len(), 2);
1003 assert!(schema.get_type("Users").is_some());
1004 assert!(schema.get_type("Posts").is_some());
1005 }
1006
1007 #[test]
1008 fn test_schema_to_sdl() {
1009 let introspector = SchemaIntrospector::new();
1010 let tables = create_test_tables();
1011 let schema = introspector.build_schema(&tables);
1012
1013 let sdl = schema.to_sdl();
1014
1015 assert!(sdl.contains("type Users"));
1016 assert!(sdl.contains("type Posts"));
1017 assert!(sdl.contains("type Query"));
1018 assert!(sdl.contains("type Mutation"));
1019 }
1020
1021 #[test]
1022 fn test_type_generation() {
1023 let introspector = SchemaIntrospector::new();
1024 let table = TableDefinition::new("users")
1025 .column(ColumnDefinition::new("id", "serial").primary_key())
1026 .column(ColumnDefinition::new("name", "varchar").nullable(false));
1027
1028 let type_def = introspector.generate_type(&table);
1029
1030 assert_eq!(type_def.name, "Users");
1031 assert_eq!(type_def.fields.len(), 2);
1032 assert_eq!(type_def.fields[0].name, "id");
1033 assert_eq!(type_def.fields[1].name, "name");
1034 }
1035
1036 #[test]
1037 fn test_relationship_generation() {
1038 let introspector = SchemaIntrospector::new();
1039 let tables = create_test_tables();
1040 let schema = introspector.build_schema(&tables);
1041
1042 let post_relationships = schema.get_relationships_for("Posts");
1043 assert_eq!(post_relationships.len(), 1);
1044 assert_eq!(post_relationships[0].to_type, "Users");
1045 assert_eq!(post_relationships[0].relation_type, RelationType::ManyToOne);
1046
1047 let user_relationships = schema.get_relationships_for("Users");
1048 assert_eq!(user_relationships.len(), 1);
1049 assert_eq!(user_relationships[0].to_type, "Posts");
1050 assert_eq!(user_relationships[0].relation_type, RelationType::OneToMany);
1051 }
1052
1053 #[test]
1054 fn test_excluded_columns() {
1055 let mut introspector = SchemaIntrospector::new();
1056 introspector.exclude_column("users", "password_hash");
1057
1058 let table = TableDefinition::new("users")
1059 .column(ColumnDefinition::new("id", "serial").primary_key())
1060 .column(ColumnDefinition::new("password_hash", "varchar"));
1061
1062 let type_def = introspector.generate_type(&table);
1063
1064 assert_eq!(type_def.fields.len(), 1);
1065 assert!(type_def.get_field("passwordHash").is_none());
1066 }
1067
1068 #[test]
1069 fn test_type_name_override() {
1070 let mut introspector = SchemaIntrospector::new();
1071 introspector.set_type_name("users", "User");
1072
1073 let table = TableDefinition::new("users")
1074 .column(ColumnDefinition::new("id", "serial").primary_key());
1075
1076 let type_def = introspector.generate_type(&table);
1077
1078 assert_eq!(type_def.name, "User");
1079 }
1080
1081 #[test]
1082 fn test_field_type_display() {
1083 assert_eq!(
1084 FieldType::scalar(GraphQLScalar::String).to_string(),
1085 "String"
1086 );
1087 assert_eq!(FieldType::object("User").to_string(), "User");
1088 assert_eq!(
1089 FieldType::list(FieldType::object("User")).to_string(),
1090 "[User]"
1091 );
1092 assert_eq!(
1093 FieldType::non_null(FieldType::list(FieldType::object("User"))).to_string(),
1094 "[User]!"
1095 );
1096 }
1097
1098 #[test]
1099 fn test_graphql_schema_default() {
1100 let schema = GraphQLSchema::default();
1101 assert!(schema.types.is_empty());
1102 assert!(schema.queries.is_empty());
1103 assert!(schema.mutations.is_empty());
1104 }
1105}