Skip to main content

mongo_graphql/schema/
builder.rs

1use std::collections::{HashMap, HashSet};
2
3use async_graphql::dynamic::{
4    Field, FieldFuture, FieldValue, InputObject, InputValue, Object, Schema,
5    SchemaBuilder as AgSchemaBuilder, TypeRef,
6};
7use mongodb::{Client, Database};
8
9use crate::error::GraphQLError;
10use crate::helpers::serialization::json_to_field_value;
11use crate::resolvers::{mutation, query};
12use crate::schema::definition::{CollectionDef, EnumDef, FieldType, RelationKind, SchemaDefinition};
13use crate::types::scalars;
14
15#[derive(Debug, Clone)]
16pub struct RuntimeConfig {
17    pub max_page_size: usize,
18}
19
20pub struct SchemaBuilder<'a> {
21    config: &'a RuntimeConfig,
22    definition: &'a SchemaDefinition,
23    registered_enums: HashSet<String>,
24    registered_nested_inputs: HashSet<String>,
25}
26
27impl<'a> SchemaBuilder<'a> {
28    pub fn new(config: &'a RuntimeConfig, definition: &'a SchemaDefinition) -> Self {
29        Self {
30            config,
31            definition,
32            registered_enums: HashSet::new(),
33            registered_nested_inputs: HashSet::new(),
34        }
35    }
36
37    pub async fn build(mut self, client: Client, db: Database) -> Result<Schema, GraphQLError> {
38        let mut ping_cmd = mongodb::bson::Document::new();
39        ping_cmd.insert("ping", 1);
40        db.run_command(ping_cmd)
41            .await
42            .map_err(|err| GraphQLError::Database(format!("Database ping failed: {}", err)))?;
43
44        let mut builder = Schema::build("Query", Some("Mutation"), None);
45        builder = scalars::register_all(builder);
46        builder = self.register_page_info(builder);
47        builder = self.register_delete_result(builder);
48        builder = self.register_filter_types(builder);
49
50        let mut query_root = Object::new("Query");
51        let mut mutation_root = Object::new("Mutation");
52
53        for collection in &self.definition.collections {
54            let where_unique = InputObject::new(format!("{}WhereUniqueInput", collection.type_name()))
55                .field(InputValue::new("id", TypeRef::named_nn("ID")));
56            builder = builder.register(where_unique);
57        }
58
59        for collection in &self.definition.collections {
60            (builder, query_root, mutation_root) = self.register_collection(
61                builder, query_root, mutation_root, collection, &client, &db,
62            )?;
63        }
64
65        builder = builder.register(query_root);
66        builder = builder.register(mutation_root);
67
68        let schema = builder
69            .data(client)
70            .data(db)
71            .data(self.config.clone())
72            .data(self.definition.clone())
73            .finish()
74            .map_err(|err| GraphQLError::SchemaBuild(format!("Failed to build schema: {}", err)))?;
75
76        Ok(schema)
77    }
78
79    fn register_collection(
80        &mut self,
81        mut builder: AgSchemaBuilder,
82        mut query_root: Object,
83        mut mutation_root: Object,
84        collection: &CollectionDef,
85        client: &Client,
86        db: &Database,
87    ) -> Result<(AgSchemaBuilder, Object, Object), GraphQLError> {
88        let type_name = collection.type_name();
89
90        for field in &collection.fields {
91            if let Some(enum_def) = &field.r#enum {
92                builder = self.register_enum(builder, enum_def);
93            }
94        }
95
96        let collection_map: HashMap<&str, &CollectionDef> = self
97            .definition
98            .collections
99            .iter()
100            .map(|collection| (collection.collection.as_str(), collection))
101            .collect();
102
103        // Collect reverse relations: other collections with OneToMany or OneToOne
104        // pointing to THIS collection get a reverse field on this collection's inputs.
105        let (reverse_to_many, reverse_to_one) = self.collect_reverse_relations(collection);
106
107        let mut obj = Object::new(type_name.clone());
108        for field in &collection.fields {
109            if matches!(field.field_type, FieldType::Relation(_)) {
110                continue;
111            }
112            let field_type = scalars::type_ref(&field.field_type);
113            let field_name = field.graphql_name();
114            obj = obj.field(Field::new(
115                field_name.clone(),
116                field_type,
117                move |ctx| {
118                    let name = field_name.clone();
119                    FieldFuture::new(async move {
120                        let value = ctx
121                            .parent_value
122                            .as_value()
123                            .and_then(|parent| extract_nested(parent, &name));
124                        match value {
125                            Some(val) => Ok(Some(FieldValue::value(val))),
126                            None => Ok(None),
127                        }
128                    })
129                },
130            ));
131        }
132
133        for field in &collection.fields {
134            let relation = match &field.field_type {
135                FieldType::Relation(relation) => relation,
136                _ => continue,
137            };
138            let target_def = match collection_map.get(relation.collection.as_str()) {
139                Some(collection_def) => (*collection_def).clone(),
140                None => continue,
141            };
142            let target_type = target_def.type_name();
143            let db_for_rel = db.clone();
144
145            match relation.kind {
146                RelationKind::OneToMany | RelationKind::OneToOne => {
147                    let target_coll_name = relation.collection.clone();
148                    let fk_mongo_name = field.name.clone();
149                    let target_def_for_closure = target_def.clone();
150                    obj = obj.field(Field::new(
151                        field.graphql_name(),
152                        TypeRef::named(target_type),
153                        move |ctx| {
154                            let db = db_for_rel.clone();
155                            let target_coll = target_coll_name.clone();
156                            let target_def = target_def_for_closure.clone();
157                            let fk_name = fk_mongo_name.clone();
158                            FieldFuture::new(async move {
159                                let result = crate::resolvers::query_relation::resolve_forward_to_one(
160                                    &ctx, &db, &target_coll, &target_def, &fk_name,
161                                )
162                                .await;
163                                resolve_to_field_value(result)
164                            })
165                        },
166                    ));
167                }
168                RelationKind::ManyToMany => {
169                    let junction = match &relation.junction {
170                        Some(junction_def) => junction_def.clone(),
171                        None => continue,
172                    };
173                    let target_def_for_closure = target_def.clone();
174                    obj = obj.field(Field::new(
175                        field.graphql_name(),
176                        TypeRef::named_nn_list(&target_type),
177                        move |ctx| {
178                            let db = db_for_rel.clone();
179                            let junction = junction.clone();
180                            let target_def = target_def_for_closure.clone();
181                            FieldFuture::new(async move {
182                                let result = crate::resolvers::query_relation::resolve_forward_many_to_many(
183                                    &ctx, &db, &junction, &target_def,
184                                )
185                                .await;
186                                resolve_to_field_value(result)
187                            })
188                        },
189                    ));
190                }
191            }
192        }
193
194        for (reverse_name, target_coll_name, fk_field) in &reverse_to_many {
195            let target_def = match collection_map.get(target_coll_name.as_str()) {
196                Some(collection_def) => (*collection_def).clone(),
197                None => continue,
198            };
199            let target_type = target_def.type_name();
200            let fk_field_for_closure = fk_field.clone();
201            let target_def_for_closure = target_def.clone();
202            let db_for_rel = db.clone();
203            obj = obj.field(Field::new(
204                reverse_name.clone(),
205                TypeRef::named_nn_list(target_type),
206                move |ctx| {
207                    let db = db_for_rel.clone();
208                    let target_def = target_def_for_closure.clone();
209                    let fk_field = fk_field_for_closure.clone();
210                    FieldFuture::new(async move {
211                        let result = crate::resolvers::query_relation::resolve_reverse_to_many(
212                            &ctx, &db, &target_def, &fk_field,
213                        )
214                        .await;
215                        resolve_to_field_value(result)
216                    })
217                },
218            ));
219        }
220
221        for (reverse_name, target_coll_name, fk_field) in &reverse_to_one {
222            let target_def = match collection_map.get(target_coll_name.as_str()) {
223                Some(collection_def) => (*collection_def).clone(),
224                None => continue,
225            };
226            let target_type = target_def.type_name();
227            let fk_field_for_closure = fk_field.clone();
228            let target_def_for_closure = target_def.clone();
229            let db_for_rel = db.clone();
230            obj = obj.field(Field::new(
231                reverse_name.clone(),
232                TypeRef::named(target_type),
233                move |ctx| {
234                    let db = db_for_rel.clone();
235                    let target_def = target_def_for_closure.clone();
236                    let fk_field = fk_field_for_closure.clone();
237                    FieldFuture::new(async move {
238                        let result = crate::resolvers::query_relation::resolve_reverse_to_one(
239                            &ctx, &db, &target_def, &fk_field,
240                        )
241                        .await;
242                        resolve_to_field_value(result)
243                    })
244                },
245            ));
246        }
247
248        builder = builder.register(obj);
249
250        for field in &collection.fields {
251            if let FieldType::Relation(_rel) = &field.field_type {
252                builder = self.register_nested_inputs_for_field(builder, collection, field);
253            }
254        }
255
256        for (_, target_coll_name, _) in &reverse_to_many {
257            if let Some(target_def) = collection_map.get(target_coll_name.as_str()) {
258                (builder, _) = self.register_reverse_inputs(
259                    builder, collection, target_def, RelationKind::OneToMany,
260                );
261            }
262        }
263        for (_, target_coll_name, _) in &reverse_to_one {
264            if let Some(target_def) = collection_map.get(target_coll_name.as_str()) {
265                (builder, _) = self.register_reverse_inputs(
266                    builder, collection, target_def, RelationKind::OneToOne,
267                );
268            }
269        }
270
271        let mut create_input = InputObject::new(format!("{}CreateInput", type_name));
272        create_input = self.build_input_fields(create_input, collection, &collection_map, "Create");
273        create_input = self.add_reverse_fields_to_input(create_input, &reverse_to_many, &reverse_to_one, &collection_map, &type_name, "Create");
274        builder = builder.register(create_input);
275
276        let mut update_input = InputObject::new(format!("{}UpdateInput", type_name));
277        update_input = self.build_input_fields(update_input, collection, &collection_map, "Update");
278        update_input = self.add_reverse_fields_to_input(update_input, &reverse_to_many, &reverse_to_one, &collection_map, &type_name, "Update");
279        builder = builder.register(update_input);
280
281        let mut where_input =
282            InputObject::new(format!("{}WhereInput", type_name))
283                .field(InputValue::new("id", TypeRef::named("IdFilter")));
284        for field in &collection.fields {
285            if field.name == "id" || field.name == "_id" {
286                continue;
287            }
288            if matches!(field.field_type, FieldType::Relation(_)) {
289                continue;
290            }
291            let filter_type = filter_type_name(&field.field_type);
292            where_input =
293                where_input.field(InputValue::new(field.graphql_name(), TypeRef::named(filter_type)));
294        }
295        builder = builder.register(where_input);
296
297        let mut sort_input = InputObject::new(format!("{}SortInput", type_name));
298        for field in &collection.fields {
299            if field.name == "id" || field.name == "_id" {
300                continue;
301            }
302            if matches!(field.field_type, FieldType::Relation(_) | FieldType::Json | FieldType::List(_)) {
303                continue;
304            }
305            sort_input =
306                sort_input.field(InputValue::new(field.graphql_name(), TypeRef::named("SortDirection")));
307        }
308        builder = builder.register(sort_input);
309
310        let object_ref = TypeRef::named(type_name.clone());
311
312        let conn_name = format!("{}Connection", type_name);
313        let conn_obj = Object::new(conn_name.clone())
314            .field(nested_field("edges", TypeRef::named_nn_list(type_name.clone())))
315            .field(nested_field("pageInfo", TypeRef::named_nn("PageInfo")))
316            .field(nested_field("totalCount", TypeRef::named_nn("Int")));
317        builder = builder.register(conn_obj);
318
319        let coll_for_get = collection.clone();
320        let db_for_get = db.clone();
321        query_root = query_root.field(
322            Field::new(collection.singular_name(), object_ref.clone(), move |ctx| {
323                let coll_def = coll_for_get.clone();
324                let db = db_for_get.clone();
325                FieldFuture::new(async move {
326                    let result = query::resolve_get(ctx, &coll_def, &db).await;
327                    (match result {
328                        Ok(Some(value)) => json_to_field_value(value).map(Some),
329                        Ok(None) => Ok(None),
330                        Err(err) => Err(err),
331                    })
332                    .map_err(|err| err.into_graphql_error())
333                })
334            })
335            .argument(InputValue::new(
336                "where",
337                TypeRef::named_nn(format!("{}WhereUniqueInput", type_name)),
338            )),
339        );
340
341        let coll_for_list = collection.clone();
342        let db_for_list = db.clone();
343        let page_size = self.config.max_page_size;
344        query_root = query_root.field(
345            Field::new(
346                collection.plural_name(),
347                TypeRef::named_nn(conn_name.clone()),
348                move |ctx| {
349                    let coll_def = coll_for_list.clone();
350                    let db = db_for_list.clone();
351                    FieldFuture::new(async move {
352                        let result = query::resolve_list(ctx, &coll_def, &db, page_size).await;
353                        (match result {
354                            Ok(value) => json_to_field_value(value).map(Some),
355                            Err(err) => Err(err),
356                        })
357                        .map_err(|err| err.into_graphql_error())
358                    })
359                },
360            )
361            .argument(InputValue::new("first", TypeRef::named("Int")))
362            .argument(InputValue::new("after", TypeRef::named("String")))
363            .argument(InputValue::new("last", TypeRef::named("Int")))
364            .argument(InputValue::new("before", TypeRef::named("String")))
365            .argument(InputValue::new("where", TypeRef::named(format!("{}WhereInput", type_name))))
366            .argument(InputValue::new("sort", TypeRef::named(format!("{}SortInput", type_name)))),
367        );
368
369        let coll_for_create = collection.clone();
370        let client_for_create = client.clone();
371        let db_for_create = db.clone();
372        mutation_root = mutation_root.field(
373            Field::new(
374                format!("create{}", type_name),
375                object_ref.clone(),
376                move |ctx| {
377                    let coll_def = coll_for_create.clone();
378                    let client = client_for_create.clone();
379                    let db = db_for_create.clone();
380                    FieldFuture::new(async move {
381                        let result = mutation::resolve_create(ctx, &coll_def, &client, &db).await;
382                        resolve_to_field_value(result)
383                    })
384                },
385            )
386            .argument(InputValue::new(
387                "input",
388                TypeRef::named_nn(format!("{}CreateInput", type_name)),
389            )),
390        );
391
392        let coll_for_update = collection.clone();
393        let client_for_update = client.clone();
394        let db_for_update = db.clone();
395        mutation_root = mutation_root.field(
396            Field::new(
397                format!("update{}", type_name),
398                object_ref.clone(),
399                move |ctx| {
400                    let coll_def = coll_for_update.clone();
401                    let client = client_for_update.clone();
402                    let db = db_for_update.clone();
403                    FieldFuture::new(async move {
404                        let result = mutation::resolve_update(ctx, &coll_def, &client, &db).await;
405                        resolve_to_field_value(result)
406                    })
407                },
408            )
409            .argument(InputValue::new(
410                "where",
411                TypeRef::named_nn(format!("{}WhereUniqueInput", type_name)),
412            ))
413            .argument(InputValue::new(
414                "input",
415                TypeRef::named_nn(format!("{}UpdateInput", type_name)),
416            )),
417        );
418
419        let coll_for_delete = collection.clone();
420        let client_for_delete = client.clone();
421        let db_for_delete = db.clone();
422        mutation_root = mutation_root.field(
423            Field::new(
424                format!("delete{}", type_name),
425                TypeRef::named_nn("DeleteResult"),
426                move |ctx| {
427                    let coll_def = coll_for_delete.clone();
428                    let client = client_for_delete.clone();
429                    let db = db_for_delete.clone();
430                    FieldFuture::new(async move {
431                        let result = mutation::resolve_delete(ctx, &coll_def, &client, &db).await;
432                        resolve_to_field_value(result)
433                    })
434                },
435            )
436            .argument(InputValue::new(
437                "where",
438                TypeRef::named_nn(format!("{}WhereUniqueInput", type_name)),
439            )),
440        );
441
442        Ok((builder, query_root, mutation_root))
443    }
444
445    fn collect_reverse_relations(
446        &self,
447        collection: &CollectionDef,
448    ) -> (Vec<(String, String, String)>, Vec<(String, String, String)>) {
449        let mut to_many = Vec::new();
450        let mut to_one = Vec::new();
451        for other in &self.definition.collections {
452            if other.collection == collection.collection {
453                continue;
454            }
455            for field in &other.fields {
456                if let FieldType::Relation(relation) = &field.field_type {
457                    if relation.collection != collection.collection {
458                        continue;
459                    }
460                    match relation.kind {
461                        RelationKind::OneToMany => {
462                            let name = relation
463                                .reverse_name
464                                .clone()
465                                .unwrap_or_else(|| other.plural_name());
466                            to_many.push((name, other.collection.clone(), field.name.clone()));
467                        }
468                        RelationKind::OneToOne => {
469                            let name = relation
470                                .reverse_name
471                                .clone()
472                                .unwrap_or_else(|| other.singular_name());
473                            to_one.push((name, other.collection.clone(), field.name.clone()));
474                        }
475                        _ => {}
476                    }
477                }
478            }
479        }
480        (to_many, to_one)
481    }
482
483    /// Build scalar + relation fields for CreateInput or UpdateInput.
484    fn build_input_fields(
485        &self,
486        mut input: InputObject,
487        collection: &CollectionDef,
488        collection_map: &HashMap<&str, &CollectionDef>,
489        mutation: &str,
490    ) -> InputObject {
491        for field in &collection.fields {
492            if field.name == "id" || field.name == "_id" {
493                continue;
494            }
495            if let FieldType::Relation(relation) = &field.field_type {
496                let target_type = collection_map
497                    .get(relation.collection.as_str())
498                    .map(|collection| collection.type_name())
499                    .unwrap_or_else(|| relation.collection.clone());
500                let input_name = Self::relation_nested_input_name(relation.kind, &target_type, &collection.type_name(), mutation);
501                input = input.field(InputValue::new(field.graphql_name(), TypeRef::named(&input_name)));
502                continue;
503            }
504            let field_type = scalars::type_ref(&field.field_type);
505            let input_type = if field.required && mutation == "Create" {
506                TypeRef::named_nn(field_type.type_name())
507            } else {
508                field_type
509            };
510            input = input.field(InputValue::new(field.graphql_name(), input_type));
511        }
512        input
513    }
514
515    /// Add reverse to-many and to-one fields to a CreateInput or UpdateInput.
516    fn add_reverse_fields_to_input(
517        &self,
518        mut input: InputObject,
519        reverse_to_many: &[(String, String, String)],
520        reverse_to_one: &[(String, String, String)],
521        collection_map: &HashMap<&str, &CollectionDef>,
522        source_type: &str,
523        mutation: &str,
524    ) -> InputObject {
525        for (reverse_name, target_coll_name, _) in reverse_to_many {
526            if let Some(target_def) = collection_map.get(target_coll_name.as_str()) {
527                let input_name = Self::relation_nested_input_name(
528                    RelationKind::ManyToMany, &target_def.type_name(), source_type, mutation,
529                );
530                input = input.field(InputValue::new(reverse_name.clone(), TypeRef::named(&input_name)));
531            }
532        }
533        for (reverse_name, target_coll_name, _) in reverse_to_one {
534            if let Some(target_def) = collection_map.get(target_coll_name.as_str()) {
535                let input_name = Self::relation_nested_input_name(
536                    RelationKind::OneToOne, &target_def.type_name(), source_type, mutation,
537                );
538                input = input.field(InputValue::new(reverse_name.clone(), TypeRef::named(&input_name)));
539            }
540        }
541        input
542    }
543
544    /// Build the scalar + forward relation fields for a `CreateWithout` or
545    /// `UpdateWithout` input. Skips id/_id and relations pointing back to
546    /// `source_coll`. For `mutation == "Create"`, required scalars are NN.
547    fn build_without_fields(
548        &self,
549        mut without_input: InputObject,
550        target_coll: &CollectionDef,
551        source_coll: &CollectionDef,
552        mutation: &str,
553    ) -> InputObject {
554        for field in &target_coll.fields {
555            if field.name == "id" || field.name == "_id" {
556                continue;
557            }
558            if let FieldType::Relation(relation) = &field.field_type {
559                if relation.collection == source_coll.collection {
560                    continue;
561                }
562                let target_type = self
563                    .definition
564                    .collection_by_name(&relation.collection)
565                    .map(|collection| collection.type_name())
566                    .unwrap_or_else(|| relation.collection.clone());
567                let input_name = Self::relation_nested_input_name(
568                    relation.kind, &target_type, &target_coll.type_name(), mutation,
569                );
570                without_input =
571                    without_input.field(InputValue::new(field.graphql_name(), TypeRef::named(&input_name)));
572                continue;
573            }
574            let field_type = scalars::type_ref(&field.field_type);
575            let input_type = if field.required && mutation == "Create" {
576                TypeRef::named_nn(field_type.type_name())
577            } else {
578                field_type
579            };
580            without_input =
581                without_input.field(InputValue::new(field.graphql_name(), input_type));
582        }
583        without_input
584    }
585
586    /// Ensure a CreateWithout or UpdateWithout input type is registered for
587    /// target_coll with source_coll omitted. Returns the type name.
588    fn ensure_without_type(
589        &mut self,
590        mut builder: AgSchemaBuilder,
591        target_coll: &CollectionDef,
592        source_coll: &CollectionDef,
593        mutation: &str,
594    ) -> (AgSchemaBuilder, String) {
595        let target_type = target_coll.type_name();
596        let source_type = source_coll.type_name();
597        let without_name = match mutation {
598            "Create" => format!("{}CreateWithout{}Input", target_type, source_type),
599            "Update" => format!("{}UpdateWithout{}Input", target_type, source_type),
600            _ => panic!("unknown mutation: {}", mutation),
601        };
602        if !self.registered_nested_inputs.contains(&without_name) {
603            let base = self.build_without_fields(
604                InputObject::new(&without_name), target_coll, source_coll, mutation,
605            );
606            let with_reverse;
607            (builder, with_reverse) = self.add_reverse_fields_to_without(
608                builder, base, target_coll, source_coll,
609            );
610            builder = builder.register(with_reverse);
611            self.registered_nested_inputs.insert(without_name.clone());
612        }
613        (builder, without_name)
614    }
615
616    fn register_nested_inputs_for_field(
617        &mut self,
618        mut builder: AgSchemaBuilder,
619        source_coll: &CollectionDef,
620        field: &crate::schema::definition::FieldDef,
621    ) -> AgSchemaBuilder {
622        let relation = match &field.field_type {
623            FieldType::Relation(relation) => relation,
624            _ => return builder,
625        };
626        let target_def = match self.definition.collection_by_name(&relation.collection) {
627            Some(collection_def) => collection_def,
628            None => return builder,
629        };
630        let target_type = target_def.type_name();
631        let source_type = source_coll.type_name();
632
633        let create_without;
634        (builder, create_without) = self.ensure_without_type(builder, target_def, source_coll, "Create");
635        let update_without;
636        (builder, update_without) = self.ensure_without_type(builder, target_def, source_coll, "Update");
637
638        let is_to_many = matches!(relation.kind, RelationKind::ManyToMany);
639        builder = self.register_one_or_many_inputs(
640            builder, &target_type, &source_type, &create_without, &update_without, is_to_many,
641        );
642
643        builder
644    }
645
646    /// Register CreateNested{One/Many}Without{Source}Input and
647    /// Update{One/Many}Without{Source}NestedInput.
648    fn register_one_or_many_inputs(
649        &mut self,
650        mut builder: AgSchemaBuilder,
651        target_type: &str,
652        source_type: &str,
653        create_without_name: &str,
654        update_without_name: &str,
655        is_to_many: bool,
656    ) -> AgSchemaBuilder {
657        let where_unique = format!("{}WhereUniqueInput", target_type);
658        let (cardinality, create_ref, connect_ref, disconnect_delete_ref) = if is_to_many {
659            ("Many", TypeRef::named_nn_list(create_without_name), TypeRef::named_nn_list(&where_unique), TypeRef::named_nn_list(&where_unique))
660        } else {
661            ("One", TypeRef::named(create_without_name), TypeRef::named(&where_unique), TypeRef::named("Boolean"))
662        };
663
664        let create_name = format!("{}CreateNested{}Without{}Input", target_type, cardinality, source_type);
665        if !self.registered_nested_inputs.contains(&create_name) {
666            let input = InputObject::new(&create_name)
667                .field(InputValue::new("create", create_ref.clone()))
668                .field(InputValue::new("connect", connect_ref.clone()));
669            builder = builder.register(input);
670            self.registered_nested_inputs.insert(create_name);
671        }
672
673        // Build update field ref: to-one is nullable named, to-many needs WithWhere wrapper
674        let update_ref = if is_to_many {
675            let with_where_name = format!("{}UpdateWithWhereUniqueWithout{}Input", target_type, source_type);
676            if !self.registered_nested_inputs.contains(&with_where_name) {
677                let with_where = InputObject::new(&with_where_name)
678                    .field(InputValue::new("where", TypeRef::named_nn(&where_unique)))
679                    .field(InputValue::new("data", TypeRef::named_nn(update_without_name)));
680                builder = builder.register(with_where);
681                self.registered_nested_inputs.insert(with_where_name.clone());
682            }
683            TypeRef::named_nn_list(&with_where_name)
684        } else {
685            TypeRef::named(update_without_name)
686        };
687
688        let update_name = format!("{}Update{}Without{}NestedInput", target_type, cardinality, source_type);
689        if !self.registered_nested_inputs.contains(&update_name) {
690            let input = InputObject::new(&update_name)
691                .field(InputValue::new("create", create_ref))
692                .field(InputValue::new("connect", connect_ref))
693                .field(InputValue::new("disconnect", disconnect_delete_ref.clone()))
694                .field(InputValue::new("delete", disconnect_delete_ref))
695                .field(InputValue::new("update", update_ref));
696            builder = builder.register(input);
697            self.registered_nested_inputs.insert(update_name);
698        }
699
700        builder
701    }
702
703    /// Unified reverse input registration. Used for both OneToMany and OneToOne
704    /// reverse fields. Threads `builder` through for recursive 3-level support.
705    fn register_reverse_inputs(
706        &mut self,
707        mut builder: AgSchemaBuilder,
708        source_coll: &CollectionDef,
709        target_coll: &CollectionDef,
710        kind: RelationKind,
711    ) -> (AgSchemaBuilder, String) {
712        let target_type = target_coll.type_name();
713        let source_type = source_coll.type_name();
714        let is_to_many = matches!(kind, RelationKind::OneToMany);
715
716        let create_without;
717        (builder, create_without) = self.ensure_without_type(builder, target_coll, source_coll, "Create");
718        let update_without;
719        (builder, update_without) = self.ensure_without_type(builder, target_coll, source_coll, "Update");
720
721        builder = self.register_one_or_many_inputs(
722            builder, &target_type, &source_type, &create_without, &update_without, is_to_many,
723        );
724
725        let nested_name = if is_to_many {
726            format!("{}CreateNestedManyWithout{}Input", target_type, source_type)
727        } else {
728            format!("{}CreateNestedOneWithout{}Input", target_type, source_type)
729        };
730
731        (builder, nested_name)
732    }
733
734    /// Add reverse fields (OneToMany and OneToOne) to a CreateWithout or
735    /// UpdateWithout input for 3-level nesting support.
736    fn add_reverse_fields_to_without(
737        &mut self,
738        mut builder: AgSchemaBuilder,
739        mut without_input: InputObject,
740        target_coll: &CollectionDef,
741        source_coll: &CollectionDef,
742    ) -> (AgSchemaBuilder, InputObject) {
743        for other_coll in &self.definition.collections {
744            if other_coll.collection == target_coll.collection {
745                continue;
746            }
747            for other_field in &other_coll.fields {
748                let relation = match &other_field.field_type {
749                    FieldType::Relation(relation) if relation.collection == target_coll.collection => relation,
750                    _ => continue,
751                };
752                // Skip reverse fields originating from the source collection —
753                // they would create a circular type reference back to source.
754                if other_coll.collection == source_coll.collection {
755                    continue;
756                }
757                let (reverse_name, kind) = match relation.kind {
758                    RelationKind::OneToMany => (
759                        relation.reverse_name.clone().unwrap_or_else(|| other_coll.plural_name()),
760                        RelationKind::OneToMany,
761                    ),
762                    RelationKind::OneToOne => (
763                        relation.reverse_name.clone().unwrap_or_else(|| other_coll.singular_name()),
764                        RelationKind::OneToOne,
765                    ),
766                    _ => continue,
767                };
768                let type_name;
769                (builder, type_name) = self.register_reverse_inputs(
770                    builder, target_coll, other_coll, kind,
771                );
772                without_input = without_input.field(InputValue::new(
773                    reverse_name,
774                    TypeRef::named(&type_name),
775                ));
776            }
777        }
778        (builder, without_input)
779    }
780
781    fn relation_nested_input_name(kind: RelationKind, target_type: &str, source_type: &str, mutation: &str) -> String {
782        let cardinality = if matches!(kind, RelationKind::ManyToMany) { "Many" } else { "One" };
783        match mutation {
784            "Create" => format!("{}CreateNested{}Without{}Input", target_type, cardinality, source_type),
785            "Update" => format!("{}Update{}Without{}NestedInput", target_type, cardinality, source_type),
786            _ => panic!("unknown mutation: {}", mutation),
787        }
788    }
789
790    fn register_enum(
791        &mut self,
792        mut builder: AgSchemaBuilder,
793        enum_def: &EnumDef,
794    ) -> AgSchemaBuilder {
795        if self.registered_enums.contains(&enum_def.name) {
796            return builder;
797        }
798        let items: Vec<async_graphql::dynamic::EnumItem> = enum_def
799            .values
800            .iter()
801            .map(|value| async_graphql::dynamic::EnumItem::new(value.clone()))
802            .collect();
803        builder = builder.register(async_graphql::dynamic::Enum::new(
804            enum_def.name.clone(),
805        ).items(items));
806        self.registered_enums.insert(enum_def.name.clone());
807        builder
808    }
809
810    fn register_page_info(&self, builder: AgSchemaBuilder) -> AgSchemaBuilder {
811        let fields = [
812            ("hasNextPage", "Boolean", false),
813            ("hasPreviousPage", "Boolean", false),
814            ("startCursor", "String", false),
815            ("endCursor", "String", false),
816        ];
817        let mut page_info = Object::new("PageInfo");
818        for (field_name, type_name, non_null) in fields {
819            let type_ref = if non_null { TypeRef::named_nn(type_name) } else { TypeRef::named(type_name) };
820            page_info = page_info.field(nested_field(field_name, type_ref));
821        }
822        builder.register(page_info)
823    }
824
825    fn register_delete_result(&self, builder: AgSchemaBuilder) -> AgSchemaBuilder {
826        let mut delete_result = Object::new("DeleteResult");
827        delete_result = delete_result
828            .field(nested_field("success", TypeRef::named_nn("Boolean")))
829            .field(nested_field("deletedId", TypeRef::named_nn("ID")));
830        builder.register(delete_result)
831    }
832
833    fn register_filter_types(&self, builder: AgSchemaBuilder) -> AgSchemaBuilder {
834        builder
835            .register(
836                async_graphql::dynamic::Enum::new("SortDirection")
837                    .item(async_graphql::dynamic::EnumItem::new("ASC"))
838                    .item(async_graphql::dynamic::EnumItem::new("DESC")),
839            )
840            .register(InputObject::new("IdFilter")
841                .field(InputValue::new("eq", TypeRef::named("ID")))
842                .field(InputValue::new("ne", TypeRef::named("ID"))))
843            .register(InputObject::new("StringFilter")
844                .field(InputValue::new("eq", TypeRef::named("String")))
845                .field(InputValue::new("ne", TypeRef::named("String")))
846                .field(InputValue::new("contains", TypeRef::named("String")))
847                .field(InputValue::new("startsWith", TypeRef::named("String")))
848                .field(InputValue::new("endsWith", TypeRef::named("String"))))
849            .register(InputObject::new("IntFilter")
850                .field(InputValue::new("eq", TypeRef::named("Int")))
851                .field(InputValue::new("ne", TypeRef::named("Int")))
852                .field(InputValue::new("gt", TypeRef::named("Int")))
853                .field(InputValue::new("gte", TypeRef::named("Int")))
854                .field(InputValue::new("lt", TypeRef::named("Int")))
855                .field(InputValue::new("lte", TypeRef::named("Int"))))
856            .register(InputObject::new("FloatFilter")
857                .field(InputValue::new("eq", TypeRef::named("Float")))
858                .field(InputValue::new("ne", TypeRef::named("Float")))
859                .field(InputValue::new("gt", TypeRef::named("Float")))
860                .field(InputValue::new("gte", TypeRef::named("Float")))
861                .field(InputValue::new("lt", TypeRef::named("Float")))
862                .field(InputValue::new("lte", TypeRef::named("Float"))))
863            .register(InputObject::new("BooleanFilter")
864                .field(InputValue::new("eq", TypeRef::named("Boolean"))))
865            .register(InputObject::new("DateTimeFilter")
866                .field(InputValue::new("eq", TypeRef::named("DateTime")))
867                .field(InputValue::new("ne", TypeRef::named("DateTime")))
868                .field(InputValue::new("gt", TypeRef::named("DateTime")))
869                .field(InputValue::new("gte", TypeRef::named("DateTime")))
870                .field(InputValue::new("lt", TypeRef::named("DateTime")))
871                .field(InputValue::new("lte", TypeRef::named("DateTime"))))
872    }
873}
874
875fn extract_nested(parent: &async_graphql::Value, field_name: &str) -> Option<async_graphql::Value> {
876    match parent {
877        async_graphql::Value::Object(map) => map.get(field_name).cloned(),
878        _ => None,
879    }
880}
881
882fn filter_type_name(field_type: &FieldType) -> &'static str {
883    match field_type {
884        FieldType::ID => "IdFilter",
885        FieldType::String => "StringFilter",
886        FieldType::Int => "IntFilter",
887        FieldType::Float => "FloatFilter",
888        FieldType::Boolean => "BooleanFilter",
889        FieldType::DateTime => "DateTimeFilter",
890        FieldType::Json => "StringFilter",
891        FieldType::List(_) => "StringFilter",
892        FieldType::Relation(_) => "IdFilter",
893    }
894}
895
896fn nested_field(name: &str, type_ref: TypeRef) -> Field {
897    let field_name = name.to_string();
898    Field::new(name, type_ref, move |ctx| {
899        let name = field_name.clone();
900        FieldFuture::new(async move {
901            let value = ctx
902                .parent_value
903                .as_value()
904                .and_then(|parent| extract_nested(parent, &name));
905            match value {
906                Some(val) => Ok(Some(FieldValue::value(val))),
907                None => Ok(None),
908            }
909        })
910    })
911}
912
913fn resolve_to_field_value(
914    result: Result<Option<serde_json::Value>, GraphQLError>,
915) -> Result<Option<FieldValue<'static>>, async_graphql::Error> {
916    match result {
917        Ok(Some(value)) => json_to_field_value(value).map(Some),
918        Ok(None) => Ok(None),
919        Err(err) => Err(err),
920    }
921    .map_err(|err| err.into_graphql_error())
922}