Auto-indexing service and GraphQL API for AT Protocol Records quickslice.slices.network/
atproto gleam graphql
at main 814 lines 19 kB view raw
1import database/executor.{type Executor, Int, Text} 2import database/queries/where_clause 3import database/repositories/records 4import gleam/dict 5import gleam/list 6import gleam/option.{None, Some} 7import gleam/result 8import gleam/string 9import gleeunit 10import gleeunit/should 11import test_helpers 12 13pub fn main() { 14 gleeunit.main() 15} 16 17// Helper to setup test database with sample records 18fn setup_test_db() -> Result(Executor, String) { 19 use exec <- result.try( 20 test_helpers.create_test_db() 21 |> result.map_error(fn(_) { "Failed to connect" }), 22 ) 23 use _ <- result.try( 24 test_helpers.create_record_table(exec) 25 |> result.map_error(fn(_) { "Failed to create table" }), 26 ) 27 28 // Insert test records 29 use _ <- result.try( 30 records.insert( 31 exec, 32 "at://did:plc:1/app.bsky.feed.post/1", 33 "cid1", 34 "did:plc:1", 35 "app.bsky.feed.post", 36 "{\"text\":\"Hello World\",\"likes\":100}", 37 ) 38 |> result.map_error(fn(_) { "Failed to insert record" }), 39 ) 40 41 use _ <- result.try( 42 records.insert( 43 exec, 44 "at://did:plc:2/app.bsky.feed.post/2", 45 "cid2", 46 "did:plc:2", 47 "app.bsky.feed.post", 48 "{\"text\":\"Goodbye World\",\"likes\":50}", 49 ) 50 |> result.map_error(fn(_) { "Failed to insert record" }), 51 ) 52 53 use _ <- result.try( 54 records.insert( 55 exec, 56 "at://did:plc:3/app.bsky.feed.post/3", 57 "cid3", 58 "did:plc:3", 59 "app.bsky.feed.post", 60 "{\"text\":\"Test post\",\"likes\":200}", 61 ) 62 |> result.map_error(fn(_) { "Failed to insert record" }), 63 ) 64 65 use _ <- result.try( 66 records.insert( 67 exec, 68 "at://did:plc:1/app.bsky.actor.profile/1", 69 "cid4", 70 "did:plc:1", 71 "app.bsky.actor.profile", 72 "{\"displayName\":\"Alice\"}", 73 ) 74 |> result.map_error(fn(_) { "Failed to insert record" }), 75 ) 76 77 Ok(exec) 78} 79 80// Test: Filter by DID (table column) 81pub fn filter_by_did_test() { 82 let assert Ok(exec) = setup_test_db() 83 84 let where_clause = 85 where_clause.WhereClause( 86 conditions: dict.from_list([ 87 #( 88 "did", 89 where_clause.WhereCondition( 90 eq: Some(Text("did:plc:1")), 91 in_values: None, 92 contains: None, 93 gt: None, 94 gte: None, 95 lt: None, 96 lte: None, 97 is_null: None, 98 is_numeric: False, 99 ), 100 ), 101 ]), 102 and: None, 103 or: None, 104 ) 105 106 let result = 107 records.get_by_collection_paginated_with_where( 108 exec, 109 "app.bsky.feed.post", 110 Some(10), 111 None, 112 None, 113 None, 114 None, 115 Some(where_clause), 116 ) 117 118 case result { 119 Ok(#(records, _, _, _)) -> { 120 list.length(records) |> should.equal(1) 121 case list.first(records) { 122 Ok(record) -> record.did |> should.equal("did:plc:1") 123 Error(_) -> should.fail() 124 } 125 } 126 Error(_) -> should.fail() 127 } 128} 129 130// Test: Filter by JSON field with contains 131pub fn filter_by_json_contains_test() { 132 let assert Ok(exec) = setup_test_db() 133 134 let where_clause = 135 where_clause.WhereClause( 136 conditions: dict.from_list([ 137 #( 138 "text", 139 where_clause.WhereCondition( 140 eq: None, 141 in_values: None, 142 contains: Some("Hello"), 143 gt: None, 144 gte: None, 145 lt: None, 146 lte: None, 147 is_null: None, 148 is_numeric: False, 149 ), 150 ), 151 ]), 152 and: None, 153 or: None, 154 ) 155 156 let result = 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_clause), 166 ) 167 168 case result { 169 Ok(#(records, _, _, _)) -> { 170 list.length(records) |> should.equal(1) 171 case list.first(records) { 172 Ok(record) -> should.be_true(string.contains(record.json, "Hello")) 173 Error(_) -> should.fail() 174 } 175 } 176 Error(_) -> should.fail() 177 } 178} 179 180// Test: Filter by JSON field comparison (gt) 181pub fn filter_by_json_comparison_test() { 182 let assert Ok(exec) = setup_test_db() 183 184 let where_clause = 185 where_clause.WhereClause( 186 conditions: dict.from_list([ 187 #( 188 "likes", 189 where_clause.WhereCondition( 190 eq: None, 191 in_values: None, 192 contains: None, 193 gt: Some(Int(75)), 194 gte: None, 195 lt: None, 196 lte: None, 197 is_null: None, 198 is_numeric: False, 199 ), 200 ), 201 ]), 202 and: None, 203 or: None, 204 ) 205 206 let result = 207 records.get_by_collection_paginated_with_where( 208 exec, 209 "app.bsky.feed.post", 210 Some(10), 211 None, 212 None, 213 None, 214 None, 215 Some(where_clause), 216 ) 217 218 case result { 219 Ok(#(records, _, _, _)) -> { 220 // Should match records with likes > 75 (100 and 200) 221 list.length(records) |> should.equal(2) 222 } 223 Error(_) -> should.fail() 224 } 225} 226 227// Test: Range query with gte and lt 228pub fn filter_range_query_test() { 229 let assert Ok(exec) = setup_test_db() 230 231 let where_clause = 232 where_clause.WhereClause( 233 conditions: dict.from_list([ 234 #( 235 "likes", 236 where_clause.WhereCondition( 237 eq: None, 238 in_values: None, 239 contains: None, 240 gt: None, 241 gte: Some(Int(50)), 242 lt: Some(Int(150)), 243 lte: None, 244 is_null: None, 245 is_numeric: False, 246 ), 247 ), 248 ]), 249 and: None, 250 or: None, 251 ) 252 253 let result = 254 records.get_by_collection_paginated_with_where( 255 exec, 256 "app.bsky.feed.post", 257 Some(10), 258 None, 259 None, 260 None, 261 None, 262 Some(where_clause), 263 ) 264 265 case result { 266 Ok(#(records, _, _, _)) -> { 267 // Should match records with 50 <= likes < 150 (50 and 100) 268 list.length(records) |> should.equal(2) 269 } 270 Error(_) -> should.fail() 271 } 272} 273 274// Test: Nested AND with multiple conditions 275pub fn filter_nested_and_test() { 276 let assert Ok(exec) = setup_test_db() 277 278 let did_clause = 279 where_clause.WhereClause( 280 conditions: dict.from_list([ 281 #( 282 "did", 283 where_clause.WhereCondition( 284 eq: Some(Text("did:plc:1")), 285 in_values: None, 286 contains: None, 287 gt: None, 288 gte: None, 289 lt: None, 290 lte: None, 291 is_null: None, 292 is_numeric: False, 293 ), 294 ), 295 ]), 296 and: None, 297 or: None, 298 ) 299 300 let likes_clause = 301 where_clause.WhereClause( 302 conditions: dict.from_list([ 303 #( 304 "likes", 305 where_clause.WhereCondition( 306 eq: None, 307 in_values: None, 308 contains: None, 309 gt: Some(Int(50)), 310 gte: None, 311 lt: None, 312 lte: None, 313 is_null: None, 314 is_numeric: False, 315 ), 316 ), 317 ]), 318 and: None, 319 or: None, 320 ) 321 322 let root_clause = 323 where_clause.WhereClause( 324 conditions: dict.new(), 325 and: Some([did_clause, likes_clause]), 326 or: None, 327 ) 328 329 let result = 330 records.get_by_collection_paginated_with_where( 331 exec, 332 "app.bsky.feed.post", 333 Some(10), 334 None, 335 None, 336 None, 337 None, 338 Some(root_clause), 339 ) 340 341 case result { 342 Ok(#(records, _, _, _)) -> { 343 // Should only match did:plc:1 with likes > 50 (the first record) 344 list.length(records) |> should.equal(1) 345 case list.first(records) { 346 Ok(record) -> { 347 record.did |> should.equal("did:plc:1") 348 should.be_true(string.contains(record.json, "100")) 349 } 350 Error(_) -> should.fail() 351 } 352 } 353 Error(_) -> should.fail() 354 } 355} 356 357// Test: Nested OR with multiple conditions 358pub fn filter_nested_or_test() { 359 let assert Ok(exec) = setup_test_db() 360 361 let did1_clause = 362 where_clause.WhereClause( 363 conditions: dict.from_list([ 364 #( 365 "did", 366 where_clause.WhereCondition( 367 eq: Some(Text("did:plc:1")), 368 in_values: None, 369 contains: None, 370 gt: None, 371 gte: None, 372 lt: None, 373 lte: None, 374 is_null: None, 375 is_numeric: False, 376 ), 377 ), 378 ]), 379 and: None, 380 or: None, 381 ) 382 383 let did2_clause = 384 where_clause.WhereClause( 385 conditions: dict.from_list([ 386 #( 387 "did", 388 where_clause.WhereCondition( 389 eq: Some(Text("did:plc:2")), 390 in_values: None, 391 contains: None, 392 gt: None, 393 gte: None, 394 lt: None, 395 lte: None, 396 is_null: None, 397 is_numeric: False, 398 ), 399 ), 400 ]), 401 and: None, 402 or: None, 403 ) 404 405 let root_clause = 406 where_clause.WhereClause( 407 conditions: dict.new(), 408 and: None, 409 or: Some([did1_clause, did2_clause]), 410 ) 411 412 let result = 413 records.get_by_collection_paginated_with_where( 414 exec, 415 "app.bsky.feed.post", 416 Some(10), 417 None, 418 None, 419 None, 420 None, 421 Some(root_clause), 422 ) 423 424 case result { 425 Ok(#(records, _, _, _)) -> { 426 // Should match both did:plc:1 and did:plc:2 427 list.length(records) |> should.equal(2) 428 } 429 Error(_) -> should.fail() 430 } 431} 432 433// Test: Empty where clause returns all records 434pub fn filter_empty_where_test() { 435 let assert Ok(exec) = setup_test_db() 436 437 let where_clause = where_clause.empty_clause() 438 439 let result = 440 records.get_by_collection_paginated_with_where( 441 exec, 442 "app.bsky.feed.post", 443 Some(10), 444 None, 445 None, 446 None, 447 None, 448 Some(where_clause), 449 ) 450 451 case result { 452 Ok(#(records, _, _, _)) -> { 453 // Should return all 3 posts 454 list.length(records) |> should.equal(3) 455 } 456 Error(_) -> should.fail() 457 } 458} 459 460// Test: Where clause with pagination 461pub fn filter_with_pagination_test() { 462 let assert Ok(exec) = setup_test_db() 463 464 let where_clause = 465 where_clause.WhereClause( 466 conditions: dict.from_list([ 467 #( 468 "likes", 469 where_clause.WhereCondition( 470 eq: None, 471 in_values: None, 472 contains: None, 473 gt: Some(Int(25)), 474 gte: None, 475 lt: None, 476 lte: None, 477 is_null: None, 478 is_numeric: False, 479 ), 480 ), 481 ]), 482 and: None, 483 or: None, 484 ) 485 486 // First page: limit 2 487 let result = 488 records.get_by_collection_paginated_with_where( 489 exec, 490 "app.bsky.feed.post", 491 Some(2), 492 None, 493 None, 494 None, 495 None, 496 Some(where_clause), 497 ) 498 499 case result { 500 Ok(#(records, next_cursor, has_next, has_prev)) -> { 501 list.length(records) |> should.equal(2) 502 should.be_true(has_next) 503 should.be_false(has_prev) 504 should.be_true(option.is_some(next_cursor)) 505 } 506 Error(_) -> should.fail() 507 } 508} 509 510// Test: Numeric comparison with is_numeric=True uses INTEGER cast 511pub fn filter_numeric_with_cast_test() { 512 let assert Ok(exec) = setup_test_db() 513 514 // Test that numeric comparisons work with is_numeric: True 515 let where_clause = 516 where_clause.WhereClause( 517 conditions: dict.from_list([ 518 #( 519 "likes", 520 where_clause.WhereCondition( 521 eq: None, 522 in_values: None, 523 contains: None, 524 gt: Some(Int(75)), 525 gte: None, 526 lt: None, 527 lte: None, 528 is_null: None, 529 is_numeric: True, 530 ), 531 ), 532 ]), 533 and: None, 534 or: None, 535 ) 536 537 let result = 538 records.get_by_collection_paginated_with_where( 539 exec, 540 "app.bsky.feed.post", 541 Some(10), 542 None, 543 None, 544 None, 545 None, 546 Some(where_clause), 547 ) 548 549 case result { 550 Ok(#(records, _, _, _)) -> { 551 // Should match records with likes > 75 (100 and 200) 552 list.length(records) |> should.equal(2) 553 } 554 Error(_) -> should.fail() 555 } 556} 557 558// Test: String datetime comparison without INTEGER cast 559pub fn filter_datetime_string_comparison_test() { 560 let assert Ok(exec) = setup_test_db() 561 562 // Insert records with ISO datetime strings in JSON 563 let assert Ok(_) = 564 executor.exec( 565 exec, 566 "INSERT INTO record (uri, cid, did, collection, json, indexed_at) 567 VALUES (?, 'cid5', 'did:plc:5', 'app.bsky.feed.post', ?, datetime('now'))", 568 [ 569 Text("at://did:plc:5/app.bsky.feed.post/5"), 570 Text("{\"text\":\"Old post\",\"playedTime\":\"2024-01-01T00:00:00Z\"}"), 571 ], 572 ) 573 574 let assert Ok(_) = 575 executor.exec( 576 exec, 577 "INSERT INTO record (uri, cid, did, collection, json, indexed_at) 578 VALUES (?, 'cid6', 'did:plc:6', 'app.bsky.feed.post', ?, datetime('now'))", 579 [ 580 Text("at://did:plc:6/app.bsky.feed.post/6"), 581 Text("{\"text\":\"New post\",\"playedTime\":\"2024-12-01T00:00:00Z\"}"), 582 ], 583 ) 584 585 // Filter for records with playedTime >= "2024-06-01" (string comparison) 586 let where_clause = 587 where_clause.WhereClause( 588 conditions: dict.from_list([ 589 #( 590 "playedTime", 591 where_clause.WhereCondition( 592 eq: None, 593 in_values: None, 594 contains: None, 595 gt: None, 596 gte: Some(Text("2024-06-01T00:00:00Z")), 597 lt: None, 598 lte: None, 599 is_null: None, 600 is_numeric: False, 601 ), 602 ), 603 ]), 604 and: None, 605 or: None, 606 ) 607 608 let result = 609 records.get_by_collection_paginated_with_where( 610 exec, 611 "app.bsky.feed.post", 612 Some(10), 613 None, 614 None, 615 None, 616 None, 617 Some(where_clause), 618 ) 619 620 case result { 621 Ok(#(records, _, _, _)) -> { 622 // Should only match the "New post" with playedTime in December 623 list.length(records) |> should.equal(1) 624 case list.first(records) { 625 Ok(record) -> should.be_true(string.contains(record.json, "New post")) 626 Error(_) -> should.fail() 627 } 628 } 629 Error(_) -> should.fail() 630 } 631} 632 633// ===== isNull End-to-End Tests ===== 634 635/// Test isNull: true end-to-end from GraphQL parsing through SQL execution 636pub fn is_null_true_end_to_end_test() { 637 let assert Ok(exec) = test_helpers.create_test_db() 638 let assert Ok(_) = test_helpers.create_record_table(exec) 639 640 // Insert records - some with replyParent, some without 641 let assert Ok(_) = 642 executor.exec( 643 exec, 644 "INSERT INTO record (uri, cid, did, collection, json, indexed_at) 645 VALUES (?, 'cid1', 'did:plc:1', 'app.bsky.feed.post', ?, datetime('now'))", 646 [ 647 Text("at://did:plc:1/app.bsky.feed.post/1"), 648 Text("{\"text\":\"Root post\"}"), 649 ], 650 ) 651 652 let assert Ok(_) = 653 executor.exec( 654 exec, 655 "INSERT INTO record (uri, cid, did, collection, json, indexed_at) 656 VALUES (?, 'cid2', 'did:plc:2', 'app.bsky.feed.post', ?, datetime('now'))", 657 [ 658 Text("at://did:plc:2/app.bsky.feed.post/2"), 659 Text( 660 "{\"text\":\"Reply post\",\"replyParent\":\"at://did:plc:1/app.bsky.feed.post/1\"}", 661 ), 662 ], 663 ) 664 665 let assert Ok(_) = 666 executor.exec( 667 exec, 668 "INSERT INTO record (uri, cid, did, collection, json, indexed_at) 669 VALUES (?, 'cid3', 'did:plc:3', 'app.bsky.feed.post', ?, datetime('now'))", 670 [ 671 Text("at://did:plc:3/app.bsky.feed.post/3"), 672 Text("{\"text\":\"Another root post\"}"), 673 ], 674 ) 675 676 // Filter for records where replyParent IS NULL (root posts only) 677 let where_clause = 678 where_clause.WhereClause( 679 conditions: dict.from_list([ 680 #( 681 "replyParent", 682 where_clause.WhereCondition( 683 eq: None, 684 in_values: None, 685 contains: None, 686 gt: None, 687 gte: None, 688 lt: None, 689 lte: None, 690 is_null: Some(True), 691 is_numeric: False, 692 ), 693 ), 694 ]), 695 and: None, 696 or: None, 697 ) 698 699 let result = 700 records.get_by_collection_paginated_with_where( 701 exec, 702 "app.bsky.feed.post", 703 Some(10), 704 None, 705 None, 706 None, 707 None, 708 Some(where_clause), 709 ) 710 711 case result { 712 Ok(#(records_list, _, _, _)) -> { 713 // Should only match the 2 root posts (without replyParent) 714 list.length(records_list) |> should.equal(2) 715 // Verify none of them are replies 716 list.all(records_list, fn(record) { 717 !string.contains(record.json, "replyParent") 718 }) 719 |> should.be_true 720 } 721 Error(_) -> should.fail() 722 } 723} 724 725/// Test isNull: false end-to-end from GraphQL parsing through SQL execution 726pub fn is_null_false_end_to_end_test() { 727 let assert Ok(exec) = test_helpers.create_test_db() 728 let assert Ok(_) = test_helpers.create_record_table(exec) 729 730 // Insert records - some with replyParent, some without 731 let assert Ok(_) = 732 executor.exec( 733 exec, 734 "INSERT INTO record (uri, cid, did, collection, json, indexed_at) 735 VALUES (?, 'cid1', 'did:plc:1', 'app.bsky.feed.post', ?, datetime('now'))", 736 [ 737 Text("at://did:plc:1/app.bsky.feed.post/1"), 738 Text("{\"text\":\"Root post\"}"), 739 ], 740 ) 741 742 let assert Ok(_) = 743 executor.exec( 744 exec, 745 "INSERT INTO record (uri, cid, did, collection, json, indexed_at) 746 VALUES (?, 'cid2', 'did:plc:2', 'app.bsky.feed.post', ?, datetime('now'))", 747 [ 748 Text("at://did:plc:2/app.bsky.feed.post/2"), 749 Text( 750 "{\"text\":\"Reply post\",\"replyParent\":\"at://did:plc:1/app.bsky.feed.post/1\"}", 751 ), 752 ], 753 ) 754 755 let assert Ok(_) = 756 executor.exec( 757 exec, 758 "INSERT INTO record (uri, cid, did, collection, json, indexed_at) 759 VALUES (?, 'cid3', 'did:plc:3', 'app.bsky.feed.post', ?, datetime('now'))", 760 [ 761 Text("at://did:plc:3/app.bsky.feed.post/3"), 762 Text("{\"text\":\"Another root post\"}"), 763 ], 764 ) 765 766 // Filter for records where replyParent IS NOT NULL (replies only) 767 let where_clause = 768 where_clause.WhereClause( 769 conditions: dict.from_list([ 770 #( 771 "replyParent", 772 where_clause.WhereCondition( 773 eq: None, 774 in_values: None, 775 contains: None, 776 gt: None, 777 gte: None, 778 lt: None, 779 lte: None, 780 is_null: Some(False), 781 is_numeric: False, 782 ), 783 ), 784 ]), 785 and: None, 786 or: None, 787 ) 788 789 let result = 790 records.get_by_collection_paginated_with_where( 791 exec, 792 "app.bsky.feed.post", 793 Some(10), 794 None, 795 None, 796 None, 797 None, 798 Some(where_clause), 799 ) 800 801 case result { 802 Ok(#(records_list, _, _, _)) -> { 803 // Should only match the 1 reply post (with replyParent) 804 list.length(records_list) |> should.equal(1) 805 // Verify it's a reply 806 case list.first(records_list) { 807 Ok(record) -> 808 should.be_true(string.contains(record.json, "replyParent")) 809 Error(_) -> should.fail() 810 } 811 } 812 Error(_) -> should.fail() 813 } 814}