Skip to main content

postrust_graphql/input/
mutation.rs

1//! Mutation input types for inserts and updates.
2//!
3//! Provides input type generation for GraphQL mutations based on table metadata.
4
5use crate::types::{pg_type_to_graphql, GraphQLType};
6use postrust_core::schema_cache::{Column, Table};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Represents a field in an insert input type.
11#[derive(Debug, Clone)]
12pub struct InsertField {
13    /// Field name
14    pub name: String,
15    /// GraphQL type
16    pub graphql_type: GraphQLType,
17    /// Whether the field is required (no default value and not nullable)
18    pub required: bool,
19    /// Field description
20    pub description: Option<String>,
21}
22
23impl InsertField {
24    /// Create an InsertField from a column.
25    pub fn from_column(column: &Column) -> Self {
26        let graphql_type = pg_type_to_graphql(&column.nominal_type);
27
28        // A field is required if:
29        // 1. It's not nullable AND
30        // 2. It has no default value AND
31        // 3. It's not a primary key with serial/auto-increment default
32        let has_auto_default = column
33            .default
34            .as_ref()
35            .is_some_and(|d| d.contains("nextval") || d.contains("gen_random_uuid"));
36
37        let required = !column.nullable && column.default.is_none() && !has_auto_default;
38
39        Self {
40            name: column.name.clone(),
41            description: column.description.clone(),
42            graphql_type,
43            required,
44        }
45    }
46
47    /// Get the GraphQL type string for input.
48    pub fn type_string(&self) -> String {
49        let base = format!("{}", self.graphql_type);
50        if self.required {
51            format!("{}!", base)
52        } else {
53            base
54        }
55    }
56}
57
58/// Represents a field in an update input type.
59#[derive(Debug, Clone)]
60pub struct UpdateField {
61    /// Field name
62    pub name: String,
63    /// GraphQL type
64    pub graphql_type: GraphQLType,
65    /// Field description
66    pub description: Option<String>,
67    /// Whether this is a primary key (cannot be updated)
68    pub is_pk: bool,
69}
70
71impl UpdateField {
72    /// Create an UpdateField from a column.
73    pub fn from_column(column: &Column) -> Self {
74        let graphql_type = pg_type_to_graphql(&column.nominal_type);
75
76        Self {
77            name: column.name.clone(),
78            description: column.description.clone(),
79            graphql_type,
80            is_pk: column.is_pk,
81        }
82    }
83
84    /// Get the GraphQL type string for input (always nullable for updates).
85    pub fn type_string(&self) -> String {
86        format!("{}", self.graphql_type)
87    }
88
89    /// Check if this field can be updated (non-PK fields only).
90    pub fn is_updatable(&self) -> bool {
91        !self.is_pk
92    }
93}
94
95/// Represents an insert input type for a table.
96#[derive(Debug, Clone)]
97pub struct InsertInput {
98    /// Table being inserted into
99    pub table_name: String,
100    /// GraphQL type name (e.g., "UsersInsertInput")
101    pub type_name: String,
102    /// Fields that can be inserted
103    pub fields: Vec<InsertField>,
104}
105
106impl InsertInput {
107    /// Create an InsertInput from a table.
108    pub fn from_table(table: &Table) -> Self {
109        let type_name = format!("{}InsertInput", to_pascal_case(&table.name));
110
111        let fields = table
112            .columns
113            .values()
114            .map(InsertField::from_column)
115            .collect();
116
117        Self {
118            table_name: table.name.clone(),
119            type_name,
120            fields,
121        }
122    }
123
124    /// Get required fields.
125    pub fn required_fields(&self) -> Vec<&InsertField> {
126        self.fields.iter().filter(|f| f.required).collect()
127    }
128
129    /// Get optional fields.
130    pub fn optional_fields(&self) -> Vec<&InsertField> {
131        self.fields.iter().filter(|f| !f.required).collect()
132    }
133
134    /// Check if the table has any required fields.
135    pub fn has_required_fields(&self) -> bool {
136        self.fields.iter().any(|f| f.required)
137    }
138}
139
140/// Represents an update input type for a table.
141#[derive(Debug, Clone)]
142pub struct UpdateInput {
143    /// Table being updated
144    pub table_name: String,
145    /// GraphQL type name (e.g., "UsersSetInput")
146    pub type_name: String,
147    /// Fields that can be updated
148    pub fields: Vec<UpdateField>,
149}
150
151impl UpdateInput {
152    /// Create an UpdateInput from a table.
153    pub fn from_table(table: &Table) -> Self {
154        let type_name = format!("{}SetInput", to_pascal_case(&table.name));
155
156        let fields = table
157            .columns
158            .values()
159            .filter(|c| !c.is_pk) // Exclude primary keys from update
160            .map(UpdateField::from_column)
161            .collect();
162
163        Self {
164            table_name: table.name.clone(),
165            type_name,
166            fields,
167        }
168    }
169
170    /// Get updatable fields.
171    pub fn updatable_fields(&self) -> Vec<&UpdateField> {
172        self.fields.iter().filter(|f| f.is_updatable()).collect()
173    }
174}
175
176/// A dynamic input value that can hold different types.
177#[derive(Debug, Clone, Serialize, Deserialize)]
178#[serde(untagged)]
179pub enum InputValue {
180    /// Null value
181    Null,
182    /// Boolean value
183    Bool(bool),
184    /// Integer value
185    Int(i64),
186    /// Float value
187    Float(f64),
188    /// String value
189    String(String),
190    /// JSON object value
191    Object(HashMap<String, InputValue>),
192    /// JSON array value
193    Array(Vec<InputValue>),
194}
195
196impl InputValue {
197    /// Check if this is null.
198    pub fn is_null(&self) -> bool {
199        matches!(self, Self::Null)
200    }
201
202    /// Try to get as string.
203    pub fn as_string(&self) -> Option<&str> {
204        match self {
205            Self::String(s) => Some(s),
206            _ => None,
207        }
208    }
209
210    /// Try to get as i64.
211    pub fn as_int(&self) -> Option<i64> {
212        match self {
213            Self::Int(i) => Some(*i),
214            _ => None,
215        }
216    }
217
218    /// Try to get as f64.
219    pub fn as_float(&self) -> Option<f64> {
220        match self {
221            Self::Float(f) => Some(*f),
222            Self::Int(i) => Some(*i as f64),
223            _ => None,
224        }
225    }
226
227    /// Try to get as bool.
228    pub fn as_bool(&self) -> Option<bool> {
229        match self {
230            Self::Bool(b) => Some(*b),
231            _ => None,
232        }
233    }
234
235    /// Convert to SQL string representation.
236    pub fn to_sql_string(&self) -> String {
237        match self {
238            Self::Null => "NULL".to_string(),
239            Self::Bool(b) => if *b { "true" } else { "false" }.to_string(),
240            Self::Int(i) => i.to_string(),
241            Self::Float(f) => f.to_string(),
242            Self::String(s) => s.clone(),
243            Self::Object(o) => serde_json::to_string(o).unwrap_or_default(),
244            Self::Array(a) => serde_json::to_string(a).unwrap_or_default(),
245        }
246    }
247}
248
249/// Helper to convert snake_case to PascalCase.
250fn to_pascal_case(s: &str) -> String {
251    s.split('_')
252        .map(|word| {
253            let mut chars = word.chars();
254            match chars.next() {
255                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
256                None => String::new(),
257            }
258        })
259        .collect()
260}
261
262/// Check if a table is insertable based on its permissions.
263pub fn is_insertable(table: &Table) -> bool {
264    table.insertable
265}
266
267/// Check if a table is updatable based on its permissions.
268pub fn is_updatable(table: &Table) -> bool {
269    table.updatable
270}
271
272/// Check if a table is deletable based on its permissions.
273pub fn is_deletable(table: &Table) -> bool {
274    table.deletable
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use indexmap::IndexMap;
281    use pretty_assertions::assert_eq;
282
283    fn create_test_table() -> Table {
284        let mut columns = IndexMap::new();
285        columns.insert(
286            "id".into(),
287            Column {
288                name: "id".into(),
289                description: Some("Primary key".into()),
290                nullable: false,
291                data_type: "integer".into(),
292                nominal_type: "int4".into(),
293                max_len: None,
294                default: Some("nextval('users_id_seq')".into()),
295                enum_values: vec![],
296                is_pk: true,
297                position: 1,
298            },
299        );
300        columns.insert(
301            "name".into(),
302            Column {
303                name: "name".into(),
304                description: Some("User name".into()),
305                nullable: false,
306                data_type: "text".into(),
307                nominal_type: "text".into(),
308                max_len: None,
309                default: None,
310                enum_values: vec![],
311                is_pk: false,
312                position: 2,
313            },
314        );
315        columns.insert(
316            "email".into(),
317            Column {
318                name: "email".into(),
319                description: None,
320                nullable: true,
321                data_type: "text".into(),
322                nominal_type: "text".into(),
323                max_len: None,
324                default: None,
325                enum_values: vec![],
326                is_pk: false,
327                position: 3,
328            },
329        );
330        columns.insert(
331            "created_at".into(),
332            Column {
333                name: "created_at".into(),
334                description: None,
335                nullable: false,
336                data_type: "timestamptz".into(),
337                nominal_type: "timestamptz".into(),
338                max_len: None,
339                default: Some("now()".into()),
340                enum_values: vec![],
341                is_pk: false,
342                position: 4,
343            },
344        );
345
346        Table {
347            schema: "public".into(),
348            name: "users".into(),
349            description: Some("User accounts".into()),
350            is_view: false,
351            insertable: true,
352            updatable: true,
353            deletable: true,
354            pk_cols: vec!["id".into()],
355            columns,
356        }
357    }
358
359    fn create_readonly_table() -> Table {
360        let mut table = create_test_table();
361        table.insertable = false;
362        table.updatable = false;
363        table.deletable = false;
364        table
365    }
366
367    // ============================================================================
368    // InsertField Tests
369    // ============================================================================
370
371    #[test]
372    fn test_insert_field_required() {
373        let table = create_test_table();
374        let name_col = table.columns.get("name").unwrap();
375        let field = InsertField::from_column(name_col);
376
377        assert_eq!(field.name, "name");
378        assert!(field.required); // Not nullable, no default
379        assert_eq!(field.type_string(), "String!");
380    }
381
382    #[test]
383    fn test_insert_field_optional_nullable() {
384        let table = create_test_table();
385        let email_col = table.columns.get("email").unwrap();
386        let field = InsertField::from_column(email_col);
387
388        assert_eq!(field.name, "email");
389        assert!(!field.required); // Nullable
390        assert_eq!(field.type_string(), "String");
391    }
392
393    #[test]
394    fn test_insert_field_optional_with_default() {
395        let table = create_test_table();
396        let created_at_col = table.columns.get("created_at").unwrap();
397        let field = InsertField::from_column(created_at_col);
398
399        assert_eq!(field.name, "created_at");
400        assert!(!field.required); // Has default
401        assert_eq!(field.type_string(), "DateTime");
402    }
403
404    #[test]
405    fn test_insert_field_auto_pk() {
406        let table = create_test_table();
407        let id_col = table.columns.get("id").unwrap();
408        let field = InsertField::from_column(id_col);
409
410        assert_eq!(field.name, "id");
411        assert!(!field.required); // Has auto-increment default
412    }
413
414    // ============================================================================
415    // UpdateField Tests
416    // ============================================================================
417
418    #[test]
419    fn test_update_field_non_pk() {
420        let table = create_test_table();
421        let name_col = table.columns.get("name").unwrap();
422        let field = UpdateField::from_column(name_col);
423
424        assert_eq!(field.name, "name");
425        assert!(!field.is_pk);
426        assert!(field.is_updatable());
427        assert_eq!(field.type_string(), "String"); // All update fields are nullable
428    }
429
430    #[test]
431    fn test_update_field_pk() {
432        let table = create_test_table();
433        let id_col = table.columns.get("id").unwrap();
434        let field = UpdateField::from_column(id_col);
435
436        assert_eq!(field.name, "id");
437        assert!(field.is_pk);
438        assert!(!field.is_updatable());
439    }
440
441    // ============================================================================
442    // InsertInput Tests
443    // ============================================================================
444
445    #[test]
446    fn test_insert_input_from_table() {
447        let table = create_test_table();
448        let input = InsertInput::from_table(&table);
449
450        assert_eq!(input.table_name, "users");
451        assert_eq!(input.type_name, "UsersInsertInput");
452        assert_eq!(input.fields.len(), 4);
453    }
454
455    #[test]
456    fn test_insert_input_required_fields() {
457        let table = create_test_table();
458        let input = InsertInput::from_table(&table);
459
460        let required = input.required_fields();
461        assert_eq!(required.len(), 1); // Only "name" is required
462        assert_eq!(required[0].name, "name");
463    }
464
465    #[test]
466    fn test_insert_input_optional_fields() {
467        let table = create_test_table();
468        let input = InsertInput::from_table(&table);
469
470        let optional = input.optional_fields();
471        assert_eq!(optional.len(), 3); // id, email, created_at
472    }
473
474    #[test]
475    fn test_insert_input_has_required_fields() {
476        let table = create_test_table();
477        let input = InsertInput::from_table(&table);
478
479        assert!(input.has_required_fields());
480    }
481
482    // ============================================================================
483    // UpdateInput Tests
484    // ============================================================================
485
486    #[test]
487    fn test_update_input_from_table() {
488        let table = create_test_table();
489        let input = UpdateInput::from_table(&table);
490
491        assert_eq!(input.table_name, "users");
492        assert_eq!(input.type_name, "UsersSetInput");
493        assert_eq!(input.fields.len(), 3); // Excludes PK
494    }
495
496    #[test]
497    fn test_update_input_excludes_pk() {
498        let table = create_test_table();
499        let input = UpdateInput::from_table(&table);
500
501        let field_names: Vec<_> = input.fields.iter().map(|f| f.name.as_str()).collect();
502        assert!(!field_names.contains(&"id"));
503    }
504
505    #[test]
506    fn test_update_input_updatable_fields() {
507        let table = create_test_table();
508        let input = UpdateInput::from_table(&table);
509
510        let updatable = input.updatable_fields();
511        assert_eq!(updatable.len(), 3);
512    }
513
514    // ============================================================================
515    // InputValue Tests
516    // ============================================================================
517
518    #[test]
519    fn test_input_value_null() {
520        let value = InputValue::Null;
521        assert!(value.is_null());
522        assert_eq!(value.to_sql_string(), "NULL");
523    }
524
525    #[test]
526    fn test_input_value_bool() {
527        let value = InputValue::Bool(true);
528        assert_eq!(value.as_bool(), Some(true));
529        assert_eq!(value.to_sql_string(), "true");
530
531        let value = InputValue::Bool(false);
532        assert_eq!(value.to_sql_string(), "false");
533    }
534
535    #[test]
536    fn test_input_value_int() {
537        let value = InputValue::Int(42);
538        assert_eq!(value.as_int(), Some(42));
539        assert_eq!(value.as_float(), Some(42.0)); // Can coerce to float
540        assert_eq!(value.to_sql_string(), "42");
541    }
542
543    #[test]
544    fn test_input_value_float() {
545        let value = InputValue::Float(1.5);
546        assert_eq!(value.as_float(), Some(1.5));
547        assert_eq!(value.to_sql_string(), "1.5");
548    }
549
550    #[test]
551    fn test_input_value_string() {
552        let value = InputValue::String("hello".to_string());
553        assert_eq!(value.as_string(), Some("hello"));
554        assert_eq!(value.to_sql_string(), "hello");
555    }
556
557    // ============================================================================
558    // Table Permission Tests
559    // ============================================================================
560
561    #[test]
562    fn test_is_insertable() {
563        let table = create_test_table();
564        assert!(is_insertable(&table));
565
566        let readonly = create_readonly_table();
567        assert!(!is_insertable(&readonly));
568    }
569
570    #[test]
571    fn test_is_updatable() {
572        let table = create_test_table();
573        assert!(is_updatable(&table));
574
575        let readonly = create_readonly_table();
576        assert!(!is_updatable(&readonly));
577    }
578
579    #[test]
580    fn test_is_deletable() {
581        let table = create_test_table();
582        assert!(is_deletable(&table));
583
584        let readonly = create_readonly_table();
585        assert!(!is_deletable(&readonly));
586    }
587
588    // ============================================================================
589    // PascalCase Tests
590    // ============================================================================
591
592    #[test]
593    fn test_to_pascal_case() {
594        assert_eq!(to_pascal_case("users"), "Users");
595        assert_eq!(to_pascal_case("user_accounts"), "UserAccounts");
596        assert_eq!(to_pascal_case("my_table_name"), "MyTableName");
597    }
598}