1pub mod aggregate;
7pub mod object;
8pub mod relationship;
9
10use crate::input::mutation::{is_deletable, is_insertable, is_updatable};
11use crate::schema::object::TableObjectType;
12use crate::schema::relationship::RelationshipField;
13use postrust_core::schema_cache::{SchemaCache, Table};
14use std::collections::{HashMap, HashSet};
15
16#[derive(Debug, Clone)]
18pub struct SchemaConfig {
19 pub exposed_schemas: Vec<String>,
21 pub enable_mutations: bool,
23 pub enable_subscriptions: bool,
25
26 pub subscription_refresh_seconds: u64,
35 pub enum_values: HashMap<String, Vec<(String, Option<String>)>>,
44 pub names: crate::names::NameOverrides,
49 pub max_rows: Option<i64>,
54
55 pub role: Option<String>,
62}
63
64impl Default for SchemaConfig {
65 fn default() -> Self {
66 Self {
67 exposed_schemas: vec!["public".to_string()],
68 enum_values: HashMap::new(),
69 names: crate::names::NameOverrides::default(),
70 enable_mutations: true,
71 enable_subscriptions: false,
72 subscription_refresh_seconds: 30,
73 max_rows: None,
74 role: None,
75 }
76 }
77}
78
79impl SchemaConfig {
80 pub fn new() -> Self {
82 Self::default()
83 }
84
85 pub fn with_schemas(mut self, schemas: Vec<String>) -> Self {
87 self.exposed_schemas = schemas;
88 self
89 }
90
91 pub fn with_mutations(mut self, enable: bool) -> Self {
93 self.enable_mutations = enable;
94 self
95 }
96
97 pub fn with_subscriptions(mut self, enable: bool) -> Self {
99 self.enable_subscriptions = enable;
100 self
101 }
102
103 pub fn subscription_refresh(&self) -> std::time::Duration {
105 match self.subscription_refresh_seconds {
106 0 => std::time::Duration::from_secs(60 * 60 * 24 * 365),
109 seconds => std::time::Duration::from_secs(seconds),
110 }
111 }
112
113 pub fn is_schema_exposed(&self, schema: &str) -> bool {
115 self.exposed_schemas.iter().any(|s| s == schema)
116 }
117
118 pub fn default_schema(&self) -> &str {
123 self.exposed_schemas
124 .first()
125 .map(|s| s.as_str())
126 .unwrap_or("public")
127 }
128}
129
130fn has_whole_key(table: &Table) -> bool {
143 !table.pk_cols.is_empty()
144 && table
145 .pk_cols
146 .iter()
147 .all(|column| table.get_column(column).is_some())
148}
149
150fn pk_columns_of(table: &Table) -> Vec<(String, String)> {
151 table
152 .pk_cols
153 .iter()
154 .map(|col_name| {
155 let pg_type = table
156 .get_column(col_name)
157 .map(|c| c.nominal_type.clone())
158 .unwrap_or_else(|| "text".to_string());
159 (col_name.clone(), pg_type)
160 })
161 .collect()
162}
163
164pub(crate) fn relationship_keys(rel: &postrust_core::schema_cache::Relationship) -> Vec<String> {
176 use postrust_core::schema_cache::Relationship;
177
178 if let Relationship::Computed { function, .. } = rel {
179 return vec![function.name.clone()];
180 }
181
182 let mut keys = Vec::new();
183 match rel.constraint_name() {
184 "" => {}
185 constraint => keys.push(constraint.to_string()),
186 }
187 if let Some(column) = rel.single_foreign_column() {
193 keys.push(format!("{}.{}", rel.foreign_table().name, column));
194 }
195 if !rel.is_one_to_many() {
196 if let Some(column) = rel.single_local_column() {
197 keys.push(column.to_string());
198 }
199 }
200 let joined = |mut columns: Vec<String>| {
207 columns.sort();
208 columns.join(",")
209 };
210 let columns = rel.join_columns();
211 if columns.len() > 1 {
212 keys.push(format!(
213 "{}.{}",
214 rel.foreign_table().name,
215 joined(columns.iter().map(|(_, foreign)| foreign.clone()).collect())
216 ));
217 if !rel.is_one_to_many() {
218 keys.push(joined(
219 columns.iter().map(|(local, _)| local.clone()).collect(),
220 ));
221 }
222 }
223 keys
224}
225
226fn type_name_of(rendered: &str) -> (Option<String>, String) {
234 let (schema, name) = match rendered.rsplit_once('.') {
235 Some((schema, name)) => (Some(schema.trim_matches('"').to_string()), name),
236 None => (None, rendered),
237 };
238 (schema, name.trim_matches('"').to_string())
239}
240
241fn base_name_for(table: &Table, config: &SchemaConfig) -> String {
249 if let Some(given) = config.names.base_name(&table.schema, &table.name) {
252 return given.to_string();
253 }
254 if table.schema == config.default_schema() {
255 table.name.clone()
256 } else {
257 format!("{}_{}", table.schema, table.name)
258 }
259}
260
261#[derive(Debug, Clone)]
263pub struct GeneratedSchema {
264 pub object_types: HashMap<String, TableObjectType>,
266 pub query_fields: Vec<QueryField>,
268 pub mutation_fields: Vec<MutationField>,
270 pub relationship_fields: HashMap<String, Vec<RelationshipField>>,
272 pub enum_types: HashMap<String, Vec<(String, Option<String>)>>,
275 pub function_fields: Vec<FunctionField>,
277}
278
279#[derive(Debug, Clone)]
291pub struct FunctionField {
292 pub name: String,
294 pub schema_name: String,
296 pub function_name: String,
298 pub returns: String,
300 pub returns_table: (String, String),
302 pub arguments: Vec<(String, String, bool)>,
305 pub session_argument: Option<String>,
312 pub volatile: bool,
314 pub description: Option<String>,
316}
317
318fn reflected_fields(
327 table: &Table,
328 schema_cache: &SchemaCache,
329 config: &SchemaConfig,
330 base_names: &HashMap<(String, String), String>,
331) -> Vec<RelationshipField> {
332 schema_cache
333 .get_relationships(&table.qualified_identifier(), &table.schema)
334 .map(|relationships| {
335 relationships
336 .iter()
337 .filter_map(|rel| {
338 let foreign = rel.foreign_table();
341 let target_base =
342 base_names.get(&(foreign.schema.clone(), foreign.name.clone()))?;
343 let mut field = RelationshipField::from_relationship_named(
344 rel,
345 target_base,
346 relationship_keys(rel).iter().find_map(|key| {
347 config.names.relationship(&table.schema, &table.name, key)
348 }),
349 );
350 field.arguments = caller_arguments(rel, schema_cache);
351 Some(field)
352 })
353 .collect()
354 })
355 .unwrap_or_default()
356}
357
358fn declared_fields(
371 declared: &[crate::names::DeclaredRelationship],
372 table: &Table,
373 schema_cache: &SchemaCache,
374 config: &SchemaConfig,
375 base_names: &HashMap<(String, String), String>,
376) -> Vec<RelationshipField> {
377 use postrust_core::schema_cache::Relationship;
378
379 let reflected = schema_cache
380 .get_relationships(&table.qualified_identifier(), &table.schema)
381 .map(|found| found.to_vec())
382 .unwrap_or_default();
383
384 let mut fields = Vec::with_capacity(declared.len());
385 for entry in declared {
386 if let Some(key) = entry.using.as_deref() {
388 let Some(rel) = reflected
389 .iter()
390 .find(|rel| relationship_keys(rel).iter().any(|found| found == key))
391 else {
392 tracing::warn!(
393 "GraphQL: {}.{} declares the relationship \"{}\" through \"{}\", \
394 which no foreign key carries; it is left out",
395 table.schema,
396 table.name,
397 entry.name,
398 key
399 );
400 continue;
401 };
402 let foreign = rel.foreign_table();
403 let Some(target_base) = base_names.get(&(foreign.schema.clone(), foreign.name.clone()))
404 else {
405 continue;
406 };
407 let mut field =
408 RelationshipField::from_relationship_named(rel, target_base, Some(&entry.name));
409 field.arguments = caller_arguments(rel, schema_cache);
410 fields.push(field);
411 continue;
412 }
413
414 let Some((schema, name)) = entry.target(config.default_schema()) else {
416 tracing::warn!(
417 "GraphQL: {}.{} declares the relationship \"{}\" with neither a key \
418 nor a table to join to; it is left out",
419 table.schema,
420 table.name,
421 entry.name
422 );
423 continue;
424 };
425 let Some(target_base) = base_names.get(&(schema, name)) else {
426 continue;
427 };
428 match mapped_relationship_field(
429 entry,
430 table,
431 schema_cache,
432 config.default_schema(),
433 target_base,
434 ) {
435 Some(field) => fields.push(field),
436 None => tracing::warn!(
437 "GraphQL: {}.{} declares the relationship \"{}\" with no columns to \
438 join on; it is left out",
439 table.schema,
440 table.name,
441 entry.name
442 ),
443 }
444 }
445
446 let already: std::collections::HashSet<String> =
451 fields.iter().map(|field| field.name.clone()).collect();
452 for rel in reflected
453 .iter()
454 .filter(|rel| matches!(rel, Relationship::Computed { .. }))
455 {
456 let foreign = rel.foreign_table();
457 let Some(target_base) = base_names.get(&(foreign.schema.clone(), foreign.name.clone()))
458 else {
459 continue;
460 };
461 let mut field = RelationshipField::from_relationship_named(
462 rel,
463 target_base,
464 relationship_keys(rel)
465 .iter()
466 .find_map(|key| config.names.relationship(&table.schema, &table.name, key)),
467 );
468 if already.contains(&field.name) {
469 continue;
470 }
471 field.arguments = caller_arguments(rel, schema_cache);
472 fields.push(field);
473 }
474 fields
475}
476
477pub(crate) fn mapped_relationship_field(
484 entry: &crate::names::DeclaredRelationship,
485 table: &Table,
486 schema_cache: &SchemaCache,
487 default_schema: &str,
488 target_base: &str,
489) -> Option<RelationshipField> {
490 use postrust_core::schema_cache::{Cardinality, Relationship};
491
492 let (schema, name) = entry.target(default_schema)?;
493 if entry.columns.is_empty() {
494 return None;
495 }
496 let foreign_table = postrust_core::api_request::QualifiedIdentifier::new(&schema, &name);
497 let here = table.qualified_identifier();
498 let columns: Vec<(String, String)> = entry
499 .columns
500 .iter()
501 .map(|(mine, theirs)| (mine.clone(), theirs.clone()))
502 .collect();
503 let constraint = format!("{}.{}", table.name, entry.name);
504 let cardinality = match (entry.to_one, entry.after_parent) {
510 (true, false) => Cardinality::M2O {
511 constraint: constraint.clone(),
512 columns,
513 },
514 (true, true) => Cardinality::O2O {
515 constraint: constraint.clone(),
516 columns,
517 is_parent: true,
518 },
519 (false, _) => Cardinality::O2M {
520 constraint: constraint.clone(),
521 columns,
522 },
523 };
524 let rel = Relationship::ForeignKey {
525 is_self: here == foreign_table,
526 table: here,
527 foreign_table_is_view: schema_cache
528 .get_table(&foreign_table)
529 .is_some_and(|t| t.is_view),
530 foreign_table,
531 cardinality,
532 table_is_view: table.is_view,
533 constraint_name: constraint,
534 };
535 Some(RelationshipField::from_relationship_named(
536 &rel,
537 target_base,
538 Some(&entry.name),
539 ))
540}
541
542fn caller_arguments(
543 relationship: &postrust_core::schema_cache::Relationship,
544 schema_cache: &SchemaCache,
545) -> Vec<(String, String, bool)> {
546 use postrust_core::schema_cache::Relationship;
547 let Relationship::Computed {
548 function,
549 row_argument,
550 ..
551 } = relationship
552 else {
553 return Vec::new();
554 };
555 let Some(row_argument) = row_argument else {
556 return Vec::new();
557 };
558 schema_cache
559 .routines
560 .get(function)
561 .into_iter()
562 .flatten()
563 .find(|routine| routine.name == function.name)
564 .map(|routine| {
565 routine
566 .params
567 .iter()
568 .filter(|param| ¶m.name != row_argument)
569 .filter(|param| !is_session_argument(param))
570 .filter(|param| !param.name.is_empty())
571 .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
572 .collect()
573 })
574 .unwrap_or_default()
575}
576
577pub(crate) fn computed_caller_arguments(
583 computed: &postrust_core::schema_cache::ComputedColumn,
584 schema_cache: &SchemaCache,
585) -> Vec<(String, String, bool)> {
586 let Some(row_argument) = &computed.row_argument else {
587 return Vec::new();
588 };
589 schema_cache
590 .routines
591 .get(&computed.function)
592 .into_iter()
593 .flatten()
594 .find(|routine| routine.name == computed.function.name)
595 .map(|routine| {
596 routine
597 .params
598 .iter()
599 .filter(|param| ¶m.name != row_argument)
600 .filter(|param| !is_session_argument(param))
601 .filter(|param| !param.name.is_empty())
602 .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
603 .collect()
604 })
605 .unwrap_or_default()
606}
607
608fn is_session_argument(param: &postrust_core::schema_cache::RoutineParam) -> bool {
614 param.name == "hasura_session" && matches!(param.param_type.as_str(), "json" | "jsonb")
615}
616
617impl GeneratedSchema {
618 pub fn get_object_type(&self, name: &str) -> Option<&TableObjectType> {
620 self.object_types.get(name)
621 }
622
623 pub fn get_query_field(&self, table_name: &str) -> Option<&QueryField> {
625 self.query_fields
626 .iter()
627 .find(|f| f.table_name == table_name)
628 }
629
630 pub fn get_mutation_fields(&self, table_name: &str) -> Vec<&MutationField> {
632 self.mutation_fields
633 .iter()
634 .filter(|f| f.table_name == table_name)
635 .collect()
636 }
637
638 pub fn get_relationship_fields(&self, type_name: &str) -> Option<&Vec<RelationshipField>> {
640 self.relationship_fields.get(type_name)
641 }
642
643 pub fn table_names(&self) -> Vec<&str> {
645 self.object_types
646 .values()
647 .map(|t| t.table.name.as_str())
648 .collect()
649 }
650
651 pub fn type_names(&self) -> Vec<&str> {
653 self.object_types.keys().map(|s| s.as_str()).collect()
654 }
655}
656
657#[derive(Debug, Clone)]
659pub struct QueryField {
660 pub name: String,
662 pub table_name: String,
664 pub schema_name: String,
666 pub type_name: String,
668 pub return_type: String,
670 pub is_list: bool,
672 pub is_by_pk: bool,
674 pub pk_columns: Vec<(String, String)>,
680 pub description: Option<String>,
682 pub aggregate_name: Option<String>,
688 pub aggregate_description: Option<String>,
691 pub aggregates: bool,
701 pub rows: bool,
709}
710
711impl QueryField {
712 pub fn list(table: &Table) -> Self {
714 Self::list_named(table, &table.name)
715 }
716
717 pub fn list_named(table: &Table, base_name: &str) -> Self {
719 let type_name = base_name.to_string();
720 let name = base_name.to_string();
721
722 Self {
723 name,
724 table_name: table.name.clone(),
725 schema_name: table.schema.clone(),
726 type_name: type_name.clone(),
727 return_type: format!("[{}!]!", type_name),
728 is_list: true,
729 is_by_pk: false,
730 pk_columns: Vec::new(),
731 description: Some(format!("fetch data from the table: \"{}\"", table.name)),
732 aggregate_name: None,
733 aggregate_description: None,
734 aggregates: true,
735 rows: true,
736 }
737 }
738
739 pub fn by_pk(table: &Table) -> Option<Self> {
741 Self::by_pk_named(table, &table.name)
742 }
743
744 pub fn by_pk_named(table: &Table, base_name: &str) -> Option<Self> {
746 if !has_whole_key(table) {
747 return None;
748 }
749
750 let type_name = base_name.to_string();
751 let field_name = format!("{}_by_pk", base_name);
752
753 let pk_columns = pk_columns_of(table);
756
757 Some(Self {
758 name: field_name,
759 table_name: table.name.clone(),
760 schema_name: table.schema.clone(),
761 type_name: type_name.clone(),
762 return_type: type_name,
763 is_list: false,
764 is_by_pk: true,
765 pk_columns,
766 description: Some(format!(
767 "fetch data from the table: \"{}\" using primary key columns",
768 table.name
769 )),
770 aggregate_name: None,
771 aggregate_description: None,
772 aggregates: true,
773 rows: true,
774 })
775 }
776}
777
778#[derive(Debug, Clone)]
780pub struct MutationField {
781 pub name: String,
783 pub table_name: String,
785 pub schema_name: String,
787 pub mutation_type: MutationType,
789 pub pk_columns: Vec<(String, String)>,
794 pub return_type: String,
796 pub description: Option<String>,
798}
799
800#[derive(Debug, Clone, Copy, PartialEq, Eq)]
802pub enum MutationType {
803 Insert,
805 InsertOne,
807 Update,
809 UpdateByPk,
811 UpdateMany,
813 Delete,
815 DeleteByPk,
817}
818
819impl MutationField {
820 pub fn insert_fields(table: &Table) -> Vec<Self> {
822 Self::insert_fields_named(table, &table.name)
823 }
824
825 pub fn insert_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
828 if !is_insertable(table) {
829 return vec![];
830 }
831
832 let type_name = base_name.to_string();
833
834 let mut fields = vec![];
835
836 let name = format!("insert_{}", base_name);
838 fields.push(Self {
839 name,
840 table_name: table.name.clone(),
841 schema_name: table.schema.clone(),
842 mutation_type: MutationType::Insert,
843 pk_columns: Vec::new(),
844 return_type: format!("{}_mutation_response", type_name),
845 description: Some(format!("insert data into the table: \"{}\"", table.name)),
846 });
847
848 let name = format!("insert_{}_one", base_name);
850 fields.push(Self {
851 name,
852 table_name: table.name.clone(),
853 schema_name: table.schema.clone(),
854 mutation_type: MutationType::InsertOne,
855 pk_columns: Vec::new(),
856 return_type: type_name.clone(),
857 description: Some(format!(
858 "insert a single row into the table: \"{}\"",
859 table.name
860 )),
861 });
862
863 fields
864 }
865
866 pub fn update_fields(table: &Table) -> Vec<Self> {
868 Self::update_fields_named(table, &table.name)
869 }
870
871 pub fn update_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
874 if !is_updatable(table) {
875 return vec![];
876 }
877
878 let type_name = base_name.to_string();
879
880 let mut fields = vec![];
881
882 let name = format!("update_{}", base_name);
884 fields.push(Self {
885 name,
886 table_name: table.name.clone(),
887 schema_name: table.schema.clone(),
888 mutation_type: MutationType::Update,
889 pk_columns: Vec::new(),
890 return_type: format!("{}_mutation_response", type_name),
891 description: Some(format!("update data of the table: \"{}\"", table.name)),
892 });
893
894 let name = format!("update_{}_many", base_name);
896 fields.push(Self {
897 name,
898 table_name: table.name.clone(),
899 schema_name: table.schema.clone(),
900 mutation_type: MutationType::UpdateMany,
901 pk_columns: Vec::new(),
902 return_type: format!("[{}_mutation_response]", type_name),
903 description: Some(format!(
904 "update multiples rows of table: \"{}\"",
905 table.name
906 )),
907 });
908
909 if has_whole_key(table) {
911 let name = format!("update_{}_by_pk", base_name);
912 fields.push(Self {
913 name,
914 table_name: table.name.clone(),
915 schema_name: table.schema.clone(),
916 mutation_type: MutationType::UpdateByPk,
917 pk_columns: pk_columns_of(table),
918 return_type: type_name,
919 description: Some(format!(
920 "update single row of the table: \"{}\"",
921 table.name
922 )),
923 });
924 }
925
926 fields
927 }
928
929 pub fn delete_fields(table: &Table) -> Vec<Self> {
931 Self::delete_fields_named(table, &table.name)
932 }
933
934 pub fn delete_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
937 if !is_deletable(table) {
938 return vec![];
939 }
940
941 let type_name = base_name.to_string();
942
943 let mut fields = vec![];
944
945 let name = format!("delete_{}", base_name);
947 fields.push(Self {
948 name,
949 table_name: table.name.clone(),
950 schema_name: table.schema.clone(),
951 mutation_type: MutationType::Delete,
952 pk_columns: Vec::new(),
953 return_type: format!("{}_mutation_response", type_name),
954 description: Some(format!("delete data from the table: \"{}\"", table.name)),
955 });
956
957 if has_whole_key(table) {
959 let name = format!("delete_{}_by_pk", base_name);
960 fields.push(Self {
961 name,
962 table_name: table.name.clone(),
963 schema_name: table.schema.clone(),
964 mutation_type: MutationType::DeleteByPk,
965 pk_columns: pk_columns_of(table),
966 return_type: type_name,
967 description: Some(format!(
968 "delete single row from the table: \"{}\"",
969 table.name
970 )),
971 });
972 }
973
974 fields
975 }
976}
977
978pub fn build_schema(schema_cache: &SchemaCache, config: &SchemaConfig) -> GeneratedSchema {
980 let mut object_types = HashMap::new();
981 let mut query_fields = Vec::new();
982 let mut mutation_fields = Vec::new();
983 let mut relationship_fields = HashMap::new();
984
985 let mut tables: Vec<&Table> = schema_cache
988 .tables
989 .values()
990 .filter(|table| config.is_schema_exposed(&table.schema))
991 .collect();
992 tables.sort_by(|a, b| (&a.schema, &a.name).cmp(&(&b.schema, &b.name)));
993
994 let mut used_base_names: HashMap<String, u32> = HashMap::new();
999 let mut base_names: HashMap<(String, String), String> = HashMap::new();
1002
1003 for table in &tables {
1004 let preferred = base_name_for(table, config);
1005 let base_name = match used_base_names.get_mut(&preferred) {
1006 None => {
1007 used_base_names.insert(preferred.clone(), 1);
1008 preferred
1009 }
1010 Some(count) => {
1011 *count += 1;
1012 let disambiguated = format!("{}_{}", preferred, count);
1013 tracing::warn!(
1014 "GraphQL name collision: {}.{} would generate the same names as \
1015 an earlier table; exposing it as \"{}\" instead",
1016 table.schema,
1017 table.name,
1018 disambiguated
1019 );
1020 disambiguated
1021 }
1022 };
1023
1024 base_names.insert(
1025 (table.schema.clone(), table.name.clone()),
1026 base_name.clone(),
1027 );
1028 }
1029
1030 let mut readable_bases: HashSet<String> = HashSet::new();
1033
1034 for table in &tables {
1035 let base_name = base_names
1036 .get(&(table.schema.clone(), table.name.clone()))
1037 .expect("every visited table has a resolved base name")
1038 .clone();
1039
1040 let mut obj_type = TableObjectType::from_table_named(table, &base_name, &config.names);
1042 let readable = config.role.as_deref().is_none_or(|role| {
1052 config
1053 .names
1054 .permissions(&table.schema, &table.name, role)
1055 .is_none_or(|granted| crate::role::reads_anything(granted, table))
1056 });
1057 if readable {
1058 readable_bases.insert(base_name.clone());
1059 }
1060 for field in &mut obj_type.fields {
1063 let Some(function) = config
1064 .names
1065 .computed_source(&table.schema, &table.name, &field.name)
1066 .or(Some(field.name.as_str()))
1067 else {
1068 continue;
1069 };
1070 let Some(computed) = table.get_computed_column(function) else {
1071 continue;
1072 };
1073 field.arguments = computed_caller_arguments(computed, schema_cache);
1074 }
1075 let type_name = obj_type.name.clone();
1076
1077 let rename = |field: &mut QueryField, kind: &str| {
1084 if let Some(given) = config.names.root(&table.schema, &table.name, kind) {
1085 field.name = given.to_string();
1086 }
1087 if let Some(comment) = config.names.root_comment(&table.schema, &table.name, kind) {
1088 field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
1089 }
1090 };
1091 let mut list = QueryField::list_named(table, &base_name);
1092 rename(&mut list, "select");
1093 list.aggregate_name = config
1097 .names
1098 .root(&table.schema, &table.name, "select_aggregate")
1099 .map(str::to_string);
1100 list.aggregate_description = config
1101 .names
1102 .root_comment(&table.schema, &table.name, "select_aggregate")
1103 .map(str::to_string);
1104 if let Some(role) = &config.role {
1108 list.aggregates =
1109 crate::role::allows_aggregations(&config.names, &table.schema, &table.name, role);
1110 }
1111 list.rows = readable;
1117 if readable || list.aggregates {
1118 query_fields.push(list);
1119 }
1120 if readable {
1121 if let Some(mut by_pk) = QueryField::by_pk_named(table, &base_name) {
1122 rename(&mut by_pk, "select_by_pk");
1123 query_fields.push(by_pk);
1124 }
1125 }
1126
1127 if config.enable_mutations {
1129 let mut fields: Vec<MutationField> = Vec::new();
1130 fields.extend(MutationField::insert_fields_named(table, &base_name));
1131 fields.extend(MutationField::update_fields_named(table, &base_name));
1132 fields.extend(MutationField::delete_fields_named(table, &base_name));
1133 if !readable {
1135 fields.retain(|field| {
1136 !matches!(
1137 field.mutation_type,
1138 MutationType::InsertOne
1139 | MutationType::UpdateByPk
1140 | MutationType::DeleteByPk
1141 )
1142 });
1143 }
1144 for field in &mut fields {
1145 let kind = match field.mutation_type {
1146 MutationType::Insert => "insert",
1147 MutationType::InsertOne => "insert_one",
1148 MutationType::Update => "update",
1149 MutationType::UpdateByPk => "update_by_pk",
1150 MutationType::UpdateMany => "update_many",
1151 MutationType::Delete => "delete",
1152 MutationType::DeleteByPk => "delete_by_pk",
1153 };
1154 if let Some(given) = config.names.root(&table.schema, &table.name, kind) {
1155 field.name = given.to_string();
1156 }
1157 if let Some(comment) = config.names.root_comment(&table.schema, &table.name, kind) {
1158 field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
1159 }
1160 }
1161 mutation_fields.extend(fields);
1162 }
1163
1164 let rels = match config
1169 .names
1170 .declared_relationships(&table.schema, &table.name)
1171 {
1172 Some(declared) => declared_fields(declared, table, schema_cache, config, &base_names),
1173 None => reflected_fields(table, schema_cache, config, &base_names),
1174 };
1175
1176 if !rels.is_empty() {
1177 relationship_fields.insert(type_name.clone(), rels);
1178 }
1179
1180 object_types.insert(type_name, obj_type);
1181 }
1182
1183 let mut enum_types: HashMap<String, Vec<(String, Option<String>)>> = HashMap::new();
1188 let mut enum_type_of: HashMap<(String, String), String> = HashMap::new();
1189
1190 for (schema_name, table_name) in config.names.enum_tables() {
1191 let base = match base_names.get(&(schema_name.clone(), table_name.clone())) {
1198 Some(found) => found.clone(),
1199 None => config
1200 .names
1201 .base_name(&schema_name, &table_name)
1202 .map(str::to_string)
1203 .unwrap_or_else(|| match schema_name == config.default_schema() {
1204 true => table_name.clone(),
1205 false => format!("{}_{}", schema_name, table_name),
1206 }),
1207 };
1208 let key = format!("{}.{}", schema_name, table_name);
1209 let members: Vec<(String, Option<String>)> = config
1210 .enum_values
1211 .get(&key)
1212 .map(|values| {
1213 values
1214 .iter()
1215 .filter(|(value, _)| is_graphql_name(value))
1216 .cloned()
1217 .collect()
1218 })
1219 .unwrap_or_default();
1220
1221 if members.is_empty() {
1222 tracing::warn!(
1225 "{}.{} is marked as an enumeration but has no values that can name an \
1226 enum member; exposing it as an ordinary table",
1227 schema_name,
1228 table_name
1229 );
1230 continue;
1231 }
1232
1233 let type_name = format!("{}_enum", base);
1234 enum_type_of.insert((schema_name.clone(), table_name.clone()), type_name.clone());
1235 enum_types.insert(type_name, members);
1236 }
1237
1238 if !enum_type_of.is_empty() {
1248 for table in &tables {
1249 let Some(type_name) = base_names.get(&(table.schema.clone(), table.name.clone()))
1250 else {
1251 continue;
1252 };
1253 let found = schema_cache
1254 .get_relationships(&table.qualified_identifier(), &table.schema)
1255 .map(|found| found.to_vec())
1256 .unwrap_or_default();
1257 for relationship in &found {
1258 if !relationship.is_to_one() {
1259 continue;
1260 }
1261 let foreign = relationship.foreign_table();
1262 let Some(enum_type) =
1263 enum_type_of.get(&(foreign.schema.clone(), foreign.name.clone()))
1264 else {
1265 continue;
1266 };
1267 let Some(column) = relationship.single_local_column() else {
1268 continue;
1269 };
1270 if let Some(object) = object_types.get_mut(type_name) {
1271 let exposed = config
1274 .names
1275 .column(&object.table.schema, &object.table.name, column)
1276 .unwrap_or(column)
1277 .to_string();
1278 for field in &mut object.fields {
1279 if field.name == exposed {
1280 field.graphql_type =
1281 crate::types::GraphQLType::Custom(enum_type.clone());
1282 }
1283 }
1284 }
1285 }
1286 }
1287 }
1288
1289 if !enum_type_of.is_empty() {
1295 for relationships in relationship_fields.values_mut() {
1296 relationships.retain(|relationship| {
1297 let foreign = relationship.relationship.foreign_table();
1298 !enum_type_of.contains_key(&(foreign.schema.clone(), foreign.name.clone()))
1299 });
1300 }
1301 }
1302
1303 for object in object_types.values_mut() {
1312 object.writable_fields = object.fields.clone();
1316 if let Some(role) = &config.role {
1317 let (schema_name, table_name) =
1318 (object.table.schema.clone(), object.table.name.clone());
1319 let Some(granted) = config.names.permissions(&schema_name, &table_name, role) else {
1320 continue;
1321 };
1322 let table = object.table.clone();
1323 object.fields.retain(|field| {
1324 let column = config
1325 .names
1326 .column_source(&schema_name, &table_name, &field.name)
1327 .unwrap_or(&field.name);
1328 match table.get_column(column) {
1329 None => true,
1332 Some(_) => granted
1333 .select
1334 .as_ref()
1335 .is_some_and(|select| select.columns.allows(column)),
1336 }
1337 });
1338 }
1339 }
1340
1341 let mut function_fields = Vec::new();
1346 for routines in schema_cache.routines.values() {
1347 for routine in routines {
1348 if !config.is_schema_exposed(&routine.schema) || routine.is_procedure {
1349 continue;
1350 }
1351 let postrust_core::schema_cache::RetType::SetOf(returned) = &routine.return_type else {
1352 continue;
1353 };
1354 if routine.params.iter().any(|param| {
1359 let (_, named) = type_name_of(¶m.param_type);
1360 base_names.keys().any(|(_, table)| table == &named)
1361 }) {
1362 continue;
1363 }
1364 let (returned_schema, returned_table) = type_name_of(returned);
1370 let found = base_names
1371 .iter()
1372 .find(|((schema, table), _)| {
1373 table == &returned_table
1374 && schema == returned_schema.as_ref().unwrap_or(&routine.schema)
1375 })
1376 .or_else(|| {
1377 base_names
1378 .iter()
1379 .find(|((_, table), _)| table == &returned_table)
1380 });
1381 let Some(((target_schema, target_table), base)) = found else {
1382 continue;
1383 };
1384 if !readable_bases.contains(base) {
1388 continue;
1389 }
1390 let target = (target_schema.clone(), target_table.clone());
1391
1392 let volatile = match config.names.exposed_as(&routine.schema, &routine.name) {
1397 Some("query") => false,
1398 Some("mutation") => true,
1399 _ => matches!(
1400 routine.volatility,
1401 postrust_core::schema_cache::FuncVolatility::Volatile
1402 ),
1403 };
1404 if volatile {
1412 if let Some(role) = config.role.as_deref() {
1413 if !config
1414 .names
1415 .function_grants(&routine.schema, &routine.name, role)
1416 {
1417 continue;
1418 }
1419 }
1420 }
1421
1422 function_fields.push(FunctionField {
1423 name: routine.name.clone(),
1424 schema_name: routine.schema.clone(),
1425 function_name: routine.name.clone(),
1426 returns: base.clone(),
1427 returns_table: target,
1428 arguments: routine
1429 .params
1430 .iter()
1431 .filter(|param| !param.name.is_empty())
1432 .filter(|param| !is_session_argument(param))
1433 .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
1434 .collect(),
1435 session_argument: routine
1436 .params
1437 .iter()
1438 .find(|param| is_session_argument(param))
1439 .map(|param| param.name.clone()),
1440 volatile,
1441 description: routine.description.clone(),
1442 });
1443 }
1444 }
1445 function_fields.sort_by(|a, b| a.name.cmp(&b.name));
1446
1447 GeneratedSchema {
1448 object_types,
1449 query_fields,
1450 mutation_fields,
1451 relationship_fields,
1452 enum_types,
1453 function_fields,
1454 }
1455}
1456
1457fn is_graphql_name(value: &str) -> bool {
1464 let mut chars = value.chars();
1465 match chars.next() {
1466 Some(first) if first == '_' || first.is_ascii_alphabetic() => {}
1467 _ => return false,
1468 }
1469 chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
1470}
1471
1472#[cfg(test)]
1473mod tests {
1474 use super::*;
1475 use indexmap::IndexMap;
1476 use postrust_core::schema_cache::Column;
1477 use pretty_assertions::assert_eq;
1478
1479 fn create_test_table(name: &str, insertable: bool, updatable: bool, deletable: bool) -> Table {
1480 let mut columns = IndexMap::new();
1481 columns.insert(
1482 "id".into(),
1483 Column {
1484 name: "id".into(),
1485 description: None,
1486 nullable: false,
1487 data_type: "integer".into(),
1488 nominal_type: "int4".into(),
1489 max_len: None,
1490 default: Some("nextval('id_seq')".into()),
1491 enum_values: vec![],
1492 is_pk: true,
1493 position: 1,
1494 always_generated: false,
1495 domain_type: None,
1496 },
1497 );
1498 columns.insert(
1499 "name".into(),
1500 Column {
1501 name: "name".into(),
1502 description: None,
1503 nullable: false,
1504 data_type: "text".into(),
1505 nominal_type: "text".into(),
1506 max_len: None,
1507 default: None,
1508 enum_values: vec![],
1509 is_pk: false,
1510 position: 2,
1511 always_generated: false,
1512 domain_type: None,
1513 },
1514 );
1515
1516 Table {
1517 schema: "public".into(),
1518 name: name.into(),
1519 description: None,
1520 is_view: false,
1521 insertable,
1522 updatable,
1523 deletable,
1524 pk_cols: vec!["id".into()],
1525 unique_constraints: Vec::new(),
1526 columns,
1527 computed_columns: Default::default(),
1528 is_partitioned: false,
1529 }
1530 }
1531
1532 fn create_test_schema_cache() -> SchemaCache {
1533 use std::collections::{HashMap, HashSet};
1534
1535 let mut tables = HashMap::new();
1536
1537 let users = create_test_table("users", true, true, true);
1538 let posts = create_test_table("posts", true, true, true);
1539 let comments = create_test_table("comments", true, false, false);
1540
1541 tables.insert(users.qualified_identifier(), users);
1542 tables.insert(posts.qualified_identifier(), posts);
1543 tables.insert(comments.qualified_identifier(), comments);
1544
1545 SchemaCache {
1546 tables,
1547 relationships: HashMap::new(),
1548 routines: HashMap::new(),
1549 timezones: HashSet::new(),
1550 media_handlers: HashMap::new(),
1551 pg_version: 150000,
1552 representations: Default::default(),
1553 }
1554 }
1555
1556 #[test]
1561 fn test_schema_config_default() {
1562 let config = SchemaConfig::default();
1563 assert!(config.is_schema_exposed("public"));
1564 assert!(!config.is_schema_exposed("private"));
1565 assert!(config.enable_mutations);
1566 assert!(!config.enable_subscriptions);
1567 }
1568
1569 #[test]
1570 fn test_schema_config_with_schemas() {
1571 let config =
1572 SchemaConfig::new().with_schemas(vec!["api".to_string(), "public".to_string()]);
1573 assert!(config.is_schema_exposed("api"));
1574 assert!(config.is_schema_exposed("public"));
1575 assert!(!config.is_schema_exposed("private"));
1576 }
1577
1578 #[test]
1579 fn test_schema_config_mutations_disabled() {
1580 let config = SchemaConfig::new().with_mutations(false);
1581 assert!(!config.enable_mutations);
1582 }
1583
1584 #[test]
1589 fn test_query_field_list() {
1590 let table = create_test_table("users", true, true, true);
1591 let field = QueryField::list(&table);
1592
1593 assert_eq!(field.name, "users");
1594 assert_eq!(field.return_type, "[users!]!");
1595 assert!(field.is_list);
1596 assert!(!field.is_by_pk);
1597 }
1598
1599 #[test]
1600 fn test_query_field_by_pk() {
1601 let table = create_test_table("users", true, true, true);
1602 let field = QueryField::by_pk(&table).unwrap();
1603
1604 assert_eq!(field.name, "users_by_pk");
1605 assert_eq!(field.return_type, "users");
1606 assert!(!field.is_list);
1607 assert!(field.is_by_pk);
1608 }
1609
1610 #[test]
1611 fn test_query_field_by_pk_no_pk() {
1612 let mut table = create_test_table("users", true, true, true);
1613 table.pk_cols = vec![];
1614 let field = QueryField::by_pk(&table);
1615
1616 assert!(field.is_none());
1617 }
1618
1619 #[test]
1624 fn test_mutation_field_insert() {
1625 let table = create_test_table("users", true, true, true);
1626 let fields = MutationField::insert_fields(&table);
1627
1628 assert_eq!(fields.len(), 2);
1629 assert_eq!(fields[0].name, "insert_users");
1630 assert_eq!(fields[0].mutation_type, MutationType::Insert);
1631 assert_eq!(fields[1].name, "insert_users_one");
1632 assert_eq!(fields[1].mutation_type, MutationType::InsertOne);
1633 }
1634
1635 #[test]
1636 fn test_mutation_field_insert_not_insertable() {
1637 let table = create_test_table("users", false, true, true);
1638 let fields = MutationField::insert_fields(&table);
1639
1640 assert!(fields.is_empty());
1641 }
1642
1643 #[test]
1644 fn test_mutation_field_update() {
1645 let table = create_test_table("users", true, true, true);
1646 let fields = MutationField::update_fields(&table);
1647
1648 assert_eq!(fields.len(), 3);
1649 assert_eq!(fields[0].name, "update_users");
1650 assert_eq!(fields[0].mutation_type, MutationType::Update);
1651 assert_eq!(fields[1].name, "update_users_many");
1652 assert_eq!(fields[1].mutation_type, MutationType::UpdateMany);
1653 assert_eq!(fields[2].name, "update_users_by_pk");
1654 assert_eq!(fields[2].mutation_type, MutationType::UpdateByPk);
1655 }
1656
1657 #[test]
1658 fn test_mutation_field_update_not_updatable() {
1659 let table = create_test_table("users", true, false, true);
1660 let fields = MutationField::update_fields(&table);
1661
1662 assert!(fields.is_empty());
1663 }
1664
1665 #[test]
1666 fn test_mutation_field_delete() {
1667 let table = create_test_table("users", true, true, true);
1668 let fields = MutationField::delete_fields(&table);
1669
1670 assert_eq!(fields.len(), 2);
1671 assert_eq!(fields[0].name, "delete_users");
1672 assert_eq!(fields[0].mutation_type, MutationType::Delete);
1673 assert_eq!(fields[1].name, "delete_users_by_pk");
1674 assert_eq!(fields[1].mutation_type, MutationType::DeleteByPk);
1675 }
1676
1677 #[test]
1678 fn test_mutation_field_delete_not_deletable() {
1679 let table = create_test_table("users", true, true, false);
1680 let fields = MutationField::delete_fields(&table);
1681
1682 assert!(fields.is_empty());
1683 }
1684
1685 #[test]
1694 fn test_build_schema_object_types() {
1695 let cache = create_test_schema_cache();
1696 let config = SchemaConfig::default();
1697 let schema = build_schema(&cache, &config);
1698
1699 assert_eq!(schema.object_types.len(), 3);
1700 assert!(schema.get_object_type("users").is_some());
1701 assert!(schema.get_object_type("posts").is_some());
1702 assert!(schema.get_object_type("comments").is_some());
1703 }
1704
1705 #[test]
1706 fn test_build_schema_query_fields() {
1707 let cache = create_test_schema_cache();
1708 let config = SchemaConfig::default();
1709 let schema = build_schema(&cache, &config);
1710
1711 assert_eq!(schema.query_fields.len(), 6);
1713
1714 let users_field = schema.get_query_field("users").unwrap();
1716 assert_eq!(users_field.name, "users");
1717 assert!(users_field.is_list);
1718 }
1719
1720 #[test]
1721 fn test_build_schema_mutation_fields() {
1722 let cache = create_test_schema_cache();
1723 let config = SchemaConfig::default();
1724 let schema = build_schema(&cache, &config);
1725
1726 assert_eq!(schema.mutation_fields.len(), 16);
1731
1732 let users_mutations = schema.get_mutation_fields("users");
1733 assert_eq!(users_mutations.len(), 7);
1734 }
1735
1736 #[test]
1737 fn test_build_schema_mutations_disabled() {
1738 let cache = create_test_schema_cache();
1739 let config = SchemaConfig::new().with_mutations(false);
1740 let schema = build_schema(&cache, &config);
1741
1742 assert!(schema.mutation_fields.is_empty());
1743 }
1744
1745 #[test]
1746 fn test_build_schema_table_names() {
1747 let cache = create_test_schema_cache();
1748 let config = SchemaConfig::default();
1749 let schema = build_schema(&cache, &config);
1750
1751 let names = schema.table_names();
1752 assert_eq!(names.len(), 3);
1753 assert!(names.contains(&"users"));
1754 assert!(names.contains(&"posts"));
1755 assert!(names.contains(&"comments"));
1756 }
1757
1758 #[test]
1759 fn test_build_schema_type_names() {
1760 let cache = create_test_schema_cache();
1761 let config = SchemaConfig::default();
1762 let schema = build_schema(&cache, &config);
1763
1764 let names = schema.type_names();
1765 assert_eq!(names.len(), 3);
1766 assert!(names.contains(&"users"));
1767 assert!(names.contains(&"posts"));
1768 assert!(names.contains(&"comments"));
1769 }
1770
1771 #[test]
1772 fn test_build_schema_exposed_schemas() {
1773 let mut cache = create_test_schema_cache();
1774
1775 let private_table = Table {
1777 schema: "private".into(),
1778 name: "secrets".into(),
1779 description: None,
1780 is_view: false,
1781 insertable: true,
1782 updatable: true,
1783 deletable: true,
1784 pk_cols: vec!["id".into()],
1785 unique_constraints: Vec::new(),
1786 columns: indexmap::IndexMap::new(),
1787 computed_columns: Default::default(),
1788 is_partitioned: false,
1789 };
1790 cache
1791 .tables
1792 .insert(private_table.qualified_identifier(), private_table);
1793
1794 let config = SchemaConfig::default(); let schema = build_schema(&cache, &config);
1796
1797 assert_eq!(schema.object_types.len(), 3);
1799 assert!(schema.get_object_type("Secrets").is_none());
1800 }
1801
1802 #[test]
1807 fn test_generated_schema_get_object_type() {
1808 let cache = create_test_schema_cache();
1809 let config = SchemaConfig::default();
1810 let schema = build_schema(&cache, &config);
1811
1812 let users = schema.get_object_type("users").unwrap();
1813 assert_eq!(users.table.name, "users");
1814 }
1815
1816 #[test]
1817 fn test_generated_schema_get_query_field() {
1818 let cache = create_test_schema_cache();
1819 let config = SchemaConfig::default();
1820 let schema = build_schema(&cache, &config);
1821
1822 let field = schema.get_query_field("posts").unwrap();
1823 assert_eq!(field.table_name, "posts");
1824 }
1825
1826 #[test]
1827 fn test_generated_schema_get_mutation_fields() {
1828 let cache = create_test_schema_cache();
1829 let config = SchemaConfig::default();
1830 let schema = build_schema(&cache, &config);
1831
1832 let fields = schema.get_mutation_fields("comments");
1833 assert_eq!(fields.len(), 2); }
1836}