1use crate::types::{pg_type_to_graphql, GraphQLType};
4use postrust_core::schema_cache::{Column, Table};
5
6#[derive(Debug, Clone)]
8pub struct GraphQLField {
9 pub name: String,
11 pub description: Option<String>,
13 pub graphql_type: GraphQLType,
15 pub nullable: bool,
17 pub is_pk: bool,
19}
20
21impl GraphQLField {
22 pub fn from_column(column: &Column) -> Self {
24 let graphql_type = pg_type_to_graphql(&column.nominal_type);
25 let nullable = column.nullable && !column.is_pk;
26
27 Self {
28 name: column.name.clone(),
29 description: column.description.clone(),
30 graphql_type,
31 nullable,
32 is_pk: column.is_pk,
33 }
34 }
35
36 pub fn type_string(&self) -> String {
38 let base = format!("{}", self.graphql_type);
39 if self.nullable {
40 base
41 } else {
42 format!("{}!", base)
43 }
44 }
45}
46
47#[derive(Debug, Clone)]
49pub struct TableObjectType {
50 pub table: Table,
52 pub name: String,
54 pub fields: Vec<GraphQLField>,
56}
57
58impl TableObjectType {
59 pub fn from_table(table: &Table) -> Self {
61 let name = to_pascal_case(&table.name);
62 let fields = table
63 .columns
64 .values()
65 .map(GraphQLField::from_column)
66 .collect();
67
68 Self {
69 table: table.clone(),
70 name,
71 fields,
72 }
73 }
74
75 pub fn name(&self) -> &str {
77 &self.name
78 }
79
80 pub fn description(&self) -> Option<&str> {
82 self.table.description.as_deref()
83 }
84
85 pub fn fields(&self) -> &[GraphQLField] {
87 &self.fields
88 }
89
90 pub fn get_field(&self, name: &str) -> Option<&GraphQLField> {
92 self.fields.iter().find(|f| f.name == name)
93 }
94
95 pub fn has_field(&self, name: &str) -> bool {
97 self.get_field(name).is_some()
98 }
99
100 pub fn pk_fields(&self) -> Vec<&GraphQLField> {
102 self.fields.iter().filter(|f| f.is_pk).collect()
103 }
104}
105
106pub fn to_pascal_case(s: &str) -> String {
108 s.split('_')
109 .map(|word| {
110 let mut chars = word.chars();
111 match chars.next() {
112 Some(first) => {
113 first.to_uppercase().collect::<String>() + chars.as_str()
114 }
115 None => String::new(),
116 }
117 })
118 .collect()
119}
120
121pub fn to_camel_case(s: &str) -> String {
123 let pascal = to_pascal_case(s);
124 let mut chars = pascal.chars();
125 match chars.next() {
126 Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
127 None => String::new(),
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use indexmap::IndexMap;
135 use pretty_assertions::assert_eq;
136
137 fn create_test_table() -> Table {
138 let mut columns = IndexMap::new();
139 columns.insert(
140 "id".into(),
141 Column {
142 name: "id".into(),
143 description: Some("Primary key".into()),
144 nullable: false,
145 data_type: "integer".into(),
146 nominal_type: "int4".into(),
147 max_len: None,
148 default: Some("nextval('users_id_seq')".into()),
149 enum_values: vec![],
150 is_pk: true,
151 position: 1,
152 },
153 );
154 columns.insert(
155 "name".into(),
156 Column {
157 name: "name".into(),
158 description: Some("User name".into()),
159 nullable: false,
160 data_type: "text".into(),
161 nominal_type: "text".into(),
162 max_len: None,
163 default: None,
164 enum_values: vec![],
165 is_pk: false,
166 position: 2,
167 },
168 );
169 columns.insert(
170 "email".into(),
171 Column {
172 name: "email".into(),
173 description: None,
174 nullable: true,
175 data_type: "text".into(),
176 nominal_type: "text".into(),
177 max_len: None,
178 default: None,
179 enum_values: vec![],
180 is_pk: false,
181 position: 3,
182 },
183 );
184 columns.insert(
185 "metadata".into(),
186 Column {
187 name: "metadata".into(),
188 description: Some("JSON metadata".into()),
189 nullable: true,
190 data_type: "jsonb".into(),
191 nominal_type: "jsonb".into(),
192 max_len: None,
193 default: None,
194 enum_values: vec![],
195 is_pk: false,
196 position: 4,
197 },
198 );
199
200 Table {
201 schema: "public".into(),
202 name: "users".into(),
203 description: Some("User accounts".into()),
204 is_view: false,
205 insertable: true,
206 updatable: true,
207 deletable: true,
208 pk_cols: vec!["id".into()],
209 columns,
210 }
211 }
212
213 #[test]
214 fn test_to_pascal_case() {
215 assert_eq!(to_pascal_case("users"), "Users");
216 assert_eq!(to_pascal_case("user_accounts"), "UserAccounts");
217 assert_eq!(to_pascal_case("my_table_name"), "MyTableName");
218 assert_eq!(to_pascal_case(""), "");
219 }
220
221 #[test]
222 fn test_to_camel_case() {
223 assert_eq!(to_camel_case("user_id"), "userId");
224 assert_eq!(to_camel_case("my_field"), "myField");
225 assert_eq!(to_camel_case("name"), "name");
226 }
227
228 #[test]
229 fn test_table_to_graphql_object_name() {
230 let table = create_test_table();
231 let obj = TableObjectType::from_table(&table);
232
233 assert_eq!(obj.name(), "Users"); }
235
236 #[test]
237 fn test_table_to_graphql_object_description() {
238 let table = create_test_table();
239 let obj = TableObjectType::from_table(&table);
240
241 assert_eq!(obj.description(), Some("User accounts"));
242 }
243
244 #[test]
245 fn test_table_to_graphql_object_fields() {
246 let table = create_test_table();
247 let obj = TableObjectType::from_table(&table);
248 let fields = obj.fields();
249
250 assert_eq!(fields.len(), 4);
251 assert!(obj.has_field("id"));
252 assert!(obj.has_field("name"));
253 assert!(obj.has_field("email"));
254 assert!(obj.has_field("metadata"));
255 }
256
257 #[test]
258 fn test_field_types() {
259 let table = create_test_table();
260 let obj = TableObjectType::from_table(&table);
261
262 let id_field = obj.get_field("id").unwrap();
263 assert_eq!(id_field.graphql_type, GraphQLType::Int);
264
265 let name_field = obj.get_field("name").unwrap();
266 assert_eq!(name_field.graphql_type, GraphQLType::String);
267
268 let metadata_field = obj.get_field("metadata").unwrap();
269 assert_eq!(metadata_field.graphql_type, GraphQLType::Json);
270 }
271
272 #[test]
273 fn test_field_nullability() {
274 let table = create_test_table();
275 let obj = TableObjectType::from_table(&table);
276
277 let id_field = obj.get_field("id").unwrap();
278 assert!(!id_field.nullable); let name_field = obj.get_field("name").unwrap();
281 assert!(!name_field.nullable); let email_field = obj.get_field("email").unwrap();
284 assert!(email_field.nullable); }
286
287 #[test]
288 fn test_field_descriptions() {
289 let table = create_test_table();
290 let obj = TableObjectType::from_table(&table);
291
292 let id_field = obj.get_field("id").unwrap();
293 assert_eq!(id_field.description, Some("Primary key".into()));
294
295 let email_field = obj.get_field("email").unwrap();
296 assert_eq!(email_field.description, None);
297 }
298
299 #[test]
300 fn test_field_type_string() {
301 let table = create_test_table();
302 let obj = TableObjectType::from_table(&table);
303
304 let id_field = obj.get_field("id").unwrap();
305 assert_eq!(id_field.type_string(), "Int!"); let email_field = obj.get_field("email").unwrap();
308 assert_eq!(email_field.type_string(), "String"); }
310
311 #[test]
312 fn test_pk_fields() {
313 let table = create_test_table();
314 let obj = TableObjectType::from_table(&table);
315
316 let pk_fields = obj.pk_fields();
317 assert_eq!(pk_fields.len(), 1);
318 assert_eq!(pk_fields[0].name, "id");
319 }
320
321 #[test]
322 fn test_table_with_underscore_name() {
323 let mut table = create_test_table();
324 table.name = "user_accounts".into();
325
326 let obj = TableObjectType::from_table(&table);
327 assert_eq!(obj.name(), "UserAccounts");
328 }
329}