1use crate::context::GraphQLContext;
7use crate::error::GraphQLError;
8use crate::schema::object::TableObjectType;
9use crate::schema::relationship::RelationshipField;
10use crate::schema::{build_schema, GeneratedSchema, MutationType, SchemaConfig};
11use crate::subscription::{
12 generate_subscription_fields, NotifyBroker, SubscriptionField as SubField, TableChangePayload,
13};
14use async_graphql::dynamic::*;
15use async_graphql::Value;
16use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
17use axum::extract::State;
18use axum::response::IntoResponse;
19use futures::stream::StreamExt;
20use postrust_core::schema_cache::SchemaCache;
21use sqlx::PgPool;
22use std::collections::HashMap;
23use std::sync::Arc;
24use tokio::sync::RwLock;
25use tracing::{debug, info, trace};
26
27pub struct GraphQLState {
29 pub pool: PgPool,
31 pub schema_cache: Arc<SchemaCache>,
33 pub generated_schema: GeneratedSchema,
35 pub schema: Schema,
37 pub config: SchemaConfig,
39 pub subscription_fields: Vec<SubField>,
41 pub broker: Arc<RwLock<Option<NotifyBroker>>>,
43}
44
45impl GraphQLState {
46 pub fn new(
48 pool: PgPool,
49 schema_cache: Arc<SchemaCache>,
50 config: SchemaConfig,
51 ) -> Result<Self, GraphQLError> {
52 let generated_schema = build_schema(&schema_cache, &config);
53 let subscription_fields = if config.enable_subscriptions {
54 generate_subscription_fields(&schema_cache, &generated_schema)
55 } else {
56 Vec::new()
57 };
58 let schema = build_dynamic_schema(
59 &generated_schema,
60 &schema_cache,
61 if config.enable_subscriptions {
62 Some(subscription_fields.as_slice())
63 } else {
64 None
65 },
66 config.max_rows,
67 )?;
68
69 Ok(Self {
70 pool: pool.clone(),
71 schema_cache,
72 generated_schema,
73 schema,
74 config,
75 subscription_fields,
76 broker: Arc::new(RwLock::new(None)),
77 })
78 }
79
80 pub fn rebuild(&mut self) -> Result<(), GraphQLError> {
82 self.generated_schema = build_schema(&self.schema_cache, &self.config);
83 self.subscription_fields = if self.config.enable_subscriptions {
84 generate_subscription_fields(&self.schema_cache, &self.generated_schema)
85 } else {
86 Vec::new()
87 };
88 self.schema = build_dynamic_schema(
89 &self.generated_schema,
90 &self.schema_cache,
91 if self.config.enable_subscriptions {
92 Some(self.subscription_fields.as_slice())
93 } else {
94 None
95 },
96 self.config.max_rows,
97 )?;
98 Ok(())
99 }
100
101 pub async fn init_subscriptions(&self) -> Result<(), crate::subscription::BrokerError> {
105 if !self.config.enable_subscriptions {
106 return Ok(());
107 }
108
109 let broker = NotifyBroker::new(self.pool.clone());
110
111 let channels: Vec<String> = self
113 .subscription_fields
114 .iter()
115 .map(|f| f.channel_name())
116 .collect();
117
118 if !channels.is_empty() {
119 broker.start(channels).await?;
120 info!(
121 "Subscription broker started with {} channels",
122 self.subscription_fields.len()
123 );
124 }
125
126 let mut broker_guard = self.broker.write().await;
128 *broker_guard = Some(broker);
129
130 Ok(())
131 }
132
133 pub async fn stop_subscriptions(&self) {
135 let broker_guard = self.broker.read().await;
136 if let Some(broker) = broker_guard.as_ref() {
137 broker.stop().await;
138 }
139 }
140
141 pub async fn get_broker(&self) -> Option<Arc<RwLock<Option<NotifyBroker>>>> {
143 Some(Arc::clone(&self.broker))
144 }
145}
146
147pub async fn graphql_handler(
149 State(state): State<Arc<GraphQLState>>,
150 ctx: GraphQLContext,
151 req: GraphQLRequest,
152) -> GraphQLResponse {
153 let request = req
154 .into_inner()
155 .data(ctx)
156 .data(state.pool.clone())
157 .data(Arc::clone(&state.broker));
158 state.schema.execute(request).await.into()
159}
160
161pub async fn graphql_ws_handler(
166 State(state): State<Arc<GraphQLState>>,
167 protocol: async_graphql_axum::GraphQLProtocol,
168 ws: axum::extract::WebSocketUpgrade,
169) -> impl IntoResponse {
170 let schema = state.schema.clone();
171 let pool = state.pool.clone();
172 let broker = Arc::clone(&state.broker);
173
174 ws.protocols(["graphql-transport-ws", "graphql-ws"])
175 .on_upgrade(move |socket| async move {
176 let mut data = async_graphql::Data::default();
177 data.insert(pool);
178 data.insert(broker);
179
180 async_graphql_axum::GraphQLWebSocket::new(socket, schema, protocol)
181 .with_data(data)
182 .serve()
183 .await
184 })
185}
186
187pub async fn graphql_playground() -> impl axum::response::IntoResponse {
189 axum::response::Html(async_graphql::http::playground_source(
190 async_graphql::http::GraphQLPlaygroundConfig::new("/graphql")
191 .subscription_endpoint("/graphql/ws"),
192 ))
193}
194
195fn build_dynamic_schema(
197 generated: &GeneratedSchema,
198 _schema_cache: &SchemaCache,
199 subscription_fields: Option<&[SubField]>,
200 max_rows: Option<i64>,
201) -> Result<Schema, GraphQLError> {
202 let mut object_types: HashMap<String, Object> = HashMap::new();
204
205 for (type_name, obj) in &generated.object_types {
206 let relationships = generated
207 .relationship_fields
208 .get(type_name)
209 .map(|r| r.as_slice())
210 .unwrap_or(&[]);
211 let table_obj = create_object_type(obj, relationships);
212 object_types.insert(type_name.clone(), table_obj);
213 }
214
215 let relationships = Arc::new(generated.relationship_fields.clone());
218 let query = create_query_type(generated, max_rows, Arc::clone(&relationships));
219
220 let mutation = if !generated.mutation_fields.is_empty() {
222 Some(create_mutation_type(generated))
223 } else {
224 None
225 };
226
227 let subscription = subscription_fields.map(create_subscription_type);
229
230 let mut builder = Schema::build(
232 "Query",
233 mutation.as_ref().map(|_| "Mutation"),
234 subscription.as_ref().map(|_| "Subscription"),
235 );
236
237 for (_, obj) in object_types {
239 builder = builder.register(obj);
240 }
241
242 builder = builder.register(query);
244
245 if let Some(mutation) = mutation {
247 builder = builder.register(mutation);
248 }
249
250 if let Some(subscription) = subscription {
252 builder = builder.register(subscription);
253 }
254
255 builder = builder.register(create_bigint_scalar());
257 builder = builder.register(create_bigdecimal_scalar());
258 builder = builder.register(create_json_scalar());
259 builder = builder.register(create_uuid_scalar());
260 builder = builder.register(create_date_scalar());
261 builder = builder.register(create_datetime_scalar());
262 builder = builder.register(create_time_scalar());
263
264 builder = register_filter_input_types(builder);
266
267 builder
268 .finish()
269 .map_err(|e| GraphQLError::SchemaError(e.to_string()))
270}
271
272fn create_object_type(obj: &TableObjectType, relationships: &[RelationshipField]) -> Object {
274 let mut object = Object::new(&obj.name);
275
276 if let Some(desc) = obj.description() {
277 object = object.description(desc);
278 }
279
280 for field in &obj.fields {
281 let field_name = field.name.clone();
282 let field_type = graphql_type_ref(&field.type_string());
283
284 let gql_field = Field::new(&field.name, field_type, move |ctx| {
288 let field_name = field_name.clone();
289 FieldFuture::new(async move {
290 if let Some(Value::Object(map)) = ctx.parent_value.as_value() {
292 let key = async_graphql::Name::new(&field_name);
294 if let Some(val) = map.get(&key) {
295 return Ok(Some(FieldValue::value(val.clone())));
296 }
297 }
298
299 Ok(None)
301 })
302 });
303
304 let gql_field = if let Some(desc) = &field.description {
305 gql_field.description(desc)
306 } else {
307 gql_field
308 };
309
310 object = object.field(gql_field);
311 }
312
313 for rel in relationships {
317 let field_name = rel.name.clone();
318 let field_type = if rel.is_list {
319 TypeRef::named_nn_list_nn(&rel.target_type)
320 } else {
321 TypeRef::named(&rel.target_type)
322 };
323
324 let gql_field = Field::new(&rel.name, field_type, move |ctx| {
325 let field_name = field_name.clone();
326 FieldFuture::new(async move {
327 if let Some(Value::Object(map)) = ctx.parent_value.as_value() {
328 let key = async_graphql::Name::new(&field_name);
329 if let Some(val) = map.get(&key) {
330 return Ok(Some(FieldValue::value(val.clone())));
331 }
332 }
333 Ok(None)
334 })
335 });
336
337 let gql_field = if let Some(desc) = &rel.description {
338 gql_field.description(desc)
339 } else {
340 gql_field
341 };
342
343 object = object.field(gql_field);
344 }
345
346 object
347}
348
349fn create_query_type(
351 generated: &GeneratedSchema,
352 max_rows: Option<i64>,
353 relationships: Arc<HashMap<String, Vec<RelationshipField>>>,
354) -> Object {
355 let mut query = Object::new("Query");
356
357 for field in &generated.query_fields {
358 let table_name = field.table_name.clone();
359 let schema_name = field.schema_name.clone();
360 let type_name = field.type_name.clone();
361 let is_by_pk = field.is_by_pk;
362 let pk_columns = field.pk_columns.clone();
363 let return_type = graphql_type_ref(&field.return_type);
364
365 let spec = Arc::new(QueryFieldSpec {
366 schema_name,
367 table_name,
368 type_name,
369 is_by_pk,
370 pk_columns: pk_columns.clone(),
371 max_rows,
372 relationships: Arc::clone(&relationships),
373 });
374
375 let mut gql_field = Field::new(&field.name, return_type, move |ctx| {
376 let spec = Arc::clone(&spec);
377 FieldFuture::new(async move { resolve_query(&ctx, &spec).await })
378 });
379
380 if !is_by_pk {
382 gql_field = gql_field
383 .argument(InputValue::new("filter", TypeRef::named("JSON")))
384 .argument(InputValue::new("orderBy", TypeRef::named_list("String")))
385 .argument(InputValue::new("limit", TypeRef::named("Int")))
386 .argument(InputValue::new("offset", TypeRef::named("Int")));
387 } else {
388 for (col_name, pg_type) in &pk_columns {
391 gql_field = gql_field.argument(InputValue::new(
392 col_name,
393 TypeRef::named_nn(pk_argument_type(pg_type)),
394 ));
395 }
396 }
397
398 if let Some(desc) = &field.description {
399 gql_field = gql_field.description(desc);
400 }
401
402 query = query.field(gql_field);
403 }
404
405 query = query.field(
407 Field::new("_schema", TypeRef::named("String"), |_| {
408 FieldFuture::new(async move {
409 Ok(Some(Value::String("Postrust GraphQL Schema".to_string())))
410 })
411 })
412 .description("Schema introspection"),
413 );
414
415 query
416}
417
418fn create_mutation_type(generated: &GeneratedSchema) -> Object {
420 let mut mutation = Object::new("Mutation");
421
422 for field in &generated.mutation_fields {
423 let table_name = field.table_name.clone();
424 let schema_name = field.schema_name.clone();
425 let mutation_type = field.mutation_type;
426 let pk_columns = field.pk_columns.clone();
427 let return_type = graphql_type_ref(&field.return_type);
428
429 let resolver_pk_columns = pk_columns.clone();
430 let mut gql_field = Field::new(&field.name, return_type, move |ctx| {
431 let table_name = table_name.clone();
432 let schema_name = schema_name.clone();
433 let pk_columns = resolver_pk_columns.clone();
434 FieldFuture::new(async move {
435 resolve_mutation(&ctx, &schema_name, &table_name, mutation_type, &pk_columns).await
436 })
437 });
438
439 match mutation_type {
445 MutationType::Insert | MutationType::InsertOne => {
446 gql_field =
447 gql_field.argument(InputValue::new("objects", TypeRef::named_nn_list("JSON")));
448 }
449 MutationType::UpdateByPk => {
450 gql_field = gql_field.argument(InputValue::new("set", TypeRef::named_nn("JSON")));
451 for (col_name, pg_type) in &pk_columns {
452 gql_field = gql_field.argument(InputValue::new(
453 col_name,
454 TypeRef::named_nn(pk_argument_type(pg_type)),
455 ));
456 }
457 }
458 MutationType::Update => {
459 gql_field = gql_field
460 .argument(InputValue::new("where", TypeRef::named("JSON")))
461 .argument(InputValue::new("set", TypeRef::named_nn("JSON")));
462 }
463 MutationType::DeleteByPk => {
464 for (col_name, pg_type) in &pk_columns {
465 gql_field = gql_field.argument(InputValue::new(
466 col_name,
467 TypeRef::named_nn(pk_argument_type(pg_type)),
468 ));
469 }
470 }
471 MutationType::Delete => {
472 gql_field = gql_field.argument(InputValue::new("where", TypeRef::named("JSON")));
473 }
474 }
475
476 if let Some(desc) = &field.description {
477 gql_field = gql_field.description(desc);
478 }
479
480 mutation = mutation.field(gql_field);
481 }
482
483 mutation
484}
485
486fn create_subscription_type(fields: &[SubField]) -> Subscription {
488 let mut subscription = Subscription::new("Subscription");
489
490 for field in fields {
491 let channel_name = field.channel_name();
492 let return_type = TypeRef::named(&field.return_type);
493 let field_name = field.name.clone();
494 let description = field.description.clone();
495
496 let gql_field = SubscriptionField::new(&field_name, return_type, move |ctx| {
497 let channel_name = channel_name.clone();
498 SubscriptionFieldFuture::new(async move {
499 let broker_arc = ctx.data::<Arc<RwLock<Option<NotifyBroker>>>>()?;
500 let broker_guard = broker_arc.read().await;
501
502 let broker = broker_guard.as_ref().ok_or_else(|| {
503 async_graphql::Error::new("Subscription broker not initialized")
504 })?;
505
506 let stream = broker
507 .subscribe(&channel_name)
508 .await
509 .map_err(|e| async_graphql::Error::new(format!("Subscription error: {}", e)))?;
510
511 let value_stream = stream.filter_map(|notification| async move {
514 match TableChangePayload::from_payload(¬ification.payload) {
515 Ok(payload) => payload
516 .data()
517 .map(|data| Ok(FieldValue::value(json_to_value(data.clone())))),
518 Err(e) => {
519 debug!("Failed to parse notification payload: {}", e);
520 None
521 }
522 }
523 });
524
525 Ok(value_stream)
526 })
527 });
528
529 let gql_field = if let Some(desc) = description {
530 gql_field.description(desc)
531 } else {
532 gql_field
533 };
534
535 subscription = subscription.field(gql_field);
536 }
537
538 subscription
539}
540
541struct QueryFieldSpec {
543 schema_name: String,
544 table_name: String,
545 type_name: String,
546 is_by_pk: bool,
547 pk_columns: Vec<(String, String)>,
548 max_rows: Option<i64>,
549 relationships: Arc<HashMap<String, Vec<RelationshipField>>>,
550}
551
552async fn resolve_query<'a>(
554 ctx: &ResolverContext<'a>,
555 spec: &QueryFieldSpec,
556) -> Result<Option<FieldValue<'a>>, async_graphql::Error> {
557 let schema_name = spec.schema_name.as_str();
558 let table_name = spec.table_name.as_str();
559 let type_name = spec.type_name.as_str();
560 let is_by_pk = spec.is_by_pk;
561 let pk_columns = spec.pk_columns.as_slice();
562 let max_rows = spec.max_rows;
563 let relationships = spec.relationships.as_ref();
564
565 let pool = ctx.data::<PgPool>()?;
566 let gql_ctx = ctx.data::<GraphQLContext>()?;
567
568 debug!("Resolving query for table: {}", table_name);
569
570 let requested_limit: Option<i64> = ctx.args.try_get("limit").ok().and_then(|v| v.i64().ok());
572
573 let offset: Option<i64> = ctx.args.try_get("offset").ok().and_then(|v| v.i64().ok());
574
575 let limit: Option<i64> = if is_by_pk {
580 Some(1)
581 } else {
582 match (requested_limit, max_rows) {
583 (Some(requested), Some(ceiling)) => Some(requested.min(ceiling)),
584 (Some(requested), None) => Some(requested),
585 (None, ceiling) => ceiling,
586 }
587 };
588
589 let mut where_sql = String::new();
597 let mut bound_values: Vec<serde_json::Value> = Vec::new();
598
599 if is_by_pk {
600 if pk_columns.is_empty() {
601 return Err(async_graphql::Error::new(format!(
602 "\"{}\" has no primary key, so it cannot be queried by key",
603 table_name
604 )));
605 }
606
607 let mut conditions = Vec::with_capacity(pk_columns.len());
608 for (idx, (col_name, pg_type)) in pk_columns.iter().enumerate() {
609 let value = ctx.args.try_get(col_name).map_err(|_| {
610 async_graphql::Error::new(format!(
611 "missing required primary key argument \"{}\"",
612 col_name
613 ))
614 })?;
615
616 conditions.push(format!(
617 "{} = ${}::{}",
618 postrust_sql::escape_ident(col_name),
619 idx + 1,
620 pg_type
621 ));
622 bound_values.push(accessor_to_json(&value));
623 }
624 where_sql = format!(" WHERE {}", conditions.join(" AND "));
625 } else if let Some(filter) = ctx
626 .args
627 .try_get("filter")
628 .ok()
629 .map(|v| accessor_to_json(&v))
630 {
631 let (filter_sql, filter_values) = build_where_clause(Some(&filter), 1)?;
632 if !filter_sql.is_empty() {
633 where_sql = format!(" {}", filter_sql);
634 bound_values = filter_values;
635 }
636 }
637
638 let order_sql = if is_by_pk {
642 String::new()
643 } else {
644 build_order_by_clause(ctx, &gql_ctx.schema_cache, schema_name, table_name).await?
645 };
646
647 let mut inner = format!(
651 "SELECT * FROM {}.{}{}{}",
652 postrust_sql::escape_ident(schema_name),
653 postrust_sql::escape_ident(table_name),
654 where_sql,
655 order_sql
656 );
657
658 if let Some(limit) = limit {
659 inner.push_str(&format!(" LIMIT {}", limit));
660 }
661
662 if let Some(offset) = offset {
663 inner.push_str(&format!(" OFFSET {}", offset));
664 }
665
666 let embed_expressions = {
669 let guard = gql_ctx
670 .schema_cache
671 .get()
672 .await
673 .map_err(|e| async_graphql::Error::new(e.to_string()))?;
674 match guard.as_ref() {
675 Some(cache) => build_embed_expressions(
676 cache,
677 relationships,
678 type_name,
679 "src",
680 ctx.field(),
681 max_rows,
682 &mut 0,
683 )?,
684 None => Vec::new(),
685 }
686 };
687
688 let inner = if embed_expressions.is_empty() {
689 inner
690 } else {
691 let mut projection = String::from("src.*");
692 for (field_name, expression) in &embed_expressions {
693 projection.push_str(", ");
694 projection.push_str(expression);
695 projection.push_str(" AS ");
696 projection.push_str(&postrust_sql::escape_ident(field_name));
697 }
698 format!("SELECT {} FROM ({}) AS src", projection, inner)
699 };
700
701 let sql = format!("SELECT row_to_json(t) FROM ({}) t", inner);
702
703 let mut tx = begin_with_role(pool, gql_ctx.role()).await?;
707
708 let mut result = execute_query_on(&mut tx, &sql, &bound_values).await?;
710
711 if embed_expressions.is_empty() {
713 let guard = gql_ctx
714 .schema_cache
715 .get()
716 .await
717 .map_err(|e| async_graphql::Error::new(e.to_string()))?;
718 let cache = guard
719 .as_ref()
720 .ok_or_else(|| async_graphql::Error::new("schema cache is not loaded"))?;
721
722 let embed_ctx = EmbedContext {
723 schema_cache: cache,
724 relationships,
725 max_rows,
726 };
727
728 embed_relationships(&mut tx, &embed_ctx, type_name, ctx.field(), &mut result).await?;
729 }
730
731 tx.commit().await?;
732
733 if is_by_pk {
734 Ok(result
737 .into_iter()
738 .next()
739 .map(|v| FieldValue::value(json_to_value(v))))
740 } else {
741 let items: Vec<FieldValue> = result
743 .into_iter()
744 .map(|v| FieldValue::value(json_to_value(v)))
745 .collect();
746 Ok(Some(FieldValue::list(items)))
747 }
748}
749
750async fn resolve_mutation<'a>(
752 ctx: &ResolverContext<'a>,
753 schema_name: &str,
754 table_name: &str,
755 mutation_type: MutationType,
756 pk_columns: &[(String, String)],
757) -> Result<Option<FieldValue<'a>>, async_graphql::Error> {
758 let pool = ctx.data::<PgPool>()?;
759 let gql_ctx = ctx.data::<GraphQLContext>()?;
760
761 debug!(
762 "Resolving mutation for table: {} type: {:?}",
763 table_name, mutation_type
764 );
765
766 let result = match mutation_type {
767 MutationType::Insert | MutationType::InsertOne => {
768 let objects = ctx
769 .args
770 .try_get("objects")
771 .ok()
772 .map(|v| accessor_to_json(&v))
773 .unwrap_or_else(|| serde_json::Value::Array(vec![]));
774
775 execute_insert(
776 pool,
777 schema_name,
778 table_name,
779 gql_ctx.role(),
780 objects,
781 mutation_type,
782 )
783 .await?
784 }
785 MutationType::Update | MutationType::UpdateByPk => {
786 let set_value = ctx
787 .args
788 .try_get("set")
789 .ok()
790 .map(|v| accessor_to_json(&v))
791 .unwrap_or_else(|| serde_json::json!({}));
792
793 let where_clause = if mutation_type == MutationType::UpdateByPk {
794 Some(pk_where_from_args(ctx, table_name, pk_columns)?)
795 } else {
796 ctx.args.try_get("where").ok().map(|v| accessor_to_json(&v))
797 };
798
799 execute_update(
800 pool,
801 schema_name,
802 table_name,
803 gql_ctx.role(),
804 set_value,
805 where_clause,
806 mutation_type,
807 )
808 .await?
809 }
810 MutationType::Delete | MutationType::DeleteByPk => {
811 let where_clause = if mutation_type == MutationType::DeleteByPk {
812 Some(pk_where_from_args(ctx, table_name, pk_columns)?)
813 } else {
814 ctx.args.try_get("where").ok().map(|v| accessor_to_json(&v))
815 };
816
817 execute_delete(
818 pool,
819 schema_name,
820 table_name,
821 gql_ctx.role(),
822 where_clause,
823 mutation_type,
824 )
825 .await?
826 }
827 };
828
829 Ok(result)
830}
831
832async fn begin_with_role(
841 pool: &PgPool,
842 role: &str,
843) -> Result<sqlx::Transaction<'static, sqlx::Postgres>, async_graphql::Error> {
844 let mut tx = pool.begin().await?;
845 sqlx::query(&format!(
846 "SET LOCAL ROLE {}",
847 postrust_sql::escape_ident(role)
848 ))
849 .execute(&mut *tx)
850 .await?;
851 Ok(tx)
852}
853
854async fn execute_query_on(
857 conn: &mut sqlx::PgConnection,
858 sql: &str,
859 params: &[serde_json::Value],
860) -> Result<Vec<serde_json::Value>, async_graphql::Error> {
861 use sqlx::Row;
862
863 trace!("Executing SQL: {}", sql);
864
865 let mut query = sqlx::query(sql);
867 for param in params {
868 query = bind_json_value(query, param);
869 }
870 let rows = query.fetch_all(&mut *conn).await?;
871
872 let results: Vec<serde_json::Value> = rows
875 .into_iter()
876 .filter_map(|row| row.try_get::<serde_json::Value, _>(0).ok())
877 .collect();
878
879 Ok(results)
880}
881
882async fn execute_insert<'a>(
884 pool: &PgPool,
885 schema_name: &str,
886 table_name: &str,
887 role: &str,
888 objects: serde_json::Value,
889 mutation_type: MutationType,
890) -> Result<Option<FieldValue<'a>>, async_graphql::Error> {
891 use sqlx::Row;
892
893 trace!("Insert mutation for {}: {:?}", table_name, objects);
894
895 let objects_array = match objects {
897 serde_json::Value::Array(arr) => arr,
898 serde_json::Value::Object(obj) => vec![serde_json::Value::Object(obj)],
899 _ => {
900 return Err(async_graphql::Error::new(
901 "objects must be an array or object",
902 ))
903 }
904 };
905
906 if objects_array.is_empty() {
907 return Err(async_graphql::Error::new("objects cannot be empty"));
908 }
909
910 let mut conn = begin_with_role(pool, role).await?;
911
912 let mut inserted: Vec<FieldValue> = Vec::new();
913
914 for obj in objects_array {
915 if let serde_json::Value::Object(map) = obj {
916 let columns: Vec<&str> = map.keys().map(|k| k.as_str()).collect();
918 let placeholders: Vec<String> =
919 (1..=columns.len()).map(|i| format!("${}", i)).collect();
920
921 let sql = format!(
922 "INSERT INTO {}.{} ({}) VALUES ({}) RETURNING row_to_json({}.{}.*)",
923 postrust_sql::escape_ident(schema_name),
924 postrust_sql::escape_ident(table_name),
925 columns
926 .iter()
927 .map(|c| postrust_sql::escape_ident(c))
928 .collect::<Vec<_>>()
929 .join(", "),
930 placeholders.join(", "),
931 postrust_sql::escape_ident(schema_name),
932 postrust_sql::escape_ident(table_name)
933 );
934
935 trace!("Executing INSERT SQL: {}", sql);
936
937 let mut query = sqlx::query(&sql);
939 for col in &columns {
940 if let Some(val) = map.get(*col) {
941 query = bind_json_value(query, val);
942 }
943 }
944
945 let row = query.fetch_one(&mut *conn).await?;
946
947 if let Ok(json_val) = row.try_get::<serde_json::Value, _>(0) {
948 inserted.push(FieldValue::value(json_to_value(json_val)));
949 }
950 }
951 }
952
953 conn.commit().await?;
956
957 match mutation_type {
959 MutationType::InsertOne => {
960 Ok(inserted.into_iter().next())
962 }
963 _ => {
964 Ok(Some(FieldValue::list(inserted)))
966 }
967 }
968}
969
970fn bind_json_value<'q>(
972 query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
973 value: &serde_json::Value,
974) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
975 match value {
976 serde_json::Value::Null => query.bind(None::<String>),
977 serde_json::Value::Bool(b) => query.bind(*b),
978 serde_json::Value::Number(n) => {
979 if let Some(i) = n.as_i64() {
980 query.bind(i)
981 } else if let Some(f) = n.as_f64() {
982 query.bind(f)
983 } else {
984 query.bind(n.to_string())
985 }
986 }
987 serde_json::Value::String(s) => query.bind(s.clone()),
988 _ => query.bind(value.to_string()),
989 }
990}
991
992async fn execute_update<'a>(
994 pool: &PgPool,
995 schema_name: &str,
996 table_name: &str,
997 role: &str,
998 set_value: serde_json::Value,
999 where_clause: Option<serde_json::Value>,
1000 mutation_type: MutationType,
1001) -> Result<Option<FieldValue<'a>>, async_graphql::Error> {
1002 use sqlx::Row;
1003
1004 trace!("Update mutation for {}: {:?}", table_name, set_value);
1005
1006 let set_map = match set_value {
1007 serde_json::Value::Object(map) => map,
1008 _ => return Err(async_graphql::Error::new("set must be an object")),
1009 };
1010
1011 if set_map.is_empty() {
1012 return Err(async_graphql::Error::new("set cannot be empty"));
1013 }
1014
1015 let mut conn = begin_with_role(pool, role).await?;
1016
1017 let mut set_parts: Vec<String> = Vec::new();
1019 let mut param_idx = 1;
1020 for key in set_map.keys() {
1021 set_parts.push(format!(
1022 "{} = ${}",
1023 postrust_sql::escape_ident(key),
1024 param_idx
1025 ));
1026 param_idx += 1;
1027 }
1028
1029 let (where_sql, where_values) = build_where_clause(where_clause.as_ref(), param_idx)?;
1031
1032 if where_sql.is_empty() {
1035 return Err(async_graphql::Error::new(format!(
1036 "update on \"{}\" requires a `where` argument with at least one \
1037 recognised condition; refusing to update every row",
1038 table_name
1039 )));
1040 }
1041
1042 let sql = format!(
1043 "UPDATE {}.{} SET {} {} RETURNING row_to_json({}.{}.*)",
1044 postrust_sql::escape_ident(schema_name),
1045 postrust_sql::escape_ident(table_name),
1046 set_parts.join(", "),
1047 where_sql,
1048 postrust_sql::escape_ident(schema_name),
1049 postrust_sql::escape_ident(table_name)
1050 );
1051
1052 trace!("Executing UPDATE SQL: {}", sql);
1053
1054 let mut query = sqlx::query(&sql);
1056
1057 for val in set_map.values() {
1059 query = bind_json_value(query, val);
1060 }
1061
1062 for val in &where_values {
1064 query = bind_json_value(query, val);
1065 }
1066
1067 let rows = query.fetch_all(&mut *conn).await?;
1068
1069 let updated: Vec<FieldValue> = rows
1070 .iter()
1071 .filter_map(|row| row.try_get::<serde_json::Value, _>(0).ok())
1072 .map(|v| FieldValue::value(json_to_value(v)))
1073 .collect();
1074
1075 conn.commit().await?;
1077
1078 match mutation_type {
1079 MutationType::UpdateByPk => Ok(updated.into_iter().next()),
1080 _ => Ok(Some(FieldValue::list(updated))),
1081 }
1082}
1083
1084async fn execute_delete<'a>(
1086 pool: &PgPool,
1087 schema_name: &str,
1088 table_name: &str,
1089 role: &str,
1090 where_clause: Option<serde_json::Value>,
1091 mutation_type: MutationType,
1092) -> Result<Option<FieldValue<'a>>, async_graphql::Error> {
1093 use sqlx::Row;
1094
1095 trace!("Delete mutation for {}", table_name);
1096
1097 let mut conn = begin_with_role(pool, role).await?;
1098
1099 let (where_sql, where_values) = build_where_clause(where_clause.as_ref(), 1)?;
1101
1102 if where_sql.is_empty() {
1105 return Err(async_graphql::Error::new(format!(
1106 "delete on \"{}\" requires a `where` argument with at least one \
1107 recognised condition; refusing to delete every row",
1108 table_name
1109 )));
1110 }
1111
1112 let sql = format!(
1113 "DELETE FROM {}.{} {} RETURNING row_to_json({}.{}.*)",
1114 postrust_sql::escape_ident(schema_name),
1115 postrust_sql::escape_ident(table_name),
1116 where_sql,
1117 postrust_sql::escape_ident(schema_name),
1118 postrust_sql::escape_ident(table_name)
1119 );
1120
1121 trace!("Executing DELETE SQL: {}", sql);
1122
1123 let mut query = sqlx::query(&sql);
1125
1126 for val in &where_values {
1128 query = bind_json_value(query, val);
1129 }
1130
1131 let rows = query.fetch_all(&mut *conn).await?;
1132
1133 let deleted: Vec<FieldValue> = rows
1134 .iter()
1135 .filter_map(|row| row.try_get::<serde_json::Value, _>(0).ok())
1136 .map(|v| FieldValue::value(json_to_value(v)))
1137 .collect();
1138
1139 conn.commit().await?;
1141
1142 match mutation_type {
1143 MutationType::DeleteByPk => Ok(deleted.into_iter().next()),
1144 _ => Ok(Some(FieldValue::list(deleted))),
1145 }
1146}
1147
1148fn build_where_clause(
1150 where_value: Option<&serde_json::Value>,
1151 start_param_idx: usize,
1152) -> Result<(String, Vec<serde_json::Value>), async_graphql::Error> {
1153 let mut conditions: Vec<String> = Vec::new();
1154 let mut values: Vec<serde_json::Value> = Vec::new();
1155 let mut param_idx = start_param_idx;
1156
1157 if let Some(serde_json::Value::Object(map)) = where_value {
1158 for (key, val) in map {
1159 let column = postrust_sql::escape_ident(key);
1160
1161 match val {
1162 serde_json::Value::Object(op_map) => {
1163 for (op, op_val) in op_map {
1164 let binary_operator = match op.as_str() {
1166 "eq" | "_eq" => Some("="),
1167 "neq" | "_neq" => Some("!="),
1168 "gt" | "_gt" => Some(">"),
1169 "gte" | "_gte" => Some(">="),
1170 "lt" | "_lt" => Some("<"),
1171 "lte" | "_lte" => Some("<="),
1172 "like" | "_like" => Some("LIKE"),
1173 "ilike" | "_ilike" => Some("ILIKE"),
1174 _ => None,
1175 };
1176
1177 if let Some(sql_operator) = binary_operator {
1178 conditions.push(format!("{} {} ${}", column, sql_operator, param_idx));
1179 values.push(op_val.clone());
1180 param_idx += 1;
1181 continue;
1182 }
1183
1184 match op.as_str() {
1185 "is_null" | "_is_null" | "isNull" => {
1186 if op_val.as_bool().unwrap_or(false) {
1187 conditions.push(format!("{} IS NULL", column));
1188 } else {
1189 conditions.push(format!("{} IS NOT NULL", column));
1190 }
1191 }
1192 "in" | "_in" => {
1193 let items = op_val.as_array().ok_or_else(|| {
1194 async_graphql::Error::new(format!(
1195 "the `in` filter on \"{}\" requires a list of values",
1196 key
1197 ))
1198 })?;
1199
1200 if items.is_empty() {
1201 conditions.push("false".to_string());
1204 continue;
1205 }
1206
1207 let mut placeholders = Vec::with_capacity(items.len());
1208 for item in items {
1209 placeholders.push(format!("${}", param_idx));
1210 values.push(item.clone());
1211 param_idx += 1;
1212 }
1213 conditions.push(format!(
1214 "{} IN ({})",
1215 column,
1216 placeholders.join(", ")
1217 ));
1218 }
1219 other => {
1220 return Err(async_graphql::Error::new(format!(
1225 "unsupported filter operator \"{}\" on \"{}\"",
1226 other, key
1227 )));
1228 }
1229 }
1230 }
1231 }
1232 _ => {
1233 conditions.push(format!("{} = ${}", column, param_idx));
1235 values.push(val.clone());
1236 param_idx += 1;
1237 }
1238 }
1239 }
1240 }
1241
1242 let where_sql = if conditions.is_empty() {
1243 String::new()
1244 } else {
1245 format!("WHERE {}", conditions.join(" AND "))
1246 };
1247
1248 Ok((where_sql, values))
1249}
1250
1251async fn build_order_by_clause(
1258 ctx: &ResolverContext<'_>,
1259 schema_cache: &postrust_core::schema_cache::SchemaCacheRef,
1260 schema_name: &str,
1261 table_name: &str,
1262) -> Result<String, async_graphql::Error> {
1263 let Ok(order_arg) = ctx.args.try_get("orderBy") else {
1264 return Ok(String::new());
1265 };
1266
1267 let entries = match order_arg.list() {
1268 Ok(list) => list
1269 .iter()
1270 .map(|item| item.string().map(|s| s.to_string()))
1271 .collect::<Result<Vec<_>, _>>()
1272 .map_err(|_| async_graphql::Error::new("orderBy entries must be strings"))?,
1273 Err(_) => match order_arg.string() {
1275 Ok(single) => vec![single.to_string()],
1276 Err(_) => {
1277 return Err(async_graphql::Error::new(
1278 "orderBy must be a string or a list of strings",
1279 ))
1280 }
1281 },
1282 };
1283
1284 if entries.is_empty() {
1285 return Ok(String::new());
1286 }
1287
1288 let guard = schema_cache
1289 .get()
1290 .await
1291 .map_err(|e| async_graphql::Error::new(e.to_string()))?;
1292 let cache = guard
1293 .as_ref()
1294 .ok_or_else(|| async_graphql::Error::new("schema cache is not loaded"))?;
1295 let qi = postrust_core::api_request::QualifiedIdentifier::new(schema_name, table_name);
1296 let table = cache
1297 .get_table(&qi)
1298 .ok_or_else(|| async_graphql::Error::new(format!("unknown table \"{}\"", table_name)))?;
1299
1300 let mut terms = Vec::with_capacity(entries.len());
1301 for entry in entries {
1302 let (column, direction) = match entry.split_once('.') {
1303 Some((column, direction)) => (column, Some(direction)),
1304 None => (entry.as_str(), None),
1305 };
1306
1307 if table.get_column(column).is_none() {
1308 return Err(async_graphql::Error::new(format!(
1309 "cannot order by unknown column \"{}\" on \"{}\"",
1310 column, table_name
1311 )));
1312 }
1313
1314 let direction_sql = match direction.map(|d| d.to_ascii_lowercase()) {
1315 None => "",
1316 Some(d) if d == "asc" => " ASC",
1317 Some(d) if d == "desc" => " DESC",
1318 Some(other) => {
1319 return Err(async_graphql::Error::new(format!(
1320 "invalid order direction \"{}\"; expected \"asc\" or \"desc\"",
1321 other
1322 )))
1323 }
1324 };
1325
1326 terms.push(format!(
1327 "{}{}",
1328 postrust_sql::escape_ident(column),
1329 direction_sql
1330 ));
1331 }
1332
1333 Ok(format!(" ORDER BY {}", terms.join(", ")))
1334}
1335
1336fn build_embed_expressions(
1342 schema_cache: &SchemaCache,
1343 relationships: &HashMap<String, Vec<RelationshipField>>,
1344 type_name: &str,
1345 parent_alias: &str,
1346 selection: async_graphql::SelectionField<'_>,
1347 max_rows: Option<i64>,
1348 alias_counter: &mut usize,
1349) -> Result<Vec<(String, String)>, async_graphql::Error> {
1350 let Some(available) = relationships.get(type_name) else {
1351 return Ok(Vec::new());
1352 };
1353
1354 let mut expressions = Vec::new();
1355
1356 for field in selection.selection_set() {
1357 let Some(rel) = available.iter().find(|r| r.name == field.name()) else {
1358 continue;
1359 };
1360
1361 let plan = postrust_core::embed::EmbedPlan::resolve(&rel.relationship, schema_cache)
1362 .map_err(|e| async_graphql::Error::new(e.to_string()))?;
1363
1364 *alias_counter += 1;
1365 let child_alias = format!("e{}", alias_counter);
1366
1367 let nested = build_embed_expressions(
1368 schema_cache,
1369 relationships,
1370 &rel.target_type,
1371 &child_alias,
1372 field,
1373 max_rows,
1374 alias_counter,
1375 )?;
1376
1377 let child_relationships = relationships.get(&rel.target_type);
1380 let mut parts: Vec<String> = Vec::new();
1381 for sub in field.selection_set() {
1382 let name = sub.name();
1383 let is_relationship = child_relationships
1384 .map(|rels| rels.iter().any(|r| r.name == name))
1385 .unwrap_or(false);
1386 if !is_relationship {
1387 parts.push(postrust_sql::escape_ident(name));
1388 }
1389 }
1390 if parts.is_empty() && nested.is_empty() {
1391 parts.push(format!("{}.*", postrust_sql::escape_ident(&child_alias)));
1392 }
1393 for (field_name, expression) in nested {
1394 parts.push(format!(
1395 "{} AS {}",
1396 expression,
1397 postrust_sql::escape_ident(&field_name)
1398 ));
1399 }
1400
1401 let expression = plan
1402 .embed_expression(parent_alias, &child_alias, &parts.join(", "), max_rows)
1403 .map_err(|e| async_graphql::Error::new(e.to_string()))?;
1404
1405 expressions.push((rel.name.clone(), expression));
1406 }
1407
1408 Ok(expressions)
1409}
1410
1411struct EmbedContext<'c> {
1417 schema_cache: &'c SchemaCache,
1418 relationships: &'c HashMap<String, Vec<RelationshipField>>,
1419 max_rows: Option<i64>,
1420}
1421
1422fn embed_relationships<'f>(
1428 conn: &'f mut sqlx::PgConnection,
1429 ctx: &'f EmbedContext<'f>,
1430 type_name: &'f str,
1431 selection: async_graphql::SelectionField<'f>,
1432 rows: &'f mut [serde_json::Value],
1433) -> futures::future::BoxFuture<'f, Result<(), async_graphql::Error>> {
1434 Box::pin(async move {
1435 if rows.is_empty() {
1436 return Ok(());
1437 }
1438
1439 let Some(available) = ctx.relationships.get(type_name) else {
1440 return Ok(());
1441 };
1442
1443 for requested in selection.selection_set() {
1444 let Some(rel) = available.iter().find(|r| r.name == requested.name()) else {
1445 continue;
1446 };
1447
1448 let plan =
1449 postrust_core::embed::EmbedPlan::resolve(&rel.relationship, ctx.schema_cache)
1450 .map_err(|e| async_graphql::Error::new(e.to_string()))?;
1451
1452 let keys = postrust_core::embed::parent_keys(rows, &plan.local_column);
1453
1454 let mut child_columns: Vec<String> = Vec::new();
1464 let mut project_everything = false;
1465 let child_relationships = ctx.relationships.get(&rel.target_type);
1466 for field in requested.selection_set() {
1467 let name = field.name();
1468 match child_relationships.and_then(|rels| rels.iter().find(|r| r.name == name)) {
1469 Some(nested_rel) => {
1470 match postrust_core::embed::EmbedPlan::resolve(
1471 &nested_rel.relationship,
1472 ctx.schema_cache,
1473 ) {
1474 Ok(nested_plan) => child_columns.push(nested_plan.local_column),
1475 Err(_) => project_everything = true,
1476 }
1477 }
1478 None => child_columns.push(name.to_string()),
1479 }
1480 }
1481 if project_everything || child_columns.is_empty() {
1482 child_columns.clear();
1483 }
1484
1485 let mut grouped = if keys.is_empty() {
1486 std::collections::HashMap::new()
1487 } else {
1488 let sql = plan
1489 .children_grouped_sql(ctx.max_rows, &child_columns)
1490 .map_err(|e| async_graphql::Error::new(e.to_string()))?;
1491
1492 let fetched = sqlx::query(&sql).bind(&keys).fetch_all(&mut *conn).await?;
1493
1494 use sqlx::Row;
1497 let pairs: Vec<(serde_json::Value, serde_json::Value)> = fetched
1498 .into_iter()
1499 .filter_map(|row| {
1500 Some((
1501 row.try_get::<serde_json::Value, _>(0).ok()?,
1502 row.try_get::<serde_json::Value, _>(1).ok()?,
1503 ))
1504 })
1505 .collect();
1506
1507 postrust_core::embed::group_from_aggregated(pairs)
1508 };
1509
1510 let has_deeper_embed = ctx
1515 .relationships
1516 .get(&rel.target_type)
1517 .map(|rels| {
1518 requested
1519 .selection_set()
1520 .any(|field| rels.iter().any(|r| r.name == field.name()))
1521 })
1522 .unwrap_or(false);
1523
1524 if has_deeper_embed {
1525 let mut order: Vec<(String, usize)> = Vec::with_capacity(grouped.len());
1526 let mut flat: Vec<serde_json::Value> = Vec::new();
1527 for (key, children) in grouped.drain() {
1528 order.push((key, children.len()));
1529 flat.extend(children);
1530 }
1531
1532 embed_relationships(&mut *conn, ctx, &rel.target_type, requested, &mut flat)
1533 .await?;
1534
1535 let mut rest = flat.into_iter();
1536 for (key, count) in order {
1537 grouped.insert(key, rest.by_ref().take(count).collect());
1538 }
1539 }
1540
1541 for row in rows.iter_mut() {
1542 postrust_core::embed::attach_to_parent(row, &rel.name, &plan, &grouped);
1543 }
1544 }
1545
1546 Ok(())
1547 })
1548}
1549
1550fn pk_where_from_args(
1555 ctx: &ResolverContext<'_>,
1556 table_name: &str,
1557 pk_columns: &[(String, String)],
1558) -> Result<serde_json::Value, async_graphql::Error> {
1559 if pk_columns.is_empty() {
1560 return Err(async_graphql::Error::new(format!(
1561 "\"{}\" has no primary key, so it cannot be mutated by key",
1562 table_name
1563 )));
1564 }
1565
1566 let mut conditions = serde_json::Map::new();
1567 for (col_name, _) in pk_columns {
1568 let value = ctx.args.try_get(col_name).map_err(|_| {
1569 async_graphql::Error::new(format!(
1570 "missing required primary key argument \"{}\"",
1571 col_name
1572 ))
1573 })?;
1574 conditions.insert(
1575 col_name.clone(),
1576 serde_json::json!({ "eq": accessor_to_json(&value) }),
1577 );
1578 }
1579
1580 Ok(serde_json::Value::Object(conditions))
1581}
1582
1583fn pk_argument_type(pg_type: &str) -> String {
1590 let rendered = crate::types::pg_type_to_graphql(pg_type).to_string();
1591 if rendered.starts_with('[') {
1592 "String".to_string()
1593 } else {
1594 rendered
1595 }
1596}
1597
1598fn graphql_type_ref(type_str: &str) -> TypeRef {
1600 let is_list = type_str.starts_with('[');
1602 let is_nn = type_str.ends_with('!');
1603
1604 let inner = if is_list {
1606 let stripped = type_str
1607 .trim_end_matches('!') .trim_start_matches('[') .trim_end_matches(']'); stripped
1611 } else {
1612 type_str.trim_end_matches('!')
1613 };
1614
1615 let inner_nn = inner.ends_with('!');
1616 let base_type = inner.trim_end_matches('!');
1617
1618 if is_list {
1619 if is_nn {
1620 if inner_nn {
1621 TypeRef::named_nn_list_nn(base_type)
1622 } else {
1623 TypeRef::named_list_nn(base_type)
1624 }
1625 } else if inner_nn {
1626 TypeRef::named_nn_list(base_type)
1627 } else {
1628 TypeRef::named_list(base_type)
1629 }
1630 } else if is_nn {
1631 TypeRef::named_nn(base_type)
1632 } else {
1633 TypeRef::named(base_type)
1634 }
1635}
1636
1637fn accessor_to_json(accessor: &ValueAccessor<'_>) -> serde_json::Value {
1639 if accessor.is_null() {
1641 serde_json::Value::Null
1642 } else if let Ok(b) = accessor.boolean() {
1643 serde_json::Value::Bool(b)
1644 } else if let Ok(i) = accessor.i64() {
1645 serde_json::Value::Number(i.into())
1646 } else if let Ok(f) = accessor.f64() {
1647 serde_json::Number::from_f64(f)
1648 .map(serde_json::Value::Number)
1649 .unwrap_or(serde_json::Value::Null)
1650 } else if let Ok(s) = accessor.string() {
1651 serde_json::Value::String(s.to_string())
1652 } else if let Ok(list) = accessor.list() {
1653 serde_json::Value::Array(list.iter().map(|v| accessor_to_json(&v)).collect())
1654 } else if let Ok(obj) = accessor.object() {
1655 let map: serde_json::Map<String, serde_json::Value> = obj
1656 .iter()
1657 .map(|(k, v)| (k.to_string(), accessor_to_json(&v)))
1658 .collect();
1659 serde_json::Value::Object(map)
1660 } else {
1661 serde_json::Value::Null
1662 }
1663}
1664
1665#[allow(dead_code)]
1667fn value_to_json(value: &Value) -> serde_json::Value {
1668 match value {
1669 Value::Null => serde_json::Value::Null,
1670 Value::Boolean(b) => serde_json::Value::Bool(*b),
1671 Value::Number(n) => {
1672 if let Some(i) = n.as_i64() {
1673 serde_json::Value::Number(i.into())
1674 } else if let Some(f) = n.as_f64() {
1675 serde_json::Value::Number(serde_json::Number::from_f64(f).unwrap())
1676 } else {
1677 serde_json::Value::Null
1678 }
1679 }
1680 Value::String(s) => serde_json::Value::String(s.clone()),
1681 Value::List(arr) => serde_json::Value::Array(arr.iter().map(value_to_json).collect()),
1682 Value::Object(obj) => {
1683 let map: serde_json::Map<String, serde_json::Value> = obj
1684 .iter()
1685 .map(|(k, v)| (k.to_string(), value_to_json(v)))
1686 .collect();
1687 serde_json::Value::Object(map)
1688 }
1689 Value::Binary(b) => serde_json::Value::String(base64::Engine::encode(
1690 &base64::engine::general_purpose::STANDARD,
1691 b,
1692 )),
1693 Value::Enum(e) => serde_json::Value::String(e.to_string()),
1694 }
1695}
1696
1697fn json_to_value(json: serde_json::Value) -> Value {
1699 match json {
1700 serde_json::Value::Null => Value::Null,
1701 serde_json::Value::Bool(b) => Value::Boolean(b),
1702 serde_json::Value::Number(n) => {
1703 if let Some(i) = n.as_i64() {
1704 Value::Number(i.into())
1705 } else if let Some(f) = n.as_f64() {
1706 Value::Number(async_graphql::Number::from_f64(f).unwrap())
1707 } else {
1708 Value::Null
1709 }
1710 }
1711 serde_json::Value::String(s) => Value::String(s),
1712 serde_json::Value::Array(arr) => Value::List(arr.into_iter().map(json_to_value).collect()),
1713 serde_json::Value::Object(obj) => {
1714 let map: indexmap::IndexMap<async_graphql::Name, Value> = obj
1715 .into_iter()
1716 .map(|(k, v)| (async_graphql::Name::new(k), json_to_value(v)))
1717 .collect();
1718 Value::Object(map)
1719 }
1720 }
1721}
1722
1723fn create_bigint_scalar() -> Scalar {
1725 Scalar::new("BigInt")
1726 .description("64-bit integer")
1727 .specified_by_url("https://spec.graphql.org/draft/#sec-Int")
1728}
1729
1730fn create_bigdecimal_scalar() -> Scalar {
1732 Scalar::new("BigDecimal").description("Arbitrary precision decimal number")
1733}
1734
1735fn create_json_scalar() -> Scalar {
1737 Scalar::new("JSON")
1738 .description("Arbitrary JSON value")
1739 .specified_by_url("https://spec.graphql.org/draft/#sec-Scalars")
1740}
1741
1742fn create_uuid_scalar() -> Scalar {
1744 Scalar::new("UUID").description("UUID string")
1745}
1746
1747fn create_date_scalar() -> Scalar {
1749 Scalar::new("Date").description("ISO 8601 date string (YYYY-MM-DD)")
1750}
1751
1752fn create_datetime_scalar() -> Scalar {
1754 Scalar::new("DateTime").description("ISO 8601 datetime string")
1755}
1756
1757fn create_time_scalar() -> Scalar {
1759 Scalar::new("Time").description("ISO 8601 time string (HH:MM:SS)")
1760}
1761
1762fn register_filter_input_types(builder: SchemaBuilder) -> SchemaBuilder {
1772 let string_filter = InputObject::new("StringFilterInput")
1773 .field(InputValue::new("eq", TypeRef::named("String")))
1774 .field(InputValue::new("neq", TypeRef::named("String")))
1775 .field(InputValue::new("like", TypeRef::named("String")))
1776 .field(InputValue::new("ilike", TypeRef::named("String")))
1777 .field(InputValue::new("in", TypeRef::named_list("String")))
1778 .field(InputValue::new("isNull", TypeRef::named("Boolean")));
1779
1780 let int_filter = InputObject::new("IntFilterInput")
1781 .field(InputValue::new("eq", TypeRef::named("Int")))
1782 .field(InputValue::new("neq", TypeRef::named("Int")))
1783 .field(InputValue::new("gt", TypeRef::named("Int")))
1784 .field(InputValue::new("gte", TypeRef::named("Int")))
1785 .field(InputValue::new("lt", TypeRef::named("Int")))
1786 .field(InputValue::new("lte", TypeRef::named("Int")))
1787 .field(InputValue::new("in", TypeRef::named_list("Int")));
1788
1789 let boolean_filter = InputObject::new("BooleanFilterInput")
1790 .field(InputValue::new("eq", TypeRef::named("Boolean")));
1791
1792 builder
1793 .register(string_filter)
1794 .register(int_filter)
1795 .register(boolean_filter)
1796}
1797
1798#[cfg(test)]
1799mod tests {
1800 use super::*;
1801 use indexmap::IndexMap;
1802 use postrust_core::schema_cache::{Column, Table};
1803 use std::collections::{HashMap, HashSet};
1804
1805 fn create_test_table(name: &str) -> Table {
1806 let mut columns = IndexMap::new();
1807 columns.insert(
1808 "id".into(),
1809 Column {
1810 name: "id".into(),
1811 description: None,
1812 nullable: false,
1813 data_type: "integer".into(),
1814 nominal_type: "int4".into(),
1815 max_len: None,
1816 default: Some("nextval('id_seq')".into()),
1817 enum_values: vec![],
1818 is_pk: true,
1819 position: 1,
1820 },
1821 );
1822 columns.insert(
1823 "name".into(),
1824 Column {
1825 name: "name".into(),
1826 description: None,
1827 nullable: false,
1828 data_type: "text".into(),
1829 nominal_type: "text".into(),
1830 max_len: None,
1831 default: None,
1832 enum_values: vec![],
1833 is_pk: false,
1834 position: 2,
1835 },
1836 );
1837
1838 Table {
1839 schema: "public".into(),
1840 name: name.into(),
1841 description: None,
1842 is_view: false,
1843 insertable: true,
1844 updatable: true,
1845 deletable: true,
1846 pk_cols: vec!["id".into()],
1847 columns,
1848 }
1849 }
1850
1851 fn create_test_schema_cache() -> SchemaCache {
1852 let mut tables = HashMap::new();
1853 let users = create_test_table("users");
1854 tables.insert(users.qualified_identifier(), users);
1855
1856 SchemaCache {
1857 tables,
1858 relationships: HashMap::new(),
1859 routines: HashMap::new(),
1860 timezones: HashSet::new(),
1861 pg_version: 150000,
1862 }
1863 }
1864
1865 #[test]
1870 fn test_graphql_type_ref_simple() {
1871 let _type_ref = graphql_type_ref("String");
1872 }
1874
1875 #[test]
1876 fn test_graphql_type_ref_non_null() {
1877 let _type_ref = graphql_type_ref("String!");
1878 }
1879
1880 #[test]
1881 fn test_graphql_type_ref_list() {
1882 let _type_ref = graphql_type_ref("[String]");
1883 }
1884
1885 #[test]
1886 fn test_graphql_type_ref_list_non_null() {
1887 let _type_ref = graphql_type_ref("[String!]!");
1888 }
1889
1890 #[test]
1895 fn test_value_to_json_null() {
1896 let value = Value::Null;
1897 let json = value_to_json(&value);
1898 assert_eq!(json, serde_json::Value::Null);
1899 }
1900
1901 #[test]
1902 fn test_value_to_json_boolean() {
1903 let value = Value::Boolean(true);
1904 let json = value_to_json(&value);
1905 assert_eq!(json, serde_json::Value::Bool(true));
1906 }
1907
1908 #[test]
1909 fn test_value_to_json_number() {
1910 let value = Value::Number(42.into());
1911 let json = value_to_json(&value);
1912 assert_eq!(json, serde_json::json!(42));
1913 }
1914
1915 #[test]
1916 fn test_value_to_json_string() {
1917 let value = Value::String("hello".to_string());
1918 let json = value_to_json(&value);
1919 assert_eq!(json, serde_json::Value::String("hello".to_string()));
1920 }
1921
1922 #[test]
1923 fn test_value_to_json_list() {
1924 let value = Value::List(vec![Value::Number(1.into()), Value::Number(2.into())]);
1925 let json = value_to_json(&value);
1926 assert_eq!(json, serde_json::json!([1, 2]));
1927 }
1928
1929 #[test]
1930 fn test_json_to_value_null() {
1931 let json = serde_json::Value::Null;
1932 let value = json_to_value(json);
1933 assert!(matches!(value, Value::Null));
1934 }
1935
1936 #[test]
1937 fn test_json_to_value_boolean() {
1938 let json = serde_json::Value::Bool(false);
1939 let value = json_to_value(json);
1940 assert!(matches!(value, Value::Boolean(false)));
1941 }
1942
1943 #[test]
1944 fn test_json_to_value_number() {
1945 let json = serde_json::json!(123);
1946 let value = json_to_value(json);
1947 assert!(matches!(value, Value::Number(_)));
1948 }
1949
1950 #[test]
1951 fn test_json_to_value_string() {
1952 let json = serde_json::Value::String("test".to_string());
1953 let value = json_to_value(json);
1954 assert!(matches!(value, Value::String(_)));
1955 }
1956
1957 #[test]
1958 fn test_json_to_value_array() {
1959 let json = serde_json::json!([1, 2, 3]);
1960 let value = json_to_value(json);
1961 assert!(matches!(value, Value::List(_)));
1962 }
1963
1964 #[test]
1965 fn test_json_to_value_object() {
1966 let json = serde_json::json!({"key": "value"});
1967 let value = json_to_value(json);
1968 assert!(matches!(value, Value::Object(_)));
1969 }
1970
1971 #[test]
1976 fn test_build_dynamic_schema() {
1977 let cache = create_test_schema_cache();
1978 let config = SchemaConfig::default();
1979 let generated = build_schema(&cache, &config);
1980
1981 let result = build_dynamic_schema(&generated, &cache, None, None);
1982 if let Err(ref e) = result {
1983 eprintln!("Schema build error: {:?}", e);
1984 }
1985 assert!(result.is_ok(), "Schema build failed: {:?}", result.err());
1986 }
1987
1988 #[test]
1989 fn test_create_object_type() {
1990 let table = create_test_table("users");
1991 let obj = TableObjectType::from_table(&table);
1992 let _gql_obj = create_object_type(&obj, &[]);
1993 }
1994
1995 #[test]
1996 fn test_create_query_type() {
1997 let cache = create_test_schema_cache();
1998 let config = SchemaConfig::default();
1999 let generated = build_schema(&cache, &config);
2000
2001 let _query = create_query_type(&generated, None, Arc::new(HashMap::new()));
2002 }
2003
2004 #[test]
2005 fn test_create_mutation_type() {
2006 let cache = create_test_schema_cache();
2007 let config = SchemaConfig::default();
2008 let generated = build_schema(&cache, &config);
2009
2010 let _mutation = create_mutation_type(&generated);
2011 }
2012
2013 #[test]
2018 fn test_create_scalars() {
2019 let _bigint = create_bigint_scalar();
2020 let _json = create_json_scalar();
2021 let _uuid = create_uuid_scalar();
2022 let _datetime = create_datetime_scalar();
2023 }
2024
2025 #[test]
2030 fn test_register_filter_input_types() {
2031 let cache = create_test_schema_cache();
2032 let config = SchemaConfig::default();
2033 let _generated = build_schema(&cache, &config);
2034
2035 let query =
2037 Object::new("Query").field(Field::new("test", TypeRef::named("String"), |_| {
2038 FieldFuture::new(async { Ok(None::<FieldValue>) })
2039 }));
2040
2041 let mut builder = Schema::build("Query", None::<&str>, None);
2042 builder = builder.register(query);
2043 builder = register_filter_input_types(builder);
2044
2045 let result = builder.finish();
2046 assert!(result.is_ok());
2047 }
2048
2049 #[test]
2054 fn test_build_schema_with_subscriptions() {
2055 let cache = create_test_schema_cache();
2056 let config = SchemaConfig {
2057 enable_subscriptions: true,
2058 ..SchemaConfig::default()
2059 };
2060 let generated = build_schema(&cache, &config);
2061
2062 let sub_fields = generate_subscription_fields(&cache, &generated);
2064 assert!(!sub_fields.is_empty(), "Should have subscription fields");
2065
2066 let result = build_dynamic_schema(&generated, &cache, Some(&sub_fields), None);
2068 assert!(result.is_ok(), "Schema with subscriptions should build");
2069 }
2070
2071 #[test]
2072 fn test_subscription_field_generation() {
2073 let cache = create_test_schema_cache();
2074 let config = SchemaConfig::default();
2075 let generated = build_schema(&cache, &config);
2076
2077 let fields = generate_subscription_fields(&cache, &generated);
2078
2079 assert_eq!(fields.len(), 1);
2081 assert_eq!(fields[0].name, "users");
2082 assert_eq!(fields[0].table_name, "users");
2083 assert_eq!(fields[0].channel_name(), "postrust_public_users");
2084 }
2085
2086 #[test]
2087 fn test_create_subscription_type() {
2088 use crate::subscription::SubscriptionField as SubField;
2089
2090 let fields = vec![
2091 SubField::for_table("public", "users", "Users"),
2092 SubField::for_table("public", "orders", "Orders"),
2093 ];
2094
2095 let _subscription = create_subscription_type(&fields);
2096 }
2098}