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 pub arguments: Vec<(String, String, bool)>,
23}
24
25impl GraphQLField {
26 pub fn from_column(column: &Column) -> Self {
28 let graphql_type = pg_type_to_graphql(&column.nominal_type);
29 let nullable = column.nullable && !column.is_pk;
30
31 Self {
32 name: column.name.clone(),
33 description: column.description.clone(),
34 graphql_type,
35 nullable,
36 is_pk: column.is_pk,
37 arguments: Vec::new(),
38 }
39 }
40
41 pub fn type_string(&self) -> String {
43 let base = format!("{}", self.graphql_type);
44 if self.nullable {
45 base
46 } else {
47 format!("{}!", base)
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct TableObjectType {
55 pub table: Table,
57 pub name: String,
59 pub fields: Vec<GraphQLField>,
64 pub writable_fields: Vec<GraphQLField>,
71 pub description: Option<String>,
75}
76
77impl TableObjectType {
78 pub fn from_table(table: &Table) -> Self {
84 Self::from_table_named(table, &table.name, &crate::names::NameOverrides::default())
85 }
86
87 fn computed_field(
95 name: &str,
96 computed: &postrust_core::schema_cache::ComputedColumn,
97 ) -> GraphQLField {
98 GraphQLField {
99 name: name.to_string(),
100 description: computed.description.clone().or_else(|| {
104 Some(format!(
105 "A computed field, executes function \"{}\"",
106 computed.function.name
107 ))
108 }),
109 graphql_type: pg_type_to_graphql(&computed.data_type),
110 nullable: true,
111 is_pk: false,
112 arguments: Vec::new(),
113 }
114 }
115
116 pub fn from_table_named(
121 table: &Table,
122 base_name: &str,
123 names: &crate::names::NameOverrides,
124 ) -> Self {
125 let name = base_name.to_string();
131 let mut fields: Vec<GraphQLField> = table
136 .columns
137 .values()
138 .map(|column| {
139 let mut field = GraphQLField::from_column(column);
140 if let Some(given) = names.column(&table.schema, &table.name, &column.name) {
144 field.name = given.to_string();
145 }
146 if let Some(comment) =
149 names.column_comment(&table.schema, &table.name, &column.name)
150 {
151 field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
152 }
153 field
154 })
155 .collect();
156 let mut computed: Vec<(&String, &postrust_core::schema_cache::ComputedColumn)> =
157 table.computed_columns.iter().collect();
158 computed.sort_by_key(|(name, _)| (*name).clone());
159 for (function, definition) in computed {
160 let exposed = names
163 .computed_field(&table.schema, &table.name, function)
164 .unwrap_or(function);
165 if fields.iter().any(|f| f.name == exposed) {
166 continue;
167 }
168 let mut field = Self::computed_field(exposed, definition);
169 if let Some(comment) = names.computed_comment(&table.schema, &table.name, function) {
170 field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
171 }
172 fields.push(field);
173 }
174
175 let description = names
176 .table_comment(&table.schema, &table.name)
177 .map(str::to_string)
178 .or_else(|| table.description.clone());
179
180 Self {
181 table: table.clone(),
182 name,
183 writable_fields: fields.clone(),
186 fields,
187 description,
188 }
189 }
190
191 pub fn name(&self) -> &str {
193 &self.name
194 }
195
196 pub fn description(&self) -> Option<&str> {
198 self.description.as_deref()
199 }
200
201 pub fn table_name(&self) -> &str {
204 &self.table.name
205 }
206
207 pub fn fields(&self) -> &[GraphQLField] {
209 &self.fields
210 }
211
212 pub fn get_field(&self, name: &str) -> Option<&GraphQLField> {
214 self.fields.iter().find(|f| f.name == name)
215 }
216
217 pub fn has_field(&self, name: &str) -> bool {
219 self.get_field(name).is_some()
220 }
221
222 pub fn pk_fields(&self) -> Vec<&GraphQLField> {
224 self.fields.iter().filter(|f| f.is_pk).collect()
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use indexmap::IndexMap;
232 use pretty_assertions::assert_eq;
233
234 fn create_test_table() -> Table {
235 let mut columns = IndexMap::new();
236 columns.insert(
237 "id".into(),
238 Column {
239 name: "id".into(),
240 description: Some("Primary key".into()),
241 nullable: false,
242 data_type: "integer".into(),
243 nominal_type: "int4".into(),
244 max_len: None,
245 default: Some("nextval('users_id_seq')".into()),
246 enum_values: vec![],
247 is_pk: true,
248 position: 1,
249 always_generated: false,
250 domain_type: None,
251 },
252 );
253 columns.insert(
254 "name".into(),
255 Column {
256 name: "name".into(),
257 description: Some("User name".into()),
258 nullable: false,
259 data_type: "text".into(),
260 nominal_type: "text".into(),
261 max_len: None,
262 default: None,
263 enum_values: vec![],
264 is_pk: false,
265 position: 2,
266 always_generated: false,
267 domain_type: None,
268 },
269 );
270 columns.insert(
271 "email".into(),
272 Column {
273 name: "email".into(),
274 description: None,
275 nullable: true,
276 data_type: "text".into(),
277 nominal_type: "text".into(),
278 max_len: None,
279 default: None,
280 enum_values: vec![],
281 is_pk: false,
282 position: 3,
283 always_generated: false,
284 domain_type: None,
285 },
286 );
287 columns.insert(
288 "metadata".into(),
289 Column {
290 name: "metadata".into(),
291 description: Some("JSON metadata".into()),
292 nullable: true,
293 data_type: "jsonb".into(),
294 nominal_type: "jsonb".into(),
295 max_len: None,
296 default: None,
297 enum_values: vec![],
298 is_pk: false,
299 position: 4,
300 always_generated: false,
301 domain_type: None,
302 },
303 );
304
305 Table {
306 schema: "public".into(),
307 name: "users".into(),
308 description: Some("User accounts".into()),
309 is_view: false,
310 insertable: true,
311 updatable: true,
312 deletable: true,
313 pk_cols: vec!["id".into()],
314 unique_constraints: Vec::new(),
315 columns,
316 computed_columns: Default::default(),
317 is_partitioned: false,
318 }
319 }
320
321 #[test]
322 fn test_table_to_graphql_object_name() {
323 let table = create_test_table();
324 let obj = TableObjectType::from_table(&table);
325
326 assert_eq!(obj.name(), "users"); }
328
329 #[test]
330 fn test_table_to_graphql_object_description() {
331 let table = create_test_table();
332 let obj = TableObjectType::from_table(&table);
333
334 assert_eq!(obj.description(), Some("User accounts"));
335 }
336
337 #[test]
338 fn test_table_to_graphql_object_fields() {
339 let table = create_test_table();
340 let obj = TableObjectType::from_table(&table);
341 let fields = obj.fields();
342
343 assert_eq!(fields.len(), 4);
344 assert!(obj.has_field("id"));
345 assert!(obj.has_field("name"));
346 assert!(obj.has_field("email"));
347 assert!(obj.has_field("metadata"));
348 }
349
350 #[test]
351 fn test_field_types() {
352 let table = create_test_table();
353 let obj = TableObjectType::from_table(&table);
354
355 let id_field = obj.get_field("id").unwrap();
356 assert_eq!(id_field.graphql_type, GraphQLType::Int);
357
358 let name_field = obj.get_field("name").unwrap();
359 assert_eq!(name_field.graphql_type, GraphQLType::String);
360
361 let metadata_field = obj.get_field("metadata").unwrap();
362 assert_eq!(metadata_field.graphql_type, GraphQLType::Json);
363 }
364
365 #[test]
366 fn test_field_nullability() {
367 let table = create_test_table();
368 let obj = TableObjectType::from_table(&table);
369
370 let id_field = obj.get_field("id").unwrap();
371 assert!(!id_field.nullable); let name_field = obj.get_field("name").unwrap();
374 assert!(!name_field.nullable); let email_field = obj.get_field("email").unwrap();
377 assert!(email_field.nullable); }
379
380 #[test]
381 fn test_field_descriptions() {
382 let table = create_test_table();
383 let obj = TableObjectType::from_table(&table);
384
385 let id_field = obj.get_field("id").unwrap();
386 assert_eq!(id_field.description, Some("Primary key".into()));
387
388 let email_field = obj.get_field("email").unwrap();
389 assert_eq!(email_field.description, None);
390 }
391
392 #[test]
393 fn test_field_type_string() {
394 let table = create_test_table();
395 let obj = TableObjectType::from_table(&table);
396
397 let id_field = obj.get_field("id").unwrap();
398 assert_eq!(id_field.type_string(), "Int!"); let email_field = obj.get_field("email").unwrap();
401 assert_eq!(email_field.type_string(), "String"); }
403
404 #[test]
405 fn test_pk_fields() {
406 let table = create_test_table();
407 let obj = TableObjectType::from_table(&table);
408
409 let pk_fields = obj.pk_fields();
410 assert_eq!(pk_fields.len(), 1);
411 assert_eq!(pk_fields[0].name, "id");
412 }
413
414 #[test]
415 fn test_table_with_underscore_name() {
416 let mut table = create_test_table();
417 table.name = "user_accounts".into();
418
419 let obj = TableObjectType::from_table(&table);
420 assert_eq!(obj.name(), "user_accounts");
421 }
422}