Skip to main content

postrust_graphql/
handler.rs

1//! Axum handler for the /graphql endpoint.
2//!
3//! Provides GraphQL request handling using async-graphql with dynamic schema
4//! generation from the PostgreSQL schema cache.
5
6use crate::context::GraphQLContext;
7use crate::error::GraphQLError;
8use crate::schema::object::TableObjectType;
9use crate::schema::{build_schema, GeneratedSchema, MutationType, SchemaConfig};
10use crate::subscription::{
11    generate_subscription_fields, NotifyBroker, SubscriptionField as SubField, TableChangePayload,
12};
13use async_graphql::dynamic::*;
14use async_graphql::Value;
15use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
16use axum::extract::State;
17use axum::response::IntoResponse;
18use futures::stream::StreamExt;
19use postrust_core::schema_cache::SchemaCache;
20use sqlx::PgPool;
21use std::collections::HashMap;
22use std::sync::Arc;
23use tokio::sync::RwLock;
24use tracing::{debug, info, trace};
25
26/// GraphQL execution state shared across requests.
27pub struct GraphQLState {
28    /// Database connection pool
29    pub pool: PgPool,
30    /// Schema cache
31    pub schema_cache: Arc<SchemaCache>,
32    /// Generated GraphQL schema
33    pub generated_schema: GeneratedSchema,
34    /// async-graphql Schema (built dynamically)
35    pub schema: Schema,
36    /// Schema configuration
37    pub config: SchemaConfig,
38    /// Subscription fields
39    pub subscription_fields: Vec<SubField>,
40    /// Notification broker for subscriptions
41    pub broker: Arc<RwLock<Option<NotifyBroker>>>,
42}
43
44impl GraphQLState {
45    /// Create new GraphQL state from schema cache.
46    pub fn new(
47        pool: PgPool,
48        schema_cache: Arc<SchemaCache>,
49        config: SchemaConfig,
50    ) -> Result<Self, GraphQLError> {
51        let generated_schema = build_schema(&schema_cache, &config);
52        let subscription_fields = if config.enable_subscriptions {
53            generate_subscription_fields(&schema_cache, &generated_schema)
54        } else {
55            Vec::new()
56        };
57        let schema = build_dynamic_schema(
58            &generated_schema,
59            &schema_cache,
60            if config.enable_subscriptions {
61                Some(subscription_fields.as_slice())
62            } else {
63                None
64            },
65        )?;
66
67        Ok(Self {
68            pool: pool.clone(),
69            schema_cache,
70            generated_schema,
71            schema,
72            config,
73            subscription_fields,
74            broker: Arc::new(RwLock::new(None)),
75        })
76    }
77
78    /// Rebuild the schema (e.g., after schema cache refresh).
79    pub fn rebuild(&mut self) -> Result<(), GraphQLError> {
80        self.generated_schema = build_schema(&self.schema_cache, &self.config);
81        self.subscription_fields = if self.config.enable_subscriptions {
82            generate_subscription_fields(&self.schema_cache, &self.generated_schema)
83        } else {
84            Vec::new()
85        };
86        self.schema = build_dynamic_schema(
87            &self.generated_schema,
88            &self.schema_cache,
89            if self.config.enable_subscriptions {
90                Some(self.subscription_fields.as_slice())
91            } else {
92                None
93            },
94        )?;
95        Ok(())
96    }
97
98    /// Initialize the subscription broker.
99    ///
100    /// This should be called after creating the state to enable subscriptions.
101    pub async fn init_subscriptions(&self) -> Result<(), crate::subscription::BrokerError> {
102        if !self.config.enable_subscriptions {
103            return Ok(());
104        }
105
106        let broker = NotifyBroker::new(self.pool.clone());
107
108        // Collect all channels to listen on
109        let channels: Vec<String> = self
110            .subscription_fields
111            .iter()
112            .map(|f| f.channel_name())
113            .collect();
114
115        if !channels.is_empty() {
116            broker.start(channels).await?;
117            info!(
118                "Subscription broker started with {} channels",
119                self.subscription_fields.len()
120            );
121        }
122
123        // Store the broker
124        let mut broker_guard = self.broker.write().await;
125        *broker_guard = Some(broker);
126
127        Ok(())
128    }
129
130    /// Stop the subscription broker.
131    pub async fn stop_subscriptions(&self) {
132        let broker_guard = self.broker.read().await;
133        if let Some(broker) = broker_guard.as_ref() {
134            broker.stop().await;
135        }
136    }
137
138    /// Get the notification broker.
139    pub async fn get_broker(&self) -> Option<Arc<RwLock<Option<NotifyBroker>>>> {
140        Some(Arc::clone(&self.broker))
141    }
142}
143
144/// Handle a GraphQL request.
145pub async fn graphql_handler(
146    State(state): State<Arc<GraphQLState>>,
147    ctx: GraphQLContext,
148    req: GraphQLRequest,
149) -> GraphQLResponse {
150    let request = req
151        .into_inner()
152        .data(ctx)
153        .data(state.pool.clone())
154        .data(Arc::clone(&state.broker));
155    state.schema.execute(request).await.into()
156}
157
158/// Handle GraphQL WebSocket subscription upgrade.
159///
160/// This should be called with a WebSocket upgrade request to enable
161/// GraphQL subscriptions over WebSocket.
162pub async fn graphql_ws_handler(
163    State(state): State<Arc<GraphQLState>>,
164    protocol: async_graphql_axum::GraphQLProtocol,
165    ws: axum::extract::WebSocketUpgrade,
166) -> impl IntoResponse {
167    let schema = state.schema.clone();
168    let pool = state.pool.clone();
169    let broker = Arc::clone(&state.broker);
170
171    ws.protocols(["graphql-transport-ws", "graphql-ws"])
172        .on_upgrade(move |socket| async move {
173            let mut data = async_graphql::Data::default();
174            data.insert(pool);
175            data.insert(broker);
176
177            async_graphql_axum::GraphQLWebSocket::new(socket, schema, protocol)
178                .with_data(data)
179                .serve()
180                .await
181        })
182}
183
184/// Handle GraphQL playground request.
185pub async fn graphql_playground() -> impl axum::response::IntoResponse {
186    axum::response::Html(async_graphql::http::playground_source(
187        async_graphql::http::GraphQLPlaygroundConfig::new("/graphql")
188            .subscription_endpoint("/graphql/ws"),
189    ))
190}
191
192/// Build the dynamic async-graphql schema from our generated schema.
193fn build_dynamic_schema(
194    generated: &GeneratedSchema,
195    _schema_cache: &SchemaCache,
196    subscription_fields: Option<&[SubField]>,
197) -> Result<Schema, GraphQLError> {
198    // Create object types for each table
199    let mut object_types: HashMap<String, Object> = HashMap::new();
200
201    for (type_name, obj) in &generated.object_types {
202        let table_obj = create_object_type(obj);
203        object_types.insert(type_name.clone(), table_obj);
204    }
205
206    // Create query type
207    let query = create_query_type(generated);
208
209    // Create mutation type
210    let mutation = if !generated.mutation_fields.is_empty() {
211        Some(create_mutation_type(generated))
212    } else {
213        None
214    };
215
216    // Create subscription type if enabled
217    let subscription = subscription_fields.map(create_subscription_type);
218
219    // Build schema
220    let mut builder = Schema::build(
221        "Query",
222        mutation.as_ref().map(|_| "Mutation"),
223        subscription.as_ref().map(|_| "Subscription"),
224    );
225
226    // Register all object types
227    for (_, obj) in object_types {
228        builder = builder.register(obj);
229    }
230
231    // Register query type
232    builder = builder.register(query);
233
234    // Register mutation type if present
235    if let Some(mutation) = mutation {
236        builder = builder.register(mutation);
237    }
238
239    // Register subscription type if present
240    if let Some(subscription) = subscription {
241        builder = builder.register(subscription);
242    }
243
244    // Register scalar types
245    builder = builder.register(create_bigint_scalar());
246    builder = builder.register(create_bigdecimal_scalar());
247    builder = builder.register(create_json_scalar());
248    builder = builder.register(create_uuid_scalar());
249    builder = builder.register(create_date_scalar());
250    builder = builder.register(create_datetime_scalar());
251    builder = builder.register(create_time_scalar());
252
253    // Register input types
254    builder = register_filter_input_types(builder);
255
256    builder
257        .finish()
258        .map_err(|e| GraphQLError::SchemaError(e.to_string()))
259}
260
261/// Create an object type from a TableObjectType.
262fn create_object_type(obj: &TableObjectType) -> Object {
263    let mut object = Object::new(&obj.name);
264
265    if let Some(desc) = obj.description() {
266        object = object.description(desc);
267    }
268
269    for field in &obj.fields {
270        let field_name = field.name.clone();
271        let field_type = graphql_type_ref(&field.type_string());
272
273        // Create field with resolver that extracts from parent async_graphql::Value
274        // The query resolver stores rows as FieldValue::value(Value::Object)
275        // so we use as_value() to get the Value and extract fields from the Object
276        let gql_field = Field::new(&field.name, field_type, move |ctx| {
277            let field_name = field_name.clone();
278            FieldFuture::new(async move {
279                // Get the parent value as async_graphql::Value using as_value()
280                if let Some(Value::Object(map)) = ctx.parent_value.as_value() {
281                    // Convert field name to async_graphql::Name for lookup
282                    let key = async_graphql::Name::new(&field_name);
283                    if let Some(val) = map.get(&key) {
284                        return Ok(Some(FieldValue::value(val.clone())));
285                    }
286                }
287
288                // Field not found or parent not a Value::Object
289                Ok(None)
290            })
291        });
292
293        let gql_field = if let Some(desc) = &field.description {
294            gql_field.description(desc)
295        } else {
296            gql_field
297        };
298
299        object = object.field(gql_field);
300    }
301
302    object
303}
304
305/// Create the Query type with all table query fields.
306fn create_query_type(generated: &GeneratedSchema) -> Object {
307    let mut query = Object::new("Query");
308
309    for field in &generated.query_fields {
310        let table_name = field.table_name.clone();
311        let type_name = field.type_name.clone();
312        let is_by_pk = field.is_by_pk;
313        let return_type = graphql_type_ref(&field.return_type);
314
315        let mut gql_field = Field::new(&field.name, return_type, move |ctx| {
316            let table_name = table_name.clone();
317            let type_name = type_name.clone();
318            FieldFuture::new(
319                async move { resolve_query(&ctx, &table_name, &type_name, is_by_pk).await },
320            )
321        });
322
323        // Add standard query arguments
324        if !is_by_pk {
325            gql_field = gql_field
326                .argument(InputValue::new("filter", TypeRef::named("JSON")))
327                .argument(InputValue::new("orderBy", TypeRef::named_list("String")))
328                .argument(InputValue::new("limit", TypeRef::named("Int")))
329                .argument(InputValue::new("offset", TypeRef::named("Int")));
330        } else {
331            // Add PK arguments
332            gql_field = gql_field.argument(InputValue::new("id", TypeRef::named_nn("Int")));
333        }
334
335        if let Some(desc) = &field.description {
336            gql_field = gql_field.description(desc);
337        }
338
339        query = query.field(gql_field);
340    }
341
342    // Add introspection queries
343    query = query.field(
344        Field::new("_schema", TypeRef::named("String"), |_| {
345            FieldFuture::new(async move {
346                Ok(Some(Value::String("Postrust GraphQL Schema".to_string())))
347            })
348        })
349        .description("Schema introspection"),
350    );
351
352    query
353}
354
355/// Create the Mutation type with all mutation fields.
356fn create_mutation_type(generated: &GeneratedSchema) -> Object {
357    let mut mutation = Object::new("Mutation");
358
359    for field in &generated.mutation_fields {
360        let table_name = field.table_name.clone();
361        let mutation_type = field.mutation_type;
362        let return_type = graphql_type_ref(&field.return_type);
363
364        let mut gql_field = Field::new(&field.name, return_type, move |ctx| {
365            let table_name = table_name.clone();
366            FieldFuture::new(
367                async move { resolve_mutation(&ctx, &table_name, mutation_type).await },
368            )
369        });
370
371        // Add mutation-specific arguments
372        match mutation_type {
373            MutationType::Insert | MutationType::InsertOne => {
374                gql_field =
375                    gql_field.argument(InputValue::new("objects", TypeRef::named_nn_list("JSON")));
376            }
377            MutationType::Update | MutationType::UpdateByPk => {
378                gql_field = gql_field
379                    .argument(InputValue::new("where", TypeRef::named("JSON")))
380                    .argument(InputValue::new("set", TypeRef::named_nn("JSON")));
381            }
382            MutationType::Delete | MutationType::DeleteByPk => {
383                gql_field = gql_field.argument(InputValue::new("where", TypeRef::named("JSON")));
384            }
385        }
386
387        if let Some(desc) = &field.description {
388            gql_field = gql_field.description(desc);
389        }
390
391        mutation = mutation.field(gql_field);
392    }
393
394    mutation
395}
396
397/// Create the Subscription type with all subscription fields.
398fn create_subscription_type(fields: &[SubField]) -> Subscription {
399    let mut subscription = Subscription::new("Subscription");
400
401    for field in fields {
402        let channel_name = field.channel_name();
403        let return_type = TypeRef::named(&field.return_type);
404        let field_name = field.name.clone();
405        let description = field.description.clone();
406
407        let gql_field = SubscriptionField::new(&field_name, return_type, move |ctx| {
408            let channel_name = channel_name.clone();
409            SubscriptionFieldFuture::new(async move {
410                let broker_arc = ctx.data::<Arc<RwLock<Option<NotifyBroker>>>>()?;
411                let broker_guard = broker_arc.read().await;
412
413                let broker = broker_guard.as_ref().ok_or_else(|| {
414                    async_graphql::Error::new("Subscription broker not initialized")
415                })?;
416
417                let stream = broker
418                    .subscribe(&channel_name)
419                    .await
420                    .map_err(|e| async_graphql::Error::new(format!("Subscription error: {}", e)))?;
421
422                // Transform notification stream to GraphQL values
423                // Use FieldValue::value() so field resolvers can use as_value()
424                let value_stream = stream.filter_map(|notification| async move {
425                    match TableChangePayload::from_payload(&notification.payload) {
426                        Ok(payload) => payload
427                            .data()
428                            .map(|data| Ok(FieldValue::value(json_to_value(data.clone())))),
429                        Err(e) => {
430                            debug!("Failed to parse notification payload: {}", e);
431                            None
432                        }
433                    }
434                });
435
436                Ok(value_stream)
437            })
438        });
439
440        let gql_field = if let Some(desc) = description {
441            gql_field.description(desc)
442        } else {
443            gql_field
444        };
445
446        subscription = subscription.field(gql_field);
447    }
448
449    subscription
450}
451
452/// Resolve a query field.
453async fn resolve_query<'a>(
454    ctx: &ResolverContext<'a>,
455    table_name: &str,
456    _type_name: &str,
457    is_by_pk: bool,
458) -> Result<Option<FieldValue<'a>>, async_graphql::Error> {
459    let pool = ctx.data::<PgPool>()?;
460    let gql_ctx = ctx.data::<GraphQLContext>()?;
461
462    debug!("Resolving query for table: {}", table_name);
463
464    // Extract pagination arguments
465    let limit: Option<i64> = ctx.args.try_get("limit").ok().and_then(|v| v.i64().ok());
466
467    let offset: Option<i64> = ctx.args.try_get("offset").ok().and_then(|v| v.i64().ok());
468
469    // Build simple query
470    let mut sql = format!(
471        "SELECT row_to_json(t) FROM (SELECT * FROM public.{}) t",
472        table_name
473    );
474
475    if let Some(limit) = limit {
476        sql.push_str(&format!(" LIMIT {}", limit));
477    }
478
479    if let Some(offset) = offset {
480        sql.push_str(&format!(" OFFSET {}", offset));
481    }
482
483    // Execute query - returns Vec<serde_json::Value>
484    let result = execute_query(pool, &sql, gql_ctx.role()).await?;
485
486    if is_by_pk {
487        // Return single item as Value::Object
488        // json_to_value converts serde_json to async_graphql Value
489        Ok(result
490            .into_iter()
491            .next()
492            .map(|v| FieldValue::value(json_to_value(v))))
493    } else {
494        // Return list with each item as Value::Object
495        let items: Vec<FieldValue> = result
496            .into_iter()
497            .map(|v| FieldValue::value(json_to_value(v)))
498            .collect();
499        Ok(Some(FieldValue::list(items)))
500    }
501}
502
503/// Resolve a mutation field.
504async fn resolve_mutation<'a>(
505    ctx: &ResolverContext<'a>,
506    table_name: &str,
507    mutation_type: MutationType,
508) -> Result<Option<FieldValue<'a>>, async_graphql::Error> {
509    let pool = ctx.data::<PgPool>()?;
510    let gql_ctx = ctx.data::<GraphQLContext>()?;
511
512    debug!(
513        "Resolving mutation for table: {} type: {:?}",
514        table_name, mutation_type
515    );
516
517    let result = match mutation_type {
518        MutationType::Insert | MutationType::InsertOne => {
519            let objects = ctx
520                .args
521                .try_get("objects")
522                .ok()
523                .map(|v| accessor_to_json(&v))
524                .unwrap_or_else(|| serde_json::Value::Array(vec![]));
525
526            execute_insert(pool, table_name, gql_ctx.role(), objects, mutation_type).await?
527        }
528        MutationType::Update | MutationType::UpdateByPk => {
529            let set_value = ctx
530                .args
531                .try_get("set")
532                .ok()
533                .map(|v| accessor_to_json(&v))
534                .unwrap_or_else(|| serde_json::json!({}));
535
536            let where_clause = ctx.args.try_get("where").ok().map(|v| accessor_to_json(&v));
537
538            execute_update(
539                pool,
540                table_name,
541                gql_ctx.role(),
542                set_value,
543                where_clause,
544                mutation_type,
545            )
546            .await?
547        }
548        MutationType::Delete | MutationType::DeleteByPk => {
549            let where_clause = ctx.args.try_get("where").ok().map(|v| accessor_to_json(&v));
550
551            execute_delete(
552                pool,
553                table_name,
554                gql_ctx.role(),
555                where_clause,
556                mutation_type,
557            )
558            .await?
559        }
560    };
561
562    Ok(result)
563}
564
565/// Execute a SQL query and return results as serde_json::Value.
566/// We keep data as serde_json::Value so field resolvers can use try_downcast_ref.
567async fn execute_query(
568    pool: &PgPool,
569    sql: &str,
570    role: &str,
571) -> Result<Vec<serde_json::Value>, async_graphql::Error> {
572    use sqlx::Row;
573
574    trace!("Executing SQL: {}", sql);
575
576    let mut conn = pool.acquire().await?;
577
578    // Set role
579    sqlx::query(&format!(
580        "SET LOCAL ROLE {}",
581        postrust_sql::escape_ident(role)
582    ))
583    .execute(&mut *conn)
584    .await?;
585
586    // Execute query
587    let rows = sqlx::query(sql).fetch_all(&mut *conn).await?;
588
589    // Return raw JSON values - don't convert to async_graphql::Value
590    // This allows field resolvers to use try_downcast_ref::<serde_json::Value>()
591    let results: Vec<serde_json::Value> = rows
592        .iter()
593        .filter_map(|row| row.try_get::<serde_json::Value, _>(0).ok())
594        .collect();
595
596    Ok(results)
597}
598
599/// Execute an insert mutation.
600async fn execute_insert<'a>(
601    pool: &PgPool,
602    table_name: &str,
603    role: &str,
604    objects: serde_json::Value,
605    mutation_type: MutationType,
606) -> Result<Option<FieldValue<'a>>, async_graphql::Error> {
607    use sqlx::Row;
608
609    trace!("Insert mutation for {}: {:?}", table_name, objects);
610
611    // Handle both array and single object
612    let objects_array = match objects {
613        serde_json::Value::Array(arr) => arr,
614        serde_json::Value::Object(obj) => vec![serde_json::Value::Object(obj)],
615        _ => {
616            return Err(async_graphql::Error::new(
617                "objects must be an array or object",
618            ))
619        }
620    };
621
622    if objects_array.is_empty() {
623        return Err(async_graphql::Error::new("objects cannot be empty"));
624    }
625
626    let mut conn = pool.acquire().await?;
627
628    // Set role
629    sqlx::query(&format!(
630        "SET LOCAL ROLE {}",
631        postrust_sql::escape_ident(role)
632    ))
633    .execute(&mut *conn)
634    .await?;
635
636    let mut inserted: Vec<FieldValue> = Vec::new();
637
638    for obj in objects_array {
639        if let serde_json::Value::Object(map) = obj {
640            // Build INSERT query
641            let columns: Vec<&str> = map.keys().map(|k| k.as_str()).collect();
642            let placeholders: Vec<String> =
643                (1..=columns.len()).map(|i| format!("${}", i)).collect();
644
645            let sql = format!(
646                "INSERT INTO public.{} ({}) VALUES ({}) RETURNING row_to_json(public.{}.*)",
647                postrust_sql::escape_ident(table_name),
648                columns
649                    .iter()
650                    .map(|c| postrust_sql::escape_ident(c))
651                    .collect::<Vec<_>>()
652                    .join(", "),
653                placeholders.join(", "),
654                postrust_sql::escape_ident(table_name)
655            );
656
657            trace!("Executing INSERT SQL: {}", sql);
658
659            // Build query with parameters
660            let mut query = sqlx::query(&sql);
661            for col in &columns {
662                if let Some(val) = map.get(*col) {
663                    query = bind_json_value(query, val);
664                }
665            }
666
667            let row = query.fetch_one(&mut *conn).await?;
668            if let Ok(json_val) = row.try_get::<serde_json::Value, _>(0) {
669                inserted.push(FieldValue::value(json_to_value(json_val)));
670            }
671        }
672    }
673
674    // Return based on mutation type
675    match mutation_type {
676        MutationType::InsertOne => {
677            // Return single item
678            Ok(inserted.into_iter().next())
679        }
680        _ => {
681            // Return list
682            Ok(Some(FieldValue::list(inserted)))
683        }
684    }
685}
686
687/// Bind a JSON value to a sqlx query.
688fn bind_json_value<'q>(
689    query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
690    value: &serde_json::Value,
691) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
692    match value {
693        serde_json::Value::Null => query.bind(None::<String>),
694        serde_json::Value::Bool(b) => query.bind(*b),
695        serde_json::Value::Number(n) => {
696            if let Some(i) = n.as_i64() {
697                query.bind(i)
698            } else if let Some(f) = n.as_f64() {
699                query.bind(f)
700            } else {
701                query.bind(n.to_string())
702            }
703        }
704        serde_json::Value::String(s) => query.bind(s.clone()),
705        _ => query.bind(value.to_string()),
706    }
707}
708
709/// Execute an update mutation.
710async fn execute_update<'a>(
711    pool: &PgPool,
712    table_name: &str,
713    role: &str,
714    set_value: serde_json::Value,
715    where_clause: Option<serde_json::Value>,
716    mutation_type: MutationType,
717) -> Result<Option<FieldValue<'a>>, async_graphql::Error> {
718    use sqlx::Row;
719
720    trace!("Update mutation for {}: {:?}", table_name, set_value);
721
722    let set_map = match set_value {
723        serde_json::Value::Object(map) => map,
724        _ => return Err(async_graphql::Error::new("set must be an object")),
725    };
726
727    if set_map.is_empty() {
728        return Err(async_graphql::Error::new("set cannot be empty"));
729    }
730
731    let mut conn = pool.acquire().await?;
732
733    // Set role
734    sqlx::query(&format!(
735        "SET LOCAL ROLE {}",
736        postrust_sql::escape_ident(role)
737    ))
738    .execute(&mut *conn)
739    .await?;
740
741    // Build SET clause
742    let mut set_parts: Vec<String> = Vec::new();
743    let mut param_idx = 1;
744    for key in set_map.keys() {
745        set_parts.push(format!(
746            "{} = ${}",
747            postrust_sql::escape_ident(key),
748            param_idx
749        ));
750        param_idx += 1;
751    }
752
753    // Build WHERE clause
754    let (where_sql, where_values) = build_where_clause(where_clause.as_ref(), param_idx)?;
755
756    let sql = format!(
757        "UPDATE public.{} SET {} {} RETURNING row_to_json(public.{}.*)",
758        postrust_sql::escape_ident(table_name),
759        set_parts.join(", "),
760        where_sql,
761        postrust_sql::escape_ident(table_name)
762    );
763
764    trace!("Executing UPDATE SQL: {}", sql);
765
766    // Build query with parameters
767    let mut query = sqlx::query(&sql);
768
769    // Bind SET values
770    for val in set_map.values() {
771        query = bind_json_value(query, val);
772    }
773
774    // Bind WHERE values
775    for val in &where_values {
776        query = bind_json_value(query, val);
777    }
778
779    let rows = query.fetch_all(&mut *conn).await?;
780
781    let updated: Vec<FieldValue> = rows
782        .iter()
783        .filter_map(|row| row.try_get::<serde_json::Value, _>(0).ok())
784        .map(|v| FieldValue::value(json_to_value(v)))
785        .collect();
786
787    // Return based on mutation type
788    match mutation_type {
789        MutationType::UpdateByPk => Ok(updated.into_iter().next()),
790        _ => Ok(Some(FieldValue::list(updated))),
791    }
792}
793
794/// Execute a delete mutation.
795async fn execute_delete<'a>(
796    pool: &PgPool,
797    table_name: &str,
798    role: &str,
799    where_clause: Option<serde_json::Value>,
800    mutation_type: MutationType,
801) -> Result<Option<FieldValue<'a>>, async_graphql::Error> {
802    use sqlx::Row;
803
804    trace!("Delete mutation for {}", table_name);
805
806    let mut conn = pool.acquire().await?;
807
808    // Set role
809    sqlx::query(&format!(
810        "SET LOCAL ROLE {}",
811        postrust_sql::escape_ident(role)
812    ))
813    .execute(&mut *conn)
814    .await?;
815
816    // Build WHERE clause
817    let (where_sql, where_values) = build_where_clause(where_clause.as_ref(), 1)?;
818
819    let sql = format!(
820        "DELETE FROM public.{} {} RETURNING row_to_json(public.{}.*)",
821        postrust_sql::escape_ident(table_name),
822        where_sql,
823        postrust_sql::escape_ident(table_name)
824    );
825
826    trace!("Executing DELETE SQL: {}", sql);
827
828    // Build query with parameters
829    let mut query = sqlx::query(&sql);
830
831    // Bind WHERE values
832    for val in &where_values {
833        query = bind_json_value(query, val);
834    }
835
836    let rows = query.fetch_all(&mut *conn).await?;
837
838    let deleted: Vec<FieldValue> = rows
839        .iter()
840        .filter_map(|row| row.try_get::<serde_json::Value, _>(0).ok())
841        .map(|v| FieldValue::value(json_to_value(v)))
842        .collect();
843
844    // Return based on mutation type
845    match mutation_type {
846        MutationType::DeleteByPk => Ok(deleted.into_iter().next()),
847        _ => Ok(Some(FieldValue::list(deleted))),
848    }
849}
850
851/// Build a WHERE clause from a JSON filter object.
852fn build_where_clause(
853    where_value: Option<&serde_json::Value>,
854    start_param_idx: usize,
855) -> Result<(String, Vec<serde_json::Value>), async_graphql::Error> {
856    let mut conditions: Vec<String> = Vec::new();
857    let mut values: Vec<serde_json::Value> = Vec::new();
858    let mut param_idx = start_param_idx;
859
860    if let Some(serde_json::Value::Object(map)) = where_value {
861        for (key, val) in map {
862            match val {
863                serde_json::Value::Object(op_map) => {
864                    // Handle operators like {eq: value}, {gt: value}, etc.
865                    for (op, op_val) in op_map {
866                        let condition = match op.as_str() {
867                            "eq" | "_eq" => {
868                                format!("{} = ${}", postrust_sql::escape_ident(key), param_idx)
869                            }
870                            "neq" | "_neq" => {
871                                format!("{} != ${}", postrust_sql::escape_ident(key), param_idx)
872                            }
873                            "gt" | "_gt" => {
874                                format!("{} > ${}", postrust_sql::escape_ident(key), param_idx)
875                            }
876                            "gte" | "_gte" => {
877                                format!("{} >= ${}", postrust_sql::escape_ident(key), param_idx)
878                            }
879                            "lt" | "_lt" => {
880                                format!("{} < ${}", postrust_sql::escape_ident(key), param_idx)
881                            }
882                            "lte" | "_lte" => {
883                                format!("{} <= ${}", postrust_sql::escape_ident(key), param_idx)
884                            }
885                            "like" | "_like" => {
886                                format!("{} LIKE ${}", postrust_sql::escape_ident(key), param_idx)
887                            }
888                            "ilike" | "_ilike" => {
889                                format!("{} ILIKE ${}", postrust_sql::escape_ident(key), param_idx)
890                            }
891                            "is_null" | "_is_null" => {
892                                if op_val.as_bool().unwrap_or(false) {
893                                    format!("{} IS NULL", postrust_sql::escape_ident(key))
894                                } else {
895                                    format!("{} IS NOT NULL", postrust_sql::escape_ident(key))
896                                }
897                            }
898                            _ => continue,
899                        };
900
901                        if !op.contains("is_null") {
902                            conditions.push(condition);
903                            values.push(op_val.clone());
904                            param_idx += 1;
905                        } else {
906                            conditions.push(condition);
907                        }
908                    }
909                }
910                _ => {
911                    // Direct equality: {field: value}
912                    conditions.push(format!(
913                        "{} = ${}",
914                        postrust_sql::escape_ident(key),
915                        param_idx
916                    ));
917                    values.push(val.clone());
918                    param_idx += 1;
919                }
920            }
921        }
922    }
923
924    let where_sql = if conditions.is_empty() {
925        String::new()
926    } else {
927        format!("WHERE {}", conditions.join(" AND "))
928    };
929
930    Ok((where_sql, values))
931}
932
933/// Convert a GraphQL type string to a TypeRef.
934fn graphql_type_ref(type_str: &str) -> TypeRef {
935    // Parse type string like "[Users!]!" or "String" or "Int!"
936    let is_list = type_str.starts_with('[');
937    let is_nn = type_str.ends_with('!');
938
939    // Strip outer modifiers: first the trailing !, then the brackets
940    let inner = if is_list {
941        let stripped = type_str
942            .trim_end_matches('!') // Remove outer !
943            .trim_start_matches('[') // Remove [
944            .trim_end_matches(']'); // Remove ]
945        stripped
946    } else {
947        type_str.trim_end_matches('!')
948    };
949
950    let inner_nn = inner.ends_with('!');
951    let base_type = inner.trim_end_matches('!');
952
953    if is_list {
954        if is_nn {
955            if inner_nn {
956                TypeRef::named_nn_list_nn(base_type)
957            } else {
958                TypeRef::named_list_nn(base_type)
959            }
960        } else if inner_nn {
961            TypeRef::named_nn_list(base_type)
962        } else {
963            TypeRef::named_list(base_type)
964        }
965    } else if is_nn {
966        TypeRef::named_nn(base_type)
967    } else {
968        TypeRef::named(base_type)
969    }
970}
971
972/// Convert ValueAccessor to JSON.
973fn accessor_to_json(accessor: &ValueAccessor<'_>) -> serde_json::Value {
974    // Use the deserialize method if available, or convert manually
975    if accessor.is_null() {
976        serde_json::Value::Null
977    } else if let Ok(b) = accessor.boolean() {
978        serde_json::Value::Bool(b)
979    } else if let Ok(i) = accessor.i64() {
980        serde_json::Value::Number(i.into())
981    } else if let Ok(f) = accessor.f64() {
982        serde_json::Number::from_f64(f)
983            .map(serde_json::Value::Number)
984            .unwrap_or(serde_json::Value::Null)
985    } else if let Ok(s) = accessor.string() {
986        serde_json::Value::String(s.to_string())
987    } else if let Ok(list) = accessor.list() {
988        serde_json::Value::Array(list.iter().map(|v| accessor_to_json(&v)).collect())
989    } else if let Ok(obj) = accessor.object() {
990        let map: serde_json::Map<String, serde_json::Value> = obj
991            .iter()
992            .map(|(k, v)| (k.to_string(), accessor_to_json(&v)))
993            .collect();
994        serde_json::Value::Object(map)
995    } else {
996        serde_json::Value::Null
997    }
998}
999
1000/// Convert async-graphql Value to JSON.
1001#[allow(dead_code)]
1002fn value_to_json(value: &Value) -> serde_json::Value {
1003    match value {
1004        Value::Null => serde_json::Value::Null,
1005        Value::Boolean(b) => serde_json::Value::Bool(*b),
1006        Value::Number(n) => {
1007            if let Some(i) = n.as_i64() {
1008                serde_json::Value::Number(i.into())
1009            } else if let Some(f) = n.as_f64() {
1010                serde_json::Value::Number(serde_json::Number::from_f64(f).unwrap())
1011            } else {
1012                serde_json::Value::Null
1013            }
1014        }
1015        Value::String(s) => serde_json::Value::String(s.clone()),
1016        Value::List(arr) => serde_json::Value::Array(arr.iter().map(value_to_json).collect()),
1017        Value::Object(obj) => {
1018            let map: serde_json::Map<String, serde_json::Value> = obj
1019                .iter()
1020                .map(|(k, v)| (k.to_string(), value_to_json(v)))
1021                .collect();
1022            serde_json::Value::Object(map)
1023        }
1024        Value::Binary(b) => serde_json::Value::String(base64::Engine::encode(
1025            &base64::engine::general_purpose::STANDARD,
1026            b,
1027        )),
1028        Value::Enum(e) => serde_json::Value::String(e.to_string()),
1029    }
1030}
1031
1032/// Convert JSON to async-graphql Value.
1033fn json_to_value(json: serde_json::Value) -> Value {
1034    match json {
1035        serde_json::Value::Null => Value::Null,
1036        serde_json::Value::Bool(b) => Value::Boolean(b),
1037        serde_json::Value::Number(n) => {
1038            if let Some(i) = n.as_i64() {
1039                Value::Number(i.into())
1040            } else if let Some(f) = n.as_f64() {
1041                Value::Number(async_graphql::Number::from_f64(f).unwrap())
1042            } else {
1043                Value::Null
1044            }
1045        }
1046        serde_json::Value::String(s) => Value::String(s),
1047        serde_json::Value::Array(arr) => Value::List(arr.into_iter().map(json_to_value).collect()),
1048        serde_json::Value::Object(obj) => {
1049            let map: indexmap::IndexMap<async_graphql::Name, Value> = obj
1050                .into_iter()
1051                .map(|(k, v)| (async_graphql::Name::new(k), json_to_value(v)))
1052                .collect();
1053            Value::Object(map)
1054        }
1055    }
1056}
1057
1058/// Create BigInt scalar type.
1059fn create_bigint_scalar() -> Scalar {
1060    Scalar::new("BigInt")
1061        .description("64-bit integer")
1062        .specified_by_url("https://spec.graphql.org/draft/#sec-Int")
1063}
1064
1065/// Create BigDecimal scalar type.
1066fn create_bigdecimal_scalar() -> Scalar {
1067    Scalar::new("BigDecimal").description("Arbitrary precision decimal number")
1068}
1069
1070/// Create JSON scalar type.
1071fn create_json_scalar() -> Scalar {
1072    Scalar::new("JSON")
1073        .description("Arbitrary JSON value")
1074        .specified_by_url("https://spec.graphql.org/draft/#sec-Scalars")
1075}
1076
1077/// Create UUID scalar type.
1078fn create_uuid_scalar() -> Scalar {
1079    Scalar::new("UUID").description("UUID string")
1080}
1081
1082/// Create Date scalar type.
1083fn create_date_scalar() -> Scalar {
1084    Scalar::new("Date").description("ISO 8601 date string (YYYY-MM-DD)")
1085}
1086
1087/// Create DateTime scalar type.
1088fn create_datetime_scalar() -> Scalar {
1089    Scalar::new("DateTime").description("ISO 8601 datetime string")
1090}
1091
1092/// Create Time scalar type.
1093fn create_time_scalar() -> Scalar {
1094    Scalar::new("Time").description("ISO 8601 time string (HH:MM:SS)")
1095}
1096
1097/// Register filter input types.
1098fn register_filter_input_types(builder: SchemaBuilder) -> SchemaBuilder {
1099    let string_filter = InputObject::new("StringFilterInput")
1100        .field(InputValue::new("eq", TypeRef::named("String")))
1101        .field(InputValue::new("neq", TypeRef::named("String")))
1102        .field(InputValue::new("like", TypeRef::named("String")))
1103        .field(InputValue::new("ilike", TypeRef::named("String")))
1104        .field(InputValue::new("in", TypeRef::named_list("String")))
1105        .field(InputValue::new("isNull", TypeRef::named("Boolean")));
1106
1107    let int_filter = InputObject::new("IntFilterInput")
1108        .field(InputValue::new("eq", TypeRef::named("Int")))
1109        .field(InputValue::new("neq", TypeRef::named("Int")))
1110        .field(InputValue::new("gt", TypeRef::named("Int")))
1111        .field(InputValue::new("gte", TypeRef::named("Int")))
1112        .field(InputValue::new("lt", TypeRef::named("Int")))
1113        .field(InputValue::new("lte", TypeRef::named("Int")))
1114        .field(InputValue::new("in", TypeRef::named_list("Int")));
1115
1116    let boolean_filter = InputObject::new("BooleanFilterInput")
1117        .field(InputValue::new("eq", TypeRef::named("Boolean")));
1118
1119    builder
1120        .register(string_filter)
1121        .register(int_filter)
1122        .register(boolean_filter)
1123}
1124
1125#[cfg(test)]
1126mod tests {
1127    use super::*;
1128    use indexmap::IndexMap;
1129    use postrust_core::schema_cache::{Column, Table};
1130    use std::collections::{HashMap, HashSet};
1131
1132    fn create_test_table(name: &str) -> Table {
1133        let mut columns = IndexMap::new();
1134        columns.insert(
1135            "id".into(),
1136            Column {
1137                name: "id".into(),
1138                description: None,
1139                nullable: false,
1140                data_type: "integer".into(),
1141                nominal_type: "int4".into(),
1142                max_len: None,
1143                default: Some("nextval('id_seq')".into()),
1144                enum_values: vec![],
1145                is_pk: true,
1146                position: 1,
1147            },
1148        );
1149        columns.insert(
1150            "name".into(),
1151            Column {
1152                name: "name".into(),
1153                description: None,
1154                nullable: false,
1155                data_type: "text".into(),
1156                nominal_type: "text".into(),
1157                max_len: None,
1158                default: None,
1159                enum_values: vec![],
1160                is_pk: false,
1161                position: 2,
1162            },
1163        );
1164
1165        Table {
1166            schema: "public".into(),
1167            name: name.into(),
1168            description: None,
1169            is_view: false,
1170            insertable: true,
1171            updatable: true,
1172            deletable: true,
1173            pk_cols: vec!["id".into()],
1174            columns,
1175        }
1176    }
1177
1178    fn create_test_schema_cache() -> SchemaCache {
1179        let mut tables = HashMap::new();
1180        let users = create_test_table("users");
1181        tables.insert(users.qualified_identifier(), users);
1182
1183        SchemaCache {
1184            tables,
1185            relationships: HashMap::new(),
1186            routines: HashMap::new(),
1187            timezones: HashSet::new(),
1188            pg_version: 150000,
1189        }
1190    }
1191
1192    // ============================================================================
1193    // Type Reference Tests
1194    // ============================================================================
1195
1196    #[test]
1197    fn test_graphql_type_ref_simple() {
1198        let _type_ref = graphql_type_ref("String");
1199        // TypeRef doesn't implement PartialEq, so we just test it doesn't panic
1200    }
1201
1202    #[test]
1203    fn test_graphql_type_ref_non_null() {
1204        let _type_ref = graphql_type_ref("String!");
1205    }
1206
1207    #[test]
1208    fn test_graphql_type_ref_list() {
1209        let _type_ref = graphql_type_ref("[String]");
1210    }
1211
1212    #[test]
1213    fn test_graphql_type_ref_list_non_null() {
1214        let _type_ref = graphql_type_ref("[String!]!");
1215    }
1216
1217    // ============================================================================
1218    // Value Conversion Tests
1219    // ============================================================================
1220
1221    #[test]
1222    fn test_value_to_json_null() {
1223        let value = Value::Null;
1224        let json = value_to_json(&value);
1225        assert_eq!(json, serde_json::Value::Null);
1226    }
1227
1228    #[test]
1229    fn test_value_to_json_boolean() {
1230        let value = Value::Boolean(true);
1231        let json = value_to_json(&value);
1232        assert_eq!(json, serde_json::Value::Bool(true));
1233    }
1234
1235    #[test]
1236    fn test_value_to_json_number() {
1237        let value = Value::Number(42.into());
1238        let json = value_to_json(&value);
1239        assert_eq!(json, serde_json::json!(42));
1240    }
1241
1242    #[test]
1243    fn test_value_to_json_string() {
1244        let value = Value::String("hello".to_string());
1245        let json = value_to_json(&value);
1246        assert_eq!(json, serde_json::Value::String("hello".to_string()));
1247    }
1248
1249    #[test]
1250    fn test_value_to_json_list() {
1251        let value = Value::List(vec![Value::Number(1.into()), Value::Number(2.into())]);
1252        let json = value_to_json(&value);
1253        assert_eq!(json, serde_json::json!([1, 2]));
1254    }
1255
1256    #[test]
1257    fn test_json_to_value_null() {
1258        let json = serde_json::Value::Null;
1259        let value = json_to_value(json);
1260        assert!(matches!(value, Value::Null));
1261    }
1262
1263    #[test]
1264    fn test_json_to_value_boolean() {
1265        let json = serde_json::Value::Bool(false);
1266        let value = json_to_value(json);
1267        assert!(matches!(value, Value::Boolean(false)));
1268    }
1269
1270    #[test]
1271    fn test_json_to_value_number() {
1272        let json = serde_json::json!(123);
1273        let value = json_to_value(json);
1274        assert!(matches!(value, Value::Number(_)));
1275    }
1276
1277    #[test]
1278    fn test_json_to_value_string() {
1279        let json = serde_json::Value::String("test".to_string());
1280        let value = json_to_value(json);
1281        assert!(matches!(value, Value::String(_)));
1282    }
1283
1284    #[test]
1285    fn test_json_to_value_array() {
1286        let json = serde_json::json!([1, 2, 3]);
1287        let value = json_to_value(json);
1288        assert!(matches!(value, Value::List(_)));
1289    }
1290
1291    #[test]
1292    fn test_json_to_value_object() {
1293        let json = serde_json::json!({"key": "value"});
1294        let value = json_to_value(json);
1295        assert!(matches!(value, Value::Object(_)));
1296    }
1297
1298    // ============================================================================
1299    // Schema Building Tests
1300    // ============================================================================
1301
1302    #[test]
1303    fn test_build_dynamic_schema() {
1304        let cache = create_test_schema_cache();
1305        let config = SchemaConfig::default();
1306        let generated = build_schema(&cache, &config);
1307
1308        let result = build_dynamic_schema(&generated, &cache, None);
1309        if let Err(ref e) = result {
1310            eprintln!("Schema build error: {:?}", e);
1311        }
1312        assert!(result.is_ok(), "Schema build failed: {:?}", result.err());
1313    }
1314
1315    #[test]
1316    fn test_create_object_type() {
1317        let table = create_test_table("users");
1318        let obj = TableObjectType::from_table(&table);
1319        let _gql_obj = create_object_type(&obj);
1320    }
1321
1322    #[test]
1323    fn test_create_query_type() {
1324        let cache = create_test_schema_cache();
1325        let config = SchemaConfig::default();
1326        let generated = build_schema(&cache, &config);
1327
1328        let _query = create_query_type(&generated);
1329    }
1330
1331    #[test]
1332    fn test_create_mutation_type() {
1333        let cache = create_test_schema_cache();
1334        let config = SchemaConfig::default();
1335        let generated = build_schema(&cache, &config);
1336
1337        let _mutation = create_mutation_type(&generated);
1338    }
1339
1340    // ============================================================================
1341    // Scalar Tests
1342    // ============================================================================
1343
1344    #[test]
1345    fn test_create_scalars() {
1346        let _bigint = create_bigint_scalar();
1347        let _json = create_json_scalar();
1348        let _uuid = create_uuid_scalar();
1349        let _datetime = create_datetime_scalar();
1350    }
1351
1352    // ============================================================================
1353    // Filter Input Type Tests
1354    // ============================================================================
1355
1356    #[test]
1357    fn test_register_filter_input_types() {
1358        let cache = create_test_schema_cache();
1359        let config = SchemaConfig::default();
1360        let _generated = build_schema(&cache, &config);
1361
1362        // Build a minimal schema with filter types
1363        let query =
1364            Object::new("Query").field(Field::new("test", TypeRef::named("String"), |_| {
1365                FieldFuture::new(async { Ok(None::<FieldValue>) })
1366            }));
1367
1368        let mut builder = Schema::build("Query", None::<&str>, None);
1369        builder = builder.register(query);
1370        builder = register_filter_input_types(builder);
1371
1372        let result = builder.finish();
1373        assert!(result.is_ok());
1374    }
1375
1376    // ============================================================================
1377    // Subscription Tests
1378    // ============================================================================
1379
1380    #[test]
1381    fn test_build_schema_with_subscriptions() {
1382        let cache = create_test_schema_cache();
1383        let config = SchemaConfig {
1384            enable_subscriptions: true,
1385            ..SchemaConfig::default()
1386        };
1387        let generated = build_schema(&cache, &config);
1388
1389        // Generate subscription fields
1390        let sub_fields = generate_subscription_fields(&cache, &generated);
1391        assert!(!sub_fields.is_empty(), "Should have subscription fields");
1392
1393        // Build schema with subscriptions
1394        let result = build_dynamic_schema(&generated, &cache, Some(&sub_fields));
1395        assert!(result.is_ok(), "Schema with subscriptions should build");
1396    }
1397
1398    #[test]
1399    fn test_subscription_field_generation() {
1400        let cache = create_test_schema_cache();
1401        let config = SchemaConfig::default();
1402        let generated = build_schema(&cache, &config);
1403
1404        let fields = generate_subscription_fields(&cache, &generated);
1405
1406        // Should have one subscription field for the users table
1407        assert_eq!(fields.len(), 1);
1408        assert_eq!(fields[0].name, "users");
1409        assert_eq!(fields[0].table_name, "users");
1410        assert_eq!(fields[0].channel_name(), "postrust_public_users");
1411    }
1412
1413    #[test]
1414    fn test_create_subscription_type() {
1415        use crate::subscription::SubscriptionField as SubField;
1416
1417        let fields = vec![
1418            SubField::for_table("public", "users", "Users"),
1419            SubField::for_table("public", "orders", "Orders"),
1420        ];
1421
1422        let _subscription = create_subscription_type(&fields);
1423        // Just test that it doesn't panic
1424    }
1425}