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 return_type: String,
132 pub is_list: bool,
134 pub is_by_pk: bool,
136 pub description: Option<String>,
138}
139
140impl QueryField {
141 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 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#[derive(Debug, Clone)]
196pub struct MutationField {
197 pub name: String,
199 pub table_name: String,
201 pub mutation_type: MutationType,
203 pub return_type: String,
205 pub description: Option<String>,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211pub enum MutationType {
212 Insert,
214 InsertOne,
216 Update,
218 UpdateByPk,
220 Delete,
222 DeleteByPk,
224}
225
226impl MutationField {
227 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 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 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 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 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 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 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 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 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
358pub 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 for table in schema_cache.tables.values() {
367 if !config.is_schema_exposed(&table.schema) {
369 continue;
370 }
371
372 let obj_type = TableObjectType::from_table(table);
374 let type_name = obj_type.name.clone();
375
376 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 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 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
415fn 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 #[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 #[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 #[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 #[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 #[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 assert_eq!(schema.query_fields.len(), 6);
703
704 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 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 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(); let schema = build_schema(&cache, &config);
781
782 assert_eq!(schema.object_types.len(), 3);
784 assert!(schema.get_object_type("Secrets").is_none());
785 }
786
787 #[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 assert_eq!(fields.len(), 2); }
821}