postrust_graphql/input/
order_by.rs1use crate::schema::object::TableObjectType;
25use crate::schema::relationship::RelationshipField;
26use async_graphql::dynamic::{Enum, EnumItem, InputObject, InputValue, TypeRef};
27use std::collections::{HashMap, HashSet};
28
29pub const DIRECTION_ENUM: &str = "order_by";
31
32const DIRECTIONS: &[(&str, &str)] = &[
34 ("asc", "ASC"),
35 ("asc_nulls_first", "ASC NULLS FIRST"),
36 ("asc_nulls_last", "ASC NULLS LAST"),
37 ("desc", "DESC"),
38 ("desc_nulls_first", "DESC NULLS FIRST"),
39 ("desc_nulls_last", "DESC NULLS LAST"),
40];
41
42pub fn order_by_type_name(base_name: &str) -> String {
44 format!("{}_order_by", base_name)
45}
46
47pub fn select_column_type_name(base_name: &str) -> String {
49 format!("{}_select_column", base_name)
50}
51
52pub fn aggregate_order_by_type_name(base_name: &str) -> String {
54 format!("{}_aggregate_order_by", base_name)
55}
56
57pub fn function_order_by_type_name(base_name: &str, function: &str) -> String {
59 format!("{}_{}_order_by", base_name, function)
60}
61
62pub fn direction_sql(name: &str) -> Option<&'static str> {
64 DIRECTIONS
65 .iter()
66 .find(|(enum_name, _)| *enum_name == name)
67 .map(|(_, sql)| *sql)
68}
69
70pub fn direction_enum() -> Enum {
72 let mut direction = Enum::new(DIRECTION_ENUM)
73 .description("Sort direction, with where nulls go. `asc` is `asc_nulls_last`.");
74 for (name, _) in DIRECTIONS {
75 direction = direction.item(EnumItem::new(*name));
76 }
77 direction
78}
79
80pub fn build_inputs(
82 object_types: &HashMap<String, TableObjectType>,
83 relationship_fields: &HashMap<String, Vec<RelationshipField>>,
84) -> (Vec<InputObject>, Vec<Enum>) {
85 let mut inputs = Vec::new();
86 let mut enums = vec![direction_enum()];
87
88 for (type_name, object) in object_types {
89 if object.fields.is_empty() {
92 continue;
93 }
94
95 let mut input = InputObject::new(order_by_type_name(type_name)).description(format!(
96 "Order rows of {} by a column, by a related row's column, or by an \
97 aggregate of its children.",
98 type_name
99 ));
100 let mut columns = Enum::new(select_column_type_name(type_name))
101 .description(format!("A column of {}.", type_name));
102
103 let mut taken: HashSet<String> = HashSet::new();
104 for field in &object.fields {
105 if !taken.insert(field.name.clone()) {
106 continue;
107 }
108 input = input.field(InputValue::new(&field.name, TypeRef::named(DIRECTION_ENUM)));
109 columns = columns.item(EnumItem::new(&field.name));
110 }
111
112 for relationship in relationship_fields.get(type_name).into_iter().flatten() {
118 if relationship.is_list {
119 let field = format!("{}_aggregate", relationship.name);
120 if taken.insert(field.clone()) {
121 input = input.field(InputValue::new(
122 &field,
123 TypeRef::named(aggregate_order_by_type_name(&relationship.target_type)),
124 ));
125 }
126 } else if taken.insert(relationship.name.clone()) {
127 input = input.field(InputValue::new(
128 &relationship.name,
129 TypeRef::named(order_by_type_name(&relationship.target_type)),
130 ));
131 }
132 }
133
134 inputs.push(input);
135 enums.push(columns);
136
137 let mut aggregate = InputObject::new(aggregate_order_by_type_name(type_name))
139 .description(format!("Order by an aggregate of {} rows.", type_name))
140 .field(InputValue::new("count", TypeRef::named(DIRECTION_ENUM)));
141
142 for (function, _, columns) in crate::schema::aggregate::functions_for(object) {
143 let function_type = function_order_by_type_name(type_name, function);
144 let mut per_column = InputObject::new(&function_type).description(format!(
145 "Order by the `{}` of a {} column.",
146 function, type_name
147 ));
148 for column in &columns {
149 per_column =
150 per_column.field(InputValue::new(column, TypeRef::named(DIRECTION_ENUM)));
151 }
152 inputs.push(per_column);
153 aggregate = aggregate.field(InputValue::new(function, TypeRef::named(&function_type)));
154 }
155 inputs.push(aggregate);
156 }
157
158 (inputs, enums)
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use pretty_assertions::assert_eq;
165
166 #[test]
167 fn a_tables_ordering_is_named_after_it() {
168 assert_eq!(order_by_type_name("author"), "author_order_by");
169 assert_eq!(select_column_type_name("author"), "author_select_column");
170 }
171
172 #[test]
173 fn plain_asc_places_nulls_where_postgres_does() {
174 assert_eq!(direction_sql("asc"), Some("ASC"));
178 assert_eq!(direction_sql("asc_nulls_last"), Some("ASC NULLS LAST"));
179 assert_eq!(direction_sql("asc_nulls_first"), Some("ASC NULLS FIRST"));
180 }
181
182 #[test]
183 fn a_direction_that_is_not_one_has_no_sql() {
184 assert_eq!(direction_sql("sideways"), None);
185 assert_eq!(direction_sql("ASC"), None);
186 }
187}