Auto-indexing service and GraphQL API for AT Protocol Records
at main 440 lines 13 kB view raw
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}