postrust_graphql/schema/
mod.rs

1//! GraphQL schema generation from PostgreSQL schema cache.
2//!
3//! Builds a dynamic GraphQL schema from the database schema cache,
4//! creating query and mutation types for each table.
5
6pub mod object;
7pub mod relationship;
8
9use crate::input::mutation::{is_deletable, is_insertable, is_updatable};
10use crate::schema::object::{to_camel_case, to_pascal_case, TableObjectType};
11use crate::schema::relationship::RelationshipField;
12use postrust_core::schema_cache::{SchemaCache, Table};
13use std::collections::HashMap;
14
15/// Configuration for schema generation.
16#[derive(Debug, Clone)]
17pub struct SchemaConfig {
18    /// Schemas to expose in GraphQL (e.g., ["public"])
19    pub exposed_schemas: Vec<String>,
20    /// Whether to generate mutation types
21    pub enable_mutations: bool,
22    /// Whether to generate subscription types
23    pub enable_subscriptions: bool,
24    /// Prefix for query fields (e.g., "all" -> "allUsers")
25    pub query_prefix: Option<String>,
26    /// Suffix for query fields (e.g., "Collection" -> "usersCollection")
27    pub query_suffix: Option<String>,
28    /// Whether to use camelCase for field names
29    pub use_camel_case: bool,
30}
31
32impl Default for SchemaConfig {
33    fn default() -> Self {
34        Self {
35            exposed_schemas: vec!["public".to_string()],
36            enable_mutations: true,
37            enable_subscriptions: false,
38            query_prefix: None,
39            query_suffix: None,
40            use_camel_case: true,
41        }
42    }
43}
44
45impl SchemaConfig {
46    /// Create a new schema config.
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Set the exposed schemas.
52    pub fn with_schemas(mut self, schemas: Vec<String>) -> Self {
53        self.exposed_schemas = schemas;
54        self
55    }
56
57    /// Enable or disable mutations.
58    pub fn with_mutations(mut self, enable: bool) -> Self {
59        self.enable_mutations = enable;
60        self
61    }
62
63    /// Enable or disable subscriptions.
64    pub fn with_subscriptions(mut self, enable: bool) -> Self {
65        self.enable_subscriptions = enable;
66        self
67    }
68
69    /// Check if a schema is exposed.
70    pub fn is_schema_exposed(&self, schema: &str) -> bool {
71        self.exposed_schemas.iter().any(|s| s == schema)
72    }
73}
74
75/// Represents a generated GraphQL schema.
76#[derive(Debug, Clone)]
77pub struct GeneratedSchema {
78    /// Object types for each table
79    pub object_types: HashMap<String, TableObjectType>,
80    /// Query fields
81    pub query_fields: Vec<QueryField>,
82    /// Mutation fields (if enabled)
83    pub mutation_fields: Vec<MutationField>,
84    /// Relationship fields for each type
85    pub relationship_fields: HashMap<String, Vec<RelationshipField>>,
86}
87
88impl GeneratedSchema {
89    /// Get an object type by name.
90    pub fn get_object_type(&self, name: &str) -> Option<&TableObjectType> {
91        self.object_types.get(name)
92    }
93
94    /// Get query fields for a table.
95    pub fn get_query_field(&self, table_name: &str) -> Option<&QueryField> {
96        self.query_fields.iter().find(|f| f.table_name == table_name)
97    }
98
99    /// Get mutation fields for a table.
100    pub fn get_mutation_fields(&self, table_name: &str) -> Vec<&MutationField> {
101        self.mutation_fields
102            .iter()
103            .filter(|f| f.table_name == table_name)
104            .collect()
105    }
106
107    /// Get relationship fields for a type.
108    pub fn get_relationship_fields(&self, type_name: &str) -> Option<&Vec<RelationshipField>> {
109        self.relationship_fields.get(type_name)
110    }
111
112    /// Get all table names.
113    pub fn table_names(&self) -> Vec<&str> {
114        self.object_types.values().map(|t| t.table.name.as_str()).collect()
115    }
116
117    /// Get all type names.
118    pub fn type_names(&self) -> Vec<&str> {
119        self.object_types.keys().map(|s| s.as_str()).collect()
120    }
121}
122
123/// A query field for a table (e.g., users, userByPk).
124#[derive(Debug, Clone)]
125pub struct QueryField {
126    /// Field name (e.g., "users")
127    pub name: String,
128    /// Table name
129    pub table_name: String,
130    /// GraphQL return type
131    pub return_type: String,
132    /// Whether this returns a list
133    pub is_list: bool,
134    /// Whether this is a "by PK" query
135    pub is_by_pk: bool,
136    /// Field description
137    pub description: Option<String>,
138}
139
140impl QueryField {
141    /// Create a list query field (e.g., users).
142    pub fn list(table: &Table, config: &SchemaConfig) -> Self {
143        let type_name = to_pascal_case(&table.name);
144        let field_name = if config.use_camel_case {
145            to_camel_case(&table.name)
146        } else {
147            table.name.clone()
148        };
149
150        let name = match (&config.query_prefix, &config.query_suffix) {
151            (Some(prefix), None) => format!("{}{}", prefix, to_pascal_case(&field_name)),
152            (None, Some(suffix)) => format!("{}{}", field_name, suffix),
153            (Some(prefix), Some(suffix)) => {
154                format!("{}{}{}", prefix, to_pascal_case(&field_name), suffix)
155            }
156            (None, None) => field_name,
157        };
158
159        Self {
160            name,
161            table_name: table.name.clone(),
162            return_type: format!("[{}!]!", type_name),
163            is_list: true,
164            is_by_pk: false,
165            description: Some(format!("Query {} records", table.name)),
166        }
167    }
168
169    /// Create a by-PK query field (e.g., userByPk).
170    pub fn by_pk(table: &Table, config: &SchemaConfig) -> Option<Self> {
171        if table.pk_cols.is_empty() {
172            return None;
173        }
174
175        let type_name = to_pascal_case(&table.name);
176        let singular = singularize(&table.name);
177        let field_name = if config.use_camel_case {
178            format!("{}ByPk", to_camel_case(&singular))
179        } else {
180            format!("{}_by_pk", singular)
181        };
182
183        Some(Self {
184            name: field_name,
185            table_name: table.name.clone(),
186            return_type: type_name,
187            is_list: false,
188            is_by_pk: true,
189            description: Some(format!("Get a single {} by primary key", singular)),
190        })
191    }
192}
193
194/// A mutation field for a table.
195#[derive(Debug, Clone)]
196pub struct MutationField {
197    /// Field name (e.g., "insertUsers")
198    pub name: String,
199    /// Table name
200    pub table_name: String,
201    /// Mutation type
202    pub mutation_type: MutationType,
203    /// GraphQL return type
204    pub return_type: String,
205    /// Field description
206    pub description: Option<String>,
207}
208
209/// Types of mutations.
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211pub enum MutationType {
212    /// Insert multiple records
213    Insert,
214    /// Insert a single record
215    InsertOne,
216    /// Update records matching a filter
217    Update,
218    /// Update a single record by PK
219    UpdateByPk,
220    /// Delete records matching a filter
221    Delete,
222    /// Delete a single record by PK
223    DeleteByPk,
224}
225
226impl MutationField {
227    /// Create insert mutation fields for a table.
228    pub fn insert_fields(table: &Table, config: &SchemaConfig) -> Vec<Self> {
229        if !is_insertable(table) {
230            return vec![];
231        }
232
233        let type_name = to_pascal_case(&table.name);
234        let singular = singularize(&table.name);
235
236        let mut fields = vec![];
237
238        // insert_users (batch insert)
239        let name = if config.use_camel_case {
240            format!("insert{}", to_pascal_case(&table.name))
241        } else {
242            format!("insert_{}", table.name)
243        };
244        fields.push(Self {
245            name,
246            table_name: table.name.clone(),
247            mutation_type: MutationType::Insert,
248            return_type: format!("[{}!]!", type_name),
249            description: Some(format!("Insert multiple {} records", table.name)),
250        });
251
252        // insert_user_one (single insert)
253        let name = if config.use_camel_case {
254            format!("insert{}One", to_pascal_case(&singular))
255        } else {
256            format!("insert_{}_one", singular)
257        };
258        fields.push(Self {
259            name,
260            table_name: table.name.clone(),
261            mutation_type: MutationType::InsertOne,
262            return_type: type_name.clone(),
263            description: Some(format!("Insert a single {} record", singular)),
264        });
265
266        fields
267    }
268
269    /// Create update mutation fields for a table.
270    pub fn update_fields(table: &Table, config: &SchemaConfig) -> Vec<Self> {
271        if !is_updatable(table) {
272            return vec![];
273        }
274
275        let type_name = to_pascal_case(&table.name);
276        let singular = singularize(&table.name);
277
278        let mut fields = vec![];
279
280        // update_users (batch update)
281        let name = if config.use_camel_case {
282            format!("update{}", to_pascal_case(&table.name))
283        } else {
284            format!("update_{}", table.name)
285        };
286        fields.push(Self {
287            name,
288            table_name: table.name.clone(),
289            mutation_type: MutationType::Update,
290            return_type: format!("[{}!]!", type_name),
291            description: Some(format!("Update {} records", table.name)),
292        });
293
294        // update_user_by_pk (single update by PK)
295        if !table.pk_cols.is_empty() {
296            let name = if config.use_camel_case {
297                format!("update{}ByPk", to_pascal_case(&singular))
298            } else {
299                format!("update_{}_by_pk", singular)
300            };
301            fields.push(Self {
302                name,
303                table_name: table.name.clone(),
304                mutation_type: MutationType::UpdateByPk,
305                return_type: type_name,
306                description: Some(format!("Update a single {} by primary key", singular)),
307            });
308        }
309
310        fields
311    }
312
313    /// Create delete mutation fields for a table.
314    pub fn delete_fields(table: &Table, config: &SchemaConfig) -> Vec<Self> {
315        if !is_deletable(table) {
316            return vec![];
317        }
318
319        let type_name = to_pascal_case(&table.name);
320        let singular = singularize(&table.name);
321
322        let mut fields = vec![];
323
324        // delete_users (batch delete)
325        let name = if config.use_camel_case {
326            format!("delete{}", to_pascal_case(&table.name))
327        } else {
328            format!("delete_{}", table.name)
329        };
330        fields.push(Self {
331            name,
332            table_name: table.name.clone(),
333            mutation_type: MutationType::Delete,
334            return_type: format!("[{}!]!", type_name),
335            description: Some(format!("Delete {} records", table.name)),
336        });
337
338        // delete_user_by_pk (single delete by PK)
339        if !table.pk_cols.is_empty() {
340            let name = if config.use_camel_case {
341                format!("delete{}ByPk", to_pascal_case(&singular))
342            } else {
343                format!("delete_{}_by_pk", singular)
344            };
345            fields.push(Self {
346                name,
347                table_name: table.name.clone(),
348                mutation_type: MutationType::DeleteByPk,
349                return_type: type_name,
350                description: Some(format!("Delete a single {} by primary key", singular)),
351            });
352        }
353
354        fields
355    }
356}
357
358/// Build a GraphQL schema from a schema cache.
359pub fn build_schema(schema_cache: &SchemaCache, config: &SchemaConfig) -> GeneratedSchema {
360    let mut object_types = HashMap::new();
361    let mut query_fields = Vec::new();
362    let mut mutation_fields = Vec::new();
363    let mut relationship_fields = HashMap::new();
364
365    // Process each table in the schema cache
366    for table in schema_cache.tables.values() {
367        // Skip tables not in exposed schemas
368        if !config.is_schema_exposed(&table.schema) {
369            continue;
370        }
371
372        // Create object type
373        let obj_type = TableObjectType::from_table(table);
374        let type_name = obj_type.name.clone();
375
376        // Add query fields
377        query_fields.push(QueryField::list(table, config));
378        if let Some(by_pk) = QueryField::by_pk(table, config) {
379            query_fields.push(by_pk);
380        }
381
382        // Add mutation fields if enabled
383        if config.enable_mutations {
384            mutation_fields.extend(MutationField::insert_fields(table, config));
385            mutation_fields.extend(MutationField::update_fields(table, config));
386            mutation_fields.extend(MutationField::delete_fields(table, config));
387        }
388
389        // Add relationship fields
390        let rels: Vec<RelationshipField> = schema_cache
391            .get_relationships(&table.qualified_identifier(), &table.schema)
392            .map(|relationships| {
393                relationships
394                    .iter()
395                    .map(|r| RelationshipField::from_relationship(r))
396                    .collect()
397            })
398            .unwrap_or_default();
399
400        if !rels.is_empty() {
401            relationship_fields.insert(type_name.clone(), rels);
402        }
403
404        object_types.insert(type_name, obj_type);
405    }
406
407    GeneratedSchema {
408        object_types,
409        query_fields,
410        mutation_fields,
411        relationship_fields,
412    }
413}
414
415/// Simple singularization for field names.
416fn singularize(s: &str) -> String {
417    if s.ends_with("ies") {
418        format!("{}y", &s[..s.len() - 3])
419    } else if s.ends_with("es")
420        && (s.ends_with("ses") || s.ends_with("xes") || s.ends_with("ches") || s.ends_with("shes"))
421    {
422        s[..s.len() - 2].to_string()
423    } else if s.ends_with('s') && !s.ends_with("ss") {
424        s[..s.len() - 1].to_string()
425    } else {
426        s.to_string()
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use indexmap::IndexMap;
434    use postrust_core::schema_cache::Column;
435    use pretty_assertions::assert_eq;
436
437    fn create_test_table(name: &str, insertable: bool, updatable: bool, deletable: bool) -> Table {
438        let mut columns = IndexMap::new();
439        columns.insert(
440            "id".into(),
441            Column {
442                name: "id".into(),
443                description: None,
444                nullable: false,
445                data_type: "integer".into(),
446                nominal_type: "int4".into(),
447                max_len: None,
448                default: Some("nextval('id_seq')".into()),
449                enum_values: vec![],
450                is_pk: true,
451                position: 1,
452            },
453        );
454        columns.insert(
455            "name".into(),
456            Column {
457                name: "name".into(),
458                description: None,
459                nullable: false,
460                data_type: "text".into(),
461                nominal_type: "text".into(),
462                max_len: None,
463                default: None,
464                enum_values: vec![],
465                is_pk: false,
466                position: 2,
467            },
468        );
469
470        Table {
471            schema: "public".into(),
472            name: name.into(),
473            description: None,
474            is_view: false,
475            insertable,
476            updatable,
477            deletable,
478            pk_cols: vec!["id".into()],
479            columns,
480        }
481    }
482
483    fn create_test_schema_cache() -> SchemaCache {
484        use std::collections::{HashMap, HashSet};
485
486        let mut tables = HashMap::new();
487
488        let users = create_test_table("users", true, true, true);
489        let posts = create_test_table("posts", true, true, true);
490        let comments = create_test_table("comments", true, false, false);
491
492        tables.insert(users.qualified_identifier(), users);
493        tables.insert(posts.qualified_identifier(), posts);
494        tables.insert(comments.qualified_identifier(), comments);
495
496        SchemaCache {
497            tables,
498            relationships: HashMap::new(),
499            routines: HashMap::new(),
500            timezones: HashSet::new(),
501            pg_version: 150000,
502        }
503    }
504
505    // ============================================================================
506    // SchemaConfig Tests
507    // ============================================================================
508
509    #[test]
510    fn test_schema_config_default() {
511        let config = SchemaConfig::default();
512        assert!(config.is_schema_exposed("public"));
513        assert!(!config.is_schema_exposed("private"));
514        assert!(config.enable_mutations);
515        assert!(!config.enable_subscriptions);
516    }
517
518    #[test]
519    fn test_schema_config_with_schemas() {
520        let config = SchemaConfig::new()
521            .with_schemas(vec!["api".to_string(), "public".to_string()]);
522        assert!(config.is_schema_exposed("api"));
523        assert!(config.is_schema_exposed("public"));
524        assert!(!config.is_schema_exposed("private"));
525    }
526
527    #[test]
528    fn test_schema_config_mutations_disabled() {
529        let config = SchemaConfig::new().with_mutations(false);
530        assert!(!config.enable_mutations);
531    }
532
533    // ============================================================================
534    // QueryField Tests
535    // ============================================================================
536
537    #[test]
538    fn test_query_field_list() {
539        let table = create_test_table("users", true, true, true);
540        let config = SchemaConfig::default();
541        let field = QueryField::list(&table, &config);
542
543        assert_eq!(field.name, "users");
544        assert_eq!(field.return_type, "[Users!]!");
545        assert!(field.is_list);
546        assert!(!field.is_by_pk);
547    }
548
549    #[test]
550    fn test_query_field_list_with_prefix() {
551        let table = create_test_table("users", true, true, true);
552        let config = SchemaConfig {
553            query_prefix: Some("all".to_string()),
554            ..Default::default()
555        };
556        let field = QueryField::list(&table, &config);
557
558        assert_eq!(field.name, "allUsers");
559    }
560
561    #[test]
562    fn test_query_field_list_with_suffix() {
563        let table = create_test_table("users", true, true, true);
564        let config = SchemaConfig {
565            query_suffix: Some("Collection".to_string()),
566            ..Default::default()
567        };
568        let field = QueryField::list(&table, &config);
569
570        assert_eq!(field.name, "usersCollection");
571    }
572
573    #[test]
574    fn test_query_field_by_pk() {
575        let table = create_test_table("users", true, true, true);
576        let config = SchemaConfig::default();
577        let field = QueryField::by_pk(&table, &config).unwrap();
578
579        assert_eq!(field.name, "userByPk");
580        assert_eq!(field.return_type, "Users");
581        assert!(!field.is_list);
582        assert!(field.is_by_pk);
583    }
584
585    #[test]
586    fn test_query_field_by_pk_no_pk() {
587        let mut table = create_test_table("users", true, true, true);
588        table.pk_cols = vec![];
589        let config = SchemaConfig::default();
590        let field = QueryField::by_pk(&table, &config);
591
592        assert!(field.is_none());
593    }
594
595    // ============================================================================
596    // MutationField Tests
597    // ============================================================================
598
599    #[test]
600    fn test_mutation_field_insert() {
601        let table = create_test_table("users", true, true, true);
602        let config = SchemaConfig::default();
603        let fields = MutationField::insert_fields(&table, &config);
604
605        assert_eq!(fields.len(), 2);
606        assert_eq!(fields[0].name, "insertUsers");
607        assert_eq!(fields[0].mutation_type, MutationType::Insert);
608        assert_eq!(fields[1].name, "insertUserOne");
609        assert_eq!(fields[1].mutation_type, MutationType::InsertOne);
610    }
611
612    #[test]
613    fn test_mutation_field_insert_not_insertable() {
614        let table = create_test_table("users", false, true, true);
615        let config = SchemaConfig::default();
616        let fields = MutationField::insert_fields(&table, &config);
617
618        assert!(fields.is_empty());
619    }
620
621    #[test]
622    fn test_mutation_field_update() {
623        let table = create_test_table("users", true, true, true);
624        let config = SchemaConfig::default();
625        let fields = MutationField::update_fields(&table, &config);
626
627        assert_eq!(fields.len(), 2);
628        assert_eq!(fields[0].name, "updateUsers");
629        assert_eq!(fields[0].mutation_type, MutationType::Update);
630        assert_eq!(fields[1].name, "updateUserByPk");
631        assert_eq!(fields[1].mutation_type, MutationType::UpdateByPk);
632    }
633
634    #[test]
635    fn test_mutation_field_update_not_updatable() {
636        let table = create_test_table("users", true, false, true);
637        let config = SchemaConfig::default();
638        let fields = MutationField::update_fields(&table, &config);
639
640        assert!(fields.is_empty());
641    }
642
643    #[test]
644    fn test_mutation_field_delete() {
645        let table = create_test_table("users", true, true, true);
646        let config = SchemaConfig::default();
647        let fields = MutationField::delete_fields(&table, &config);
648
649        assert_eq!(fields.len(), 2);
650        assert_eq!(fields[0].name, "deleteUsers");
651        assert_eq!(fields[0].mutation_type, MutationType::Delete);
652        assert_eq!(fields[1].name, "deleteUserByPk");
653        assert_eq!(fields[1].mutation_type, MutationType::DeleteByPk);
654    }
655
656    #[test]
657    fn test_mutation_field_delete_not_deletable() {
658        let table = create_test_table("users", true, true, false);
659        let config = SchemaConfig::default();
660        let fields = MutationField::delete_fields(&table, &config);
661
662        assert!(fields.is_empty());
663    }
664
665    // ============================================================================
666    // Singularize Tests
667    // ============================================================================
668
669    #[test]
670    fn test_singularize() {
671        assert_eq!(singularize("users"), "user");
672        assert_eq!(singularize("posts"), "post");
673        assert_eq!(singularize("categories"), "category");
674        assert_eq!(singularize("boxes"), "box");
675        assert_eq!(singularize("matches"), "match");
676        assert_eq!(singularize("class"), "class");
677    }
678
679    // ============================================================================
680    // Build Schema Tests
681    // ============================================================================
682
683    #[test]
684    fn test_build_schema_object_types() {
685        let cache = create_test_schema_cache();
686        let config = SchemaConfig::default();
687        let schema = build_schema(&cache, &config);
688
689        assert_eq!(schema.object_types.len(), 3);
690        assert!(schema.get_object_type("Users").is_some());
691        assert!(schema.get_object_type("Posts").is_some());
692        assert!(schema.get_object_type("Comments").is_some());
693    }
694
695    #[test]
696    fn test_build_schema_query_fields() {
697        let cache = create_test_schema_cache();
698        let config = SchemaConfig::default();
699        let schema = build_schema(&cache, &config);
700
701        // 3 tables * 2 (list + byPk) = 6 query fields
702        assert_eq!(schema.query_fields.len(), 6);
703
704        // Check users query fields
705        let users_field = schema.get_query_field("users").unwrap();
706        assert_eq!(users_field.name, "users");
707        assert!(users_field.is_list);
708    }
709
710    #[test]
711    fn test_build_schema_mutation_fields() {
712        let cache = create_test_schema_cache();
713        let config = SchemaConfig::default();
714        let schema = build_schema(&cache, &config);
715
716        // users: 2 insert + 2 update + 2 delete = 6
717        // posts: 2 insert + 2 update + 2 delete = 6
718        // comments: 2 insert + 0 update + 0 delete = 2
719        // Total: 14
720        assert_eq!(schema.mutation_fields.len(), 14);
721
722        let users_mutations = schema.get_mutation_fields("users");
723        assert_eq!(users_mutations.len(), 6);
724    }
725
726    #[test]
727    fn test_build_schema_mutations_disabled() {
728        let cache = create_test_schema_cache();
729        let config = SchemaConfig::new().with_mutations(false);
730        let schema = build_schema(&cache, &config);
731
732        assert!(schema.mutation_fields.is_empty());
733    }
734
735    #[test]
736    fn test_build_schema_table_names() {
737        let cache = create_test_schema_cache();
738        let config = SchemaConfig::default();
739        let schema = build_schema(&cache, &config);
740
741        let names = schema.table_names();
742        assert_eq!(names.len(), 3);
743        assert!(names.contains(&"users"));
744        assert!(names.contains(&"posts"));
745        assert!(names.contains(&"comments"));
746    }
747
748    #[test]
749    fn test_build_schema_type_names() {
750        let cache = create_test_schema_cache();
751        let config = SchemaConfig::default();
752        let schema = build_schema(&cache, &config);
753
754        let names = schema.type_names();
755        assert_eq!(names.len(), 3);
756        assert!(names.contains(&"Users"));
757        assert!(names.contains(&"Posts"));
758        assert!(names.contains(&"Comments"));
759    }
760
761    #[test]
762    fn test_build_schema_exposed_schemas() {
763        let mut cache = create_test_schema_cache();
764
765        // Add a table in a different schema
766        let private_table = Table {
767            schema: "private".into(),
768            name: "secrets".into(),
769            description: None,
770            is_view: false,
771            insertable: true,
772            updatable: true,
773            deletable: true,
774            pk_cols: vec!["id".into()],
775            columns: indexmap::IndexMap::new(),
776        };
777        cache.tables.insert(private_table.qualified_identifier(), private_table);
778
779        let config = SchemaConfig::default(); // Only exposes "public"
780        let schema = build_schema(&cache, &config);
781
782        // Should only have 3 tables from public schema
783        assert_eq!(schema.object_types.len(), 3);
784        assert!(schema.get_object_type("Secrets").is_none());
785    }
786
787    // ============================================================================
788    // GeneratedSchema Tests
789    // ============================================================================
790
791    #[test]
792    fn test_generated_schema_get_object_type() {
793        let cache = create_test_schema_cache();
794        let config = SchemaConfig::default();
795        let schema = build_schema(&cache, &config);
796
797        let users = schema.get_object_type("Users").unwrap();
798        assert_eq!(users.table.name, "users");
799    }
800
801    #[test]
802    fn test_generated_schema_get_query_field() {
803        let cache = create_test_schema_cache();
804        let config = SchemaConfig::default();
805        let schema = build_schema(&cache, &config);
806
807        let field = schema.get_query_field("posts").unwrap();
808        assert_eq!(field.table_name, "posts");
809    }
810
811    #[test]
812    fn test_generated_schema_get_mutation_fields() {
813        let cache = create_test_schema_cache();
814        let config = SchemaConfig::default();
815        let schema = build_schema(&cache, &config);
816
817        let fields = schema.get_mutation_fields("comments");
818        // comments is only insertable
819        assert_eq!(fields.len(), 2); // insertComments + insertCommentOne
820    }
821}