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