Auto-indexing service and GraphQL API for AT Protocol Records
quickslice.slices.network/
atproto
gleam
graphql
1/// Tests for DID-based join field generation
2///
3/// Verifies that DID join fields are added to GraphQL schemas correctly:
4/// - All collections get DID join fields to all other collections
5/// - Cardinality is determined by has_unique_did (literal:self key)
6/// - Field naming follows {TypeName}ByDid pattern
7import gleam/dict
8import gleam/option
9import gleam/string
10import gleeunit/should
11import lexicon_graphql/schema/database as db_schema_builder
12import lexicon_graphql/types
13import swell/introspection
14import swell/schema
15import swell/sdl
16
17// Helper to create a test schema with a mock fetcher
18fn create_test_schema_from_lexicons(
19 lexicons: List(types.Lexicon),
20) -> schema.Schema {
21 // Mock fetcher that returns empty results (we're only testing schema generation)
22 let fetcher = fn(_collection, _params) {
23 Ok(#([], option.None, False, False, option.None))
24 }
25
26 case
27 db_schema_builder.build_schema_with_fetcher(
28 lexicons,
29 fetcher,
30 option.None,
31 option.None,
32 option.None,
33 option.None,
34 option.None,
35 option.None,
36 )
37 {
38 Ok(s) -> s
39 Error(_) -> panic as "Failed to build test schema"
40 }
41}
42
43// Test that collections get DID join fields to other collections
44pub fn collections_get_did_join_fields_test() {
45 // Create two collections: a status and a profile (with literal:self)
46 let status_lexicon =
47 types.Lexicon(
48 id: "xyz.statusphere.status",
49 defs: types.Defs(
50 main: option.Some(
51 types.RecordDef(type_: "record", key: option.None, properties: [
52 #(
53 "text",
54 types.Property(
55 type_: "string",
56 required: True,
57 format: option.None,
58 ref: option.None,
59 refs: option.None,
60 items: option.None,
61 ),
62 ),
63 ]),
64 ),
65 others: dict.new(),
66 ),
67 )
68
69 let profile_lexicon =
70 types.Lexicon(
71 id: "app.bsky.actor.profile",
72 defs: types.Defs(
73 main: option.Some(
74 types.RecordDef(
75 type_: "record",
76 key: option.Some("literal:self"),
77 properties: [
78 #(
79 "displayName",
80 types.Property(
81 type_: "string",
82 required: False,
83 format: option.None,
84 ref: option.None,
85 refs: option.None,
86 items: option.None,
87 ),
88 ),
89 ],
90 ),
91 ),
92 others: dict.new(),
93 ),
94 )
95
96 let test_schema =
97 create_test_schema_from_lexicons([status_lexicon, profile_lexicon])
98
99 // Get all types and serialize to SDL
100 let all_types = introspection.get_all_schema_types(test_schema)
101 let serialized = sdl.print_types(all_types)
102
103 // Verify that Status has a DID join field to Profile
104 string.contains(serialized, "appBskyActorProfileByDid")
105 |> should.be_true
106
107 // Verify that Profile has a DID join field to Status
108 string.contains(serialized, "xyzStatusphereStatusByDid")
109 |> should.be_true
110}
111
112// Test that literal:self collections return single nullable objects
113pub fn literal_self_returns_single_object_test() {
114 let status_lexicon =
115 types.Lexicon(
116 id: "xyz.statusphere.status",
117 defs: types.Defs(
118 main: option.Some(
119 types.RecordDef(type_: "record", key: option.None, properties: [
120 #(
121 "text",
122 types.Property(
123 type_: "string",
124 required: True,
125 format: option.None,
126 ref: option.None,
127 refs: option.None,
128 items: option.None,
129 ),
130 ),
131 ]),
132 ),
133 others: dict.new(),
134 ),
135 )
136
137 let profile_lexicon =
138 types.Lexicon(
139 id: "app.bsky.actor.profile",
140 defs: types.Defs(
141 main: option.Some(
142 types.RecordDef(
143 type_: "record",
144 key: option.Some("literal:self"),
145 properties: [
146 #(
147 "displayName",
148 types.Property(
149 type_: "string",
150 required: False,
151 format: option.None,
152 ref: option.None,
153 refs: option.None,
154 items: option.None,
155 ),
156 ),
157 ],
158 ),
159 ),
160 others: dict.new(),
161 ),
162 )
163
164 let test_schema =
165 create_test_schema_from_lexicons([status_lexicon, profile_lexicon])
166
167 let all_types = introspection.get_all_schema_types(test_schema)
168 let serialized = sdl.print_types(all_types)
169
170 // Profile should be returned as single object (not list) from Status
171 // We check that it's NOT wrapped in a list (no brackets)
172 // The field should be: appBskyActorProfileByDid: AppBskyActorProfile
173 string.contains(serialized, "appBskyActorProfileByDid: AppBskyActorProfile")
174 |> should.be_true
175}
176
177// Test that non-literal:self collections return lists
178pub fn non_literal_self_returns_list_test() {
179 let status_lexicon =
180 types.Lexicon(
181 id: "xyz.statusphere.status",
182 defs: types.Defs(
183 main: option.Some(
184 types.RecordDef(type_: "record", key: option.None, properties: [
185 #(
186 "text",
187 types.Property(
188 type_: "string",
189 required: True,
190 format: option.None,
191 ref: option.None,
192 refs: option.None,
193 items: option.None,
194 ),
195 ),
196 ]),
197 ),
198 others: dict.new(),
199 ),
200 )
201
202 let post_lexicon =
203 types.Lexicon(
204 id: "app.bsky.feed.post",
205 defs: types.Defs(
206 main: option.Some(
207 types.RecordDef(type_: "record", key: option.None, properties: [
208 #(
209 "text",
210 types.Property(
211 type_: "string",
212 required: True,
213 format: option.None,
214 ref: option.None,
215 refs: option.None,
216 items: option.None,
217 ),
218 ),
219 ]),
220 ),
221 others: dict.new(),
222 ),
223 )
224
225 let test_schema =
226 create_test_schema_from_lexicons([status_lexicon, post_lexicon])
227
228 let all_types = introspection.get_all_schema_types(test_schema)
229 let serialized = sdl.print_types(all_types)
230
231 // Post should be returned as a connection (paginated list) from Status
232 // The field should be: appBskyFeedPostByDid: AppBskyFeedPostConnection
233 string.contains(serialized, "appBskyFeedPostByDid: AppBskyFeedPostConnection")
234 |> should.be_true
235}
236
237// Test that multiple collections all get DID joins to each other
238pub fn multiple_collections_get_cross_joins_test() {
239 let status_lexicon =
240 types.Lexicon(
241 id: "xyz.statusphere.status",
242 defs: types.Defs(
243 main: option.Some(
244 types.RecordDef(type_: "record", key: option.None, properties: [
245 #(
246 "text",
247 types.Property(
248 type_: "string",
249 required: True,
250 format: option.None,
251 ref: option.None,
252 refs: option.None,
253 items: option.None,
254 ),
255 ),
256 ]),
257 ),
258 others: dict.new(),
259 ),
260 )
261
262 let post_lexicon =
263 types.Lexicon(
264 id: "app.bsky.feed.post",
265 defs: types.Defs(
266 main: option.Some(
267 types.RecordDef(type_: "record", key: option.None, properties: [
268 #(
269 "text",
270 types.Property(
271 type_: "string",
272 required: True,
273 format: option.None,
274 ref: option.None,
275 refs: option.None,
276 items: option.None,
277 ),
278 ),
279 ]),
280 ),
281 others: dict.new(),
282 ),
283 )
284
285 let like_lexicon =
286 types.Lexicon(
287 id: "app.bsky.feed.like",
288 defs: types.Defs(
289 main: option.Some(
290 types.RecordDef(type_: "record", key: option.None, properties: [
291 #(
292 "subject",
293 types.Property(
294 type_: "string",
295 required: True,
296 format: option.Some("at-uri"),
297 ref: option.None,
298 refs: option.None,
299 items: option.None,
300 ),
301 ),
302 ]),
303 ),
304 others: dict.new(),
305 ),
306 )
307
308 let test_schema =
309 create_test_schema_from_lexicons([
310 status_lexicon,
311 post_lexicon,
312 like_lexicon,
313 ])
314
315 let all_types = introspection.get_all_schema_types(test_schema)
316 let serialized = sdl.print_types(all_types)
317
318 // Status should have joins to Post and Like
319 string.contains(serialized, "appBskyFeedPostByDid")
320 |> should.be_true
321
322 string.contains(serialized, "appBskyFeedLikeByDid")
323 |> should.be_true
324
325 // Post should have joins to Status and Like
326 string.contains(serialized, "xyzStatusphereStatusByDid")
327 |> should.be_true
328 // Like should have joins to Status and Post
329 // (already checked above)
330}
331
332// Test that collections don't get DID join fields to themselves
333pub fn no_self_join_test() {
334 let status_lexicon =
335 types.Lexicon(
336 id: "xyz.statusphere.status",
337 defs: types.Defs(
338 main: option.Some(
339 types.RecordDef(type_: "record", key: option.None, properties: [
340 #(
341 "text",
342 types.Property(
343 type_: "string",
344 required: True,
345 format: option.None,
346 ref: option.None,
347 refs: option.None,
348 items: option.None,
349 ),
350 ),
351 ]),
352 ),
353 others: dict.new(),
354 ),
355 )
356
357 let test_schema = create_test_schema_from_lexicons([status_lexicon])
358
359 let all_types = introspection.get_all_schema_types(test_schema)
360 let serialized = sdl.print_types(all_types)
361
362 // Status should NOT have a join field to itself
363 string.contains(serialized, "xyzStatusphereStatusByDid")
364 |> should.be_false
365}