1use 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
26pub struct GraphQLState {
28 pub pool: PgPool,
30 pub schema_cache: Arc<SchemaCache>,
32 pub generated_schema: GeneratedSchema,
34 pub schema: Schema,
36 pub config: SchemaConfig,
38 pub subscription_fields: Vec<SubField>,
40 pub broker: Arc<RwLock<Option<NotifyBroker>>>,
42}
43
44impl GraphQLState {
45 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 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 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 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 let mut broker_guard = self.broker.write().await;
125 *broker_guard = Some(broker);
126
127 Ok(())
128 }
129
130 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 pub async fn get_broker(&self) -> Option<Arc<RwLock<Option<NotifyBroker>>>> {
140 Some(Arc::clone(&self.broker))
141 }
142}
143
144pub 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
158pub 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
184pub 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
192fn build_dynamic_schema(
194 generated: &GeneratedSchema,
195 _schema_cache: &SchemaCache,
196 subscription_fields: Option<&[SubField]>,
197) -> Result<Schema, GraphQLError> {
198 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 let query = create_query_type(generated);
208
209 let mutation = if !generated.mutation_fields.is_empty() {
211 Some(create_mutation_type(generated))
212 } else {
213 None
214 };
215
216 let subscription = subscription_fields.map(create_subscription_type);
218
219 let mut builder = Schema::build(
221 "Query",
222 mutation.as_ref().map(|_| "Mutation"),
223 subscription.as_ref().map(|_| "Subscription"),
224 );
225
226 for (_, obj) in object_types {
228 builder = builder.register(obj);
229 }
230
231 builder = builder.register(query);
233
234 if let Some(mutation) = mutation {
236 builder = builder.register(mutation);
237 }
238
239 if let Some(subscription) = subscription {
241 builder = builder.register(subscription);
242 }
243
244 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 builder = register_filter_input_types(builder);
255
256 builder
257 .finish()
258 .map_err(|e| GraphQLError::SchemaError(e.to_string()))
259}
260
261fn 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 let gql_field = Field::new(&field.name, field_type, move |ctx| {
277 let field_name = field_name.clone();
278 FieldFuture::new(async move {
279 if let Some(Value::Object(map)) = ctx.parent_value.as_value() {
281 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 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
305fn 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 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 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 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
355fn 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 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
397fn 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 let value_stream = stream.filter_map(|notification| async move {
425 match TableChangePayload::from_payload(¬ification.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
452async 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 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 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 let result = execute_query(pool, &sql, gql_ctx.role()).await?;
485
486 if is_by_pk {
487 Ok(result
490 .into_iter()
491 .next()
492 .map(|v| FieldValue::value(json_to_value(v))))
493 } else {
494 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
503async 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
565async 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 sqlx::query(&format!(
580 "SET LOCAL ROLE {}",
581 postrust_sql::escape_ident(role)
582 ))
583 .execute(&mut *conn)
584 .await?;
585
586 let rows = sqlx::query(sql).fetch_all(&mut *conn).await?;
588
589 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
599async 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 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 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 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 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 match mutation_type {
676 MutationType::InsertOne => {
677 Ok(inserted.into_iter().next())
679 }
680 _ => {
681 Ok(Some(FieldValue::list(inserted)))
683 }
684 }
685}
686
687fn 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
709async 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 sqlx::query(&format!(
735 "SET LOCAL ROLE {}",
736 postrust_sql::escape_ident(role)
737 ))
738 .execute(&mut *conn)
739 .await?;
740
741 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 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 let mut query = sqlx::query(&sql);
768
769 for val in set_map.values() {
771 query = bind_json_value(query, val);
772 }
773
774 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 match mutation_type {
789 MutationType::UpdateByPk => Ok(updated.into_iter().next()),
790 _ => Ok(Some(FieldValue::list(updated))),
791 }
792}
793
794async 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 sqlx::query(&format!(
810 "SET LOCAL ROLE {}",
811 postrust_sql::escape_ident(role)
812 ))
813 .execute(&mut *conn)
814 .await?;
815
816 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 let mut query = sqlx::query(&sql);
830
831 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 match mutation_type {
846 MutationType::DeleteByPk => Ok(deleted.into_iter().next()),
847 _ => Ok(Some(FieldValue::list(deleted))),
848 }
849}
850
851fn 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 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 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
933fn graphql_type_ref(type_str: &str) -> TypeRef {
935 let is_list = type_str.starts_with('[');
937 let is_nn = type_str.ends_with('!');
938
939 let inner = if is_list {
941 let stripped = type_str
942 .trim_end_matches('!') .trim_start_matches('[') .trim_end_matches(']'); 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
972fn accessor_to_json(accessor: &ValueAccessor<'_>) -> serde_json::Value {
974 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#[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
1032fn 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
1058fn 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
1065fn create_bigdecimal_scalar() -> Scalar {
1067 Scalar::new("BigDecimal").description("Arbitrary precision decimal number")
1068}
1069
1070fn 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
1077fn create_uuid_scalar() -> Scalar {
1079 Scalar::new("UUID").description("UUID string")
1080}
1081
1082fn create_date_scalar() -> Scalar {
1084 Scalar::new("Date").description("ISO 8601 date string (YYYY-MM-DD)")
1085}
1086
1087fn create_datetime_scalar() -> Scalar {
1089 Scalar::new("DateTime").description("ISO 8601 datetime string")
1090}
1091
1092fn create_time_scalar() -> Scalar {
1094 Scalar::new("Time").description("ISO 8601 time string (HH:MM:SS)")
1095}
1096
1097fn 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 #[test]
1197 fn test_graphql_type_ref_simple() {
1198 let _type_ref = graphql_type_ref("String");
1199 }
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 #[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 #[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 #[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 #[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 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 #[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 let sub_fields = generate_subscription_fields(&cache, &generated);
1391 assert!(!sub_fields.is_empty(), "Should have subscription fields");
1392
1393 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 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 }
1425}