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