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 {
65 Self::from_table_named(table, &table.name)
66 }
67
68 pub fn from_table_named(table: &Table, base_name: &str) -> Self {
70 let name = to_pascal_case(base_name);
71 let fields = table
72 .columns
73 .values()
74 .map(GraphQLField::from_column)
75 .collect();
76
77 Self {
78 table: table.clone(),
79 name,
80 fields,
81 }
82 }
83
84 pub fn name(&self) -> &str {
86 &self.name
87 }
88
89 pub fn description(&self) -> Option<&str> {
91 self.table.description.as_deref()
92 }
93
94 pub fn fields(&self) -> &[GraphQLField] {
96 &self.fields
97 }
98
99 pub fn get_field(&self, name: &str) -> Option<&GraphQLField> {
101 self.fields.iter().find(|f| f.name == name)
102 }
103
104 pub fn has_field(&self, name: &str) -> bool {
106 self.get_field(name).is_some()
107 }
108
109 pub fn pk_fields(&self) -> Vec<&GraphQLField> {
111 self.fields.iter().filter(|f| f.is_pk).collect()
112 }
113}
114
115pub fn to_pascal_case(s: &str) -> String {
117 s.split('_')
118 .map(|word| {
119 let mut chars = word.chars();
120 match chars.next() {
121 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
122 None => String::new(),
123 }
124 })
125 .collect()
126}
127
128pub fn to_camel_case(s: &str) -> String {
130 let pascal = to_pascal_case(s);
131 let mut chars = pascal.chars();
132 match chars.next() {
133 Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
134 None => String::new(),
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use indexmap::IndexMap;
142 use pretty_assertions::assert_eq;
143
144 fn create_test_table() -> Table {
145 let mut columns = IndexMap::new();
146 columns.insert(
147 "id".into(),
148 Column {
149 name: "id".into(),
150 description: Some("Primary key".into()),
151 nullable: false,
152 data_type: "integer".into(),
153 nominal_type: "int4".into(),
154 max_len: None,
155 default: Some("nextval('users_id_seq')".into()),
156 enum_values: vec![],
157 is_pk: true,
158 position: 1,
159 },
160 );
161 columns.insert(
162 "name".into(),
163 Column {
164 name: "name".into(),
165 description: Some("User name".into()),
166 nullable: false,
167 data_type: "text".into(),
168 nominal_type: "text".into(),
169 max_len: None,
170 default: None,
171 enum_values: vec![],
172 is_pk: false,
173 position: 2,
174 },
175 );
176 columns.insert(
177 "email".into(),
178 Column {
179 name: "email".into(),
180 description: None,
181 nullable: true,
182 data_type: "text".into(),
183 nominal_type: "text".into(),
184 max_len: None,
185 default: None,
186 enum_values: vec![],
187 is_pk: false,
188 position: 3,
189 },
190 );
191 columns.insert(
192 "metadata".into(),
193 Column {
194 name: "metadata".into(),
195 description: Some("JSON metadata".into()),
196 nullable: true,
197 data_type: "jsonb".into(),
198 nominal_type: "jsonb".into(),
199 max_len: None,
200 default: None,
201 enum_values: vec![],
202 is_pk: false,
203 position: 4,
204 },
205 );
206
207 Table {
208 schema: "public".into(),
209 name: "users".into(),
210 description: Some("User accounts".into()),
211 is_view: false,
212 insertable: true,
213 updatable: true,
214 deletable: true,
215 pk_cols: vec!["id".into()],
216 columns,
217 }
218 }
219
220 #[test]
221 fn test_to_pascal_case() {
222 assert_eq!(to_pascal_case("users"), "Users");
223 assert_eq!(to_pascal_case("user_accounts"), "UserAccounts");
224 assert_eq!(to_pascal_case("my_table_name"), "MyTableName");
225 assert_eq!(to_pascal_case(""), "");
226 }
227
228 #[test]
229 fn test_to_camel_case() {
230 assert_eq!(to_camel_case("user_id"), "userId");
231 assert_eq!(to_camel_case("my_field"), "myField");
232 assert_eq!(to_camel_case("name"), "name");
233 }
234
235 #[test]
236 fn test_table_to_graphql_object_name() {
237 let table = create_test_table();
238 let obj = TableObjectType::from_table(&table);
239
240 assert_eq!(obj.name(), "Users"); }
242
243 #[test]
244 fn test_table_to_graphql_object_description() {
245 let table = create_test_table();
246 let obj = TableObjectType::from_table(&table);
247
248 assert_eq!(obj.description(), Some("User accounts"));
249 }
250
251 #[test]
252 fn test_table_to_graphql_object_fields() {
253 let table = create_test_table();
254 let obj = TableObjectType::from_table(&table);
255 let fields = obj.fields();
256
257 assert_eq!(fields.len(), 4);
258 assert!(obj.has_field("id"));
259 assert!(obj.has_field("name"));
260 assert!(obj.has_field("email"));
261 assert!(obj.has_field("metadata"));
262 }
263
264 #[test]
265 fn test_field_types() {
266 let table = create_test_table();
267 let obj = TableObjectType::from_table(&table);
268
269 let id_field = obj.get_field("id").unwrap();
270 assert_eq!(id_field.graphql_type, GraphQLType::Int);
271
272 let name_field = obj.get_field("name").unwrap();
273 assert_eq!(name_field.graphql_type, GraphQLType::String);
274
275 let metadata_field = obj.get_field("metadata").unwrap();
276 assert_eq!(metadata_field.graphql_type, GraphQLType::Json);
277 }
278
279 #[test]
280 fn test_field_nullability() {
281 let table = create_test_table();
282 let obj = TableObjectType::from_table(&table);
283
284 let id_field = obj.get_field("id").unwrap();
285 assert!(!id_field.nullable); let name_field = obj.get_field("name").unwrap();
288 assert!(!name_field.nullable); let email_field = obj.get_field("email").unwrap();
291 assert!(email_field.nullable); }
293
294 #[test]
295 fn test_field_descriptions() {
296 let table = create_test_table();
297 let obj = TableObjectType::from_table(&table);
298
299 let id_field = obj.get_field("id").unwrap();
300 assert_eq!(id_field.description, Some("Primary key".into()));
301
302 let email_field = obj.get_field("email").unwrap();
303 assert_eq!(email_field.description, None);
304 }
305
306 #[test]
307 fn test_field_type_string() {
308 let table = create_test_table();
309 let obj = TableObjectType::from_table(&table);
310
311 let id_field = obj.get_field("id").unwrap();
312 assert_eq!(id_field.type_string(), "Int!"); let email_field = obj.get_field("email").unwrap();
315 assert_eq!(email_field.type_string(), "String"); }
317
318 #[test]
319 fn test_pk_fields() {
320 let table = create_test_table();
321 let obj = TableObjectType::from_table(&table);
322
323 let pk_fields = obj.pk_fields();
324 assert_eq!(pk_fields.len(), 1);
325 assert_eq!(pk_fields[0].name, "id");
326 }
327
328 #[test]
329 fn test_table_with_underscore_name() {
330 let mut table = create_test_table();
331 table.name = "user_accounts".into();
332
333 let obj = TableObjectType::from_table(&table);
334 assert_eq!(obj.name(), "UserAccounts");
335 }
336}