forked from
slices.network/quickslice
Auto-indexing service and GraphQL API for AT Protocol Records
1/// End-to-end integration tests for GraphQL where clause filtering
2///
3/// Tests the complete flow: GraphQL value → WhereInput parsing → SQL generation → Database query
4import database/executor.{type Executor}
5import database/repositories/records
6import gleam/list
7import gleam/option.{None, Some}
8import gleam/result
9import gleam/string
10import gleeunit
11import gleeunit/should
12import graphql/where_converter
13import lexicon_graphql/input/where as where_input
14import swell/value
15import test_helpers
16
17pub fn main() {
18 gleeunit.main()
19}
20
21// Helper to setup test database with sample records
22fn setup_test_db() -> Result(Executor, String) {
23 use exec <- result.try(
24 test_helpers.create_test_db()
25 |> result.map_error(fn(_) { "Failed to connect" }),
26 )
27 use _ <- result.try(
28 test_helpers.create_record_table(exec)
29 |> result.map_error(fn(_) { "Failed to create table" }),
30 )
31
32 // Insert test records with different properties
33 use _ <- result.try(
34 records.insert(
35 exec,
36 "at://did:plc:alice/app.bsky.feed.post/1",
37 "cid1",
38 "did:plc:alice",
39 "app.bsky.feed.post",
40 "{\"text\":\"Hello World\",\"likes\":100,\"author\":\"alice\"}",
41 )
42 |> result.map_error(fn(_) { "insert failed" }),
43 )
44
45 use _ <- result.try(
46 records.insert(
47 exec,
48 "at://did:plc:bob/app.bsky.feed.post/2",
49 "cid2",
50 "did:plc:bob",
51 "app.bsky.feed.post",
52 "{\"text\":\"Goodbye World\",\"likes\":50,\"author\":\"bob\"}",
53 )
54 |> result.map_error(fn(_) { "insert failed" }),
55 )
56
57 use _ <- result.try(
58 records.insert(
59 exec,
60 "at://did:plc:charlie/app.bsky.feed.post/3",
61 "cid3",
62 "did:plc:charlie",
63 "app.bsky.feed.post",
64 "{\"text\":\"Hello Universe\",\"likes\":200,\"author\":\"charlie\"}",
65 )
66 |> result.map_error(fn(_) { "insert failed" }),
67 )
68
69 use _ <- result.try(
70 records.insert(
71 exec,
72 "at://did:plc:alice/app.bsky.feed.post/4",
73 "cid4",
74 "did:plc:alice",
75 "app.bsky.feed.post",
76 "{\"text\":\"Another post\",\"likes\":75,\"author\":\"alice\"}",
77 )
78 |> result.map_error(fn(_) { "insert failed" }),
79 )
80
81 Ok(exec)
82}
83
84// Test: Simple field filter through full GraphQL stack
85pub fn graphql_simple_filter_test() {
86 case setup_test_db() {
87 Error(_) -> should.fail()
88 Ok(exec) -> {
89 // Create GraphQL where value: { text: { contains: "Hello" } }
90 let text_condition = value.Object([#("contains", value.String("Hello"))])
91
92 let where_value = value.Object([#("text", text_condition)])
93
94 // Parse GraphQL value to WhereInput
95 let where_input = where_input.parse_where_clause(where_value)
96
97 // Convert to SQL WhereClause
98 let where_sql = where_converter.convert_where_clause(where_input)
99
100 // Execute query
101 case
102 records.get_by_collection_paginated_with_where(
103 exec,
104 "app.bsky.feed.post",
105 Some(10),
106 None,
107 None,
108 None,
109 None,
110 Some(where_sql),
111 )
112 {
113 Ok(#(records, _, _, _)) -> {
114 // Should match 2 records with "Hello"
115 list.length(records) |> should.equal(2)
116 list.all(records, fn(record) { string.contains(record.json, "Hello") })
117 |> should.be_true
118 }
119 Error(_) -> should.fail()
120 }
121 }
122 }
123}
124
125// Test: OR logic through full GraphQL stack
126pub fn graphql_or_filter_test() {
127 case setup_test_db() {
128 Error(_) -> should.fail()
129 Ok(exec) -> {
130 // Create GraphQL where value:
131 // {
132 // or: [
133 // { author: { eq: "alice" } },
134 // { author: { eq: "bob" } }
135 // ]
136 // }
137 let alice_condition = value.Object([#("eq", value.String("alice"))])
138 let bob_condition = value.Object([#("eq", value.String("bob"))])
139
140 let alice_clause = value.Object([#("author", alice_condition)])
141 let bob_clause = value.Object([#("author", bob_condition)])
142
143 let where_value =
144 value.Object([#("or", value.List([alice_clause, bob_clause]))])
145
146 // Parse GraphQL value to WhereInput
147 let where_input = where_input.parse_where_clause(where_value)
148
149 // Verify OR was parsed
150 should.be_true(option.is_some(where_input.or))
151
152 // Convert to SQL WhereClause
153 let where_sql = where_converter.convert_where_clause(where_input)
154
155 // Execute query
156 case
157 records.get_by_collection_paginated_with_where(
158 exec,
159 "app.bsky.feed.post",
160 Some(10),
161 None,
162 None,
163 None,
164 None,
165 Some(where_sql),
166 )
167 {
168 Ok(#(records, _, _, _)) -> {
169 // Should match 3 records (alice has 2, bob has 1)
170 list.length(records) |> should.equal(3)
171
172 // Verify all records are from alice or bob
173 list.all(records, fn(record) {
174 string.contains(record.json, "alice")
175 || string.contains(record.json, "bob")
176 })
177 |> should.be_true
178
179 // Verify no charlie records
180 list.any(records, fn(record) {
181 string.contains(record.json, "charlie")
182 })
183 |> should.be_false
184 }
185 Error(_) -> should.fail()
186 }
187 }
188 }
189}
190
191// Test: AND logic through full GraphQL stack
192pub fn graphql_and_filter_test() {
193 case setup_test_db() {
194 Error(_) -> should.fail()
195 Ok(exec) -> {
196 // Create GraphQL where value:
197 // {
198 // and: [
199 // { author: { eq: "alice" } },
200 // { likes: { gt: "80" } }
201 // ]
202 // }
203 // Note: likes is stored as integer in JSON, so we compare as string "80"
204 let author_condition = value.Object([#("eq", value.String("alice"))])
205 let likes_condition = value.Object([#("gt", value.Int(80))])
206
207 let author_clause = value.Object([#("author", author_condition)])
208 let likes_clause = value.Object([#("likes", likes_condition)])
209
210 let where_value =
211 value.Object([#("and", value.List([author_clause, likes_clause]))])
212
213 // Parse GraphQL value to WhereInput
214 let where_input = where_input.parse_where_clause(where_value)
215
216 // Verify AND was parsed
217 should.be_true(option.is_some(where_input.and))
218
219 // Convert to SQL WhereClause
220 let where_sql = where_converter.convert_where_clause(where_input)
221
222 // Execute query
223 case
224 records.get_by_collection_paginated_with_where(
225 exec,
226 "app.bsky.feed.post",
227 Some(10),
228 None,
229 None,
230 None,
231 None,
232 Some(where_sql),
233 )
234 {
235 Ok(#(records, _, _, _)) -> {
236 // Should match 1 record (alice with likes=100)
237 list.length(records) |> should.equal(1)
238
239 case list.first(records) {
240 Ok(record) -> {
241 string.contains(record.json, "alice") |> should.be_true
242 string.contains(record.json, "100") |> should.be_true
243 }
244 Error(_) -> should.fail()
245 }
246 }
247 Error(_) -> should.fail()
248 }
249 }
250 }
251}
252
253// Test: Nested AND/OR logic through full GraphQL stack
254pub fn graphql_nested_and_or_test() {
255 case setup_test_db() {
256 Error(_) -> should.fail()
257 Ok(exec) -> {
258 // Create GraphQL where value:
259 // {
260 // and: [
261 // {
262 // or: [
263 // { text: { contains: "Hello" } },
264 // { text: { contains: "Another" } }
265 // ]
266 // },
267 // { author: { eq: "alice" } }
268 // ]
269 // }
270
271 // Inner OR: text contains "Hello" OR "Another"
272 let hello_condition = value.Object([#("contains", value.String("Hello"))])
273 let another_condition =
274 value.Object([#("contains", value.String("Another"))])
275
276 let hello_clause = value.Object([#("text", hello_condition)])
277 let another_clause = value.Object([#("text", another_condition)])
278
279 let or_clause =
280 value.Object([#("or", value.List([hello_clause, another_clause]))])
281
282 // Outer AND: (above OR) AND author = "alice"
283 let author_condition = value.Object([#("eq", value.String("alice"))])
284 let author_clause = value.Object([#("author", author_condition)])
285
286 let where_value =
287 value.Object([#("and", value.List([or_clause, author_clause]))])
288
289 // Parse GraphQL value to WhereInput
290 let where_input = where_input.parse_where_clause(where_value)
291
292 // Verify nested structure was parsed
293 should.be_true(option.is_some(where_input.and))
294
295 // Convert to SQL WhereClause
296 let where_sql = where_converter.convert_where_clause(where_input)
297
298 // Execute query
299 case
300 records.get_by_collection_paginated_with_where(
301 exec,
302 "app.bsky.feed.post",
303 Some(10),
304 None,
305 None,
306 None,
307 None,
308 Some(where_sql),
309 )
310 {
311 Ok(#(records, _, _, _)) -> {
312 // Should match 2 alice records: "Hello World" and "Another post"
313 list.length(records) |> should.equal(2)
314
315 // Verify all records are from alice
316 list.all(records, fn(record) { string.contains(record.json, "alice") })
317 |> should.be_true
318
319 // Verify records contain either "Hello" or "Another"
320 list.all(records, fn(record) {
321 string.contains(record.json, "Hello")
322 || string.contains(record.json, "Another")
323 })
324 |> should.be_true
325 }
326 Error(_) -> should.fail()
327 }
328 }
329 }
330}
331
332// Test: Complex nested logic with multiple levels
333pub fn graphql_complex_nested_test() {
334 case setup_test_db() {
335 Error(_) -> should.fail()
336 Ok(exec) -> {
337 // Simplify test: just OR two simple conditions
338 // {
339 // or: [
340 // { author: { eq: "alice" } },
341 // { author: { eq: "charlie" } }
342 // ]
343 // }
344
345 let alice_condition = value.Object([#("eq", value.String("alice"))])
346 let charlie_condition = value.Object([#("eq", value.String("charlie"))])
347
348 let alice_clause = value.Object([#("author", alice_condition)])
349 let charlie_clause = value.Object([#("author", charlie_condition)])
350
351 let where_value =
352 value.Object([#("or", value.List([alice_clause, charlie_clause]))])
353
354 // Parse GraphQL value to WhereInput
355 let where_input = where_input.parse_where_clause(where_value)
356
357 // Convert to SQL WhereClause
358 let where_sql = where_converter.convert_where_clause(where_input)
359
360 // Execute query
361 case
362 records.get_by_collection_paginated_with_where(
363 exec,
364 "app.bsky.feed.post",
365 Some(10),
366 None,
367 None,
368 None,
369 None,
370 Some(where_sql),
371 )
372 {
373 Ok(#(records, _, _, _)) -> {
374 // Should match 3 records: 2 from alice, 1 from charlie
375 list.length(records) |> should.equal(3)
376
377 // Check we got alice and charlie records
378 let has_alice =
379 list.any(records, fn(record) {
380 string.contains(record.json, "alice")
381 })
382 let has_charlie =
383 list.any(records, fn(record) {
384 string.contains(record.json, "charlie")
385 })
386
387 should.be_true(has_alice)
388 should.be_true(has_charlie)
389 }
390 Error(_) -> should.fail()
391 }
392 }
393 }
394}
395
396// Test: Empty AND/OR arrays
397pub fn graphql_empty_logic_arrays_test() {
398 case setup_test_db() {
399 Error(_) -> should.fail()
400 Ok(exec) -> {
401 // Create GraphQL where value with empty AND array: { and: [] }
402 let where_value = value.Object([#("and", value.List([]))])
403
404 // Parse GraphQL value to WhereInput
405 let where_input = where_input.parse_where_clause(where_value)
406
407 // Empty AND should result in None
408 case where_input.and {
409 None -> should.be_true(True)
410 Some(clauses) -> {
411 // Or empty list
412 list.is_empty(clauses) |> should.be_true
413 }
414 }
415
416 // Convert to SQL WhereClause
417 let where_sql = where_converter.convert_where_clause(where_input)
418
419 // Execute query - should return all records
420 case
421 records.get_by_collection_paginated_with_where(
422 exec,
423 "app.bsky.feed.post",
424 Some(10),
425 None,
426 None,
427 None,
428 None,
429 Some(where_sql),
430 )
431 {
432 Ok(#(records, _, _, _)) -> {
433 // Should return all 4 records
434 list.length(records) |> should.equal(4)
435 }
436 Error(_) -> should.fail()
437 }
438 }
439 }
440}