1use crate::types::{pg_type_to_graphql, GraphQLType};
6use postrust_core::schema_cache::{Column, Table};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone)]
12pub struct InsertField {
13 pub name: String,
15 pub graphql_type: GraphQLType,
17 pub required: bool,
19 pub description: Option<String>,
21}
22
23impl InsertField {
24 pub fn from_column(column: &Column) -> Self {
26 let graphql_type = pg_type_to_graphql(&column.nominal_type);
27
28 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 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#[derive(Debug, Clone)]
60pub struct UpdateField {
61 pub name: String,
63 pub graphql_type: GraphQLType,
65 pub description: Option<String>,
67 pub is_pk: bool,
69}
70
71impl UpdateField {
72 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 pub fn type_string(&self) -> String {
86 format!("{}", self.graphql_type)
87 }
88
89 pub fn is_updatable(&self) -> bool {
91 !self.is_pk
92 }
93}
94
95#[derive(Debug, Clone)]
97pub struct InsertInput {
98 pub table_name: String,
100 pub type_name: String,
102 pub fields: Vec<InsertField>,
104}
105
106impl InsertInput {
107 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 pub fn required_fields(&self) -> Vec<&InsertField> {
126 self.fields.iter().filter(|f| f.required).collect()
127 }
128
129 pub fn optional_fields(&self) -> Vec<&InsertField> {
131 self.fields.iter().filter(|f| !f.required).collect()
132 }
133
134 pub fn has_required_fields(&self) -> bool {
136 self.fields.iter().any(|f| f.required)
137 }
138}
139
140#[derive(Debug, Clone)]
142pub struct UpdateInput {
143 pub table_name: String,
145 pub type_name: String,
147 pub fields: Vec<UpdateField>,
149}
150
151impl UpdateInput {
152 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) .map(UpdateField::from_column)
161 .collect();
162
163 Self {
164 table_name: table.name.clone(),
165 type_name,
166 fields,
167 }
168 }
169
170 pub fn updatable_fields(&self) -> Vec<&UpdateField> {
172 self.fields.iter().filter(|f| f.is_updatable()).collect()
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178#[serde(untagged)]
179pub enum InputValue {
180 Null,
182 Bool(bool),
184 Int(i64),
186 Float(f64),
188 String(String),
190 Object(HashMap<String, InputValue>),
192 Array(Vec<InputValue>),
194}
195
196impl InputValue {
197 pub fn is_null(&self) -> bool {
199 matches!(self, Self::Null)
200 }
201
202 pub fn as_string(&self) -> Option<&str> {
204 match self {
205 Self::String(s) => Some(s),
206 _ => None,
207 }
208 }
209
210 pub fn as_int(&self) -> Option<i64> {
212 match self {
213 Self::Int(i) => Some(*i),
214 _ => None,
215 }
216 }
217
218 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 pub fn as_bool(&self) -> Option<bool> {
229 match self {
230 Self::Bool(b) => Some(*b),
231 _ => None,
232 }
233 }
234
235 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
249fn 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
262pub fn is_insertable(table: &Table) -> bool {
264 table.insertable
265}
266
267pub fn is_updatable(table: &Table) -> bool {
269 table.updatable
270}
271
272pub 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 #[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); 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); 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); 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); }
413
414 #[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"); }
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 #[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); 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); }
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 #[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); }
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 #[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)); 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 #[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 #[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}