forked from
slices.network/quickslice
Auto-indexing service and GraphQL API for AT Protocol Records
1/// Tests for reverse join field generation
2///
3/// Verifies that reverse join fields are discovered and added to GraphQL schemas
4import gleam/dict
5import gleam/option.{None, Some}
6import gleam/string
7import gleeunit/should
8import lexicon_graphql/schema/database as db_schema_builder
9import lexicon_graphql/types
10import swell/introspection
11import swell/schema
12import swell/sdl
13
14// Helper to create a test schema with a mock fetcher
15fn create_test_schema_from_lexicons(
16 lexicons: List(types.Lexicon),
17) -> schema.Schema {
18 // Mock fetcher that returns empty results (we're only testing schema generation)
19 let fetcher = fn(_collection, _params) {
20 Ok(#([], option.None, False, False, option.None))
21 }
22
23 case
24 db_schema_builder.build_schema_with_fetcher(
25 lexicons,
26 fetcher,
27 option.None,
28 option.None,
29 option.None,
30 option.None,
31 option.None,
32 option.None,
33 )
34 {
35 Ok(s) -> s
36 Error(_) -> panic as "Failed to build test schema"
37 }
38}
39
40// Test that a forward join in one collection creates a reverse join field in the target
41pub fn forward_join_creates_reverse_join_test() {
42 // Create a Post collection (target)
43 let post_lexicon =
44 types.Lexicon(
45 id: "app.bsky.feed.post",
46 defs: types.Defs(
47 main: Some(
48 types.RecordDef(type_: "record", key: None, properties: [
49 #(
50 "text",
51 types.Property(
52 type_: "string",
53 required: True,
54 format: None,
55 ref: None,
56 refs: None,
57 items: None,
58 ),
59 ),
60 ]),
61 ),
62 others: dict.new(),
63 ),
64 )
65
66 // Create a Like collection with a subject field that references posts
67 let like_lexicon =
68 types.Lexicon(
69 id: "app.bsky.feed.like",
70 defs: types.Defs(
71 main: Some(
72 types.RecordDef(type_: "record", key: None, properties: [
73 #(
74 "subject",
75 types.Property(
76 type_: "string",
77 required: True,
78 format: Some("at-uri"),
79 ref: None,
80 refs: None,
81 items: None,
82 ),
83 ),
84 ]),
85 ),
86 others: dict.new(),
87 ),
88 )
89
90 let test_schema =
91 create_test_schema_from_lexicons([post_lexicon, like_lexicon])
92
93 // Get all types and serialize to SDL
94 let all_types = introspection.get_all_schema_types(test_schema)
95 let serialized = sdl.print_types(all_types)
96
97 // Verify that the Post type has a reverse join field for likes
98 // Field name should be: appBskyFeedLikeViaSubject (camelCase)
99 string.contains(serialized, "appBskyFeedLikeViaSubject")
100 |> should.be_true
101}
102
103// Test that strongRef fields also create reverse joins
104pub fn strong_ref_creates_reverse_join_test() {
105 // Create a Post collection (target)
106 let post_lexicon =
107 types.Lexicon(
108 id: "app.bsky.feed.post",
109 defs: types.Defs(
110 main: Some(
111 types.RecordDef(type_: "record", key: None, properties: [
112 #(
113 "text",
114 types.Property(
115 type_: "string",
116 required: True,
117 format: None,
118 ref: None,
119 refs: None,
120 items: None,
121 ),
122 ),
123 ]),
124 ),
125 others: dict.new(),
126 ),
127 )
128
129 // Create a Profile collection with a pinnedPost strongRef field
130 let profile_lexicon =
131 types.Lexicon(
132 id: "app.bsky.actor.profile",
133 defs: types.Defs(
134 main: Some(
135 types.RecordDef(type_: "record", key: None, properties: [
136 #(
137 "pinnedPost",
138 types.Property(
139 type_: "ref",
140 required: False,
141 format: None,
142 ref: Some("com.atproto.repo.strongRef"),
143 refs: None,
144 items: None,
145 ),
146 ),
147 ]),
148 ),
149 others: dict.new(),
150 ),
151 )
152
153 let test_schema =
154 create_test_schema_from_lexicons([post_lexicon, profile_lexicon])
155
156 let all_types = introspection.get_all_schema_types(test_schema)
157 let serialized = sdl.print_types(all_types)
158
159 // Verify that the Post type has a reverse join field for pinned posts
160 // Field name should be: appBskyActorProfileViaPinnedPost (camelCase)
161 string.contains(serialized, "appBskyActorProfileViaPinnedPost")
162 |> should.be_true
163}
164
165// Test that multiple reverse joins are all generated
166pub fn multiple_reverse_joins_test() {
167 // Create a Post collection (target)
168 let post_lexicon =
169 types.Lexicon(
170 id: "app.bsky.feed.post",
171 defs: types.Defs(
172 main: Some(
173 types.RecordDef(type_: "record", key: None, properties: [
174 #(
175 "text",
176 types.Property(
177 type_: "string",
178 required: True,
179 format: None,
180 ref: None,
181 refs: None,
182 items: None,
183 ),
184 ),
185 ]),
186 ),
187 others: dict.new(),
188 ),
189 )
190
191 // Create a Like collection
192 let like_lexicon =
193 types.Lexicon(
194 id: "app.bsky.feed.like",
195 defs: types.Defs(
196 main: Some(
197 types.RecordDef(type_: "record", key: None, properties: [
198 #(
199 "subject",
200 types.Property(
201 type_: "string",
202 required: True,
203 format: Some("at-uri"),
204 ref: None,
205 refs: None,
206 items: None,
207 ),
208 ),
209 ]),
210 ),
211 others: dict.new(),
212 ),
213 )
214
215 // Create a Repost collection
216 let repost_lexicon =
217 types.Lexicon(
218 id: "app.bsky.feed.repost",
219 defs: types.Defs(
220 main: Some(
221 types.RecordDef(type_: "record", key: None, properties: [
222 #(
223 "subject",
224 types.Property(
225 type_: "string",
226 required: True,
227 format: Some("at-uri"),
228 ref: None,
229 refs: None,
230 items: None,
231 ),
232 ),
233 ]),
234 ),
235 others: dict.new(),
236 ),
237 )
238
239 let test_schema =
240 create_test_schema_from_lexicons([
241 post_lexicon,
242 like_lexicon,
243 repost_lexicon,
244 ])
245
246 let all_types = introspection.get_all_schema_types(test_schema)
247 let serialized = sdl.print_types(all_types)
248
249 // Check both reverse join fields exist on Post type (camelCase)
250 string.contains(serialized, "appBskyFeedLikeViaSubject")
251 |> should.be_true
252
253 string.contains(serialized, "appBskyFeedRepostViaSubject")
254 |> should.be_true
255}
256
257// Test that collections without forward join fields don't appear in reverse joins
258pub fn no_false_positive_reverse_joins_test() {
259 // Create a Post collection
260 let post_lexicon =
261 types.Lexicon(
262 id: "app.bsky.feed.post",
263 defs: types.Defs(
264 main: Some(
265 types.RecordDef(type_: "record", key: None, properties: [
266 #(
267 "text",
268 types.Property(
269 type_: "string",
270 required: True,
271 format: None,
272 ref: None,
273 refs: None,
274 items: None,
275 ),
276 ),
277 ]),
278 ),
279 others: dict.new(),
280 ),
281 )
282
283 // Create a Status collection with no join fields
284 let status_lexicon =
285 types.Lexicon(
286 id: "xyz.statusosphere.status",
287 defs: types.Defs(
288 main: Some(
289 types.RecordDef(type_: "record", key: None, properties: [
290 #(
291 "message",
292 types.Property(
293 type_: "string",
294 required: True,
295 format: None,
296 ref: None,
297 refs: None,
298 items: None,
299 ),
300 ),
301 ]),
302 ),
303 others: dict.new(),
304 ),
305 )
306
307 let test_schema =
308 create_test_schema_from_lexicons([post_lexicon, status_lexicon])
309
310 let all_types = introspection.get_all_schema_types(test_schema)
311 let serialized = sdl.print_types(all_types)
312
313 // Status collection has no forward joins, so Post should not have a reverse join field
314 // that references Status. We check that the field name XyzStatusosphereStatusVia* doesn't appear
315 string.contains(serialized, "XyzStatusosphereStatusVia")
316 |> should.be_false
317}