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};
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_type = graphql_type_ref(&field.type_string());
271        let mut gql_field = Field::new(&field.name, field_type, |_| {
272            FieldFuture::new(async move { Ok(None::<FieldValue>) })
273        });
274
275        if let Some(desc) = &field.description {
276            gql_field = gql_field.description(desc);
277        }
278
279        object = object.field(gql_field);
280    }
281
282    object
283}
284
285/// Create the Query type with all table query fields.
286fn create_query_type(generated: &GeneratedSchema) -> Object {
287    let mut query = Object::new("Query");
288
289    for field in &generated.query_fields {
290        let table_name = field.table_name.clone();
291        let is_by_pk = field.is_by_pk;
292        let return_type = graphql_type_ref(&field.return_type);
293
294        let mut gql_field = Field::new(&field.name, return_type, move |ctx| {
295            let table_name = table_name.clone();
296            FieldFuture::new(async move {
297                resolve_query(&ctx, &table_name, is_by_pk).await
298            })
299        });
300
301        // Add standard query arguments
302        if !is_by_pk {
303            gql_field = gql_field
304                .argument(InputValue::new("filter", TypeRef::named("JSON")))
305                .argument(InputValue::new("orderBy", TypeRef::named_list("String")))
306                .argument(InputValue::new("limit", TypeRef::named("Int")))
307                .argument(InputValue::new("offset", TypeRef::named("Int")));
308        } else {
309            // Add PK arguments
310            gql_field = gql_field.argument(InputValue::new("id", TypeRef::named_nn("Int")));
311        }
312
313        if let Some(desc) = &field.description {
314            gql_field = gql_field.description(desc);
315        }
316
317        query = query.field(gql_field);
318    }
319
320    // Add introspection queries
321    query = query.field(
322        Field::new("_schema", TypeRef::named("String"), |_| {
323            FieldFuture::new(async move {
324                Ok(Some(Value::String("Postrust GraphQL Schema".to_string())))
325            })
326        })
327        .description("Schema introspection"),
328    );
329
330    query
331}
332
333/// Create the Mutation type with all mutation fields.
334fn create_mutation_type(generated: &GeneratedSchema) -> Object {
335    let mut mutation = Object::new("Mutation");
336
337    for field in &generated.mutation_fields {
338        let table_name = field.table_name.clone();
339        let mutation_type = field.mutation_type;
340        let return_type = graphql_type_ref(&field.return_type);
341
342        let mut gql_field = Field::new(&field.name, return_type, move |ctx| {
343            let table_name = table_name.clone();
344            FieldFuture::new(async move {
345                resolve_mutation(&ctx, &table_name, mutation_type).await
346            })
347        });
348
349        // Add mutation-specific arguments
350        match mutation_type {
351            MutationType::Insert | MutationType::InsertOne => {
352                gql_field = gql_field
353                    .argument(InputValue::new("objects", TypeRef::named_nn_list("JSON")));
354            }
355            MutationType::Update | MutationType::UpdateByPk => {
356                gql_field = gql_field
357                    .argument(InputValue::new("where", TypeRef::named("JSON")))
358                    .argument(InputValue::new("set", TypeRef::named_nn("JSON")));
359            }
360            MutationType::Delete | MutationType::DeleteByPk => {
361                gql_field = gql_field.argument(InputValue::new("where", TypeRef::named("JSON")));
362            }
363        }
364
365        if let Some(desc) = &field.description {
366            gql_field = gql_field.description(desc);
367        }
368
369        mutation = mutation.field(gql_field);
370    }
371
372    mutation
373}
374
375/// Create the Subscription type with all subscription fields.
376fn create_subscription_type(fields: &[SubField]) -> Subscription {
377    let mut subscription = Subscription::new("Subscription");
378
379    for field in fields {
380        let channel_name = field.channel_name();
381        let return_type = TypeRef::named(&field.return_type);
382        let field_name = field.name.clone();
383        let description = field.description.clone();
384
385        let gql_field = SubscriptionField::new(&field_name, return_type, move |ctx| {
386            let channel_name = channel_name.clone();
387            SubscriptionFieldFuture::new(async move {
388                let broker_arc = ctx.data::<Arc<RwLock<Option<NotifyBroker>>>>()?;
389                let broker_guard = broker_arc.read().await;
390
391                let broker = broker_guard
392                    .as_ref()
393                    .ok_or_else(|| async_graphql::Error::new("Subscription broker not initialized"))?;
394
395                let stream = broker
396                    .subscribe(&channel_name)
397                    .await
398                    .map_err(|e| async_graphql::Error::new(format!("Subscription error: {}", e)))?;
399
400                // Transform notification stream to GraphQL values
401                let value_stream = stream.filter_map(|notification| async move {
402                    match TableChangePayload::from_payload(&notification.payload) {
403                        Ok(payload) => {
404                            if let Some(data) = payload.data() {
405                                Some(Ok(FieldValue::value(json_to_value(data.clone()))))
406                            } else {
407                                None
408                            }
409                        }
410                        Err(e) => {
411                            debug!("Failed to parse notification payload: {}", e);
412                            None
413                        }
414                    }
415                });
416
417                Ok(value_stream)
418            })
419        });
420
421        let gql_field = if let Some(desc) = description {
422            gql_field.description(desc)
423        } else {
424            gql_field
425        };
426
427        subscription = subscription.field(gql_field);
428    }
429
430    subscription
431}
432
433/// Resolve a query field.
434async fn resolve_query(
435    ctx: &ResolverContext<'_>,
436    table_name: &str,
437    is_by_pk: bool,
438) -> Result<Option<Value>, async_graphql::Error> {
439    let pool = ctx.data::<PgPool>()?;
440    let gql_ctx = ctx.data::<GraphQLContext>()?;
441
442    debug!("Resolving query for table: {}", table_name);
443
444    // Extract pagination arguments
445    let limit: Option<i64> = ctx
446        .args
447        .try_get("limit")
448        .ok()
449        .and_then(|v| v.i64().ok());
450
451    let offset: Option<i64> = ctx
452        .args
453        .try_get("offset")
454        .ok()
455        .and_then(|v| v.i64().ok());
456
457    // Build simple query
458    let mut sql = format!(
459        "SELECT row_to_json(t) FROM (SELECT * FROM public.{}) t",
460        table_name
461    );
462
463    if let Some(limit) = limit {
464        sql.push_str(&format!(" LIMIT {}", limit));
465    }
466
467    if let Some(offset) = offset {
468        sql.push_str(&format!(" OFFSET {}", offset));
469    }
470
471    // Execute query
472    let result = execute_query(pool, &sql, gql_ctx.role()).await?;
473
474    if is_by_pk {
475        Ok(result.first().cloned())
476    } else {
477        Ok(Some(Value::List(result)))
478    }
479}
480
481/// Resolve a mutation field.
482async fn resolve_mutation(
483    ctx: &ResolverContext<'_>,
484    table_name: &str,
485    mutation_type: MutationType,
486) -> Result<Option<Value>, async_graphql::Error> {
487    let pool = ctx.data::<PgPool>()?;
488    let gql_ctx = ctx.data::<GraphQLContext>()?;
489
490    debug!("Resolving mutation for table: {} type: {:?}", table_name, mutation_type);
491
492    let result = match mutation_type {
493        MutationType::Insert | MutationType::InsertOne => {
494            let objects = ctx
495                .args
496                .try_get("objects")
497                .ok()
498                .map(|v| accessor_to_json(&v))
499                .unwrap_or_else(|| serde_json::Value::Array(vec![]));
500
501            execute_insert(pool, table_name, gql_ctx.role(), objects).await?
502        }
503        MutationType::Update | MutationType::UpdateByPk => {
504            let set_value = ctx
505                .args
506                .try_get("set")
507                .ok()
508                .map(|v| accessor_to_json(&v))
509                .unwrap_or_else(|| serde_json::json!({}));
510
511            execute_update(pool, table_name, gql_ctx.role(), set_value).await?
512        }
513        MutationType::Delete | MutationType::DeleteByPk => {
514            execute_delete(pool, table_name, gql_ctx.role()).await?
515        }
516    };
517
518    Ok(Some(result))
519}
520
521/// Execute a SQL query and return results.
522async fn execute_query(
523    pool: &PgPool,
524    sql: &str,
525    role: &str,
526) -> Result<Vec<Value>, async_graphql::Error> {
527    use sqlx::Row;
528
529    debug!("Executing SQL: {}", sql);
530
531    let mut conn = pool.acquire().await?;
532
533    // Set role
534    sqlx::query(&format!("SET LOCAL ROLE {}", postrust_sql::escape_ident(role)))
535        .execute(&mut *conn)
536        .await?;
537
538    // Execute query
539    let rows = sqlx::query(sql).fetch_all(&mut *conn).await?;
540
541    // Convert to GraphQL values
542    let results: Vec<Value> = rows
543        .iter()
544        .filter_map(|row| {
545            row.try_get::<serde_json::Value, _>(0)
546                .ok()
547                .map(json_to_value)
548        })
549        .collect();
550
551    Ok(results)
552}
553
554/// Execute an insert mutation.
555async fn execute_insert(
556    _pool: &PgPool,
557    table_name: &str,
558    _role: &str,
559    objects: serde_json::Value,
560) -> Result<Value, async_graphql::Error> {
561    // For now, return empty array - full implementation would execute INSERT
562    debug!("Insert mutation for {}: {:?}", table_name, objects);
563    Ok(Value::List(vec![]))
564}
565
566/// Execute an update mutation.
567async fn execute_update(
568    _pool: &PgPool,
569    table_name: &str,
570    _role: &str,
571    set_value: serde_json::Value,
572) -> Result<Value, async_graphql::Error> {
573    // For now, return empty array - full implementation would execute UPDATE
574    debug!("Update mutation for {}: {:?}", table_name, set_value);
575    Ok(Value::List(vec![]))
576}
577
578/// Execute a delete mutation.
579async fn execute_delete(
580    _pool: &PgPool,
581    table_name: &str,
582    _role: &str,
583) -> Result<Value, async_graphql::Error> {
584    // For now, return empty array - full implementation would execute DELETE
585    debug!("Delete mutation for {}", table_name);
586    Ok(Value::List(vec![]))
587}
588
589/// Convert a GraphQL type string to a TypeRef.
590fn graphql_type_ref(type_str: &str) -> TypeRef {
591    // Parse type string like "[Users!]!" or "String" or "Int!"
592    let is_list = type_str.starts_with('[');
593    let is_nn = type_str.ends_with('!');
594
595    // Strip outer modifiers: first the trailing !, then the brackets
596    let inner = if is_list {
597        let stripped = type_str
598            .trim_end_matches('!')  // Remove outer !
599            .trim_start_matches('[')  // Remove [
600            .trim_end_matches(']');   // Remove ]
601        stripped
602    } else {
603        type_str.trim_end_matches('!')
604    };
605
606    let inner_nn = inner.ends_with('!');
607    let base_type = inner.trim_end_matches('!');
608
609    if is_list {
610        if is_nn {
611            if inner_nn {
612                TypeRef::named_nn_list_nn(base_type)
613            } else {
614                TypeRef::named_list_nn(base_type)
615            }
616        } else if inner_nn {
617            TypeRef::named_nn_list(base_type)
618        } else {
619            TypeRef::named_list(base_type)
620        }
621    } else if is_nn {
622        TypeRef::named_nn(base_type)
623    } else {
624        TypeRef::named(base_type)
625    }
626}
627
628/// Convert ValueAccessor to JSON.
629fn accessor_to_json(accessor: &ValueAccessor<'_>) -> serde_json::Value {
630    // Use the deserialize method if available, or convert manually
631    if accessor.is_null() {
632        serde_json::Value::Null
633    } else if let Ok(b) = accessor.boolean() {
634        serde_json::Value::Bool(b)
635    } else if let Ok(i) = accessor.i64() {
636        serde_json::Value::Number(i.into())
637    } else if let Ok(f) = accessor.f64() {
638        serde_json::Number::from_f64(f)
639            .map(serde_json::Value::Number)
640            .unwrap_or(serde_json::Value::Null)
641    } else if let Ok(s) = accessor.string() {
642        serde_json::Value::String(s.to_string())
643    } else if let Ok(list) = accessor.list() {
644        serde_json::Value::Array(
645            list.iter()
646                .map(|v| accessor_to_json(&v))
647                .collect()
648        )
649    } else if let Ok(obj) = accessor.object() {
650        let map: serde_json::Map<String, serde_json::Value> = obj
651            .iter()
652            .map(|(k, v)| (k.to_string(), accessor_to_json(&v)))
653            .collect();
654        serde_json::Value::Object(map)
655    } else {
656        serde_json::Value::Null
657    }
658}
659
660/// Convert async-graphql Value to JSON.
661fn value_to_json(value: &Value) -> serde_json::Value {
662    match value {
663        Value::Null => serde_json::Value::Null,
664        Value::Boolean(b) => serde_json::Value::Bool(*b),
665        Value::Number(n) => {
666            if let Some(i) = n.as_i64() {
667                serde_json::Value::Number(i.into())
668            } else if let Some(f) = n.as_f64() {
669                serde_json::Value::Number(serde_json::Number::from_f64(f).unwrap())
670            } else {
671                serde_json::Value::Null
672            }
673        }
674        Value::String(s) => serde_json::Value::String(s.clone()),
675        Value::List(arr) => {
676            serde_json::Value::Array(arr.iter().map(value_to_json).collect())
677        }
678        Value::Object(obj) => {
679            let map: serde_json::Map<String, serde_json::Value> = obj
680                .iter()
681                .map(|(k, v)| (k.to_string(), value_to_json(v)))
682                .collect();
683            serde_json::Value::Object(map)
684        }
685        Value::Binary(b) => serde_json::Value::String(base64::Engine::encode(
686            &base64::engine::general_purpose::STANDARD,
687            b,
688        )),
689        Value::Enum(e) => serde_json::Value::String(e.to_string()),
690    }
691}
692
693/// Convert JSON to async-graphql Value.
694fn json_to_value(json: serde_json::Value) -> Value {
695    match json {
696        serde_json::Value::Null => Value::Null,
697        serde_json::Value::Bool(b) => Value::Boolean(b),
698        serde_json::Value::Number(n) => {
699            if let Some(i) = n.as_i64() {
700                Value::Number(i.into())
701            } else if let Some(f) = n.as_f64() {
702                Value::Number(async_graphql::Number::from_f64(f).unwrap())
703            } else {
704                Value::Null
705            }
706        }
707        serde_json::Value::String(s) => Value::String(s),
708        serde_json::Value::Array(arr) => {
709            Value::List(arr.into_iter().map(json_to_value).collect())
710        }
711        serde_json::Value::Object(obj) => {
712            let map: indexmap::IndexMap<async_graphql::Name, Value> = obj
713                .into_iter()
714                .map(|(k, v)| (async_graphql::Name::new(k), json_to_value(v)))
715                .collect();
716            Value::Object(map)
717        }
718    }
719}
720
721/// Create BigInt scalar type.
722fn create_bigint_scalar() -> Scalar {
723    Scalar::new("BigInt")
724        .description("64-bit integer")
725        .specified_by_url("https://spec.graphql.org/draft/#sec-Int")
726}
727
728/// Create BigDecimal scalar type.
729fn create_bigdecimal_scalar() -> Scalar {
730    Scalar::new("BigDecimal")
731        .description("Arbitrary precision decimal number")
732}
733
734/// Create JSON scalar type.
735fn create_json_scalar() -> Scalar {
736    Scalar::new("JSON")
737        .description("Arbitrary JSON value")
738        .specified_by_url("https://spec.graphql.org/draft/#sec-Scalars")
739}
740
741/// Create UUID scalar type.
742fn create_uuid_scalar() -> Scalar {
743    Scalar::new("UUID").description("UUID string")
744}
745
746/// Create Date scalar type.
747fn create_date_scalar() -> Scalar {
748    Scalar::new("Date").description("ISO 8601 date string (YYYY-MM-DD)")
749}
750
751/// Create DateTime scalar type.
752fn create_datetime_scalar() -> Scalar {
753    Scalar::new("DateTime").description("ISO 8601 datetime string")
754}
755
756/// Create Time scalar type.
757fn create_time_scalar() -> Scalar {
758    Scalar::new("Time").description("ISO 8601 time string (HH:MM:SS)")
759}
760
761/// Register filter input types.
762fn register_filter_input_types(builder: SchemaBuilder) -> SchemaBuilder {
763    let string_filter = InputObject::new("StringFilterInput")
764        .field(InputValue::new("eq", TypeRef::named("String")))
765        .field(InputValue::new("neq", TypeRef::named("String")))
766        .field(InputValue::new("like", TypeRef::named("String")))
767        .field(InputValue::new("ilike", TypeRef::named("String")))
768        .field(InputValue::new("in", TypeRef::named_list("String")))
769        .field(InputValue::new("isNull", TypeRef::named("Boolean")));
770
771    let int_filter = InputObject::new("IntFilterInput")
772        .field(InputValue::new("eq", TypeRef::named("Int")))
773        .field(InputValue::new("neq", TypeRef::named("Int")))
774        .field(InputValue::new("gt", TypeRef::named("Int")))
775        .field(InputValue::new("gte", TypeRef::named("Int")))
776        .field(InputValue::new("lt", TypeRef::named("Int")))
777        .field(InputValue::new("lte", TypeRef::named("Int")))
778        .field(InputValue::new("in", TypeRef::named_list("Int")));
779
780    let boolean_filter = InputObject::new("BooleanFilterInput")
781        .field(InputValue::new("eq", TypeRef::named("Boolean")));
782
783    builder
784        .register(string_filter)
785        .register(int_filter)
786        .register(boolean_filter)
787}
788
789#[cfg(test)]
790mod tests {
791    use super::*;
792    use indexmap::IndexMap;
793    use postrust_core::schema_cache::{Column, Table};
794    use std::collections::{HashMap, HashSet};
795
796    fn create_test_table(name: &str) -> Table {
797        let mut columns = IndexMap::new();
798        columns.insert(
799            "id".into(),
800            Column {
801                name: "id".into(),
802                description: None,
803                nullable: false,
804                data_type: "integer".into(),
805                nominal_type: "int4".into(),
806                max_len: None,
807                default: Some("nextval('id_seq')".into()),
808                enum_values: vec![],
809                is_pk: true,
810                position: 1,
811            },
812        );
813        columns.insert(
814            "name".into(),
815            Column {
816                name: "name".into(),
817                description: None,
818                nullable: false,
819                data_type: "text".into(),
820                nominal_type: "text".into(),
821                max_len: None,
822                default: None,
823                enum_values: vec![],
824                is_pk: false,
825                position: 2,
826            },
827        );
828
829        Table {
830            schema: "public".into(),
831            name: name.into(),
832            description: None,
833            is_view: false,
834            insertable: true,
835            updatable: true,
836            deletable: true,
837            pk_cols: vec!["id".into()],
838            columns,
839        }
840    }
841
842    fn create_test_schema_cache() -> SchemaCache {
843        let mut tables = HashMap::new();
844        let users = create_test_table("users");
845        tables.insert(users.qualified_identifier(), users);
846
847        SchemaCache {
848            tables,
849            relationships: HashMap::new(),
850            routines: HashMap::new(),
851            timezones: HashSet::new(),
852            pg_version: 150000,
853        }
854    }
855
856    // ============================================================================
857    // Type Reference Tests
858    // ============================================================================
859
860    #[test]
861    fn test_graphql_type_ref_simple() {
862        let _type_ref = graphql_type_ref("String");
863        // TypeRef doesn't implement PartialEq, so we just test it doesn't panic
864    }
865
866    #[test]
867    fn test_graphql_type_ref_non_null() {
868        let _type_ref = graphql_type_ref("String!");
869    }
870
871    #[test]
872    fn test_graphql_type_ref_list() {
873        let _type_ref = graphql_type_ref("[String]");
874    }
875
876    #[test]
877    fn test_graphql_type_ref_list_non_null() {
878        let _type_ref = graphql_type_ref("[String!]!");
879    }
880
881    // ============================================================================
882    // Value Conversion Tests
883    // ============================================================================
884
885    #[test]
886    fn test_value_to_json_null() {
887        let value = Value::Null;
888        let json = value_to_json(&value);
889        assert_eq!(json, serde_json::Value::Null);
890    }
891
892    #[test]
893    fn test_value_to_json_boolean() {
894        let value = Value::Boolean(true);
895        let json = value_to_json(&value);
896        assert_eq!(json, serde_json::Value::Bool(true));
897    }
898
899    #[test]
900    fn test_value_to_json_number() {
901        let value = Value::Number(42.into());
902        let json = value_to_json(&value);
903        assert_eq!(json, serde_json::json!(42));
904    }
905
906    #[test]
907    fn test_value_to_json_string() {
908        let value = Value::String("hello".to_string());
909        let json = value_to_json(&value);
910        assert_eq!(json, serde_json::Value::String("hello".to_string()));
911    }
912
913    #[test]
914    fn test_value_to_json_list() {
915        let value = Value::List(vec![Value::Number(1.into()), Value::Number(2.into())]);
916        let json = value_to_json(&value);
917        assert_eq!(json, serde_json::json!([1, 2]));
918    }
919
920    #[test]
921    fn test_json_to_value_null() {
922        let json = serde_json::Value::Null;
923        let value = json_to_value(json);
924        assert!(matches!(value, Value::Null));
925    }
926
927    #[test]
928    fn test_json_to_value_boolean() {
929        let json = serde_json::Value::Bool(false);
930        let value = json_to_value(json);
931        assert!(matches!(value, Value::Boolean(false)));
932    }
933
934    #[test]
935    fn test_json_to_value_number() {
936        let json = serde_json::json!(123);
937        let value = json_to_value(json);
938        assert!(matches!(value, Value::Number(_)));
939    }
940
941    #[test]
942    fn test_json_to_value_string() {
943        let json = serde_json::Value::String("test".to_string());
944        let value = json_to_value(json);
945        assert!(matches!(value, Value::String(_)));
946    }
947
948    #[test]
949    fn test_json_to_value_array() {
950        let json = serde_json::json!([1, 2, 3]);
951        let value = json_to_value(json);
952        assert!(matches!(value, Value::List(_)));
953    }
954
955    #[test]
956    fn test_json_to_value_object() {
957        let json = serde_json::json!({"key": "value"});
958        let value = json_to_value(json);
959        assert!(matches!(value, Value::Object(_)));
960    }
961
962    // ============================================================================
963    // Schema Building Tests
964    // ============================================================================
965
966    #[test]
967    fn test_build_dynamic_schema() {
968        let cache = create_test_schema_cache();
969        let config = SchemaConfig::default();
970        let generated = build_schema(&cache, &config);
971
972        let result = build_dynamic_schema(&generated, &cache, None);
973        if let Err(ref e) = result {
974            eprintln!("Schema build error: {:?}", e);
975        }
976        assert!(result.is_ok(), "Schema build failed: {:?}", result.err());
977    }
978
979    #[test]
980    fn test_create_object_type() {
981        let table = create_test_table("users");
982        let obj = TableObjectType::from_table(&table);
983        let _gql_obj = create_object_type(&obj);
984    }
985
986    #[test]
987    fn test_create_query_type() {
988        let cache = create_test_schema_cache();
989        let config = SchemaConfig::default();
990        let generated = build_schema(&cache, &config);
991
992        let _query = create_query_type(&generated);
993    }
994
995    #[test]
996    fn test_create_mutation_type() {
997        let cache = create_test_schema_cache();
998        let config = SchemaConfig::default();
999        let generated = build_schema(&cache, &config);
1000
1001        let _mutation = create_mutation_type(&generated);
1002    }
1003
1004    // ============================================================================
1005    // Scalar Tests
1006    // ============================================================================
1007
1008    #[test]
1009    fn test_create_scalars() {
1010        let _bigint = create_bigint_scalar();
1011        let _json = create_json_scalar();
1012        let _uuid = create_uuid_scalar();
1013        let _datetime = create_datetime_scalar();
1014    }
1015
1016    // ============================================================================
1017    // Filter Input Type Tests
1018    // ============================================================================
1019
1020    #[test]
1021    fn test_register_filter_input_types() {
1022        let cache = create_test_schema_cache();
1023        let config = SchemaConfig::default();
1024        let _generated = build_schema(&cache, &config);
1025
1026        // Build a minimal schema with filter types
1027        let query = Object::new("Query").field(Field::new(
1028            "test",
1029            TypeRef::named("String"),
1030            |_| FieldFuture::new(async { Ok(None::<FieldValue>) }),
1031        ));
1032
1033        let mut builder = Schema::build("Query", None::<&str>, None);
1034        builder = builder.register(query);
1035        builder = register_filter_input_types(builder);
1036
1037        let result = builder.finish();
1038        assert!(result.is_ok());
1039    }
1040
1041    // ============================================================================
1042    // Subscription Tests
1043    // ============================================================================
1044
1045    #[test]
1046    fn test_build_schema_with_subscriptions() {
1047        let cache = create_test_schema_cache();
1048        let config = SchemaConfig {
1049            enable_subscriptions: true,
1050            ..SchemaConfig::default()
1051        };
1052        let generated = build_schema(&cache, &config);
1053
1054        // Generate subscription fields
1055        let sub_fields = generate_subscription_fields(&cache, &generated);
1056        assert!(!sub_fields.is_empty(), "Should have subscription fields");
1057
1058        // Build schema with subscriptions
1059        let result = build_dynamic_schema(&generated, &cache, Some(&sub_fields));
1060        assert!(result.is_ok(), "Schema with subscriptions should build");
1061    }
1062
1063    #[test]
1064    fn test_subscription_field_generation() {
1065        let cache = create_test_schema_cache();
1066        let config = SchemaConfig::default();
1067        let generated = build_schema(&cache, &config);
1068
1069        let fields = generate_subscription_fields(&cache, &generated);
1070
1071        // Should have one subscription field for the users table
1072        assert_eq!(fields.len(), 1);
1073        assert_eq!(fields[0].name, "users");
1074        assert_eq!(fields[0].table_name, "users");
1075        assert_eq!(fields[0].channel_name(), "postrust_public_users");
1076    }
1077
1078    #[test]
1079    fn test_create_subscription_type() {
1080        use crate::subscription::SubscriptionField as SubField;
1081
1082        let fields = vec![
1083            SubField::for_table("public", "users", "Users"),
1084            SubField::for_table("public", "orders", "Orders"),
1085        ];
1086
1087        let _subscription = create_subscription_type(&fields);
1088        // Just test that it doesn't panic
1089    }
1090}