1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#[cfg(feature = "integration")]
mod common;
#[cfg(feature = "integration")]
mod tests {
use fake::faker::lorem::en::Word;
use fake::faker::name::en::Name;
use fake::Fake;
use mongodb::bson::oid::ObjectId;
use mongo_graphql::executor;
use crate::common::{get_db, get_schema};
fn unique_alias(prefix: &str, label: &str) -> String {
let word: String = Word().fake();
let suffix = &ObjectId::new().to_hex()[..6];
format!("{}_{}_{}_{}", prefix, label, word, suffix)
}
fn unique_name(prefix: &str, label: &str) -> String {
let name: String = Name().fake();
let suffix = &ObjectId::new().to_hex()[..6];
format!("{}_{}_{}_{}", prefix, label, name, suffix)
}
#[tokio::test]
async fn three_level_nested_mutations() {
let db = get_db().await;
let schema = get_schema().await;
let prefix = format!("3L_{}", ObjectId::new().to_hex());
// ── Notes on 3-level chain constraints ──
//
// A 3-level chain requires the Level 2 CreateWithout input to expose
// nested fields. These come from two sources:
//
// 1. Forward relations NOT pointing to source (kept as nested inputs
// since build_create_without_fields only strips source-pointing relations).
// 2. Reverse OneToMany/OneToOne fields added by add_reverse_fields_to_without
// from OTHER collections, excluding those originating from source.
//
// Chains using reverse fields (members/missions on Team) are blocked when
// the reverse originates from the same collection as level 1 — they'd form
// a circular type reference. All forward-relation-only chains are valid.
// ═══════════════════════════════════════════════════════════════
// Chain: Power → heroes.create → archenemy.create
// L1: Power, L2: Hero (ManyToMany via hero_power), L3: Villain (FK archenemy_id)
// HeroCreateWithoutPowerInput keeps archenemy as nested type (Villain ≠ Power).
// ═══════════════════════════════════════════════════════════════
{
let power_name = unique_name(&prefix, "P_H_V_create");
let l2_alias = unique_alias(&prefix, "P_H_V_Hero");
let l2_identity: String = Name().fake();
let villain_alias = unique_alias(&prefix, "P_H_V_Villain");
let villain_identity: String = Name().fake();
let result = executor::execute(
schema,
&format!(
r#"mutation {{
createPower(input: {{
name: "{}",
type: "elemental",
level: 1,
created_at: "2024-01-01T00:00:00Z",
heroes: {{ create: [{{
alias: "{}",
secret_identity: "{}",
power_level: 700,
active: true,
joined_at: "2024-01-01T00:00:00Z",
created_at: "2024-01-01T00:00:00Z",
archenemy: {{ create: {{
alias: "{}",
secret_identity: "{}",
threat_level: 90,
active: true,
created_at: "2024-01-01T00:00:00Z"
}} }}
}}] }}
}}) {{ id }}
}}"#,
power_name, l2_alias, l2_identity, villain_alias, villain_identity
),
async_graphql::Variables::default(),
None,
)
.await
.unwrap();
if let Some(errors) = result.get("errors") {
panic!(
"Power → heroes.create → archenemy.create failed: {:?}",
errors
);
}
let power_oid =
ObjectId::parse_str(result["data"]["createPower"]["id"].as_str().unwrap())
.unwrap();
let junction_coll = db.collection::<mongodb::bson::Document>("hero_power");
let hero_coll = db.collection::<mongodb::bson::Document>("hero");
let l2_doc = hero_coll
.find_one(mongodb::bson::doc! { "alias": &l2_alias })
.await
.unwrap()
.expect("L2 hero should exist");
let junction = junction_coll
.find_one(mongodb::bson::doc! { "hero_id": l2_doc.get_object_id("_id").unwrap() })
.await
.unwrap()
.expect("hero_power junction should exist");
assert_eq!(
junction.get_object_id("power_id").unwrap(),
power_oid,
"junction.power_id should match L1 power"
);
let villain_coll = db.collection::<mongodb::bson::Document>("villain");
let l3_doc = villain_coll
.find_one(mongodb::bson::doc! { "alias": &villain_alias })
.await
.unwrap()
.expect("L3 villain should exist");
assert_eq!(
l2_doc.get_object_id("archenemy_id").unwrap(),
l3_doc.get_object_id("_id").unwrap(),
"hero.archenemy_id should match L3 villain"
);
}
// ═══════════════════════════════════════════════════════════════
// Chain: Power → heroes.create → archenemy.connect
// L1: Power, L2: Hero (ManyToMany via hero_power), L3: connect existing Villain
// ═══════════════════════════════════════════════════════════════
{
let villain_oid = ObjectId::new();
let villain_alias = unique_alias(&prefix, "P_H_V_connect");
db.collection::<mongodb::bson::Document>("villain")
.insert_one(mongodb::bson::doc! {
"_id": villain_oid,
"id": villain_oid,
"alias": &villain_alias,
"secret_identity": "Pre-existing Nemesis",
"threat_level": 95,
"active": true,
"created_at": mongodb::bson::DateTime::now(),
})
.await
.unwrap();
let power_name = unique_name(&prefix, "P_H_V_connect");
let l2_alias = unique_alias(&prefix, "P_H_V_HeroC");
let l2_identity: String = Name().fake();
let result = executor::execute(
schema,
&format!(
r#"mutation {{
createPower(input: {{
name: "{}",
type: "elemental",
level: 1,
created_at: "2024-01-01T00:00:00Z",
heroes: {{ create: [{{
alias: "{}",
secret_identity: "{}",
power_level: 700,
active: true,
joined_at: "2024-01-01T00:00:00Z",
created_at: "2024-01-01T00:00:00Z",
archenemy: {{ connect: {{ id: "{}" }} }}
}}] }}
}}) {{ id }}
}}"#,
power_name, l2_alias, l2_identity, villain_oid.to_hex()
),
async_graphql::Variables::default(),
None,
)
.await
.unwrap();
if let Some(errors) = result.get("errors") {
panic!(
"Power → heroes.create → archenemy.connect failed: {:?}",
errors
);
}
let hero_coll = db.collection::<mongodb::bson::Document>("hero");
let l2_doc = hero_coll
.find_one(mongodb::bson::doc! { "alias": &l2_alias })
.await
.unwrap()
.expect("L2 hero should exist");
assert_eq!(
l2_doc.get_object_id("archenemy_id").unwrap(),
villain_oid,
"hero.archenemy_id should match connected villain"
);
}
}
}