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(
117 table: &Table,
118 base_name: &str,
119 names: &crate::names::NameOverrides,
120 ) -> Self {
121 let name = base_name.to_string();
127 let mut fields: Vec<GraphQLField> = table
132 .columns
133 .values()
134 .map(|column| {
135 let mut field = GraphQLField::from_column(column);
136 if let Some(given) = names.column(&table.schema, &table.name, &column.name) {
140 field.name = given.to_string();
141 }
142 if let Some(comment) =
145 names.column_comment(&table.schema, &table.name, &column.name)
146 {
147 field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
148 }
149 field
150 })
151 .collect();
152 let mut computed: Vec<(&String, &postrust_core::schema_cache::ComputedColumn)> =
153 table.computed_columns.iter().collect();
154 computed.sort_by_key(|(name, _)| (*name).clone());
155 for (function, definition) in computed {
156 let exposed = names
159 .computed_field(&table.schema, &table.name, function)
160 .unwrap_or(function);
161 if fields.iter().any(|f| f.name == exposed) {
162 continue;
163 }
164 let mut field = Self::computed_field(exposed, definition);
165 if let Some(comment) = names.computed_comment(&table.schema, &table.name, function) {
166 field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
167 }
168 fields.push(field);
169 }
170
171 let description = names
172 .table_comment(&table.schema, &table.name)
173 .map(str::to_string)
174 .or_else(|| table.description.clone());
175
176 Self {
177 table: table.clone(),
178 name,
179 writable_fields: fields.clone(),
182 fields,
183 description,
184 }
185 }
186
187 pub fn name(&self) -> &str {
189 &self.name
190 }
191
192 pub fn description(&self) -> Option<&str> {
194 self.description.as_deref()
195 }
196
197 pub fn table_name(&self) -> &str {
200 &self.table.name
201 }
202
203 pub fn fields(&self) -> &[GraphQLField] {
205 &self.fields
206 }
207
208 pub fn get_field(&self, name: &str) -> Option<&GraphQLField> {
210 self.fields.iter().find(|f| f.name == name)
211 }
212
213 pub fn has_field(&self, name: &str) -> bool {
215 self.get_field(name).is_some()
216 }
217
218 pub fn pk_fields(&self) -> Vec<&GraphQLField> {
220 self.fields.iter().filter(|f| f.is_pk).collect()
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use indexmap::IndexMap;
228 use pretty_assertions::assert_eq;
229
230 fn create_test_table() -> Table {
231 let mut columns = IndexMap::new();
232 columns.insert(
233 "id".into(),
234 Column {
235 name: "id".into(),
236 description: Some("Primary key".into()),
237 nullable: false,
238 data_type: "integer".into(),
239 nominal_type: "int4".into(),
240 max_len: None,
241 default: Some("nextval('users_id_seq')".into()),
242 enum_values: vec![],
243 is_pk: true,
244 position: 1,
245 always_generated: false,
246 domain_type: None,
247 },
248 );
249 columns.insert(
250 "name".into(),
251 Column {
252 name: "name".into(),
253 description: Some("User name".into()),
254 nullable: false,
255 data_type: "text".into(),
256 nominal_type: "text".into(),
257 max_len: None,
258 default: None,
259 enum_values: vec![],
260 is_pk: false,
261 position: 2,
262 always_generated: false,
263 domain_type: None,
264 },
265 );
266 columns.insert(
267 "email".into(),
268 Column {
269 name: "email".into(),
270 description: None,
271 nullable: true,
272 data_type: "text".into(),
273 nominal_type: "text".into(),
274 max_len: None,
275 default: None,
276 enum_values: vec![],
277 is_pk: false,
278 position: 3,
279 always_generated: false,
280 domain_type: None,
281 },
282 );
283 columns.insert(
284 "metadata".into(),
285 Column {
286 name: "metadata".into(),
287 description: Some("JSON metadata".into()),
288 nullable: true,
289 data_type: "jsonb".into(),
290 nominal_type: "jsonb".into(),
291 max_len: None,
292 default: None,
293 enum_values: vec![],
294 is_pk: false,
295 position: 4,
296 always_generated: false,
297 domain_type: None,
298 },
299 );
300
301 Table {
302 schema: "public".into(),
303 name: "users".into(),
304 description: Some("User accounts".into()),
305 is_view: false,
306 insertable: true,
307 updatable: true,
308 deletable: true,
309 pk_cols: vec!["id".into()],
310 unique_constraints: Vec::new(),
311 columns,
312 computed_columns: Default::default(),
313 is_partitioned: false,
314 }
315 }
316
317 #[test]
318 fn test_table_to_graphql_object_name() {
319 let table = create_test_table();
320 let obj = TableObjectType::from_table(&table);
321
322 assert_eq!(obj.name(), "users"); }
324
325 #[test]
326 fn test_table_to_graphql_object_description() {
327 let table = create_test_table();
328 let obj = TableObjectType::from_table(&table);
329
330 assert_eq!(obj.description(), Some("User accounts"));
331 }
332
333 #[test]
334 fn test_table_to_graphql_object_fields() {
335 let table = create_test_table();
336 let obj = TableObjectType::from_table(&table);
337 let fields = obj.fields();
338
339 assert_eq!(fields.len(), 4);
340 assert!(obj.has_field("id"));
341 assert!(obj.has_field("name"));
342 assert!(obj.has_field("email"));
343 assert!(obj.has_field("metadata"));
344 }
345
346 #[test]
347 fn test_field_types() {
348 let table = create_test_table();
349 let obj = TableObjectType::from_table(&table);
350
351 let id_field = obj.get_field("id").unwrap();
352 assert_eq!(id_field.graphql_type, GraphQLType::Int);
353
354 let name_field = obj.get_field("name").unwrap();
355 assert_eq!(name_field.graphql_type, GraphQLType::String);
356
357 let metadata_field = obj.get_field("metadata").unwrap();
358 assert_eq!(metadata_field.graphql_type, GraphQLType::Json);
359 }
360
361 #[test]
362 fn test_field_nullability() {
363 let table = create_test_table();
364 let obj = TableObjectType::from_table(&table);
365
366 let id_field = obj.get_field("id").unwrap();
367 assert!(!id_field.nullable); let name_field = obj.get_field("name").unwrap();
370 assert!(!name_field.nullable); let email_field = obj.get_field("email").unwrap();
373 assert!(email_field.nullable); }
375
376 #[test]
377 fn test_field_descriptions() {
378 let table = create_test_table();
379 let obj = TableObjectType::from_table(&table);
380
381 let id_field = obj.get_field("id").unwrap();
382 assert_eq!(id_field.description, Some("Primary key".into()));
383
384 let email_field = obj.get_field("email").unwrap();
385 assert_eq!(email_field.description, None);
386 }
387
388 #[test]
389 fn test_field_type_string() {
390 let table = create_test_table();
391 let obj = TableObjectType::from_table(&table);
392
393 let id_field = obj.get_field("id").unwrap();
394 assert_eq!(id_field.type_string(), "Int!"); let email_field = obj.get_field("email").unwrap();
397 assert_eq!(email_field.type_string(), "String"); }
399
400 #[test]
401 fn test_pk_fields() {
402 let table = create_test_table();
403 let obj = TableObjectType::from_table(&table);
404
405 let pk_fields = obj.pk_fields();
406 assert_eq!(pk_fields.len(), 1);
407 assert_eq!(pk_fields[0].name, "id");
408 }
409
410 #[test]
411 fn test_table_with_underscore_name() {
412 let mut table = create_test_table();
413 table.name = "user_accounts".into();
414
415 let obj = TableObjectType::from_table(&table);
416 assert_eq!(obj.name(), "user_accounts");
417 }
418}