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
318impl FunctionField {
319 pub fn argument_signature(&self) -> String {
324 self.arguments
325 .iter()
326 .map(|(name, sql_type, _)| format!("{name} {sql_type}"))
327 .collect::<Vec<_>>()
328 .join(", ")
329 }
330}
331
332fn reflected_fields(
341 table: &Table,
342 schema_cache: &SchemaCache,
343 config: &SchemaConfig,
344 base_names: &HashMap<(String, String), String>,
345) -> Vec<RelationshipField> {
346 schema_cache
347 .get_relationships(&table.qualified_identifier(), &table.schema)
348 .map(|relationships| {
349 relationships
350 .iter()
351 .filter_map(|rel| {
352 let foreign = rel.foreign_table();
355 let target_base =
356 base_names.get(&(foreign.schema.clone(), foreign.name.clone()))?;
357 let mut field = RelationshipField::from_relationship_named(
358 rel,
359 target_base,
360 relationship_keys(rel).iter().find_map(|key| {
361 config.names.relationship(&table.schema, &table.name, key)
362 }),
363 );
364 field.arguments = caller_arguments(rel, schema_cache);
365 Some(field)
366 })
367 .collect()
368 })
369 .unwrap_or_default()
370}
371
372fn declared_fields(
385 declared: &[crate::names::DeclaredRelationship],
386 table: &Table,
387 schema_cache: &SchemaCache,
388 config: &SchemaConfig,
389 base_names: &HashMap<(String, String), String>,
390) -> Vec<RelationshipField> {
391 use postrust_core::schema_cache::Relationship;
392
393 let reflected = schema_cache
394 .get_relationships(&table.qualified_identifier(), &table.schema)
395 .map(|found| found.to_vec())
396 .unwrap_or_default();
397
398 let mut fields = Vec::with_capacity(declared.len());
399 for entry in declared {
400 if let Some(key) = entry.using.as_deref() {
402 let Some(rel) = reflected
403 .iter()
404 .find(|rel| relationship_keys(rel).iter().any(|found| found == key))
405 else {
406 tracing::warn!(
407 "GraphQL: {}.{} declares the relationship \"{}\" through \"{}\", \
408 which no foreign key carries; it is left out",
409 table.schema,
410 table.name,
411 entry.name,
412 key
413 );
414 continue;
415 };
416 let foreign = rel.foreign_table();
417 let Some(target_base) = base_names.get(&(foreign.schema.clone(), foreign.name.clone()))
418 else {
419 continue;
420 };
421 let mut field =
422 RelationshipField::from_relationship_named(rel, target_base, Some(&entry.name));
423 field.arguments = caller_arguments(rel, schema_cache);
424 fields.push(field);
425 continue;
426 }
427
428 let Some((schema, name)) = entry.target(config.default_schema()) else {
430 tracing::warn!(
431 "GraphQL: {}.{} declares the relationship \"{}\" with neither a key \
432 nor a table to join to; it is left out",
433 table.schema,
434 table.name,
435 entry.name
436 );
437 continue;
438 };
439 let Some(target_base) = base_names.get(&(schema, name)) else {
440 continue;
441 };
442 match mapped_relationship_field(
443 entry,
444 table,
445 schema_cache,
446 config.default_schema(),
447 target_base,
448 ) {
449 Some(field) => fields.push(field),
450 None => tracing::warn!(
451 "GraphQL: {}.{} declares the relationship \"{}\" with no columns to \
452 join on; it is left out",
453 table.schema,
454 table.name,
455 entry.name
456 ),
457 }
458 }
459
460 let already: std::collections::HashSet<String> =
465 fields.iter().map(|field| field.name.clone()).collect();
466 for rel in reflected
467 .iter()
468 .filter(|rel| matches!(rel, Relationship::Computed { .. }))
469 {
470 let foreign = rel.foreign_table();
471 let Some(target_base) = base_names.get(&(foreign.schema.clone(), foreign.name.clone()))
472 else {
473 continue;
474 };
475 let mut field = RelationshipField::from_relationship_named(
476 rel,
477 target_base,
478 relationship_keys(rel)
479 .iter()
480 .find_map(|key| config.names.relationship(&table.schema, &table.name, key)),
481 );
482 if already.contains(&field.name) {
483 continue;
484 }
485 field.arguments = caller_arguments(rel, schema_cache);
486 fields.push(field);
487 }
488 fields
489}
490
491pub(crate) fn mapped_relationship_field(
498 entry: &crate::names::DeclaredRelationship,
499 table: &Table,
500 schema_cache: &SchemaCache,
501 default_schema: &str,
502 target_base: &str,
503) -> Option<RelationshipField> {
504 use postrust_core::schema_cache::{Cardinality, Relationship};
505
506 let (schema, name) = entry.target(default_schema)?;
507 if entry.columns.is_empty() {
508 return None;
509 }
510 let foreign_table = postrust_core::api_request::QualifiedIdentifier::new(&schema, &name);
511 let here = table.qualified_identifier();
512 let columns: Vec<(String, String)> = entry
513 .columns
514 .iter()
515 .map(|(mine, theirs)| (mine.clone(), theirs.clone()))
516 .collect();
517 let constraint = format!("{}.{}", table.name, entry.name);
518 let cardinality = match (entry.to_one, entry.after_parent) {
524 (true, false) => Cardinality::M2O {
525 constraint: constraint.clone(),
526 columns,
527 },
528 (true, true) => Cardinality::O2O {
529 constraint: constraint.clone(),
530 columns,
531 is_parent: true,
532 },
533 (false, _) => Cardinality::O2M {
534 constraint: constraint.clone(),
535 columns,
536 },
537 };
538 let rel = Relationship::ForeignKey {
539 is_self: here == foreign_table,
540 table: here,
541 foreign_table_is_view: schema_cache
542 .get_table(&foreign_table)
543 .is_some_and(|t| t.is_view),
544 foreign_table,
545 cardinality,
546 table_is_view: table.is_view,
547 constraint_name: constraint,
548 };
549 Some(RelationshipField::from_relationship_named(
550 &rel,
551 target_base,
552 Some(&entry.name),
553 ))
554}
555
556fn caller_arguments(
557 relationship: &postrust_core::schema_cache::Relationship,
558 schema_cache: &SchemaCache,
559) -> Vec<(String, String, bool)> {
560 use postrust_core::schema_cache::Relationship;
561 let Relationship::Computed {
562 function,
563 row_argument,
564 ..
565 } = relationship
566 else {
567 return Vec::new();
568 };
569 let Some(row_argument) = row_argument else {
570 return Vec::new();
571 };
572 schema_cache
573 .routines
574 .get(function)
575 .into_iter()
576 .flatten()
577 .find(|routine| routine.name == function.name)
578 .map(|routine| {
579 routine
580 .params
581 .iter()
582 .filter(|param| ¶m.name != row_argument)
583 .filter(|param| !is_session_argument(param))
584 .filter(|param| !param.name.is_empty())
585 .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
586 .collect()
587 })
588 .unwrap_or_default()
589}
590
591pub(crate) fn computed_caller_arguments(
597 computed: &postrust_core::schema_cache::ComputedColumn,
598 schema_cache: &SchemaCache,
599) -> Vec<(String, String, bool)> {
600 let Some(row_argument) = &computed.row_argument else {
601 return Vec::new();
602 };
603 schema_cache
604 .routines
605 .get(&computed.function)
606 .into_iter()
607 .flatten()
608 .find(|routine| routine.name == computed.function.name)
609 .map(|routine| {
610 routine
611 .params
612 .iter()
613 .filter(|param| ¶m.name != row_argument)
614 .filter(|param| !is_session_argument(param))
615 .filter(|param| !param.name.is_empty())
616 .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
617 .collect()
618 })
619 .unwrap_or_default()
620}
621
622fn is_session_argument(param: &postrust_core::schema_cache::RoutineParam) -> bool {
628 param.name == "hasura_session" && matches!(param.param_type.as_str(), "json" | "jsonb")
629}
630
631impl GeneratedSchema {
632 pub fn get_object_type(&self, name: &str) -> Option<&TableObjectType> {
634 self.object_types.get(name)
635 }
636
637 pub fn get_query_field(&self, table_name: &str) -> Option<&QueryField> {
639 self.query_fields
640 .iter()
641 .find(|f| f.table_name == table_name)
642 }
643
644 pub fn get_mutation_fields(&self, table_name: &str) -> Vec<&MutationField> {
646 self.mutation_fields
647 .iter()
648 .filter(|f| f.table_name == table_name)
649 .collect()
650 }
651
652 pub fn get_relationship_fields(&self, type_name: &str) -> Option<&Vec<RelationshipField>> {
654 self.relationship_fields.get(type_name)
655 }
656
657 pub fn table_names(&self) -> Vec<&str> {
659 self.object_types
660 .values()
661 .map(|t| t.table.name.as_str())
662 .collect()
663 }
664
665 pub fn type_names(&self) -> Vec<&str> {
667 self.object_types.keys().map(|s| s.as_str()).collect()
668 }
669}
670
671#[derive(Debug, Clone)]
673pub struct QueryField {
674 pub name: String,
676 pub table_name: String,
678 pub schema_name: String,
680 pub type_name: String,
682 pub return_type: String,
684 pub is_list: bool,
686 pub is_by_pk: bool,
688 pub pk_columns: Vec<(String, String)>,
694 pub description: Option<String>,
696 pub aggregate_name: Option<String>,
702 pub aggregate_description: Option<String>,
705 pub aggregates: bool,
715 pub rows: bool,
723}
724
725impl QueryField {
726 pub fn list(table: &Table) -> Self {
728 Self::list_named(table, &table.name)
729 }
730
731 pub fn list_named(table: &Table, base_name: &str) -> Self {
733 let type_name = base_name.to_string();
734 let name = base_name.to_string();
735
736 Self {
737 name,
738 table_name: table.name.clone(),
739 schema_name: table.schema.clone(),
740 type_name: type_name.clone(),
741 return_type: format!("[{}!]!", type_name),
742 is_list: true,
743 is_by_pk: false,
744 pk_columns: Vec::new(),
745 description: Some(format!("fetch data from the table: \"{}\"", table.name)),
746 aggregate_name: None,
747 aggregate_description: None,
748 aggregates: true,
749 rows: true,
750 }
751 }
752
753 pub fn by_pk(table: &Table) -> Option<Self> {
755 Self::by_pk_named(table, &table.name)
756 }
757
758 pub fn by_pk_named(table: &Table, base_name: &str) -> Option<Self> {
760 if !has_whole_key(table) {
761 return None;
762 }
763
764 let type_name = base_name.to_string();
765 let field_name = format!("{}_by_pk", base_name);
766
767 let pk_columns = pk_columns_of(table);
770
771 Some(Self {
772 name: field_name,
773 table_name: table.name.clone(),
774 schema_name: table.schema.clone(),
775 type_name: type_name.clone(),
776 return_type: type_name,
777 is_list: false,
778 is_by_pk: true,
779 pk_columns,
780 description: Some(format!(
781 "fetch data from the table: \"{}\" using primary key columns",
782 table.name
783 )),
784 aggregate_name: None,
785 aggregate_description: None,
786 aggregates: true,
787 rows: true,
788 })
789 }
790}
791
792#[derive(Debug, Clone)]
794pub struct MutationField {
795 pub name: String,
797 pub table_name: String,
799 pub schema_name: String,
801 pub mutation_type: MutationType,
803 pub pk_columns: Vec<(String, String)>,
808 pub return_type: String,
810 pub description: Option<String>,
812}
813
814#[derive(Debug, Clone, Copy, PartialEq, Eq)]
816pub enum MutationType {
817 Insert,
819 InsertOne,
821 Update,
823 UpdateByPk,
825 UpdateMany,
827 Delete,
829 DeleteByPk,
831}
832
833impl MutationField {
834 pub fn insert_fields(table: &Table) -> Vec<Self> {
836 Self::insert_fields_named(table, &table.name)
837 }
838
839 pub fn insert_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
842 if !is_insertable(table) {
843 return vec![];
844 }
845
846 let type_name = base_name.to_string();
847
848 let mut fields = vec![];
849
850 let name = format!("insert_{}", base_name);
852 fields.push(Self {
853 name,
854 table_name: table.name.clone(),
855 schema_name: table.schema.clone(),
856 mutation_type: MutationType::Insert,
857 pk_columns: Vec::new(),
858 return_type: format!("{}_mutation_response", type_name),
859 description: Some(format!("insert data into the table: \"{}\"", table.name)),
860 });
861
862 let name = format!("insert_{}_one", base_name);
864 fields.push(Self {
865 name,
866 table_name: table.name.clone(),
867 schema_name: table.schema.clone(),
868 mutation_type: MutationType::InsertOne,
869 pk_columns: Vec::new(),
870 return_type: type_name.clone(),
871 description: Some(format!(
872 "insert a single row into the table: \"{}\"",
873 table.name
874 )),
875 });
876
877 fields
878 }
879
880 pub fn update_fields(table: &Table) -> Vec<Self> {
882 Self::update_fields_named(table, &table.name)
883 }
884
885 pub fn update_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
888 if !is_updatable(table) {
889 return vec![];
890 }
891
892 let type_name = base_name.to_string();
893
894 let mut fields = vec![];
895
896 let name = format!("update_{}", base_name);
898 fields.push(Self {
899 name,
900 table_name: table.name.clone(),
901 schema_name: table.schema.clone(),
902 mutation_type: MutationType::Update,
903 pk_columns: Vec::new(),
904 return_type: format!("{}_mutation_response", type_name),
905 description: Some(format!("update data of the table: \"{}\"", table.name)),
906 });
907
908 let name = format!("update_{}_many", base_name);
910 fields.push(Self {
911 name,
912 table_name: table.name.clone(),
913 schema_name: table.schema.clone(),
914 mutation_type: MutationType::UpdateMany,
915 pk_columns: Vec::new(),
916 return_type: format!("[{}_mutation_response]", type_name),
917 description: Some(format!(
918 "update multiples rows of table: \"{}\"",
919 table.name
920 )),
921 });
922
923 if has_whole_key(table) {
925 let name = format!("update_{}_by_pk", base_name);
926 fields.push(Self {
927 name,
928 table_name: table.name.clone(),
929 schema_name: table.schema.clone(),
930 mutation_type: MutationType::UpdateByPk,
931 pk_columns: pk_columns_of(table),
932 return_type: type_name,
933 description: Some(format!(
934 "update single row of the table: \"{}\"",
935 table.name
936 )),
937 });
938 }
939
940 fields
941 }
942
943 pub fn delete_fields(table: &Table) -> Vec<Self> {
945 Self::delete_fields_named(table, &table.name)
946 }
947
948 pub fn delete_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
951 if !is_deletable(table) {
952 return vec![];
953 }
954
955 let type_name = base_name.to_string();
956
957 let mut fields = vec![];
958
959 let name = format!("delete_{}", base_name);
961 fields.push(Self {
962 name,
963 table_name: table.name.clone(),
964 schema_name: table.schema.clone(),
965 mutation_type: MutationType::Delete,
966 pk_columns: Vec::new(),
967 return_type: format!("{}_mutation_response", type_name),
968 description: Some(format!("delete data from the table: \"{}\"", table.name)),
969 });
970
971 if has_whole_key(table) {
973 let name = format!("delete_{}_by_pk", base_name);
974 fields.push(Self {
975 name,
976 table_name: table.name.clone(),
977 schema_name: table.schema.clone(),
978 mutation_type: MutationType::DeleteByPk,
979 pk_columns: pk_columns_of(table),
980 return_type: type_name,
981 description: Some(format!(
982 "delete single row from the table: \"{}\"",
983 table.name
984 )),
985 });
986 }
987
988 fields
989 }
990}
991
992pub fn build_schema(schema_cache: &SchemaCache, config: &SchemaConfig) -> GeneratedSchema {
994 let mut object_types = HashMap::new();
995 let mut query_fields = Vec::new();
996 let mut mutation_fields = Vec::new();
997 let mut relationship_fields = HashMap::new();
998
999 let mut tables: Vec<&Table> = schema_cache
1002 .tables
1003 .values()
1004 .filter(|table| config.is_schema_exposed(&table.schema))
1005 .collect();
1006 tables.sort_by(|a, b| (&a.schema, &a.name).cmp(&(&b.schema, &b.name)));
1007
1008 let mut used_base_names: HashMap<String, u32> = HashMap::new();
1013 let mut base_names: HashMap<(String, String), String> = HashMap::new();
1016
1017 for table in &tables {
1018 let preferred = base_name_for(table, config);
1019 let base_name = match used_base_names.get_mut(&preferred) {
1020 None => {
1021 used_base_names.insert(preferred.clone(), 1);
1022 preferred
1023 }
1024 Some(count) => {
1025 *count += 1;
1026 let disambiguated = format!("{}_{}", preferred, count);
1027 tracing::warn!(
1028 "GraphQL name collision: {}.{} would generate the same names as \
1029 an earlier table; exposing it as \"{}\" instead",
1030 table.schema,
1031 table.name,
1032 disambiguated
1033 );
1034 disambiguated
1035 }
1036 };
1037
1038 base_names.insert(
1039 (table.schema.clone(), table.name.clone()),
1040 base_name.clone(),
1041 );
1042 }
1043
1044 let mut readable_bases: HashSet<String> = HashSet::new();
1047
1048 for table in &tables {
1049 let base_name = base_names
1050 .get(&(table.schema.clone(), table.name.clone()))
1051 .expect("every visited table has a resolved base name")
1052 .clone();
1053
1054 let mut obj_type = TableObjectType::from_table_named(table, &base_name, &config.names);
1056 let readable = config.role.as_deref().is_none_or(|role| {
1066 config
1067 .names
1068 .permissions(&table.schema, &table.name, role)
1069 .is_none_or(|granted| crate::role::reads_anything(granted, table))
1070 });
1071 if readable {
1072 readable_bases.insert(base_name.clone());
1073 }
1074 for field in &mut obj_type.fields {
1077 let Some(function) = config
1078 .names
1079 .computed_source(&table.schema, &table.name, &field.name)
1080 .or(Some(field.name.as_str()))
1081 else {
1082 continue;
1083 };
1084 let Some(computed) = table.get_computed_column(function) else {
1085 continue;
1086 };
1087 field.arguments = computed_caller_arguments(computed, schema_cache);
1088 }
1089 let type_name = obj_type.name.clone();
1090
1091 let rename = |field: &mut QueryField, kind: &str| {
1098 if let Some(given) = config.names.root(&table.schema, &table.name, kind) {
1099 field.name = given.to_string();
1100 }
1101 if let Some(comment) = config.names.root_comment(&table.schema, &table.name, kind) {
1102 field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
1103 }
1104 };
1105 let mut list = QueryField::list_named(table, &base_name);
1106 rename(&mut list, "select");
1107 list.aggregate_name = config
1111 .names
1112 .root(&table.schema, &table.name, "select_aggregate")
1113 .map(str::to_string);
1114 list.aggregate_description = config
1115 .names
1116 .root_comment(&table.schema, &table.name, "select_aggregate")
1117 .map(str::to_string);
1118 if let Some(role) = &config.role {
1122 list.aggregates =
1123 crate::role::allows_aggregations(&config.names, &table.schema, &table.name, role);
1124 }
1125 list.rows = readable;
1131 if readable || list.aggregates {
1132 query_fields.push(list);
1133 }
1134 if readable {
1135 if let Some(mut by_pk) = QueryField::by_pk_named(table, &base_name) {
1136 rename(&mut by_pk, "select_by_pk");
1137 query_fields.push(by_pk);
1138 }
1139 }
1140
1141 if config.enable_mutations {
1143 let mut fields: Vec<MutationField> = Vec::new();
1144 fields.extend(MutationField::insert_fields_named(table, &base_name));
1145 fields.extend(MutationField::update_fields_named(table, &base_name));
1146 fields.extend(MutationField::delete_fields_named(table, &base_name));
1147 if !readable {
1149 fields.retain(|field| {
1150 !matches!(
1151 field.mutation_type,
1152 MutationType::InsertOne
1153 | MutationType::UpdateByPk
1154 | MutationType::DeleteByPk
1155 )
1156 });
1157 }
1158 for field in &mut fields {
1159 let kind = match field.mutation_type {
1160 MutationType::Insert => "insert",
1161 MutationType::InsertOne => "insert_one",
1162 MutationType::Update => "update",
1163 MutationType::UpdateByPk => "update_by_pk",
1164 MutationType::UpdateMany => "update_many",
1165 MutationType::Delete => "delete",
1166 MutationType::DeleteByPk => "delete_by_pk",
1167 };
1168 if let Some(given) = config.names.root(&table.schema, &table.name, kind) {
1169 field.name = given.to_string();
1170 }
1171 if let Some(comment) = config.names.root_comment(&table.schema, &table.name, kind) {
1172 field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
1173 }
1174 }
1175 mutation_fields.extend(fields);
1176 }
1177
1178 let rels = match config
1183 .names
1184 .declared_relationships(&table.schema, &table.name)
1185 {
1186 Some(declared) => declared_fields(declared, table, schema_cache, config, &base_names),
1187 None => reflected_fields(table, schema_cache, config, &base_names),
1188 };
1189
1190 if !rels.is_empty() {
1191 relationship_fields.insert(type_name.clone(), rels);
1192 }
1193
1194 object_types.insert(type_name, obj_type);
1195 }
1196
1197 let mut enum_types: HashMap<String, Vec<(String, Option<String>)>> = HashMap::new();
1202 let mut enum_type_of: HashMap<(String, String), String> = HashMap::new();
1203
1204 for (schema_name, table_name) in config.names.enum_tables() {
1205 let base = match base_names.get(&(schema_name.clone(), table_name.clone())) {
1212 Some(found) => found.clone(),
1213 None => config
1214 .names
1215 .base_name(&schema_name, &table_name)
1216 .map(str::to_string)
1217 .unwrap_or_else(|| match schema_name == config.default_schema() {
1218 true => table_name.clone(),
1219 false => format!("{}_{}", schema_name, table_name),
1220 }),
1221 };
1222 let key = format!("{}.{}", schema_name, table_name);
1223 let members: Vec<(String, Option<String>)> = config
1224 .enum_values
1225 .get(&key)
1226 .map(|values| {
1227 values
1228 .iter()
1229 .filter(|(value, _)| is_graphql_name(value))
1230 .cloned()
1231 .collect()
1232 })
1233 .unwrap_or_default();
1234
1235 if members.is_empty() {
1236 tracing::warn!(
1239 "{}.{} is marked as an enumeration but has no values that can name an \
1240 enum member; exposing it as an ordinary table",
1241 schema_name,
1242 table_name
1243 );
1244 continue;
1245 }
1246
1247 let type_name = format!("{}_enum", base);
1248 enum_type_of.insert((schema_name.clone(), table_name.clone()), type_name.clone());
1249 enum_types.insert(type_name, members);
1250 }
1251
1252 if !enum_type_of.is_empty() {
1262 for table in &tables {
1263 let Some(type_name) = base_names.get(&(table.schema.clone(), table.name.clone()))
1264 else {
1265 continue;
1266 };
1267 let found = schema_cache
1268 .get_relationships(&table.qualified_identifier(), &table.schema)
1269 .map(|found| found.to_vec())
1270 .unwrap_or_default();
1271 for relationship in &found {
1272 if !relationship.is_to_one() {
1273 continue;
1274 }
1275 let foreign = relationship.foreign_table();
1276 let Some(enum_type) =
1277 enum_type_of.get(&(foreign.schema.clone(), foreign.name.clone()))
1278 else {
1279 continue;
1280 };
1281 let Some(column) = relationship.single_local_column() else {
1282 continue;
1283 };
1284 if let Some(object) = object_types.get_mut(type_name) {
1285 let exposed = config
1288 .names
1289 .column(&object.table.schema, &object.table.name, column)
1290 .unwrap_or(column)
1291 .to_string();
1292 for field in &mut object.fields {
1293 if field.name == exposed {
1294 field.graphql_type =
1295 crate::types::GraphQLType::Custom(enum_type.clone());
1296 }
1297 }
1298 }
1299 }
1300 }
1301 }
1302
1303 if !enum_type_of.is_empty() {
1309 for relationships in relationship_fields.values_mut() {
1310 relationships.retain(|relationship| {
1311 let foreign = relationship.relationship.foreign_table();
1312 !enum_type_of.contains_key(&(foreign.schema.clone(), foreign.name.clone()))
1313 });
1314 }
1315 }
1316
1317 for object in object_types.values_mut() {
1326 object.writable_fields = object.fields.clone();
1330 if let Some(role) = &config.role {
1331 let (schema_name, table_name) =
1332 (object.table.schema.clone(), object.table.name.clone());
1333 let Some(granted) = config.names.permissions(&schema_name, &table_name, role) else {
1334 continue;
1335 };
1336 let table = object.table.clone();
1337 object.fields.retain(|field| {
1338 let column = config
1339 .names
1340 .column_source(&schema_name, &table_name, &field.name)
1341 .unwrap_or(&field.name);
1342 match table.get_column(column) {
1343 None => true,
1346 Some(_) => granted
1347 .select
1348 .as_ref()
1349 .is_some_and(|select| select.columns.allows(column)),
1350 }
1351 });
1352 }
1353 }
1354
1355 let mut function_fields = Vec::new();
1360 for routines in schema_cache.routines.values() {
1361 for routine in routines {
1362 if !config.is_schema_exposed(&routine.schema) || routine.is_procedure {
1363 continue;
1364 }
1365 let postrust_core::schema_cache::RetType::SetOf(returned) = &routine.return_type else {
1366 continue;
1367 };
1368 if routine.params.iter().any(|param| {
1373 let (_, named) = type_name_of(¶m.param_type);
1374 base_names.keys().any(|(_, table)| table == &named)
1375 }) {
1376 continue;
1377 }
1378 let (returned_schema, returned_table) = type_name_of(returned);
1384 let found = base_names
1385 .iter()
1386 .find(|((schema, table), _)| {
1387 table == &returned_table
1388 && schema == returned_schema.as_ref().unwrap_or(&routine.schema)
1389 })
1390 .or_else(|| {
1391 base_names
1392 .iter()
1393 .find(|((_, table), _)| table == &returned_table)
1394 });
1395 let Some(((target_schema, target_table), base)) = found else {
1396 continue;
1397 };
1398 if !readable_bases.contains(base) {
1402 continue;
1403 }
1404 let target = (target_schema.clone(), target_table.clone());
1405
1406 let volatile = match config.names.exposed_as(&routine.schema, &routine.name) {
1411 Some("query") => false,
1412 Some("mutation") => true,
1413 _ => matches!(
1414 routine.volatility,
1415 postrust_core::schema_cache::FuncVolatility::Volatile
1416 ),
1417 };
1418 if volatile {
1426 if let Some(role) = config.role.as_deref() {
1427 if !config
1428 .names
1429 .function_grants(&routine.schema, &routine.name, role)
1430 {
1431 continue;
1432 }
1433 }
1434 }
1435
1436 function_fields.push(FunctionField {
1437 name: routine.name.clone(),
1438 schema_name: routine.schema.clone(),
1439 function_name: routine.name.clone(),
1440 returns: base.clone(),
1441 returns_table: target,
1442 arguments: routine
1443 .params
1444 .iter()
1445 .filter(|param| !param.name.is_empty())
1446 .filter(|param| !is_session_argument(param))
1447 .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
1448 .collect(),
1449 session_argument: routine
1450 .params
1451 .iter()
1452 .find(|param| is_session_argument(param))
1453 .map(|param| param.name.clone()),
1454 volatile,
1455 description: routine.description.clone(),
1456 });
1457 }
1458 }
1459 function_fields.sort_by(|a, b| {
1462 a.name.cmp(&b.name).then_with(|| {
1463 a.arguments
1464 .len()
1465 .cmp(&b.arguments.len())
1466 .then_with(|| a.argument_signature().cmp(&b.argument_signature()))
1467 })
1468 });
1469
1470 let mut seen: HashSet<String> = HashSet::new();
1481 function_fields.retain(|function| {
1482 if seen.insert(function.name.clone()) {
1483 return true;
1484 }
1485 tracing::warn!(
1486 "not exposing {}.{}({}) -- another overload of {} is already a root \
1487 field, and GraphQL has no overloading",
1488 function.schema_name,
1489 function.function_name,
1490 function.argument_signature(),
1491 function.name,
1492 );
1493 false
1494 });
1495
1496 GeneratedSchema {
1497 object_types,
1498 query_fields,
1499 mutation_fields,
1500 relationship_fields,
1501 enum_types,
1502 function_fields,
1503 }
1504}
1505
1506fn is_graphql_name(value: &str) -> bool {
1513 let mut chars = value.chars();
1514 match chars.next() {
1515 Some(first) if first == '_' || first.is_ascii_alphabetic() => {}
1516 _ => return false,
1517 }
1518 chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
1519}
1520
1521#[cfg(test)]
1522mod tests {
1523 use super::*;
1524 use indexmap::IndexMap;
1525 use postrust_core::schema_cache::Column;
1526 use pretty_assertions::assert_eq;
1527
1528 fn create_test_table(name: &str, insertable: bool, updatable: bool, deletable: bool) -> Table {
1529 let mut columns = IndexMap::new();
1530 columns.insert(
1531 "id".into(),
1532 Column {
1533 name: "id".into(),
1534 description: None,
1535 nullable: false,
1536 data_type: "integer".into(),
1537 nominal_type: "int4".into(),
1538 max_len: None,
1539 default: Some("nextval('id_seq')".into()),
1540 enum_values: vec![],
1541 is_pk: true,
1542 position: 1,
1543 always_generated: false,
1544 domain_type: None,
1545 },
1546 );
1547 columns.insert(
1548 "name".into(),
1549 Column {
1550 name: "name".into(),
1551 description: None,
1552 nullable: false,
1553 data_type: "text".into(),
1554 nominal_type: "text".into(),
1555 max_len: None,
1556 default: None,
1557 enum_values: vec![],
1558 is_pk: false,
1559 position: 2,
1560 always_generated: false,
1561 domain_type: None,
1562 },
1563 );
1564
1565 Table {
1566 schema: "public".into(),
1567 name: name.into(),
1568 description: None,
1569 is_view: false,
1570 insertable,
1571 updatable,
1572 deletable,
1573 pk_cols: vec!["id".into()],
1574 unique_constraints: Vec::new(),
1575 columns,
1576 computed_columns: Default::default(),
1577 is_partitioned: false,
1578 }
1579 }
1580
1581 fn create_test_schema_cache() -> SchemaCache {
1582 use std::collections::{HashMap, HashSet};
1583
1584 let mut tables = HashMap::new();
1585
1586 let users = create_test_table("users", true, true, true);
1587 let posts = create_test_table("posts", true, true, true);
1588 let comments = create_test_table("comments", true, false, false);
1589
1590 tables.insert(users.qualified_identifier(), users);
1591 tables.insert(posts.qualified_identifier(), posts);
1592 tables.insert(comments.qualified_identifier(), comments);
1593
1594 SchemaCache {
1595 tables,
1596 relationships: HashMap::new(),
1597 routines: HashMap::new(),
1598 timezones: HashSet::new(),
1599 media_handlers: HashMap::new(),
1600 pg_version: 150000,
1601 representations: Default::default(),
1602 }
1603 }
1604
1605 fn setof_users_routine(
1607 name: &str,
1608 args: &[(&str, &str)],
1609 ) -> postrust_core::schema_cache::Routine {
1610 use postrust_core::schema_cache::{FuncVolatility, RetType, Routine, RoutineParam};
1611 Routine {
1612 schema: "public".into(),
1613 name: name.into(),
1614 description: None,
1615 params: args
1616 .iter()
1617 .map(|(pname, ptype)| RoutineParam {
1618 name: (*pname).into(),
1619 param_type: (*ptype).into(),
1620 type_max_length: (*ptype).into(),
1621 required: true,
1622 variadic: false,
1623 })
1624 .collect(),
1625 return_type: RetType::SetOf("users".into()),
1626 returns_composite: true,
1627 volatility: FuncVolatility::Stable,
1628 has_variadic: false,
1629 isolation_level: None,
1630 settings: Vec::new(),
1631 is_procedure: false,
1632 media_type: None,
1633 media_base_type: None,
1634 output_columns: Vec::new(),
1635 }
1636 }
1637
1638 #[test]
1643 fn overloads_of_one_function_become_a_single_root_field() {
1644 let mut cache = create_test_schema_cache();
1645 let one = setof_users_routine("overloaded", &[("a", "integer")]);
1646 let two = setof_users_routine("overloaded", &[("a", "integer"), ("b", "text")]);
1647 cache
1648 .routines
1649 .insert(one.qualified_identifier(), vec![two, one]);
1650
1651 let generated = build_schema(&cache, &SchemaConfig::default());
1652 let named: Vec<&str> = generated
1653 .function_fields
1654 .iter()
1655 .filter(|f| f.name == "overloaded")
1656 .map(|f| f.name.as_str())
1657 .collect();
1658 assert_eq!(named.len(), 1, "exactly one root field per function name");
1659 }
1660
1661 #[test]
1664 fn the_surviving_overload_is_the_one_taking_fewest_arguments() {
1665 let long = setof_users_routine("overloaded", &[("a", "integer"), ("b", "text")]);
1666 let short = setof_users_routine("overloaded", &[("a", "integer")]);
1667
1668 for order in [vec![long.clone(), short.clone()], vec![short, long]] {
1669 let mut cache = create_test_schema_cache();
1670 cache
1671 .routines
1672 .insert(order[0].qualified_identifier(), order);
1673 let generated = build_schema(&cache, &SchemaConfig::default());
1674 let kept = generated
1675 .function_fields
1676 .iter()
1677 .find(|f| f.name == "overloaded")
1678 .expect("the function is exposed");
1679 assert_eq!(kept.argument_signature(), "a integer");
1680 }
1681 }
1682
1683 #[test]
1688 fn test_schema_config_default() {
1689 let config = SchemaConfig::default();
1690 assert!(config.is_schema_exposed("public"));
1691 assert!(!config.is_schema_exposed("private"));
1692 assert!(config.enable_mutations);
1693 assert!(!config.enable_subscriptions);
1694 }
1695
1696 #[test]
1697 fn test_schema_config_with_schemas() {
1698 let config =
1699 SchemaConfig::new().with_schemas(vec!["api".to_string(), "public".to_string()]);
1700 assert!(config.is_schema_exposed("api"));
1701 assert!(config.is_schema_exposed("public"));
1702 assert!(!config.is_schema_exposed("private"));
1703 }
1704
1705 #[test]
1706 fn test_schema_config_mutations_disabled() {
1707 let config = SchemaConfig::new().with_mutations(false);
1708 assert!(!config.enable_mutations);
1709 }
1710
1711 #[test]
1716 fn test_query_field_list() {
1717 let table = create_test_table("users", true, true, true);
1718 let field = QueryField::list(&table);
1719
1720 assert_eq!(field.name, "users");
1721 assert_eq!(field.return_type, "[users!]!");
1722 assert!(field.is_list);
1723 assert!(!field.is_by_pk);
1724 }
1725
1726 #[test]
1727 fn test_query_field_by_pk() {
1728 let table = create_test_table("users", true, true, true);
1729 let field = QueryField::by_pk(&table).unwrap();
1730
1731 assert_eq!(field.name, "users_by_pk");
1732 assert_eq!(field.return_type, "users");
1733 assert!(!field.is_list);
1734 assert!(field.is_by_pk);
1735 }
1736
1737 #[test]
1738 fn test_query_field_by_pk_no_pk() {
1739 let mut table = create_test_table("users", true, true, true);
1740 table.pk_cols = vec![];
1741 let field = QueryField::by_pk(&table);
1742
1743 assert!(field.is_none());
1744 }
1745
1746 #[test]
1751 fn test_mutation_field_insert() {
1752 let table = create_test_table("users", true, true, true);
1753 let fields = MutationField::insert_fields(&table);
1754
1755 assert_eq!(fields.len(), 2);
1756 assert_eq!(fields[0].name, "insert_users");
1757 assert_eq!(fields[0].mutation_type, MutationType::Insert);
1758 assert_eq!(fields[1].name, "insert_users_one");
1759 assert_eq!(fields[1].mutation_type, MutationType::InsertOne);
1760 }
1761
1762 #[test]
1763 fn test_mutation_field_insert_not_insertable() {
1764 let table = create_test_table("users", false, true, true);
1765 let fields = MutationField::insert_fields(&table);
1766
1767 assert!(fields.is_empty());
1768 }
1769
1770 #[test]
1771 fn test_mutation_field_update() {
1772 let table = create_test_table("users", true, true, true);
1773 let fields = MutationField::update_fields(&table);
1774
1775 assert_eq!(fields.len(), 3);
1776 assert_eq!(fields[0].name, "update_users");
1777 assert_eq!(fields[0].mutation_type, MutationType::Update);
1778 assert_eq!(fields[1].name, "update_users_many");
1779 assert_eq!(fields[1].mutation_type, MutationType::UpdateMany);
1780 assert_eq!(fields[2].name, "update_users_by_pk");
1781 assert_eq!(fields[2].mutation_type, MutationType::UpdateByPk);
1782 }
1783
1784 #[test]
1785 fn test_mutation_field_update_not_updatable() {
1786 let table = create_test_table("users", true, false, true);
1787 let fields = MutationField::update_fields(&table);
1788
1789 assert!(fields.is_empty());
1790 }
1791
1792 #[test]
1793 fn test_mutation_field_delete() {
1794 let table = create_test_table("users", true, true, true);
1795 let fields = MutationField::delete_fields(&table);
1796
1797 assert_eq!(fields.len(), 2);
1798 assert_eq!(fields[0].name, "delete_users");
1799 assert_eq!(fields[0].mutation_type, MutationType::Delete);
1800 assert_eq!(fields[1].name, "delete_users_by_pk");
1801 assert_eq!(fields[1].mutation_type, MutationType::DeleteByPk);
1802 }
1803
1804 #[test]
1805 fn test_mutation_field_delete_not_deletable() {
1806 let table = create_test_table("users", true, true, false);
1807 let fields = MutationField::delete_fields(&table);
1808
1809 assert!(fields.is_empty());
1810 }
1811
1812 #[test]
1821 fn test_build_schema_object_types() {
1822 let cache = create_test_schema_cache();
1823 let config = SchemaConfig::default();
1824 let schema = build_schema(&cache, &config);
1825
1826 assert_eq!(schema.object_types.len(), 3);
1827 assert!(schema.get_object_type("users").is_some());
1828 assert!(schema.get_object_type("posts").is_some());
1829 assert!(schema.get_object_type("comments").is_some());
1830 }
1831
1832 #[test]
1833 fn test_build_schema_query_fields() {
1834 let cache = create_test_schema_cache();
1835 let config = SchemaConfig::default();
1836 let schema = build_schema(&cache, &config);
1837
1838 assert_eq!(schema.query_fields.len(), 6);
1840
1841 let users_field = schema.get_query_field("users").unwrap();
1843 assert_eq!(users_field.name, "users");
1844 assert!(users_field.is_list);
1845 }
1846
1847 #[test]
1848 fn test_build_schema_mutation_fields() {
1849 let cache = create_test_schema_cache();
1850 let config = SchemaConfig::default();
1851 let schema = build_schema(&cache, &config);
1852
1853 assert_eq!(schema.mutation_fields.len(), 16);
1858
1859 let users_mutations = schema.get_mutation_fields("users");
1860 assert_eq!(users_mutations.len(), 7);
1861 }
1862
1863 #[test]
1864 fn test_build_schema_mutations_disabled() {
1865 let cache = create_test_schema_cache();
1866 let config = SchemaConfig::new().with_mutations(false);
1867 let schema = build_schema(&cache, &config);
1868
1869 assert!(schema.mutation_fields.is_empty());
1870 }
1871
1872 #[test]
1873 fn test_build_schema_table_names() {
1874 let cache = create_test_schema_cache();
1875 let config = SchemaConfig::default();
1876 let schema = build_schema(&cache, &config);
1877
1878 let names = schema.table_names();
1879 assert_eq!(names.len(), 3);
1880 assert!(names.contains(&"users"));
1881 assert!(names.contains(&"posts"));
1882 assert!(names.contains(&"comments"));
1883 }
1884
1885 #[test]
1886 fn test_build_schema_type_names() {
1887 let cache = create_test_schema_cache();
1888 let config = SchemaConfig::default();
1889 let schema = build_schema(&cache, &config);
1890
1891 let names = schema.type_names();
1892 assert_eq!(names.len(), 3);
1893 assert!(names.contains(&"users"));
1894 assert!(names.contains(&"posts"));
1895 assert!(names.contains(&"comments"));
1896 }
1897
1898 #[test]
1899 fn test_build_schema_exposed_schemas() {
1900 let mut cache = create_test_schema_cache();
1901
1902 let private_table = Table {
1904 schema: "private".into(),
1905 name: "secrets".into(),
1906 description: None,
1907 is_view: false,
1908 insertable: true,
1909 updatable: true,
1910 deletable: true,
1911 pk_cols: vec!["id".into()],
1912 unique_constraints: Vec::new(),
1913 columns: indexmap::IndexMap::new(),
1914 computed_columns: Default::default(),
1915 is_partitioned: false,
1916 };
1917 cache
1918 .tables
1919 .insert(private_table.qualified_identifier(), private_table);
1920
1921 let config = SchemaConfig::default(); let schema = build_schema(&cache, &config);
1923
1924 assert_eq!(schema.object_types.len(), 3);
1926 assert!(schema.get_object_type("Secrets").is_none());
1927 }
1928
1929 #[test]
1934 fn test_generated_schema_get_object_type() {
1935 let cache = create_test_schema_cache();
1936 let config = SchemaConfig::default();
1937 let schema = build_schema(&cache, &config);
1938
1939 let users = schema.get_object_type("users").unwrap();
1940 assert_eq!(users.table.name, "users");
1941 }
1942
1943 #[test]
1944 fn test_generated_schema_get_query_field() {
1945 let cache = create_test_schema_cache();
1946 let config = SchemaConfig::default();
1947 let schema = build_schema(&cache, &config);
1948
1949 let field = schema.get_query_field("posts").unwrap();
1950 assert_eq!(field.table_name, "posts");
1951 }
1952
1953 #[test]
1954 fn test_generated_schema_get_mutation_fields() {
1955 let cache = create_test_schema_cache();
1956 let config = SchemaConfig::default();
1957 let schema = build_schema(&cache, &config);
1958
1959 let fields = schema.get_mutation_fields("comments");
1960 assert_eq!(fields.len(), 2); }
1963}