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., "users_insert_input")
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!("{}_insert_input", 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., "users_set_input")
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!("{}_set_input", 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/// Check if a table is insertable based on its permissions.
250pub fn is_insertable(table: &Table) -> bool {
251    table.insertable
252}
253
254/// Check if a table is updatable based on its permissions.
255pub fn is_updatable(table: &Table) -> bool {
256    table.updatable
257}
258
259/// Check if a table is deletable based on its permissions.
260pub fn is_deletable(table: &Table) -> bool {
261    table.deletable
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use indexmap::IndexMap;
268    use pretty_assertions::assert_eq;
269
270    fn create_test_table() -> Table {
271        let mut columns = IndexMap::new();
272        columns.insert(
273            "id".into(),
274            Column {
275                name: "id".into(),
276                description: Some("Primary key".into()),
277                nullable: false,
278                data_type: "integer".into(),
279                nominal_type: "int4".into(),
280                max_len: None,
281                default: Some("nextval('users_id_seq')".into()),
282                enum_values: vec![],
283                is_pk: true,
284                position: 1,
285                always_generated: false,
286                domain_type: None,
287            },
288        );
289        columns.insert(
290            "name".into(),
291            Column {
292                name: "name".into(),
293                description: Some("User name".into()),
294                nullable: false,
295                data_type: "text".into(),
296                nominal_type: "text".into(),
297                max_len: None,
298                default: None,
299                enum_values: vec![],
300                is_pk: false,
301                position: 2,
302                always_generated: false,
303                domain_type: None,
304            },
305        );
306        columns.insert(
307            "email".into(),
308            Column {
309                name: "email".into(),
310                description: None,
311                nullable: true,
312                data_type: "text".into(),
313                nominal_type: "text".into(),
314                max_len: None,
315                default: None,
316                enum_values: vec![],
317                is_pk: false,
318                position: 3,
319                always_generated: false,
320                domain_type: None,
321            },
322        );
323        columns.insert(
324            "created_at".into(),
325            Column {
326                name: "created_at".into(),
327                description: None,
328                nullable: false,
329                data_type: "timestamptz".into(),
330                nominal_type: "timestamptz".into(),
331                max_len: None,
332                default: Some("now()".into()),
333                enum_values: vec![],
334                is_pk: false,
335                position: 4,
336                always_generated: false,
337                domain_type: None,
338            },
339        );
340
341        Table {
342            schema: "public".into(),
343            name: "users".into(),
344            description: Some("User accounts".into()),
345            is_view: false,
346            insertable: true,
347            updatable: true,
348            deletable: true,
349            pk_cols: vec!["id".into()],
350            unique_constraints: Vec::new(),
351            columns,
352            computed_columns: Default::default(),
353            is_partitioned: false,
354        }
355    }
356
357    fn create_readonly_table() -> Table {
358        let mut table = create_test_table();
359        table.insertable = false;
360        table.updatable = false;
361        table.deletable = false;
362        table
363    }
364
365    // ============================================================================
366    // InsertField Tests
367    // ============================================================================
368
369    #[test]
370    fn test_insert_field_required() {
371        let table = create_test_table();
372        let name_col = table.columns.get("name").unwrap();
373        let field = InsertField::from_column(name_col);
374
375        assert_eq!(field.name, "name");
376        assert!(field.required); // Not nullable, no default
377        assert_eq!(field.type_string(), "String!");
378    }
379
380    #[test]
381    fn test_insert_field_optional_nullable() {
382        let table = create_test_table();
383        let email_col = table.columns.get("email").unwrap();
384        let field = InsertField::from_column(email_col);
385
386        assert_eq!(field.name, "email");
387        assert!(!field.required); // Nullable
388        assert_eq!(field.type_string(), "String");
389    }
390
391    #[test]
392    fn test_insert_field_optional_with_default() {
393        let table = create_test_table();
394        let created_at_col = table.columns.get("created_at").unwrap();
395        let field = InsertField::from_column(created_at_col);
396
397        assert_eq!(field.name, "created_at");
398        assert!(!field.required); // Has default
399        assert_eq!(field.type_string(), "timestamptz");
400    }
401
402    #[test]
403    fn test_insert_field_auto_pk() {
404        let table = create_test_table();
405        let id_col = table.columns.get("id").unwrap();
406        let field = InsertField::from_column(id_col);
407
408        assert_eq!(field.name, "id");
409        assert!(!field.required); // Has auto-increment default
410    }
411
412    // ============================================================================
413    // UpdateField Tests
414    // ============================================================================
415
416    #[test]
417    fn test_update_field_non_pk() {
418        let table = create_test_table();
419        let name_col = table.columns.get("name").unwrap();
420        let field = UpdateField::from_column(name_col);
421
422        assert_eq!(field.name, "name");
423        assert!(!field.is_pk);
424        assert!(field.is_updatable());
425        assert_eq!(field.type_string(), "String"); // All update fields are nullable
426    }
427
428    #[test]
429    fn test_update_field_pk() {
430        let table = create_test_table();
431        let id_col = table.columns.get("id").unwrap();
432        let field = UpdateField::from_column(id_col);
433
434        assert_eq!(field.name, "id");
435        assert!(field.is_pk);
436        assert!(!field.is_updatable());
437    }
438
439    // ============================================================================
440    // InsertInput Tests
441    // ============================================================================
442
443    #[test]
444    fn test_insert_input_from_table() {
445        let table = create_test_table();
446        let input = InsertInput::from_table(&table);
447
448        assert_eq!(input.table_name, "users");
449        assert_eq!(input.type_name, "users_insert_input");
450        assert_eq!(input.fields.len(), 4);
451    }
452
453    #[test]
454    fn test_insert_input_required_fields() {
455        let table = create_test_table();
456        let input = InsertInput::from_table(&table);
457
458        let required = input.required_fields();
459        assert_eq!(required.len(), 1); // Only "name" is required
460        assert_eq!(required[0].name, "name");
461    }
462
463    #[test]
464    fn test_insert_input_optional_fields() {
465        let table = create_test_table();
466        let input = InsertInput::from_table(&table);
467
468        let optional = input.optional_fields();
469        assert_eq!(optional.len(), 3); // id, email, created_at
470    }
471
472    #[test]
473    fn test_insert_input_has_required_fields() {
474        let table = create_test_table();
475        let input = InsertInput::from_table(&table);
476
477        assert!(input.has_required_fields());
478    }
479
480    // ============================================================================
481    // UpdateInput Tests
482    // ============================================================================
483
484    #[test]
485    fn test_update_input_from_table() {
486        let table = create_test_table();
487        let input = UpdateInput::from_table(&table);
488
489        assert_eq!(input.table_name, "users");
490        assert_eq!(input.type_name, "users_set_input");
491        assert_eq!(input.fields.len(), 3); // Excludes PK
492    }
493
494    #[test]
495    fn test_update_input_excludes_pk() {
496        let table = create_test_table();
497        let input = UpdateInput::from_table(&table);
498
499        let field_names: Vec<_> = input.fields.iter().map(|f| f.name.as_str()).collect();
500        assert!(!field_names.contains(&"id"));
501    }
502
503    #[test]
504    fn test_update_input_updatable_fields() {
505        let table = create_test_table();
506        let input = UpdateInput::from_table(&table);
507
508        let updatable = input.updatable_fields();
509        assert_eq!(updatable.len(), 3);
510    }
511
512    // ============================================================================
513    // InputValue Tests
514    // ============================================================================
515
516    #[test]
517    fn test_input_value_null() {
518        let value = InputValue::Null;
519        assert!(value.is_null());
520        assert_eq!(value.to_sql_string(), "NULL");
521    }
522
523    #[test]
524    fn test_input_value_bool() {
525        let value = InputValue::Bool(true);
526        assert_eq!(value.as_bool(), Some(true));
527        assert_eq!(value.to_sql_string(), "true");
528
529        let value = InputValue::Bool(false);
530        assert_eq!(value.to_sql_string(), "false");
531    }
532
533    #[test]
534    fn test_input_value_int() {
535        let value = InputValue::Int(42);
536        assert_eq!(value.as_int(), Some(42));
537        assert_eq!(value.as_float(), Some(42.0)); // Can coerce to float
538        assert_eq!(value.to_sql_string(), "42");
539    }
540
541    #[test]
542    fn test_input_value_float() {
543        let value = InputValue::Float(1.5);
544        assert_eq!(value.as_float(), Some(1.5));
545        assert_eq!(value.to_sql_string(), "1.5");
546    }
547
548    #[test]
549    fn test_input_value_string() {
550        let value = InputValue::String("hello".to_string());
551        assert_eq!(value.as_string(), Some("hello"));
552        assert_eq!(value.to_sql_string(), "hello");
553    }
554
555    // ============================================================================
556    // Table Permission Tests
557    // ============================================================================
558
559    #[test]
560    fn test_is_insertable() {
561        let table = create_test_table();
562        assert!(is_insertable(&table));
563
564        let readonly = create_readonly_table();
565        assert!(!is_insertable(&readonly));
566    }
567
568    #[test]
569    fn test_is_updatable() {
570        let table = create_test_table();
571        assert!(is_updatable(&table));
572
573        let readonly = create_readonly_table();
574        assert!(!is_updatable(&readonly));
575    }
576
577    #[test]
578    fn test_is_deletable() {
579        let table = create_test_table();
580        assert!(is_deletable(&table));
581
582        let readonly = create_readonly_table();
583        assert!(!is_deletable(&readonly));
584    }
585
586    // ============================================================================
587    // PascalCase Tests
588    // ============================================================================
589}