diff --git a/Cargo.lock b/Cargo.lock index bedeb71e..6fa7c6ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1169,7 +1169,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 2.0.11", + "thiserror 2.0.16", "ucd-trie", ] @@ -1199,6 +1199,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", + "thiserror 2.0.16", "uuid", ] @@ -1921,11 +1922,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.16", ] [[package]] @@ -1941,9 +1942,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 37389517..e98b3f99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ base64 = "0.13" lazy_static = "1" bimap = { version = "0.6.3", features = ["serde"] } indexmap = "2.2" +thiserror = "2.0" [dev-dependencies] pgrx-tests = "=0.12.9" diff --git a/bin/installcheck b/bin/installcheck index 9ab8e846..caa55bbf 100755 --- a/bin/installcheck +++ b/bin/installcheck @@ -11,7 +11,7 @@ export PGDATABASE=postgres export PGTZ=UTC export PG_COLOR=auto -# PATH=~/.pgrx/15.1/pgrx-install/bin/:$PATH +# PATH=~/.pgrx/16.10/pgrx-install/bin/:$PATH #################### # Ensure Clean Env # diff --git a/src/builder.rs b/src/builder.rs index 67f5dd41..8a031267 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,3 +1,4 @@ +use crate::error::{GraphQLError, GraphQLResult}; use crate::graphql::*; use crate::gson; use crate::parser_util::*; @@ -81,13 +82,18 @@ fn read_argument<'a, T>( query_field: &graphql_parser::query::Field<'a, T>, variables: &serde_json::Value, variable_definitions: &Vec>, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef, { let input_value: __InputValue = match field.get_arg(arg_name) { Some(arg) => arg, - None => return Err(format!("Internal error 1: {}", arg_name)), + None => { + return Err(GraphQLError::internal(format!( + "Internal error 1: {}", + arg_name + ))) + } }; let user_input: Option<&graphql_parser::query::Value<'a, T>> = query_field @@ -111,7 +117,7 @@ fn read_argument_at_most<'a, T>( query_field: &graphql_parser::query::Field<'a, T>, variables: &serde_json::Value, variable_definitions: &Vec>, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef, { @@ -125,32 +131,40 @@ where .unwrap_or(gson::Value::Number(gson::Number::Integer(1))); match at_most { gson::Value::Number(gson::Number::Integer(x)) => Ok(x), - _ => Err("Internal Error: failed to parse validated atFirst".to_string()), + _ => Err(GraphQLError::internal( + "Internal Error: failed to parse validated atFirst", + )), } } -fn parse_node_id(encoded: gson::Value) -> Result { +fn parse_node_id(encoded: gson::Value) -> GraphQLResult { extern crate base64; use std::str; let node_id_base64_encoded_string: String = match encoded { gson::Value::String(s) => s, - _ => return Err("Invalid value passed to nodeId argument, Error 1".to_string()), + _ => { + return Err(GraphQLError::argument( + "Invalid value passed to nodeId argument, Error 1", + )) + } }; let node_id_json_string_utf8: Vec = base64::decode(node_id_base64_encoded_string) - .map_err(|_| "Invalid value passed to nodeId argument. Error 2".to_string())?; + .map_err(|_| GraphQLError::validation("Invalid value passed to nodeId argument. Error 2"))?; let node_id_json_string: &str = str::from_utf8(&node_id_json_string_utf8) - .map_err(|_| "Invalid value passed to nodeId argument. Error 3".to_string())?; + .map_err(|_| GraphQLError::validation("Invalid value passed to nodeId argument. Error 3"))?; let node_id_json: serde_json::Value = serde_json::from_str(node_id_json_string) - .map_err(|_| "Invalid value passed to nodeId argument. Error 4".to_string())?; + .map_err(|_| GraphQLError::validation("Invalid value passed to nodeId argument. Error 4"))?; match node_id_json { serde_json::Value::Array(x_arr) => { if x_arr.len() < 3 { - return Err("Invalid value passed to nodeId argument. Error 5".to_string()); + return Err(GraphQLError::argument( + "Invalid value passed to nodeId argument. Error 5", + )); } let mut x_arr_iter = x_arr.into_iter(); @@ -160,7 +174,9 @@ fn parse_node_id(encoded: gson::Value) -> Result { { serde_json::Value::String(s) => s, _ => { - return Err("Invalid value passed to nodeId argument. Error 6".to_string()); + return Err(GraphQLError::argument( + "Invalid value passed to nodeId argument. Error 6", + )); } }; @@ -170,7 +186,9 @@ fn parse_node_id(encoded: gson::Value) -> Result { { serde_json::Value::String(s) => s, _ => { - return Err("Invalid value passed to nodeId argument. Error 7".to_string()); + return Err(GraphQLError::argument( + "Invalid value passed to nodeId argument. Error 7", + )); } }; let values: Vec = x_arr_iter.collect(); @@ -182,7 +200,9 @@ fn parse_node_id(encoded: gson::Value) -> Result { values, }) } - _ => Err("Invalid value passed to nodeId argument. Error 10".to_string()), + _ => Err(GraphQLError::argument( + "Invalid value passed to nodeId argument. Error 10", + )), } } @@ -191,7 +211,7 @@ fn read_argument_node_id<'a, T>( query_field: &graphql_parser::query::Field<'a, T>, variables: &serde_json::Value, variable_definitions: &Vec>, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef, { @@ -212,7 +232,7 @@ fn read_argument_objects<'a, T>( query_field: &graphql_parser::query::Field<'a, T>, variables: &serde_json::Value, variable_definitions: &Vec>, -) -> Result, String> +) -> GraphQLResult> where T: Text<'a> + Eq + AsRef, { @@ -233,7 +253,7 @@ where .unmodified_type() { __Type::InsertInput(insert_type) => insert_type, - _ => return Err("Could not locate Insert Entity type".to_string()), + _ => return Err(GraphQLError::schema("Could not locate Insert Entity type")), }; let mut objects: Vec = vec![]; @@ -254,7 +274,11 @@ where let column_input_value: &__InputValue = match insert_type_field_map.get(column_field_name) { Some(input_field) => input_field, - None => return Err("Insert re-validation error 3".to_string()), + None => { + return Err(GraphQLError::validation( + "Insert re-validation error 3", + )) + } }; match &column_input_value.sql_type { @@ -267,22 +291,28 @@ where }; column_elems.insert(col.name.clone(), insert_col_builder); } - _ => return Err("Insert re-validation error 4".to_string()), + _ => { + return Err(GraphQLError::validation( + "Insert re-validation error 4", + )) + } } } } - _ => return Err("Insert re-validation errror 1".to_string()), + _ => return Err(GraphQLError::validation("Insert re-validation errror 1")), } let insert_row_builder = InsertRowBuilder { row: column_elems }; objects.push(insert_row_builder); } } - _ => return Err("Insert re-validation errror".to_string()), + _ => return Err(GraphQLError::validation("Insert re-validation errror")), }; if objects.is_empty() { - return Err("At least one record must be provided to objects".to_string()); + return Err(GraphQLError::validation( + "At least one record must be provided to objects", + )); } Ok(objects) } @@ -293,7 +323,7 @@ pub fn to_insert_builder<'a, T>( fragment_definitions: &Vec>, variables: &serde_json::Value, variable_definitions: &Vec>, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, @@ -323,7 +353,7 @@ where for selection_field in selection_fields { match field_map.get(selection_field.name.as_ref()) { - None => return Err("unknown field in insert".to_string()), + None => return Err(GraphQLError::validation("unknown field in insert")), Some(f) => builder_fields.push(match f.name().as_ref() { "affectedCount" => InsertSelection::AffectedCount { alias: alias_or_name(&selection_field), @@ -345,7 +375,11 @@ where .name() .expect("insert response type should have a name"), }, - _ => return Err("unexpected field type on insert response".to_string()), + _ => { + return Err(GraphQLError::type_error( + "unexpected field type on insert response", + )) + } }), } } @@ -355,10 +389,10 @@ where selections: builder_fields, }) } - _ => Err(format!( + _ => Err(GraphQLError::internal(format!( "can not build query for non-insert type {:?}", type_.name() - )), + ))), } } @@ -395,7 +429,7 @@ fn read_argument_set<'a, T>( query_field: &graphql_parser::query::Field<'a, T>, variables: &serde_json::Value, variable_definitions: &Vec>, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef, { @@ -409,7 +443,7 @@ where .unmodified_type() { __Type::UpdateInput(type_) => type_, - _ => return Err("Could not locate update entity type".to_string()), + _ => return Err(GraphQLError::schema("Could not locate update entity type")), }; let mut set: HashMap = HashMap::new(); @@ -425,25 +459,28 @@ where if col_input_value == &gson::Value::Absent { continue; } - let column_input_value: &__InputValue = - match update_type_field_map.get(column_field_name) { - Some(input_field) => input_field, - None => return Err("Update re-validation error 3".to_string()), - }; + let column_input_value: &__InputValue = match update_type_field_map + .get(column_field_name) + { + Some(input_field) => input_field, + None => return Err(GraphQLError::validation("Update re-validation error 3")), + }; match &column_input_value.sql_type { Some(NodeSQLType::Column(col)) => { set.insert(col.name.clone(), gson::gson_to_json(col_input_value)?); } - _ => return Err("Update re-validation error 4".to_string()), + _ => return Err(GraphQLError::validation("Update re-validation error 4")), } } } - _ => return Err("Update re-validation errror".to_string()), + _ => return Err(GraphQLError::validation("Update re-validation errror")), }; if set.is_empty() { - return Err("At least one mapping must be provided to set argument".to_string()); + return Err(GraphQLError::validation( + "At least one mapping must be provided to set argument", + )); } Ok(SetBuilder { set }) @@ -455,7 +492,7 @@ pub fn to_update_builder<'a, T>( fragment_definitions: &Vec>, variables: &serde_json::Value, variable_definitions: &Vec>, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, @@ -489,7 +526,7 @@ where for selection_field in selection_fields { match field_map.get(selection_field.name.as_ref()) { - None => return Err("unknown field in update".to_string()), + None => return Err(GraphQLError::validation("unknown field in update")), Some(f) => builder_fields.push(match f.name().as_ref() { "affectedCount" => UpdateSelection::AffectedCount { alias: alias_or_name(&selection_field), @@ -511,7 +548,11 @@ where .name() .expect("update response type should have a name"), }, - _ => return Err("unexpected field type on update response".to_string()), + _ => { + return Err(GraphQLError::type_error( + "unexpected field type on update response", + )) + } }), } } @@ -523,10 +564,10 @@ where selections: builder_fields, }) } - _ => Err(format!( + _ => Err(GraphQLError::internal(format!( "can not build query for non-update type {:?}", type_.name() - )), + ))), } } @@ -557,7 +598,7 @@ pub fn to_delete_builder<'a, T>( fragment_definitions: &Vec>, variables: &serde_json::Value, variable_definitions: &Vec>, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, @@ -589,7 +630,7 @@ where for selection_field in selection_fields { match field_map.get(selection_field.name.as_ref()) { - None => return Err("unknown field in delete".to_string()), + None => return Err(GraphQLError::validation("unknown field in delete")), Some(f) => builder_fields.push(match f.name().as_ref() { "affectedCount" => DeleteSelection::AffectedCount { alias: alias_or_name(&selection_field), @@ -611,7 +652,11 @@ where .name() .expect("delete response type should have a name"), }, - _ => return Err("unexpected field type on delete response".to_string()), + _ => { + return Err(GraphQLError::type_error( + "unexpected field type on delete response", + )) + } }), } } @@ -622,10 +667,10 @@ where selections: builder_fields, }) } - _ => Err(format!( + _ => Err(GraphQLError::internal(format!( "can not build query for non-delete type {:?}", type_.name() - )), + ))), } } @@ -663,7 +708,7 @@ pub fn to_function_call_builder<'a, T>( fragment_definitions: &Vec>, variables: &serde_json::Value, variable_definitions: &Vec>, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, @@ -709,14 +754,14 @@ where FuncCallReturnTypeBuilder::Connection(connection_builder) } _ => { - return Err(format!( + return Err(GraphQLError::internal(format!( "unsupported return type: {}", func_call_resp_type .return_type .unmodified_type() .name() .ok_or("Encountered type without name in function call builder")? - )); + ))); } }; @@ -726,10 +771,10 @@ where return_type_builder, }) } - _ => Err(format!( + _ => Err(GraphQLError::internal(format!( "can not build query for non-function type {:?}", type_.name() - )), + ))), } } @@ -739,7 +784,7 @@ fn read_func_call_args<'a, T>( variables: &serde_json::Value, func_call_resp_type: &FuncCallResponseType, variable_definitions: &Vec>, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef, { @@ -1042,7 +1087,7 @@ pub enum FunctionSelection { fn restrict_allowed_arguments<'a, T>( arg_names: &[&str], query_field: &graphql_parser::query::Field<'a, T>, -) -> Result<(), String> +) -> GraphQLResult<()> where T: Text<'a> + Eq + AsRef, { @@ -1054,7 +1099,10 @@ where .collect(); match !extra_keys.is_empty() { - true => Err(format!("Input contains extra keys {:?}", extra_keys)), + true => Err(GraphQLError::internal(format!( + "Input contains extra keys {:?}", + extra_keys + ))), false => Ok(()), } } @@ -1065,7 +1113,7 @@ fn read_argument_filter<'a, T>( query_field: &graphql_parser::query::Field<'a, T>, variables: &serde_json::Value, variable_definitions: &Vec>, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef, { @@ -1083,7 +1131,7 @@ where .type_() .unmodified_type(); if !matches!(filter_type, __Type::FilterEntity(_)) { - return Err("Could not locate Filter Entity type".to_string()); + return Err(GraphQLError::schema("Could not locate Filter Entity type")); } let filter_field_map = input_field_map(&filter_type); @@ -1096,20 +1144,24 @@ where fn create_filters( validated: &gson::Value, filter_field_map: &HashMap, -) -> Result, String> { +) -> GraphQLResult> { let mut filters = vec![]; // validated user input kv map let kv_map = match validated { gson::Value::Absent | gson::Value::Null => return Ok(filters), gson::Value::Object(kv) => kv, - _ => return Err("Filter re-validation error".to_string()), + _ => return Err(GraphQLError::validation("Filter re-validation error")), }; for (k, op_to_v) in kv_map { // k = str, v = {"eq": 1} let filter_iv: &__InputValue = match filter_field_map.get(k) { Some(filter_iv) => filter_iv, - None => return Err("Filter re-validation error in filter_iv".to_string()), + None => { + return Err(GraphQLError::validation( + "Filter re-validation error in filter_iv", + )) + } }; match op_to_v { @@ -1135,7 +1187,7 @@ fn create_filters( filters.push(filter); } } else { - return Err("Invalid `not` filter".to_string()); + return Err(GraphQLError::validation("Invalid `not` filter")); } } else { for (filter_op_str, filter_val) in filter_op_to_value_map { @@ -1180,16 +1232,19 @@ fn create_filters( compound_filters, ))) } else { - return Err( - "Only `and` and `or` filters are allowed to take an array as input." - .to_string(), - ); + return Err(GraphQLError::validation( + "Only `and` and `or` filters are allowed to take an array as input.", + )); }; filters.push(filter_builder); } } - _ => return Err("Filter re-validation errror op_to_value map".to_string()), + _ => { + return Err(GraphQLError::validation( + "Filter re-validation errror op_to_value map", + )) + } } } Ok(filters) @@ -1199,7 +1254,7 @@ fn create_filter_builder_elem( filter_iv: &__InputValue, filter_op: FilterOp, filter_val: &gson::Value, -) -> Result { +) -> GraphQLResult { Ok(match &filter_iv.sql_type { Some(NodeSQLType::Column(col)) => FilterBuilderElem::Column { column: Arc::clone(col), @@ -1209,7 +1264,11 @@ fn create_filter_builder_elem( Some(NodeSQLType::NodeId(_)) => { FilterBuilderElem::NodeId(parse_node_id(filter_val.clone())?) } - _ => return Err("Filter type error, attempted filter on non-column".to_string()), + _ => { + return Err(GraphQLError::validation( + "Filter type error, attempted filter on non-column", + )) + } }) } @@ -1219,7 +1278,7 @@ fn read_argument_order_by<'a, T>( query_field: &graphql_parser::query::Field<'a, T>, variables: &serde_json::Value, variable_definitions: &Vec>, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef, { @@ -1240,7 +1299,7 @@ where .unmodified_type() { __Type::OrderByEntity(order_entity) => order_entity, - _ => return Err("Could not locate OrderBy Entity type".to_string()), + _ => return Err(GraphQLError::schema("Could not locate OrderBy Entity type")), }; let mut orders = vec![]; @@ -1260,12 +1319,20 @@ where let order_direction = match order_direction_json { gson::Value::Absent | gson::Value::Null => continue, gson::Value::String(x) => OrderDirection::from_str(x)?, - _ => return Err("Order re-validation error 6".to_string()), + _ => { + return Err(GraphQLError::validation( + "Order re-validation error 6", + )) + } }; let column_input_value: &__InputValue = match order_field_map.get(column_field_name) { Some(input_field) => input_field, - None => return Err("Order re-validation error 3".to_string()), + None => { + return Err(GraphQLError::validation( + "Order re-validation error 3", + )) + } }; match &column_input_value.sql_type { @@ -1276,22 +1343,26 @@ where }; orders.push(order_rec); } - _ => return Err("Order re-validation error 4".to_string()), + _ => { + return Err(GraphQLError::validation( + "Order re-validation error 4", + )) + } } } } - _ => return Err("OrderBy re-validation errror 1".to_string()), + _ => return Err(GraphQLError::validation("OrderBy re-validation errror 1")), } } } - _ => return Err("OrderBy re-validation errror".to_string()), + _ => return Err(GraphQLError::validation("OrderBy re-validation errror")), }; // To acheive consistent pagination, sorting should always include primary key let pkey = &order_type .table .primary_key() - .ok_or_else(|| "Found table with no primary key".to_string())?; + .ok_or_else(|| GraphQLError::validation("Found table with no primary key"))?; for col_name in &pkey.column_names { for col in &order_type.table.columns { @@ -1315,7 +1386,7 @@ fn read_argument_cursor<'a, T>( query_field: &graphql_parser::query::Field<'a, T>, variables: &serde_json::Value, variable_definitions: &Vec>, -) -> Result, String> +) -> GraphQLResult> where T: Text<'a> + Eq + AsRef, { @@ -1333,7 +1404,12 @@ where .unmodified_type() { __Type::Scalar(x) => x, - _ => return Err(format!("Could not argument {}", arg_name)), + _ => { + return Err(GraphQLError::internal(format!( + "Could not argument {}", + arg_name + ))) + } }; match validated { @@ -1345,7 +1421,7 @@ where // so for backwards compatibility and ease of use, we'll treat null literal as absent gson::Value::Absent | gson::Value::Null => Ok(None), gson::Value::String(x) => Ok(Some(Cursor::from_str(&x)?)), - _ => Err("Cursor re-validation errror".to_string()), + _ => Err(GraphQLError::validation("Cursor re-validation errror")), } } @@ -1356,7 +1432,7 @@ pub fn to_connection_builder<'a, T>( variables: &serde_json::Value, extra_allowed_args: &[&str], variable_definitions: &Vec>, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, @@ -1384,11 +1460,15 @@ where let first: Option = match first { gson::Value::Absent | gson::Value::Null => None, gson::Value::Number(gson::Number::Integer(n)) if n < 0 => { - return Err("`first` must be an unsigned integer".to_string()) + return Err(GraphQLError::validation( + "`first` must be an unsigned integer", + )) } gson::Value::Number(gson::Number::Integer(n)) => Some(n as u64), _ => { - return Err("Internal Error: failed to parse validated first".to_string()); + return Err(GraphQLError::internal( + "Internal Error: failed to parse validated first", + )); } }; @@ -1397,11 +1477,15 @@ where let last: Option = match last { gson::Value::Absent | gson::Value::Null => None, gson::Value::Number(gson::Number::Integer(n)) if n < 0 => { - return Err("`last` must be an unsigned integer".to_string()) + return Err(GraphQLError::validation( + "`last` must be an unsigned integer", + )) } gson::Value::Number(gson::Number::Integer(n)) => Some(n as u64), _ => { - return Err("Internal Error: failed to parse validated last".to_string()); + return Err(GraphQLError::internal( + "Internal Error: failed to parse validated last", + )); } }; @@ -1415,11 +1499,15 @@ where let offset: Option = match offset { gson::Value::Absent | gson::Value::Null => None, gson::Value::Number(gson::Number::Integer(n)) if n < 0 => { - return Err("`offset` must be an unsigned integer".to_string()) + return Err(GraphQLError::validation( + "`offset` must be an unsigned integer", + )) } gson::Value::Number(gson::Number::Integer(n)) => Some(n as u64), _ => { - return Err("Internal Error: failed to parse validated offset".to_string()); + return Err(GraphQLError::internal( + "Internal Error: failed to parse validated offset", + )); } }; @@ -1444,16 +1532,26 @@ where // Validate compatible input arguments if first.is_some() && last.is_some() { - return Err("only one of \"first\" and \"last\" may be provided".to_string()); + return Err(GraphQLError::validation( + "only one of \"first\" and \"last\" may be provided", + )); } else if before.is_some() && after.is_some() { - return Err("only one of \"before\" and \"after\" may be provided".to_string()); + return Err(GraphQLError::validation( + "only one of \"before\" and \"after\" may be provided", + )); } else if first.is_some() && before.is_some() { - return Err("\"first\" may only be used with \"after\"".to_string()); + return Err(GraphQLError::validation( + "\"first\" may only be used with \"after\"", + )); } else if last.is_some() && after.is_some() { - return Err("\"last\" may only be used with \"before\"".to_string()); + return Err(GraphQLError::validation( + "\"last\" may only be used with \"before\"", + )); } else if offset.is_some() && (last.is_some() || before.is_some()) { // Only support forward pagination with offset - return Err("\"offset\" may only be used with \"first\" and \"after\"".to_string()); + return Err(GraphQLError::validation( + "\"offset\" may only be used with \"first\" and \"after\"", + )); } let filter: FilterBuilder = @@ -1479,7 +1577,7 @@ where "unknown field in connection" } .to_string(); - return Err(error); + return Err(GraphQLError::validation(error)); } Some(f) => builder_fields.push(match &f.type_.unmodified_type() { __Type::Edge(_) => ConnectionSelection::Edge(to_edge_builder( @@ -1509,10 +1607,10 @@ where alias: alias_or_name(&selection_field), } } else { - return Err(format!( + return Err(GraphQLError::internal(format!( "Unsupported field type for connection field {}", selection_field.name.as_ref() - )); + ))); } } __Type::Scalar(Scalar::String(None)) => { @@ -1524,17 +1622,17 @@ where .expect("connection type should have a name"), } } else { - return Err(format!( + return Err(GraphQLError::internal(format!( "Unsupported field type for connection field {}", selection_field.name.as_ref() - )); + ))); } } _ => { - return Err(format!( + return Err(GraphQLError::internal(format!( "unknown field type on connection: {}", selection_field.name.as_ref() - )) + ))) } }), } @@ -1557,10 +1655,10 @@ where max_rows, }) } - _ => Err(format!( + _ => Err(GraphQLError::internal(format!( "can not build query for non-connection type {:?}", type_.name() - )), + ))), } } @@ -1569,14 +1667,16 @@ fn to_aggregate_builder<'a, T>( query_field: &graphql_parser::query::Field<'a, T>, fragment_definitions: &Vec>, variables: &serde_json::Value, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, { let type_ = field.type_().unmodified_type(); let __Type::Aggregate(ref _agg_type) = type_ else { - return Err("Internal Error: Expected AggregateType in to_aggregate_builder".to_string()); + return Err(GraphQLError::internal( + "Internal Error: Expected AggregateType in to_aggregate_builder", + )); }; let alias = alias_or_name(query_field); @@ -1641,7 +1741,12 @@ where .ok_or("Name for aggregate field's type not found")? .to_string(), }, - _ => return Err(format!("Unknown aggregate field: {}", field_name)), + _ => { + return Err(GraphQLError::internal(format!( + "Unknown aggregate field: {}", + field_name + ))) + } }) } @@ -1653,14 +1758,16 @@ fn to_aggregate_column_builders<'a, T>( query_field: &graphql_parser::query::Field<'a, T>, fragment_definitions: &Vec>, variables: &serde_json::Value, -) -> Result, String> +) -> GraphQLResult> where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, { let type_ = field.type_().unmodified_type(); let __Type::AggregateNumeric(_) = type_ else { - return Err("Internal Error: Expected AggregateNumericType".to_string()); + return Err(GraphQLError::internal( + "Internal Error: Expected AggregateNumericType", + )); }; let mut column_builers = Vec::new(); let field_map = field_map(&type_); @@ -1682,16 +1789,16 @@ where })?; let __Type::Scalar(_) = sub_field.type_().unmodified_type() else { - return Err(format!( + return Err(GraphQLError::internal(format!( "Field \"{}\" on type \"{}\" is not a scalar column", col_name, type_name - )); + ))); }; let Some(NodeSQLType::Column(column)) = &sub_field.sql_type else { - return Err(format!( + return Err(GraphQLError::internal(format!( "Internal error: Missing column info for aggregate field '{}'", col_name - )); + ))); }; let alias = alias_or_name(&selection_field); @@ -1709,7 +1816,7 @@ fn to_page_info_builder<'a, T>( query_field: &graphql_parser::query::Field<'a, T>, fragment_definitions: &Vec>, variables: &serde_json::Value, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, @@ -1735,7 +1842,7 @@ where for selection_field in selection_fields { match field_map.get(selection_field.name.as_ref()) { - None => return Err("unknown field in pageInfo".to_string()), + None => return Err(GraphQLError::validation("unknown field in pageInfo")), Some(f) => builder_fields.push(match f.name().as_ref() { "startCursor" => PageInfoSelection::StartCursor { alias: alias_or_name(&selection_field), @@ -1753,7 +1860,11 @@ where alias: alias_or_name(&selection_field), typename: xtype.name().expect("page info type should have a name"), }, - _ => return Err("unexpected field type on pageInfo".to_string()), + _ => { + return Err(GraphQLError::type_error( + "unexpected field type on pageInfo", + )) + } }), } } @@ -1762,7 +1873,9 @@ where selections: builder_fields, }) } - _ => Err("can not build query for non-PageInfo type".to_string()), + _ => Err(GraphQLError::validation( + "can not build query for non-PageInfo type", + )), } } @@ -1772,7 +1885,7 @@ fn to_edge_builder<'a, T>( fragment_definitions: &Vec>, variables: &serde_json::Value, variable_definitions: &Vec>, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, @@ -1798,7 +1911,7 @@ where for selection_field in selection_fields { match field_map.get(selection_field.name.as_ref()) { - None => return Err("unknown field in edge".to_string()), + None => return Err(GraphQLError::validation("unknown field in edge")), Some(f) => builder_fields.push(match &f.type_.unmodified_type() { __Type::Node(_) => { let node_builder = to_node_builder( @@ -1819,7 +1932,11 @@ where alias: alias_or_name(&selection_field), typename: xtype.name().expect("edge type should have a name"), }, - _ => return Err("unexpected field type on edge".to_string()), + _ => { + return Err(GraphQLError::type_error( + "unexpected field type on edge", + )) + } }, }), } @@ -1829,7 +1946,9 @@ where selections: builder_fields, }) } - _ => Err("can not build query for non-edge type".to_string()), + _ => Err(GraphQLError::validation( + "can not build query for non-edge type", + )), } } @@ -1840,7 +1959,7 @@ pub fn to_node_builder<'a, T>( variables: &serde_json::Value, extra_allowed_args: &[&str], variable_definitions: &Vec>, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, @@ -1875,15 +1994,16 @@ where match xtype { Some(x) => x.clone(), None => { - return Err( - "Collection referenced by nodeId did not match any known collection" - .to_string(), - ); + return Err(GraphQLError::validation( + "Collection referenced by nodeId did not match any known collection", + )); } } } _ => { - return Err("can not build query for non-node type".to_string()); + return Err(GraphQLError::validation( + "can not build query for non-node type", + )); } }; @@ -1920,11 +2040,11 @@ where for selection_field in selection_fields { match field_map.get(selection_field.name.as_ref()) { None => { - return Err(format!( + return Err(GraphQLError::internal(format!( "Unknown field '{}' on type '{}'", selection_field.name.as_ref(), &type_name - )) + ))) } Some(f) => { let alias = alias_or_name(&selection_field); @@ -1962,7 +2082,11 @@ where )?; FunctionSelection::Connection(connection_builder) } - _ => return Err("invalid return type from function".to_string()), + _ => { + return Err(GraphQLError::validation( + "invalid return type from function", + )) + } }; NodeSelection::Function(FunctionBuilder { alias, @@ -2009,7 +2133,10 @@ where NodeSelection::Node(node_builder?) } _ => { - return Err(format!("unexpected field type on node {}", f.name())); + return Err(GraphQLError::internal(format!( + "unexpected field type on node {}", + f.name() + ))); } }, }, @@ -2185,7 +2312,7 @@ impl __Schema { query_field: &graphql_parser::query::Field<'a, T>, fragment_definitions: &Vec>, variables: &serde_json::Value, - ) -> Result<__EnumValueBuilder, String> + ) -> GraphQLResult<__EnumValueBuilder> where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, @@ -2212,10 +2339,10 @@ impl __Schema { typename: enum_value.name(), }, _ => { - return Err(format!( + return Err(GraphQLError::internal(format!( "unknown field in __EnumValue: {}", enum_value_field_name - )) + ))) } }; @@ -2238,7 +2365,7 @@ impl __Schema { fragment_definitions: &Vec>, variables: &serde_json::Value, variable_definitions: &Vec>, - ) -> Result<__InputValueBuilder, String> + ) -> GraphQLResult<__InputValueBuilder> where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, @@ -2278,10 +2405,10 @@ impl __Schema { typename: input_value.name(), }, _ => { - return Err(format!( + return Err(GraphQLError::internal(format!( "unknown field in __InputValue: {}", input_value_field_name - )) + ))) } }; @@ -2304,7 +2431,7 @@ impl __Schema { fragment_definitions: &Vec>, variables: &serde_json::Value, variable_definitions: &Vec>, - ) -> Result<__FieldBuilder, String> + ) -> GraphQLResult<__FieldBuilder> where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, @@ -2358,7 +2485,12 @@ impl __Schema { alias: alias_or_name(&selection_field), typename: field.name(), }, - _ => return Err(format!("unknown field in __Field {}", type_field_name)), + _ => { + return Err(GraphQLError::internal(format!( + "unknown field in __Field {}", + type_field_name + ))) + } }; builder_fields.push(__FieldSelection { @@ -2381,16 +2513,18 @@ impl __Schema { mut type_name: Option, variables: &serde_json::Value, variable_definitions: &Vec>, - ) -> Result, String> + ) -> GraphQLResult> where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, { if field.type_.unmodified_type() != __Type::__Type(__TypeType {}) { - return Err("can not build query for non-__type type".to_string()); + return Err(GraphQLError::validation( + "can not build query for non-__type type", + )); } - let name_arg_result: Result = + let name_arg_result: GraphQLResult = read_argument("name", field, query_field, variables, variable_definitions); let name_arg: Option = match name_arg_result { // This builder (too) is overloaded and the arg is not present in all uses @@ -2398,7 +2532,9 @@ impl __Schema { Ok(name_arg) => match name_arg { gson::Value::String(narg) => Some(narg), _ => { - return Err("Internal Error: failed to parse validated name".to_string()); + return Err(GraphQLError::internal( + "Internal Error: failed to parse validated name", + )); } }, }; @@ -2406,7 +2542,8 @@ impl __Schema { if name_arg.is_some() { type_name = name_arg; } - let type_name = type_name.ok_or("no name found for __type".to_string())?; + let type_name = + type_name.ok_or_else(|| GraphQLError::validation("no name found for __type"))?; let type_map = type_map(self); let requested_type: Option<&__Type> = type_map.get(&type_name); @@ -2434,7 +2571,7 @@ impl __Schema { fragment_definitions: &Vec>, variables: &serde_json::Value, variable_definitions: &Vec>, - ) -> Result<__TypeBuilder, String> + ) -> GraphQLResult<__TypeBuilder> where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, @@ -2454,7 +2591,12 @@ impl __Schema { let type_field_name = selection_field.name.as_ref(); // ex: type_field_field = 'name' match field_map.get(type_field_name) { - None => return Err(format!("unknown field on __Type: {}", type_field_name)), + None => { + return Err(GraphQLError::internal(format!( + "unknown field on __Type: {}", + type_field_name + ))) + } Some(f) => builder_fields.push(__TypeSelection { alias: alias_or_name(&selection_field), selection: match f.name().as_str() { @@ -2606,10 +2748,10 @@ impl __Schema { typename: type_.name(), }, _ => { - return Err(format!( + return Err(GraphQLError::internal(format!( "unexpected field {} type on __Type", type_field_name - )) + ))) } }, }), @@ -2629,7 +2771,7 @@ impl __Schema { fragment_definitions: &Vec>, variables: &serde_json::Value, variable_definitions: &Vec>, - ) -> Result<__DirectiveBuilder, String> + ) -> GraphQLResult<__DirectiveBuilder> where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, @@ -2672,11 +2814,11 @@ impl __Schema { typename: __Directive::TYPE.to_string(), }, _ => { - return Err(format!( + return Err(GraphQLError::internal(format!( "unknown field {} in {}", field_name, __Directive::TYPE, - )) + ))) } }; @@ -2699,7 +2841,7 @@ impl __Schema { fragment_definitions: &Vec>, variables: &serde_json::Value, variable_definitions: &Vec>, - ) -> Result<__SchemaBuilder, String> + ) -> GraphQLResult<__SchemaBuilder> where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, @@ -2725,7 +2867,12 @@ impl __Schema { let field_name = selection_field.name.as_ref(); match field_map.get(field_name) { - None => return Err(format!("unknown field in __Schema: {}", field_name)), + None => { + return Err(GraphQLError::internal(format!( + "unknown field in __Schema: {}", + field_name + ))) + } Some(f) => { builder_fields.push(__SchemaSelection { alias: alias_or_name(&selection_field), @@ -2737,7 +2884,7 @@ impl __Schema { .iter() // Filter out intropsection meta-types //.filter(|x| { - // !x.name().unwrap_or("".to_string()).starts_with("__") + // !x.name().unwrap_or("")).starts_with("__") //}) .map(|t| { self.to_type_builder( @@ -2804,10 +2951,10 @@ impl __Schema { typename: field.name(), }, _ => { - return Err(format!( + return Err(GraphQLError::internal(format!( "unexpected field {} type on __Schema", field_name - )) + ))) } }, }) @@ -2820,7 +2967,9 @@ impl __Schema { selections: builder_fields, }) } - _ => Err("can not build query for non-__schema type".to_string()), + _ => Err(GraphQLError::validation( + "can not build query for non-__schema type", + )), } } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..0a6618bd --- /dev/null +++ b/src/error.rs @@ -0,0 +1,113 @@ +use graphql_parser::query::ParseError as GraphQLParseError; +use thiserror::Error; + +/// Central error type for all pg_graphql operations. +/// This replaces string-based error handling with strongly-typed variants. +#[derive(Debug, Error, Clone)] +pub enum GraphQLError { + /// GraphQL query parsing errors + #[error("Parse error: {0}")] + Parse(String), + + /// Field resolution errors + #[error("Unknown field \"{field}\" on type {type_name}")] + FieldNotFound { field: String, type_name: String }, + + /// General operation errors with context + #[error("{message}")] + Operation { context: String, message: String }, +} + +impl From for GraphQLError { + fn from(err: GraphQLParseError) -> Self { + Self::Parse(err.to_string()) + } +} + +impl From for GraphQLError { + fn from(err: String) -> Self { + Self::Operation { + context: "Error".to_string(), + message: err, + } + } +} + +impl From<&str> for GraphQLError { + fn from(err: &str) -> Self { + Self::Operation { + context: "Error".to_string(), + message: err.to_string(), + } + } +} + +impl GraphQLError { + /// Creates a field not found error + pub fn field_not_found(field: impl Into, type_name: impl Into) -> Self { + Self::FieldNotFound { + field: field.into(), + type_name: type_name.into(), + } + } + + /// Creates a validation error + pub fn validation(message: impl Into) -> Self { + Self::Operation { + context: "Validation error".to_string(), + message: message.into(), + } + } + + /// Creates a schema error + pub fn schema(message: impl Into) -> Self { + Self::Operation { + context: "Schema error".to_string(), + message: message.into(), + } + } + + /// Creates a type error + pub fn type_error(message: impl Into) -> Self { + Self::Operation { + context: "Type error".to_string(), + message: message.into(), + } + } + + /// Creates an argument error + pub fn argument(message: impl Into) -> Self { + Self::Operation { + context: "Argument error".to_string(), + message: message.into(), + } + } + + /// Creates a SQL generation error + pub fn sql_generation(message: impl Into) -> Self { + Self::Operation { + context: "SQL generation error".to_string(), + message: message.into(), + } + } + + /// Creates a SQL execution error + pub fn sql_execution(message: impl Into) -> Self { + Self::Operation { + context: "SQL execution error".to_string(), + message: message.into(), + } + } + + /// Creates an internal error + pub fn internal(message: impl Into) -> Self { + Self::Operation { + context: "Internal error".to_string(), + message: message.into(), + } + } + +} + +/// Type alias for Results that use GraphQLError +pub type GraphQLResult = Result; diff --git a/src/gson.rs b/src/gson.rs index d2ed8fb3..a5825508 100644 --- a/src/gson.rs +++ b/src/gson.rs @@ -6,6 +6,8 @@ were not provided by the user. */ use std::collections::HashMap; +use crate::error::{GraphQLError, GraphQLResult}; + #[derive(Clone, Debug, PartialEq)] pub enum Value { Absent, @@ -29,7 +31,7 @@ pub enum Number { Float(f64), } -pub fn json_to_gson(val: &serde_json::Value) -> Result { +pub fn json_to_gson(val: &serde_json::Value) -> GraphQLResult { use serde_json::Value as JsonValue; let v = match val { @@ -52,9 +54,9 @@ pub fn json_to_gson(val: &serde_json::Value) -> Result { Value::Number(i_val) } None => { - let f_val: f64 = x - .as_f64() - .ok_or("Failed to handle numeric user input".to_string())?; + let f_val: f64 = x.as_f64().ok_or_else(|| { + GraphQLError::type_error("Failed to handle numeric user input") + })?; Value::Number(Number::Float(f_val)) } } @@ -71,12 +73,12 @@ pub fn json_to_gson(val: &serde_json::Value) -> Result { Ok(v) } -pub fn gson_to_json(val: &Value) -> Result { +pub fn gson_to_json(val: &Value) -> GraphQLResult { use serde_json::Value as JsonValue; let v = match val { Value::Absent => { - return Err("Encounterd `Absent` value while transforming between GraphQL intermediate object notation and JSON".to_string()) + return Err(GraphQLError::internal("Encountered `Absent` value while transforming between GraphQL intermediate object notation and JSON")) }, Value::Null => JsonValue::Null, Value::Boolean(x) => JsonValue::Bool(x.to_owned()), diff --git a/src/lib.rs b/src/lib.rs index 8d99a9d6..a70af9fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ use resolve::resolve_inner; use serde_json::json; mod builder; +mod error; mod graphql; mod gson; mod merge; @@ -57,7 +58,9 @@ fn resolve( } Err(err) => GraphQLResponse { data: Omit::Omitted, - errors: Omit::Present(vec![ErrorMessage { message: err }]), + errors: Omit::Present(vec![ErrorMessage { + message: err.to_string(), + }]), }, } } diff --git a/src/merge.rs b/src/merge.rs index fb7ca945..dffd0e6c 100644 --- a/src/merge.rs +++ b/src/merge.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, hash::Hash}; use graphql_parser::query::{Field, Text, Value}; use indexmap::IndexMap; +use crate::error::{GraphQLError, GraphQLResult}; use crate::parser_util::alias_or_name; /// Merges duplicates in a vector of fields. The fields in the vector are added to a @@ -12,7 +13,7 @@ use crate::parser_util::alias_or_name; /// /// The map is an `IndexMap` to ensure iteration order of the fields is preserved. /// This prevents tests from being flaky due to field order changing between test runs. -pub fn merge<'a, 'b, T>(fields: Vec>) -> Result>, String> +pub fn merge<'a, 'b, T>(fields: Vec>) -> GraphQLResult>> where T: Text<'a> + Eq + AsRef, T::Value: Hash, @@ -41,23 +42,23 @@ where Ok(fields) } -fn can_merge<'a, T>(field_a: &Field<'a, T>, field_b: &Field<'a, T>) -> Result +fn can_merge<'a, T>(field_a: &Field<'a, T>, field_b: &Field<'a, T>) -> GraphQLResult where T: Text<'a> + Eq + AsRef, T::Value: Hash, { if field_a.name != field_b.name { - return Err(format!( + return Err(GraphQLError::validation(format!( "Fields `{}` and `{}` are different", field_a.name.as_ref(), field_b.name.as_ref(), - )); + ))); } if !same_arguments(&field_a.arguments, &field_b.arguments) { - return Err(format!( + return Err(GraphQLError::validation(format!( "Two fields named `{}` have different arguments", field_a.name.as_ref(), - )); + ))); } Ok(true) diff --git a/src/parser_util.rs b/src/parser_util.rs index 35a9914a..0d763a2c 100644 --- a/src/parser_util.rs +++ b/src/parser_util.rs @@ -1,3 +1,4 @@ +use crate::error::{GraphQLError, GraphQLResult}; use crate::graphql::{EnumSource, __InputValue, __Type, ___Type}; use crate::{gson, merge::merge}; use graphql_parser::query::*; @@ -20,7 +21,7 @@ pub fn normalize_selection_set<'a, 'b, T>( fragment_definitions: &'b Vec>, type_name: &String, // for inline fragments variables: &serde_json::Value, // for directives -) -> Result>, String> +) -> GraphQLResult>> where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, @@ -42,7 +43,7 @@ where pub fn selection_is_skipped<'a, 'b, T>( query_selection: &'b Selection<'a, T>, variables: &serde_json::Value, -) -> Result +) -> GraphQLResult where T: Text<'a> + Eq + AsRef, { @@ -58,11 +59,16 @@ where match directive_name { "skip" => { if directive.arguments.len() != 1 { - return Err("Incorrect arguments to directive @skip".to_string()); + return Err(GraphQLError::validation( + "Incorrect arguments to directive @skip", + )); } let arg = &directive.arguments[0]; if arg.0.as_ref() != "if" { - return Err(format!("Unknown argument to @skip: {}", arg.0.as_ref())); + return Err(GraphQLError::validation(format!( + "Unknown argument to @skip: {}", + arg.0.as_ref() + ))); } // the argument to @skip(if: ) @@ -82,8 +88,9 @@ where } } _ => { - return Err("Value for \"if\" in @skip directive is required" - .to_string()); + return Err(GraphQLError::validation( + "Value for \"if\" in @skip directive is required", + )); } } } @@ -92,11 +99,16 @@ where } "include" => { if directive.arguments.len() != 1 { - return Err("Incorrect arguments to directive @include".to_string()); + return Err(GraphQLError::validation( + "Incorrect arguments to directive @include", + )); } let arg = &directive.arguments[0]; if arg.0.as_ref() != "if" { - return Err(format!("Unknown argument to @include: {}", arg.0.as_ref())); + return Err(GraphQLError::validation(format!( + "Unknown argument to @include: {}", + arg.0.as_ref() + ))); } // the argument to @include(if: ) @@ -115,17 +127,21 @@ where } } _ => { - return Err( - "Value for \"if\" in @include directive is required" - .to_string(), - ); + return Err(GraphQLError::validation( + "Value for \"if\" in @include directive is required", + )); } } } _ => (), } } - _ => return Err(format!("Unknown directive {}", directive_name)), + _ => { + return Err(GraphQLError::validation(format!( + "Unknown directive {}", + directive_name + ))) + } } } } @@ -138,7 +154,7 @@ pub fn normalize_selection<'a, 'b, T>( fragment_definitions: &'b Vec>, type_name: &String, // for inline fragments variables: &serde_json::Value, // for directives -) -> Result>, String> +) -> GraphQLResult>> where T: Text<'a> + Eq + AsRef + Clone, T::Value: Hash, @@ -168,11 +184,11 @@ where }) { Some(frag) => frag, None => { - return Err(format!( + return Err(GraphQLError::validation(format!( "no fragment named {} on type {}", frag_name.as_ref(), type_name - )) + ))) } }; @@ -215,7 +231,7 @@ pub fn to_gson<'a, T>( graphql_value: &Value<'a, T>, variables: &serde_json::Value, variable_definitions: &Vec>, -) -> Result +) -> GraphQLResult where T: Text<'a> + AsRef, { @@ -229,7 +245,7 @@ where let i_val = gson::Number::Integer(num); gson::Value::Number(i_val) } - None => return Err("Invalid Int input".to_string()), + None => return Err(GraphQLError::type_error("Invalid Int input")), } } Value::Float(x) => { @@ -276,7 +292,7 @@ where Ok(result) } -pub fn validate_arg_from_type(type_: &__Type, value: &gson::Value) -> Result { +pub fn validate_arg_from_type(type_: &__Type, value: &gson::Value) -> GraphQLResult { use crate::graphql::Scalar; use crate::gson::Number as GsonNumber; use crate::gson::Value as GsonValue; @@ -286,7 +302,12 @@ pub fn validate_arg_from_type(type_: &__Type, value: &gson::Value) -> Result match value { GsonValue::Absent | GsonValue::Null | GsonValue::String(_) => value.clone(), - _ => return Err(format!("Invalid input for {:?} type", scalar)), + _ => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {:?} type", + scalar + ))) + } }, Scalar::String(Some(max_length)) => match value { GsonValue::Absent | GsonValue::Null => value.clone(), @@ -294,51 +315,86 @@ pub fn validate_arg_from_type(type_: &__Type, value: &gson::Value) -> Result *max_length { false => value.clone(), true => { - return Err(format!( + return Err(GraphQLError::type_error(format!( "Invalid input for {} type. Maximum character length {}", scalar.name().unwrap_or("String".to_string()), max_length - )) + ))) } } } - _ => return Err(format!("Invalid input for {:?} type", scalar)), + _ => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {:?} type", + scalar + ))) + } }, Scalar::Int => match value { GsonValue::Absent => value.clone(), GsonValue::Null => value.clone(), GsonValue::Number(GsonNumber::Integer(_)) => value.clone(), - _ => return Err(format!("Invalid input for {:?} type", scalar)), + _ => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {:?} type", + scalar + ))) + } }, Scalar::Float => match value { GsonValue::Absent => value.clone(), GsonValue::Null => value.clone(), GsonValue::Number(_) => value.clone(), - _ => return Err(format!("Invalid input for {:?} type", scalar)), + _ => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {:?} type", + scalar + ))) + } }, Scalar::Boolean => match value { GsonValue::Absent | GsonValue::Null | GsonValue::Boolean(_) => value.clone(), - _ => return Err(format!("Invalid input for {:?} type", scalar)), + _ => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {:?} type", + scalar + ))) + } }, Scalar::Date => { match value { // XXX: future - validate date here GsonValue::Absent | GsonValue::Null | GsonValue::String(_) => value.clone(), - _ => return Err(format!("Invalid input for {:?} type", scalar)), + _ => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {:?} type", + scalar + ))) + } } } Scalar::Time => { match value { // XXX: future - validate time here GsonValue::Absent | GsonValue::Null | GsonValue::String(_) => value.clone(), - _ => return Err(format!("Invalid input for {:?} type", scalar)), + _ => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {:?} type", + scalar + ))) + } } } Scalar::Datetime => { match value { // XXX: future - validate datetime here GsonValue::Absent | GsonValue::Null | GsonValue::String(_) => value.clone(), - _ => return Err(format!("Invalid input for {:?} type", scalar)), + _ => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {:?} type", + scalar + ))) + } } } Scalar::BigInt => match value { @@ -346,43 +402,68 @@ pub fn validate_arg_from_type(type_: &__Type, value: &gson::Value) -> Result value.clone(), - _ => return Err(format!("Invalid input for {:?} type", scalar)), + _ => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {:?} type", + scalar + ))) + } }, Scalar::UUID => { match value { // XXX: future - validate uuid here GsonValue::Absent | GsonValue::Null | GsonValue::String(_) => value.clone(), - _ => return Err(format!("Invalid input for {:?} type", scalar)), + _ => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {:?} type", + scalar + ))) + } } } Scalar::JSON => { match value { // XXX: future - validate json here GsonValue::Absent | GsonValue::Null | GsonValue::String(_) => value.clone(), - _ => return Err(format!("Invalid input for {:?} type", scalar)), + _ => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {:?} type", + scalar + ))) + } } } Scalar::Cursor => { match value { // XXX: future - validate cursor here GsonValue::Absent | GsonValue::Null | GsonValue::String(_) => value.clone(), - _ => return Err(format!("Invalid input for {:?} type", scalar)), + _ => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {:?} type", + scalar + ))) + } } } Scalar::ID => { match value { // XXX: future - validate cursor here GsonValue::Absent | GsonValue::Null | GsonValue::String(_) => value.clone(), - _ => return Err(format!("Invalid input for {:?} type", scalar)), + _ => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {:?} type", + scalar + ))) + } } } Scalar::BigFloat => match value { GsonValue::Absent | GsonValue::Null | GsonValue::String(_) => value.clone(), _ => { - return Err(format!( + return Err(GraphQLError::type_error(format!( "Invalid input for {:?} type. String required", scalar - )) + ))) } }, // No validation possible for unknown types. Lean on postgres for parsing @@ -414,10 +495,20 @@ pub fn validate_arg_from_type(type_: &__Type, value: &gson::Value) -> Result value.clone(), } } - None => return Err(format!("Invalid input for {} type", enum_name)), + None => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {} type", + enum_name + ))) + } } } - _ => return Err(format!("Invalid input for {} type", enum_name)), + _ => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {} type", + enum_name + ))) + } } } __Type::OrderBy(enum_) => { @@ -433,10 +524,20 @@ pub fn validate_arg_from_type(type_: &__Type, value: &gson::Value) -> Result value.clone(), - None => return Err(format!("Invalid input for {} type", enum_name)), + None => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {} type", + enum_name + ))) + } } } - _ => return Err(format!("Invalid input for {} type", enum_name)), + _ => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {} type", + enum_name + ))) + } } } __Type::List(list_type) => { @@ -464,7 +565,7 @@ pub fn validate_arg_from_type(type_: &__Type, value: &gson::Value) -> Result { - return Err("Invalid input for NonNull type".to_string()) + return Err(GraphQLError::type_error("Invalid input for NonNull type")) } _ => out_elem, } @@ -475,10 +576,10 @@ pub fn validate_arg_from_type(type_: &__Type, value: &gson::Value) -> Result validate_arg_from_input_object(type_, value)?, __Type::FilterEntity(_) => validate_arg_from_input_object(type_, value)?, _ => { - return Err(format!( + return Err(GraphQLError::type_error(format!( "Invalid Type used as input argument {}", type_.name().unwrap_or_default() - )) + ))) } }; Ok(res) @@ -487,14 +588,17 @@ pub fn validate_arg_from_type(type_: &__Type, value: &gson::Value) -> Result Result { +) -> GraphQLResult { use crate::graphql::__TypeKind; use crate::gson::Value as GsonValue; let input_type_name = input_type.name().unwrap_or_default(); if input_type.kind() != __TypeKind::INPUT_OBJECT { - return Err(format!("Invalid input type {}", input_type_name)); + return Err(GraphQLError::type_error(format!( + "Invalid input type {}", + input_type_name + ))); } let res: GsonValue = match value { @@ -513,10 +617,10 @@ pub fn validate_arg_from_input_object( } } if !extra_input_keys.is_empty() { - return Err(format!( + return Err(GraphQLError::validation(format!( "Input for type {} contains extra keys {:?}", input_type_name, extra_input_keys - )); + ))); } for obj_field in type_input_fields { @@ -535,7 +639,12 @@ pub fn validate_arg_from_input_object( } GsonValue::Object(out_map) } - _ => return Err(format!("Invalid input for {} type", input_type_name)), + _ => { + return Err(GraphQLError::type_error(format!( + "Invalid input for {} type", + input_type_name + ))) + } }; Ok(res) } diff --git a/src/resolve.rs b/src/resolve.rs index 9a87e77e..07bfd995 100644 --- a/src/resolve.rs +++ b/src/resolve.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use std::hash::Hash; use crate::builder::*; +use crate::error::{GraphQLError, GraphQLResult}; use crate::graphql::*; use crate::omit::*; use crate::parser_util::*; @@ -92,7 +93,9 @@ where Err(message) => { return GraphQLResponse { data: Omit::Omitted, - errors: Omit::Present(vec![ErrorMessage { message }]), + errors: Omit::Present(vec![ErrorMessage { + message: message.to_string(), + }]), } } } @@ -220,9 +223,13 @@ where Ok(d) => { res_data[alias_or_name(selection)] = d; } - Err(msg) => res_errors.push(ErrorMessage { message: msg }), + Err(msg) => res_errors.push(ErrorMessage { + message: msg.to_string(), + }), }, - Err(msg) => res_errors.push(ErrorMessage { message: msg }), + Err(msg) => res_errors.push(ErrorMessage { + message: msg.to_string(), + }), } } __Type::NodeInterface(_) => { @@ -240,9 +247,13 @@ where Ok(d) => { res_data[alias_or_name(selection)] = d; } - Err(msg) => res_errors.push(ErrorMessage { message: msg }), + Err(msg) => res_errors.push(ErrorMessage { + message: msg.to_string(), + }), }, - Err(msg) => res_errors.push(ErrorMessage { message: msg }), + Err(msg) => res_errors.push(ErrorMessage { + message: msg.to_string(), + }), } } __Type::__Type(_) => { @@ -259,7 +270,9 @@ where Ok(builder) => { res_data[alias_or_name(selection)] = serde_json::json!(builder); } - Err(msg) => res_errors.push(ErrorMessage { message: msg }), + Err(msg) => res_errors.push(ErrorMessage { + message: msg.to_string(), + }), } } __Type::__Schema(_) => { @@ -275,7 +288,9 @@ where Ok(builder) => { res_data[alias_or_name(selection)] = serde_json::json!(builder); } - Err(msg) => res_errors.push(ErrorMessage { message: msg }), + Err(msg) => res_errors.push(ErrorMessage { + message: msg.to_string(), + }), } } _ => match field_def.name().as_ref() { @@ -308,12 +323,14 @@ where Ok(d) => { res_data[alias_or_name(selection)] = d; } - Err(msg) => { - res_errors.push(ErrorMessage { message: msg }) - } + Err(msg) => res_errors.push(ErrorMessage { + message: msg.to_string(), + }), } } - Err(msg) => res_errors.push(ErrorMessage { message: msg }), + Err(msg) => res_errors.push(ErrorMessage { + message: msg.to_string(), + }), } } }, @@ -403,9 +420,9 @@ where use pgrx::prelude::*; - let spi_result: Result = Spi::connect(|mut conn| { + let spi_result: GraphQLResult = Spi::connect(|mut conn| { let res_data: serde_json::Value = match selections[..] { - [] => Err("Selection set must not be empty".to_string())?, + [] => Err(GraphQLError::validation("Selection set must not be empty"))?, _ => { let mut res_data = json!({}); // Key name to prepared statement name @@ -414,9 +431,9 @@ where let maybe_field_def = map.get(selection.name.as_ref()); conn = match maybe_field_def { - None => Err(format!( - "Unknown field {:?} on type {}", - selection.name, mutation_type_name + None => Err(GraphQLError::field_not_found( + selection.name.as_ref(), + &mutation_type_name ))?, Some(field_def) => match field_def.type_.unmodified_type() { __Type::InsertResponse(_) => { @@ -517,7 +534,11 @@ where errors: Omit::Omitted, }, Err(err) => { - ereport!(ERROR, PgSqlErrorCode::ERRCODE_INTERNAL_ERROR, err); + ereport!( + ERROR, + PgSqlErrorCode::ERRCODE_INTERNAL_ERROR, + err.to_string() + ); } } } @@ -529,17 +550,17 @@ fn detect_fragment_cycles<'a, 'b, T>( visited: &mut HashSet<&'b str>, fragment_definitions: &'b [FragmentDefinition<'a, T>], stack_depth: u32, -) -> Result<(), String> +) -> GraphQLResult<()> where T: Text<'a>, { if stack_depth > STACK_DEPTH_LIMIT { - return Err(format!( + return Err(GraphQLError::validation(format!( "Fragment cycle depth is greater than {STACK_DEPTH_LIMIT}" - )); + ))); } if visited.contains(fragment_definition.name.as_ref()) { - return Err("Found a cycle between fragments".to_string()); + return Err(GraphQLError::validation("Found a cycle between fragments")); } else { visited.insert(fragment_definition.name.as_ref()); } @@ -559,14 +580,14 @@ fn detect_fragment_cycles_in_selection_set<'a, 'b, T>( visited: &mut HashSet<&'b str>, fragment_definitions: &'b [FragmentDefinition<'a, T>], stack_depth: u32, -) -> Result<(), String> +) -> GraphQLResult<()> where T: Text<'a>, { if stack_depth > STACK_DEPTH_LIMIT { - return Err(format!( + return Err(GraphQLError::validation(format!( "Fragment cycle depth is greater than {STACK_DEPTH_LIMIT}" - )); + ))); } for selection in &selection_set.items { match selection { diff --git a/src/sql_types.rs b/src/sql_types.rs index 0ef1bb8d..a493532f 100644 --- a/src/sql_types.rs +++ b/src/sql_types.rs @@ -4,6 +4,8 @@ use cached::SizedCache; use lazy_static::lazy_static; use pgrx::*; use serde::{Deserialize, Serialize}; + +use crate::error::GraphQLResult; use std::cmp::Ordering; use std::collections::hash_map::DefaultHasher; use std::collections::{HashMap, HashSet}; @@ -593,7 +595,9 @@ impl Table { /// Otherwise, fall back to schema-level max_rows. /// If neither is set, use the global default(set in load_sql_context.sql) pub fn max_rows(&self, schema: &Schema) -> u64 { - self.directives.max_rows.unwrap_or(schema.directives.max_rows) + self.directives + .max_rows + .unwrap_or(schema.directives.max_rows) } } @@ -823,11 +827,11 @@ pub fn calculate_hash(t: &T) -> u64 { } #[cached( - type = "SizedCache, String>>", + type = "SizedCache>>", create = "{ SizedCache::with_size(250) }", convert = r#"{ calculate_hash(_config) }"# )] -pub fn load_sql_context(_config: &Config) -> Result, String> { +pub fn load_sql_context(_config: &Config) -> GraphQLResult> { // cache value for next query let query = include_str!("../sql/load_sql_context.sql"); let sql_result: serde_json::Value = get_one_readonly::(query) @@ -989,9 +993,9 @@ pub fn load_sql_context(_config: &Config) -> Result, String> { .map(populate_table_functions) .map(Arc::new) .map_err(|e| { - format!( + crate::error::GraphQLError::schema(format!( "Error while loading schema, check comment directives. {}", e - ) + )) }) } diff --git a/src/transpile.rs b/src/transpile.rs index 062f3a80..de590ba9 100644 --- a/src/transpile.rs +++ b/src/transpile.rs @@ -1,4 +1,5 @@ use crate::builder::*; +use crate::error::{GraphQLError, GraphQLResult}; use crate::graphql::*; use crate::sql_types::{Column, ForeignKey, ForeignKeyTableInfo, Function, Table, TypeDetails}; use itertools::Itertools; @@ -39,32 +40,34 @@ pub fn rand_block_name() -> String { } pub trait MutationEntrypoint<'conn> { - fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> Result; + fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> GraphQLResult; fn execute( &self, mut conn: SpiClient<'conn>, - ) -> Result<(serde_json::Value, SpiClient<'conn>), String> { + ) -> GraphQLResult<(serde_json::Value, SpiClient<'conn>)> { let mut param_context = ParamContext { params: vec![] }; let sql = &self.to_sql_entrypoint(&mut param_context); let sql = match sql { Ok(sql) => sql, Err(err) => { - return Err(err.to_string()); + return Err(err.clone()); } }; let res_q = conn .update(sql, None, Some(param_context.params)) - .map_err(|_| "Internal Error: Failed to execute transpiled query".to_string())?; + .map_err(|_| { + GraphQLError::sql_execution("Internal Error: Failed to execute transpiled query") + })?; let res: pgrx::JsonB = match res_q.first().get::(1) { Ok(Some(dat)) => dat, Ok(None) => JsonB(serde_json::Value::Null), Err(e) => { - return Err(format!( + return Err(GraphQLError::sql_generation(format!( "Internal Error: Failed to load result from transpiled query: {e}" - )); + ))); } }; @@ -73,15 +76,15 @@ pub trait MutationEntrypoint<'conn> { } pub trait QueryEntrypoint { - fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> Result; + fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> GraphQLResult; - fn execute(&self) -> Result { + fn execute(&self) -> GraphQLResult { let mut param_context = ParamContext { params: vec![] }; let sql = &self.to_sql_entrypoint(&mut param_context); let sql = match sql { Ok(sql) => sql, Err(err) => { - return Err(err.to_string()); + return Err(err.clone()); } }; @@ -98,7 +101,9 @@ pub trait QueryEntrypoint { match spi_result { Ok(Some(jsonb)) => Ok(jsonb.0), Ok(None) => Ok(serde_json::Value::Null), - _ => Err("Internal Error: Failed to execute transpiled query".to_string()), + _ => Err(GraphQLError::internal( + "Internal Error: Failed to execute transpiled query", + )), } } } @@ -149,7 +154,7 @@ impl Table { cursor: &Cursor, param_context: &mut ParamContext, allow_equality: bool, - ) -> Result { + ) -> GraphQLResult { // When paginating, allowe_equality should be false because we don't want to // include the cursor's record in the page // @@ -172,7 +177,9 @@ impl Table { let cursor_elem = next_cursor.elems.remove(0); if order_by.elems.is_empty() { - return Err("orderBy clause incompatible with pagination cursor".to_string()); + return Err(GraphQLError::validation( + "orderBy clause incompatible with pagination cursor", + )); } let mut next_order_by = order_by.clone(); let order_elem = next_order_by.elems.remove(0); @@ -212,7 +219,7 @@ impl Table { reverse_reference: bool, quoted_block_name: &str, quoted_parent_block_name: &str, - ) -> Result { + ) -> GraphQLResult { let mut equality_clauses = vec!["true".to_string()]; let table_ref: &ForeignKeyTableInfo; @@ -254,7 +261,7 @@ impl Table { } impl MutationEntrypoint<'_> for InsertBuilder { - fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> Result { + fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> GraphQLResult { let quoted_block_name = rand_block_name(); let quoted_schema = quote_ident(&self.table.schema); let quoted_table = quote_ident(&self.table.name); @@ -331,7 +338,7 @@ impl InsertSelection { &self, block_name: &str, param_context: &mut ParamContext, - ) -> Result { + ) -> GraphQLResult { let r = match self { Self::AffectedCount { alias } => { format!("{}, count(*)", quote_literal(alias)) @@ -356,7 +363,7 @@ impl UpdateSelection { &self, block_name: &str, param_context: &mut ParamContext, - ) -> Result { + ) -> GraphQLResult { let r = match self { Self::AffectedCount { alias } => { format!("{}, count(*)", quote_literal(alias)) @@ -381,7 +388,7 @@ impl DeleteSelection { &self, block_name: &str, param_context: &mut ParamContext, - ) -> Result { + ) -> GraphQLResult { let r = match self { Self::AffectedCount { alias } => { format!("{}, count(*)", quote_literal(alias)) @@ -403,7 +410,7 @@ impl DeleteSelection { } impl MutationEntrypoint<'_> for UpdateBuilder { - fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> Result { + fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> GraphQLResult { let quoted_block_name = rand_block_name(); let quoted_schema = quote_ident(&self.table.schema); let quoted_table = quote_ident(&self.table.name); @@ -487,7 +494,7 @@ impl MutationEntrypoint<'_> for UpdateBuilder { } impl MutationEntrypoint<'_> for DeleteBuilder { - fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> Result { + fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> GraphQLResult { let quoted_block_name = rand_block_name(); let quoted_schema = quote_ident(&self.table.schema); let quoted_table = quote_ident(&self.table.name); @@ -549,7 +556,7 @@ impl MutationEntrypoint<'_> for DeleteBuilder { } impl FunctionCallBuilder { - fn to_sql(&self, param_context: &mut ParamContext) -> Result { + fn to_sql(&self, param_context: &mut ParamContext) -> GraphQLResult { let mut arg_clauses = vec![]; for (arg, arg_value) in &self.args_builder.args { if let Some(arg) = arg { @@ -596,13 +603,13 @@ impl FunctionCallBuilder { } impl MutationEntrypoint<'_> for FunctionCallBuilder { - fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> Result { + fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> GraphQLResult { self.to_sql(param_context) } } impl QueryEntrypoint for FunctionCallBuilder { - fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> Result { + fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> GraphQLResult { self.to_sql(param_context) } } @@ -626,7 +633,7 @@ impl OrderByBuilder { } } -pub fn json_to_text_datum(val: &serde_json::Value) -> Result, String> { +pub fn json_to_text_datum(val: &serde_json::Value) -> GraphQLResult> { use serde_json::Value; let null: Option = None; match val { @@ -643,10 +650,14 @@ pub fn json_to_text_datum(val: &serde_json::Value) -> Result Some(x.to_string()), Value::Number(x) => Some(x.to_string()), Value::Array(_) => { - return Err("Unexpected array in input value array".to_string()); + return Err(GraphQLError::type_error( + "Unexpected array in input value array", + )); } Value::Object(_) => { - return Err("Unexpected object in input value array".to_string()); + return Err(GraphQLError::validation( + "Unexpected object in input value array", + )); } }; inner_vals.push(str_elem); @@ -654,7 +665,7 @@ pub fn json_to_text_datum(val: &serde_json::Value) -> Result Err("Unexpected object in input value".to_string()), + Value::Object(_) => Err(GraphQLError::validation("Unexpected object in input value")), } } @@ -665,7 +676,7 @@ pub struct ParamContext { impl ParamContext { // Pushes a parameter into the context and returns a SQL clause to reference it //fn clause_for(&mut self, param: (PgOid, Option)) -> String { - fn clause_for(&mut self, value: &serde_json::Value, type_name: &str) -> Result { + fn clause_for(&mut self, value: &serde_json::Value, type_name: &str) -> GraphQLResult { let type_oid = match type_name.ends_with("[]") { true => PgOid::BuiltIn(PgBuiltInOids::TEXTARRAYOID), false => PgOid::BuiltIn(PgBuiltInOids::TEXTOID), @@ -683,7 +694,7 @@ impl FilterBuilderElem { block_name: &str, table: &Table, param_context: &mut ParamContext, - ) -> Result { + ) -> GraphQLResult { match self { Self::Column { column, op, value } => { let frag = match op { @@ -697,16 +708,16 @@ impl FilterBuilderElem { "NULL" => "is null", "NOT_NULL" => "is not null", _ => { - return Err( - "Error transpiling Is filter value".to_string() - ) + return Err(GraphQLError::sql_generation( + "Error transpiling Is filter value", + )) } } } _ => { - return Err( - "Error transpiling Is filter value type".to_string() - ); + return Err(GraphQLError::sql_generation( + "Error transpiling Is filter value type", + )); } } ) @@ -742,7 +753,9 @@ impl FilterBuilderElem { FilterOp::ContainedBy => "<@", FilterOp::Overlap => "&&", FilterOp::Is => { - return Err("Error transpiling Is filter".to_string()); + return Err(GraphQLError::sql_generation( + "Error transpiling Is filter", + )); } }, val_clause @@ -765,7 +778,7 @@ impl CompoundFilterBuilder { block_name: &str, table: &Table, param_context: &mut ParamContext, - ) -> Result { + ) -> GraphQLResult { Ok(match self { CompoundFilterBuilder::And(elements) => { let bool_expressions = elements @@ -794,7 +807,7 @@ impl FilterBuilder { block_name: &str, table: &Table, param_context: &mut ParamContext, - ) -> Result { + ) -> GraphQLResult { let mut frags = vec!["true".to_string()]; for elem in &self.elems { @@ -845,7 +858,7 @@ impl ConnectionBuilder { &self, quoted_block_name: &str, quoted_parent_block_name: &Option<&str>, - ) -> Result { + ) -> GraphQLResult { match &self.source.fkey { Some(fkey) => { let quoted_parent_block_name = quoted_parent_block_name @@ -865,7 +878,7 @@ impl ConnectionBuilder { &self, quoted_block_name: &str, param_context: &mut ParamContext, - ) -> Result { + ) -> GraphQLResult { let frags: Vec = self .selections .iter() @@ -875,7 +888,8 @@ impl ConnectionBuilder { &self.order_by, &self.source.table, param_context, - ).transpose() + ) + .transpose() }) .collect::, _>>()?; @@ -912,7 +926,7 @@ impl ConnectionBuilder { } // Generates the *contents* of the aggregate jsonb_build_object - fn aggregate_select_list(&self, quoted_block_name: &str) -> Result, String> { + fn aggregate_select_list(&self, quoted_block_name: &str) -> GraphQLResult> { let Some(agg_builder) = self.selections.iter().find_map(|sel| match sel { ConnectionSelection::Aggregate(builder) => Some(builder), _ => None, @@ -1007,7 +1021,7 @@ impl ConnectionBuilder { param_context: &mut ParamContext, from_func: Option, from_clause: Option, - ) -> Result { + ) -> GraphQLResult { let quoted_block_name = rand_block_name(); let from_clause = match from_clause { @@ -1237,7 +1251,7 @@ impl ConnectionBuilder { } impl QueryEntrypoint for ConnectionBuilder { - fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> Result { + fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> GraphQLResult { self.to_sql(None, param_context, None, None) } } @@ -1248,7 +1262,7 @@ impl PageInfoBuilder { _block_name: &str, order_by: &OrderByBuilder, table: &Table, - ) -> Result { + ) -> GraphQLResult { let frags: Vec = self .selections .iter() @@ -1267,7 +1281,7 @@ impl PageInfoSelection { block_name: &str, order_by: &OrderByBuilder, table: &Table, - ) -> Result { + ) -> GraphQLResult { let order_by_clause = order_by.to_order_by_clause(block_name); let order_by_clause_reversed = order_by.reverse().to_order_by_clause(block_name); @@ -1312,7 +1326,7 @@ impl ConnectionSelection { order_by: &OrderByBuilder, table: &Table, param_context: &mut ParamContext, - ) -> Result, String> { + ) -> GraphQLResult> { Ok(match self { Self::Edge(x) => Some(format!( "{}, {}", @@ -1347,7 +1361,7 @@ impl EdgeBuilder { order_by: &OrderByBuilder, table: &Table, param_context: &mut ParamContext, - ) -> Result { + ) -> GraphQLResult { let frags: Vec = self .selections .iter() @@ -1390,7 +1404,7 @@ impl EdgeSelection { order_by: &OrderByBuilder, table: &Table, param_context: &mut ParamContext, - ) -> Result { + ) -> GraphQLResult { Ok(match self { Self::Cursor { alias } => { let cursor_clause = table.to_cursor_clause(block_name, order_by); @@ -1413,7 +1427,7 @@ impl NodeBuilder { &self, block_name: &str, param_context: &mut ParamContext, - ) -> Result { + ) -> GraphQLResult { let frags: Vec = self .selections .iter() @@ -1436,7 +1450,7 @@ impl NodeBuilder { &self, parent_block_name: &str, param_context: &mut ParamContext, - ) -> Result { + ) -> GraphQLResult { let quoted_block_name = rand_block_name(); let quoted_schema = quote_ident(&self.table.schema); let quoted_table = quote_ident(&self.table.name); @@ -1481,7 +1495,7 @@ impl NodeBuilder { } impl QueryEntrypoint for NodeBuilder { - fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> Result { + fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> GraphQLResult { let quoted_block_name = rand_block_name(); let quoted_schema = quote_ident(&self.table.schema); let quoted_table = quote_ident(&self.table.name); @@ -1515,11 +1529,13 @@ impl NodeIdInstance { block_name: &str, table: &Table, param_context: &mut ParamContext, - ) -> Result { + ) -> GraphQLResult { // TODO: abstract this logical check into builder. It is not related to // transpiling and should not be in this module if (&self.schema_name, &self.table_name) != (&table.schema, &table.name) { - return Err("nodeId belongs to a different collection".to_string()); + return Err(GraphQLError::validation( + "nodeId belongs to a different collection", + )); } let mut col_val_pairs: Vec = vec![]; @@ -1552,7 +1568,7 @@ impl NodeSelection { &self, block_name: &str, param_context: &mut ParamContext, - ) -> Result { + ) -> GraphQLResult { Ok(match self { // TODO need to provide alias when called from node builder. Self::Connection(builder) => format!( @@ -1597,7 +1613,7 @@ impl NodeSelection { } impl ColumnBuilder { - pub fn to_sql(&self, block_name: &str) -> Result { + pub fn to_sql(&self, block_name: &str) -> GraphQLResult { let col = format!("{}.{}", &block_name, quote_ident(&self.column.name)); let maybe_enum = self.column.type_.as_ref().and_then(|t| match t.details { Some(TypeDetails::Enum(ref enum_)) => Some(enum_), @@ -1627,7 +1643,7 @@ impl ColumnBuilder { } impl NodeIdBuilder { - pub fn to_sql(&self, block_name: &str) -> Result { + pub fn to_sql(&self, block_name: &str) -> GraphQLResult { let column_selects: Vec = self .columns .iter() @@ -1647,7 +1663,7 @@ impl FunctionBuilder { &self, block_name: &str, param_context: &mut ParamContext, - ) -> Result { + ) -> GraphQLResult { let schema_name = quote_ident(&self.function.schema_name); let function_name = quote_ident(&self.function.name);