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) => first.to_uppercase().collect::<String>() + chars.as_str(),
113 None => String::new(),
114 }
115 })
116 .collect()
117}
118
119pub fn to_camel_case(s: &str) -> String {
121 let pascal = to_pascal_case(s);
122 let mut chars = pascal.chars();
123 match chars.next() {
124 Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
125 None => String::new(),
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use indexmap::IndexMap;
133 use pretty_assertions::assert_eq;
134
135 fn create_test_table() -> Table {
136 let mut columns = IndexMap::new();
137 columns.insert(
138 "id".into(),
139 Column {
140 name: "id".into(),
141 description: Some("Primary key".into()),
142 nullable: false,
143 data_type: "integer".into(),
144 nominal_type: "int4".into(),
145 max_len: None,
146 default: Some("nextval('users_id_seq')".into()),
147 enum_values: vec![],
148 is_pk: true,
149 position: 1,
150 },
151 );
152 columns.insert(
153 "name".into(),
154 Column {
155 name: "name".into(),
156 description: Some("User name".into()),
157 nullable: false,
158 data_type: "text".into(),
159 nominal_type: "text".into(),
160 max_len: None,
161 default: None,
162 enum_values: vec![],
163 is_pk: false,
164 position: 2,
165 },
166 );
167 columns.insert(
168 "email".into(),
169 Column {
170 name: "email".into(),
171 description: None,
172 nullable: true,
173 data_type: "text".into(),
174 nominal_type: "text".into(),
175 max_len: None,
176 default: None,
177 enum_values: vec![],
178 is_pk: false,
179 position: 3,
180 },
181 );
182 columns.insert(
183 "metadata".into(),
184 Column {
185 name: "metadata".into(),
186 description: Some("JSON metadata".into()),
187 nullable: true,
188 data_type: "jsonb".into(),
189 nominal_type: "jsonb".into(),
190 max_len: None,
191 default: None,
192 enum_values: vec![],
193 is_pk: false,
194 position: 4,
195 },
196 );
197
198 Table {
199 schema: "public".into(),
200 name: "users".into(),
201 description: Some("User accounts".into()),
202 is_view: false,
203 insertable: true,
204 updatable: true,
205 deletable: true,
206 pk_cols: vec!["id".into()],
207 columns,
208 }
209 }
210
211 #[test]
212 fn test_to_pascal_case() {
213 assert_eq!(to_pascal_case("users"), "Users");
214 assert_eq!(to_pascal_case("user_accounts"), "UserAccounts");
215 assert_eq!(to_pascal_case("my_table_name"), "MyTableName");
216 assert_eq!(to_pascal_case(""), "");
217 }
218
219 #[test]
220 fn test_to_camel_case() {
221 assert_eq!(to_camel_case("user_id"), "userId");
222 assert_eq!(to_camel_case("my_field"), "myField");
223 assert_eq!(to_camel_case("name"), "name");
224 }
225
226 #[test]
227 fn test_table_to_graphql_object_name() {
228 let table = create_test_table();
229 let obj = TableObjectType::from_table(&table);
230
231 assert_eq!(obj.name(), "Users"); }
233
234 #[test]
235 fn test_table_to_graphql_object_description() {
236 let table = create_test_table();
237 let obj = TableObjectType::from_table(&table);
238
239 assert_eq!(obj.description(), Some("User accounts"));
240 }
241
242 #[test]
243 fn test_table_to_graphql_object_fields() {
244 let table = create_test_table();
245 let obj = TableObjectType::from_table(&table);
246 let fields = obj.fields();
247
248 assert_eq!(fields.len(), 4);
249 assert!(obj.has_field("id"));
250 assert!(obj.has_field("name"));
251 assert!(obj.has_field("email"));
252 assert!(obj.has_field("metadata"));
253 }
254
255 #[test]
256 fn test_field_types() {
257 let table = create_test_table();
258 let obj = TableObjectType::from_table(&table);
259
260 let id_field = obj.get_field("id").unwrap();
261 assert_eq!(id_field.graphql_type, GraphQLType::Int);
262
263 let name_field = obj.get_field("name").unwrap();
264 assert_eq!(name_field.graphql_type, GraphQLType::String);
265
266 let metadata_field = obj.get_field("metadata").unwrap();
267 assert_eq!(metadata_field.graphql_type, GraphQLType::Json);
268 }
269
270 #[test]
271 fn test_field_nullability() {
272 let table = create_test_table();
273 let obj = TableObjectType::from_table(&table);
274
275 let id_field = obj.get_field("id").unwrap();
276 assert!(!id_field.nullable); let name_field = obj.get_field("name").unwrap();
279 assert!(!name_field.nullable); let email_field = obj.get_field("email").unwrap();
282 assert!(email_field.nullable); }
284
285 #[test]
286 fn test_field_descriptions() {
287 let table = create_test_table();
288 let obj = TableObjectType::from_table(&table);
289
290 let id_field = obj.get_field("id").unwrap();
291 assert_eq!(id_field.description, Some("Primary key".into()));
292
293 let email_field = obj.get_field("email").unwrap();
294 assert_eq!(email_field.description, None);
295 }
296
297 #[test]
298 fn test_field_type_string() {
299 let table = create_test_table();
300 let obj = TableObjectType::from_table(&table);
301
302 let id_field = obj.get_field("id").unwrap();
303 assert_eq!(id_field.type_string(), "Int!"); let email_field = obj.get_field("email").unwrap();
306 assert_eq!(email_field.type_string(), "String"); }
308
309 #[test]
310 fn test_pk_fields() {
311 let table = create_test_table();
312 let obj = TableObjectType::from_table(&table);
313
314 let pk_fields = obj.pk_fields();
315 assert_eq!(pk_fields.len(), 1);
316 assert_eq!(pk_fields[0].name, "id");
317 }
318
319 #[test]
320 fn test_table_with_underscore_name() {
321 let mut table = create_test_table();
322 table.name = "user_accounts".into();
323
324 let obj = TableObjectType::from_table(&table);
325 assert_eq!(obj.name(), "UserAccounts");
326 }
327}