1use crate::types::{pg_type_to_graphql, GraphQLType};
4use postrust_core::schema_cache::{Column, Table};
5
6#[derive(Debug, Clone)]
8#[non_exhaustive]
9pub struct GraphQLField {
10 pub name: String,
12 pub description: Option<String>,
14 pub graphql_type: GraphQLType,
16 pub nullable: bool,
18 pub is_pk: bool,
20 pub arguments: Vec<(String, String, bool)>,
24}
25
26impl GraphQLField {
27 pub fn from_column(column: &Column) -> Self {
29 let graphql_type = pg_type_to_graphql(&column.nominal_type);
30 let nullable = column.nullable && !column.is_pk;
31
32 Self {
33 name: column.name.clone(),
34 description: column.description.clone(),
35 graphql_type,
36 nullable,
37 is_pk: column.is_pk,
38 arguments: Vec::new(),
39 }
40 }
41
42 pub fn type_string(&self) -> String {
44 let base = format!("{}", self.graphql_type);
45 if self.nullable {
46 base
47 } else {
48 format!("{}!", base)
49 }
50 }
51}
52
53#[derive(Debug, Clone)]
55#[non_exhaustive]
56pub struct TableObjectType {
57 pub table: Table,
59 pub name: String,
61 pub local_name: String,
63 pub fields: Vec<GraphQLField>,
68 pub writable_fields: Vec<GraphQLField>,
75 pub description: Option<String>,
79}
80
81impl TableObjectType {
82 pub fn from_table(table: &Table) -> Self {
88 Self::from_table_named(table, &table.name, &crate::names::NameOverrides::default())
89 }
90
91 fn computed_field(
99 name: &str,
100 computed: &postrust_core::schema_cache::ComputedColumn,
101 ) -> GraphQLField {
102 GraphQLField {
103 name: name.to_string(),
104 description: computed.description.clone().or_else(|| {
108 Some(format!(
109 "A computed field, executes function \"{}\"",
110 computed.function.name
111 ))
112 }),
113 graphql_type: pg_type_to_graphql(&computed.data_type),
114 nullable: true,
115 is_pk: false,
116 arguments: Vec::new(),
117 }
118 }
119
120 pub fn from_table_named(
125 table: &Table,
126 base_name: &str,
127 names: &crate::names::NameOverrides,
128 ) -> Self {
129 let name = base_name.to_string();
135 let mut fields: Vec<GraphQLField> = table
140 .columns
141 .values()
142 .map(|column| {
143 let mut field = GraphQLField::from_column(column);
144 if let Some(given) = names.column_type(&table.schema, &table.name, &column.name) {
148 field.graphql_type = given;
149 }
150 if let Some(given) = names.column(&table.schema, &table.name, &column.name) {
154 field.name = given.to_string();
155 }
156 if let Some(comment) =
159 names.column_comment(&table.schema, &table.name, &column.name)
160 {
161 field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
162 }
163 field
164 })
165 .collect();
166 let mut computed: Vec<(&String, &postrust_core::schema_cache::ComputedColumn)> =
167 table.computed_columns.iter().collect();
168 computed.sort_by_key(|(name, _)| (*name).clone());
169 for (function, definition) in computed {
170 let exposed = names
173 .computed_field(&table.schema, &table.name, function)
174 .unwrap_or(function);
175 if fields.iter().any(|f| f.name == exposed) {
176 continue;
177 }
178 let mut field = Self::computed_field(exposed, definition);
179 if let Some(comment) = names.computed_comment(&table.schema, &table.name, function) {
180 field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
181 }
182 fields.push(field);
183 }
184
185 let description = names
186 .table_comment(&table.schema, &table.name)
187 .map(str::to_string)
188 .or_else(|| table.description.clone());
189
190 Self {
191 table: table.clone(),
192 local_name: name.clone(),
193 name,
194 writable_fields: fields.clone(),
197 fields,
198 description,
199 }
200 }
201
202 pub fn name(&self) -> &str {
204 &self.name
205 }
206
207 pub fn description(&self) -> Option<&str> {
209 self.description.as_deref()
210 }
211
212 pub fn table_name(&self) -> &str {
215 &self.table.name
216 }
217
218 pub fn fields(&self) -> &[GraphQLField] {
220 &self.fields
221 }
222
223 pub fn get_field(&self, name: &str) -> Option<&GraphQLField> {
225 self.fields.iter().find(|f| f.name == name)
226 }
227
228 pub fn has_field(&self, name: &str) -> bool {
230 self.get_field(name).is_some()
231 }
232
233 pub fn pk_fields(&self) -> Vec<&GraphQLField> {
235 self.fields.iter().filter(|f| f.is_pk).collect()
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use indexmap::IndexMap;
243 use pretty_assertions::assert_eq;
244
245 fn create_test_table() -> Table {
246 let mut columns = IndexMap::new();
247 columns.insert(
248 "id".into(),
249 Column {
250 name: "id".into(),
251 description: Some("Primary key".into()),
252 nullable: false,
253 data_type: "integer".into(),
254 nominal_type: "int4".into(),
255 max_len: None,
256 default: Some("nextval('users_id_seq')".into()),
257 enum_values: vec![],
258 is_pk: true,
259 position: 1,
260 always_generated: false,
261 domain_type: None,
262 },
263 );
264 columns.insert(
265 "name".into(),
266 Column {
267 name: "name".into(),
268 description: Some("User name".into()),
269 nullable: false,
270 data_type: "text".into(),
271 nominal_type: "text".into(),
272 max_len: None,
273 default: None,
274 enum_values: vec![],
275 is_pk: false,
276 position: 2,
277 always_generated: false,
278 domain_type: None,
279 },
280 );
281 columns.insert(
282 "email".into(),
283 Column {
284 name: "email".into(),
285 description: None,
286 nullable: true,
287 data_type: "text".into(),
288 nominal_type: "text".into(),
289 max_len: None,
290 default: None,
291 enum_values: vec![],
292 is_pk: false,
293 position: 3,
294 always_generated: false,
295 domain_type: None,
296 },
297 );
298 columns.insert(
299 "metadata".into(),
300 Column {
301 name: "metadata".into(),
302 description: Some("JSON metadata".into()),
303 nullable: true,
304 data_type: "jsonb".into(),
305 nominal_type: "jsonb".into(),
306 max_len: None,
307 default: None,
308 enum_values: vec![],
309 is_pk: false,
310 position: 4,
311 always_generated: false,
312 domain_type: None,
313 },
314 );
315
316 Table {
317 schema: "public".into(),
318 name: "users".into(),
319 description: Some("User accounts".into()),
320 is_view: false,
321 insertable: true,
322 updatable: true,
323 deletable: true,
324 pk_cols: vec!["id".into()],
325 unique_constraints: Vec::new(),
326 columns,
327 computed_columns: Default::default(),
328 is_partitioned: false,
329 }
330 }
331
332 #[test]
333 fn test_table_to_graphql_object_name() {
334 let table = create_test_table();
335 let obj = TableObjectType::from_table(&table);
336
337 assert_eq!(obj.name(), "users"); }
339
340 #[test]
341 fn test_table_to_graphql_object_description() {
342 let table = create_test_table();
343 let obj = TableObjectType::from_table(&table);
344
345 assert_eq!(obj.description(), Some("User accounts"));
346 }
347
348 #[test]
349 fn test_table_to_graphql_object_fields() {
350 let table = create_test_table();
351 let obj = TableObjectType::from_table(&table);
352 let fields = obj.fields();
353
354 assert_eq!(fields.len(), 4);
355 assert!(obj.has_field("id"));
356 assert!(obj.has_field("name"));
357 assert!(obj.has_field("email"));
358 assert!(obj.has_field("metadata"));
359 }
360
361 #[test]
362 fn test_field_types() {
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_eq!(id_field.graphql_type, GraphQLType::Int);
368
369 let name_field = obj.get_field("name").unwrap();
370 assert_eq!(name_field.graphql_type, GraphQLType::String);
371
372 let metadata_field = obj.get_field("metadata").unwrap();
373 assert_eq!(metadata_field.graphql_type, GraphQLType::Json);
374 }
375
376 #[test]
377 fn metadata_can_expose_a_database_column_as_graphql_id() {
378 let table = create_test_table();
379 let names = crate::names::NameOverrides::parse(
380 r#"{"tables": {"public.users": {"column_types": {"id": "ID"}}}}"#,
381 )
382 .unwrap();
383 let obj = TableObjectType::from_table_named(&table, "users", &names);
384
385 let id_field = obj.get_field("id").unwrap();
386 assert_eq!(id_field.graphql_type, GraphQLType::Id);
387 assert_eq!(id_field.type_string(), "ID!");
388 assert_eq!(obj.table.get_column("id").unwrap().nominal_type, "int4");
389 }
390
391 #[test]
392 fn test_field_nullability() {
393 let table = create_test_table();
394 let obj = TableObjectType::from_table(&table);
395
396 let id_field = obj.get_field("id").unwrap();
397 assert!(!id_field.nullable); let name_field = obj.get_field("name").unwrap();
400 assert!(!name_field.nullable); let email_field = obj.get_field("email").unwrap();
403 assert!(email_field.nullable); }
405
406 #[test]
407 fn test_field_descriptions() {
408 let table = create_test_table();
409 let obj = TableObjectType::from_table(&table);
410
411 let id_field = obj.get_field("id").unwrap();
412 assert_eq!(id_field.description, Some("Primary key".into()));
413
414 let email_field = obj.get_field("email").unwrap();
415 assert_eq!(email_field.description, None);
416 }
417
418 #[test]
419 fn test_field_type_string() {
420 let table = create_test_table();
421 let obj = TableObjectType::from_table(&table);
422
423 let id_field = obj.get_field("id").unwrap();
424 assert_eq!(id_field.type_string(), "Int!"); let email_field = obj.get_field("email").unwrap();
427 assert_eq!(email_field.type_string(), "String"); }
429
430 #[test]
431 fn test_pk_fields() {
432 let table = create_test_table();
433 let obj = TableObjectType::from_table(&table);
434
435 let pk_fields = obj.pk_fields();
436 assert_eq!(pk_fields.len(), 1);
437 assert_eq!(pk_fields[0].name, "id");
438 }
439
440 #[test]
441 fn test_table_with_underscore_name() {
442 let mut table = create_test_table();
443 table.name = "user_accounts".into();
444
445 let obj = TableObjectType::from_table(&table);
446 assert_eq!(obj.name(), "user_accounts");
447 }
448}