Auto-indexing service and GraphQL API for AT Protocol Records quickslice.slices.network/
atproto gleam graphql
at main 422 lines 12 kB view raw
1import database/queries/pagination 2import database/types.{Record} 3import gleam/option.{None, Some} 4import gleeunit/should 5import test_helpers 6 7/// Test encoding a cursor with no sort fields (just CID) 8pub fn encode_cursor_no_sort_test() { 9 let record = 10 Record( 11 uri: "at://did:plc:test/app.bsky.feed.post/123", 12 cid: "bafytest123", 13 did: "did:plc:test", 14 collection: "app.bsky.feed.post", 15 json: "{\"text\":\"Hello world\",\"createdAt\":\"2025-01-15T12:00:00Z\"}", 16 indexed_at: "2025-01-15 12:00:00", 17 rkey: "123", 18 ) 19 20 let result = pagination.generate_cursor_from_record(record, None) 21 22 // Decode the base64 to verify it's just the CID 23 let decoded = pagination.decode_base64(result) 24 should.be_ok(decoded) 25 |> should.equal("bafytest123") 26} 27 28/// Test encoding a cursor with single sort field 29pub fn encode_cursor_single_field_test() { 30 let record = 31 Record( 32 uri: "at://did:plc:test/app.bsky.feed.post/123", 33 cid: "bafytest123", 34 did: "did:plc:test", 35 collection: "app.bsky.feed.post", 36 json: "{\"text\":\"Hello world\",\"createdAt\":\"2025-01-15T12:00:00Z\"}", 37 indexed_at: "2025-01-15 12:00:00", 38 rkey: "123", 39 ) 40 41 let sort_by = Some([#("indexed_at", "desc")]) 42 43 let result = pagination.generate_cursor_from_record(record, sort_by) 44 45 // Decode the base64 to verify format 46 let decoded = pagination.decode_base64(result) 47 should.be_ok(decoded) 48 |> should.equal("2025-01-15 12:00:00|bafytest123") 49} 50 51/// Test encoding a cursor with JSON field 52pub fn encode_cursor_json_field_test() { 53 let record = 54 Record( 55 uri: "at://did:plc:test/app.bsky.feed.post/123", 56 cid: "bafytest123", 57 did: "did:plc:test", 58 collection: "app.bsky.feed.post", 59 json: "{\"text\":\"Hello world\",\"createdAt\":\"2025-01-15T12:00:00Z\"}", 60 indexed_at: "2025-01-15 12:00:00", 61 rkey: "123", 62 ) 63 64 let sort_by = Some([#("text", "desc")]) 65 66 let result = pagination.generate_cursor_from_record(record, sort_by) 67 68 let decoded = pagination.decode_base64(result) 69 should.be_ok(decoded) 70 |> should.equal("Hello world|bafytest123") 71} 72 73/// Test encoding a cursor with nested JSON field 74pub fn encode_cursor_nested_json_field_test() { 75 let record = 76 Record( 77 uri: "at://did:plc:test/app.bsky.feed.post/123", 78 cid: "bafytest123", 79 did: "did:plc:test", 80 collection: "app.bsky.feed.post", 81 json: "{\"author\":{\"name\":\"Alice\"},\"createdAt\":\"2025-01-15T12:00:00Z\"}", 82 indexed_at: "2025-01-15 12:00:00", 83 rkey: "123", 84 ) 85 86 let sort_by = Some([#("author.name", "asc")]) 87 88 let result = pagination.generate_cursor_from_record(record, sort_by) 89 90 let decoded = pagination.decode_base64(result) 91 should.be_ok(decoded) 92 |> should.equal("Alice|bafytest123") 93} 94 95/// Test encoding a cursor with multiple sort fields 96pub fn encode_cursor_multi_field_test() { 97 let record = 98 Record( 99 uri: "at://did:plc:test/app.bsky.feed.post/123", 100 cid: "bafytest123", 101 did: "did:plc:test", 102 collection: "app.bsky.feed.post", 103 json: "{\"text\":\"Hello\",\"createdAt\":\"2025-01-15T12:00:00Z\"}", 104 indexed_at: "2025-01-15 12:00:00", 105 rkey: "123", 106 ) 107 108 let sort_by = Some([#("text", "desc"), #("createdAt", "desc")]) 109 110 let result = pagination.generate_cursor_from_record(record, sort_by) 111 112 let decoded = pagination.decode_base64(result) 113 should.be_ok(decoded) 114 |> should.equal("Hello|2025-01-15T12:00:00Z|bafytest123") 115} 116 117/// Test decoding a valid cursor 118pub fn decode_cursor_valid_test() { 119 let sort_by = Some([#("indexed_at", "desc")]) 120 121 // Create a cursor: "2025-01-15 12:00:00|bafytest123" 122 let cursor_str = pagination.encode_base64("2025-01-15 12:00:00|bafytest123") 123 124 let result = pagination.decode_cursor(cursor_str, sort_by) 125 126 should.be_ok(result) 127 |> fn(decoded) { 128 decoded.field_values 129 |> should.equal(["2025-01-15 12:00:00"]) 130 131 decoded.cid 132 |> should.equal("bafytest123") 133 } 134} 135 136/// Test decoding a multi-field cursor 137pub fn decode_cursor_multi_field_test() { 138 let sort_by = Some([#("text", "desc"), #("createdAt", "desc")]) 139 140 let cursor_str = 141 pagination.encode_base64("Hello|2025-01-15T12:00:00Z|bafytest123") 142 143 let result = pagination.decode_cursor(cursor_str, sort_by) 144 145 should.be_ok(result) 146 |> fn(decoded) { 147 decoded.field_values 148 |> should.equal(["Hello", "2025-01-15T12:00:00Z"]) 149 150 decoded.cid 151 |> should.equal("bafytest123") 152 } 153} 154 155/// Test decoding with mismatched field count fails 156pub fn decode_cursor_mismatch_test() { 157 let sort_by = Some([#("text", "desc")]) 158 159 // Cursor has 2 fields but sort_by only has 1 160 let cursor_str = 161 pagination.encode_base64("Hello|2025-01-15T12:00:00Z|bafytest123") 162 163 let result = pagination.decode_cursor(cursor_str, sort_by) 164 165 should.be_error(result) 166} 167 168/// Test decoding invalid base64 fails 169pub fn decode_cursor_invalid_base64_test() { 170 let sort_by = Some([#("text", "desc")]) 171 172 let result = pagination.decode_cursor("not-valid-base64!!!", sort_by) 173 174 should.be_error(result) 175} 176 177/// Test extracting table column values 178pub fn extract_field_value_table_column_test() { 179 let record = 180 Record( 181 uri: "at://did:plc:test/app.bsky.feed.post/123", 182 cid: "bafytest123", 183 did: "did:plc:test", 184 collection: "app.bsky.feed.post", 185 json: "{}", 186 indexed_at: "2025-01-15 12:00:00", 187 rkey: "123", 188 ) 189 190 pagination.extract_field_value(record, "uri") 191 |> should.equal("at://did:plc:test/app.bsky.feed.post/123") 192 193 pagination.extract_field_value(record, "cid") 194 |> should.equal("bafytest123") 195 196 pagination.extract_field_value(record, "did") 197 |> should.equal("did:plc:test") 198 199 pagination.extract_field_value(record, "collection") 200 |> should.equal("app.bsky.feed.post") 201 202 pagination.extract_field_value(record, "indexed_at") 203 |> should.equal("2025-01-15 12:00:00") 204} 205 206/// Test extracting JSON field values 207pub fn extract_field_value_json_test() { 208 let record = 209 Record( 210 uri: "at://did:plc:test/app.bsky.feed.post/123", 211 cid: "bafytest123", 212 did: "did:plc:test", 213 collection: "app.bsky.feed.post", 214 json: "{\"text\":\"Hello world\",\"createdAt\":\"2025-01-15T12:00:00Z\",\"likeCount\":42}", 215 indexed_at: "2025-01-15 12:00:00", 216 rkey: "123", 217 ) 218 219 pagination.extract_field_value(record, "text") 220 |> should.equal("Hello world") 221 222 pagination.extract_field_value(record, "createdAt") 223 |> should.equal("2025-01-15T12:00:00Z") 224 225 pagination.extract_field_value(record, "likeCount") 226 |> should.equal("42") 227} 228 229/// Test extracting nested JSON field values 230pub fn extract_field_value_nested_json_test() { 231 let record = 232 Record( 233 uri: "at://did:plc:test/app.bsky.feed.post/123", 234 cid: "bafytest123", 235 did: "did:plc:test", 236 collection: "app.bsky.feed.post", 237 json: "{\"author\":{\"name\":\"Alice\",\"did\":\"did:plc:alice\"}}", 238 indexed_at: "2025-01-15 12:00:00", 239 rkey: "123", 240 ) 241 242 pagination.extract_field_value(record, "author.name") 243 |> should.equal("Alice") 244 245 pagination.extract_field_value(record, "author.did") 246 |> should.equal("did:plc:alice") 247} 248 249/// Test extracting missing JSON field returns NULL 250pub fn extract_field_value_missing_test() { 251 let record = 252 Record( 253 uri: "at://did:plc:test/app.bsky.feed.post/123", 254 cid: "bafytest123", 255 did: "did:plc:test", 256 collection: "app.bsky.feed.post", 257 json: "{\"text\":\"Hello\"}", 258 indexed_at: "2025-01-15 12:00:00", 259 rkey: "123", 260 ) 261 262 pagination.extract_field_value(record, "nonexistent") 263 |> should.equal("NULL") 264 265 pagination.extract_field_value(record, "author.name") 266 |> should.equal("NULL") 267} 268 269// WHERE Condition Builder Tests 270 271/// Test building WHERE clause for single field DESC 272pub fn build_where_single_field_desc_test() { 273 let assert Ok(exec) = test_helpers.create_test_db() 274 let decoded = 275 pagination.DecodedCursor( 276 field_values: ["2025-01-15 12:00:00"], 277 cid: "bafytest123", 278 ) 279 280 let sort_by = Some([#("indexed_at", "desc")]) 281 282 let #(sql, params) = 283 pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1) 284 285 // For DESC: indexed_at < cursor_value OR (indexed_at = cursor_value AND cid < cursor_cid) 286 sql 287 |> should.equal("((indexed_at < ?) OR (indexed_at = ? AND cid < ?))") 288 289 params 290 |> should.equal([ 291 "2025-01-15 12:00:00", 292 "2025-01-15 12:00:00", 293 "bafytest123", 294 ]) 295} 296 297/// Test building WHERE clause for single field ASC 298pub fn build_where_single_field_asc_test() { 299 let assert Ok(exec) = test_helpers.create_test_db() 300 let decoded = 301 pagination.DecodedCursor( 302 field_values: ["2025-01-15 12:00:00"], 303 cid: "bafytest123", 304 ) 305 306 let sort_by = Some([#("indexed_at", "asc")]) 307 308 let #(sql, params) = 309 pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1) 310 311 // For ASC: indexed_at > cursor_value OR (indexed_at = cursor_value AND cid > cursor_cid) 312 sql 313 |> should.equal("((indexed_at > ?) OR (indexed_at = ? AND cid > ?))") 314 315 params 316 |> should.equal([ 317 "2025-01-15 12:00:00", 318 "2025-01-15 12:00:00", 319 "bafytest123", 320 ]) 321} 322 323/// Test building WHERE clause for JSON field 324pub fn build_where_json_field_test() { 325 let assert Ok(exec) = test_helpers.create_test_db() 326 let decoded = 327 pagination.DecodedCursor(field_values: ["Hello world"], cid: "bafytest123") 328 329 let sort_by = Some([#("text", "desc")]) 330 331 let #(sql, params) = 332 pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1) 333 334 // JSON fields use json_extract 335 sql 336 |> should.equal( 337 "((json_extract(json, '$.text') < ?) OR (json_extract(json, '$.text') = ? AND cid < ?))", 338 ) 339 340 params 341 |> should.equal(["Hello world", "Hello world", "bafytest123"]) 342} 343 344/// Test building WHERE clause for nested JSON field 345pub fn build_where_nested_json_field_test() { 346 let assert Ok(exec) = test_helpers.create_test_db() 347 let decoded = 348 pagination.DecodedCursor(field_values: ["Alice"], cid: "bafytest123") 349 350 let sort_by = Some([#("author.name", "asc")]) 351 352 let #(sql, params) = 353 pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1) 354 355 // Nested JSON fields use $.path.to.field 356 sql 357 |> should.equal( 358 "((json_extract(json, '$.author.name') > ?) OR (json_extract(json, '$.author.name') = ? AND cid > ?))", 359 ) 360 361 params 362 |> should.equal(["Alice", "Alice", "bafytest123"]) 363} 364 365/// Test building WHERE clause for multiple fields 366pub fn build_where_multi_field_test() { 367 let assert Ok(exec) = test_helpers.create_test_db() 368 let decoded = 369 pagination.DecodedCursor( 370 field_values: ["Hello", "2025-01-15T12:00:00Z"], 371 cid: "bafytest123", 372 ) 373 374 let sort_by = Some([#("text", "desc"), #("createdAt", "desc")]) 375 376 let #(sql, params) = 377 pagination.build_cursor_where_clause(exec, decoded, sort_by, False, 1) 378 379 // Multi-field: progressive equality checks 380 // (text < ?) OR (text = ? AND createdAt < ?) OR (text = ? AND createdAt = ? AND cid < ?) 381 sql 382 |> should.equal( 383 "((json_extract(json, '$.text') < ?) OR (json_extract(json, '$.text') = ? AND json_extract(json, '$.createdAt') < ?) OR (json_extract(json, '$.text') = ? AND json_extract(json, '$.createdAt') = ? AND cid < ?))", 384 ) 385 386 params 387 |> should.equal([ 388 "Hello", 389 "Hello", 390 "2025-01-15T12:00:00Z", 391 "Hello", 392 "2025-01-15T12:00:00Z", 393 "bafytest123", 394 ]) 395} 396 397/// Test building WHERE clause for backward pagination (before) 398pub fn build_where_backward_test() { 399 let assert Ok(exec) = test_helpers.create_test_db() 400 let decoded = 401 pagination.DecodedCursor( 402 field_values: ["2025-01-15 12:00:00"], 403 cid: "bafytest123", 404 ) 405 406 let sort_by = Some([#("indexed_at", "desc")]) 407 408 // is_before = True reverses the comparison operators 409 let #(sql, params) = 410 pagination.build_cursor_where_clause(exec, decoded, sort_by, True, 1) 411 412 // For before with DESC: indexed_at > cursor_value OR (indexed_at = cursor_value AND cid > cursor_cid) 413 sql 414 |> should.equal("((indexed_at > ?) OR (indexed_at = ? AND cid > ?))") 415 416 params 417 |> should.equal([ 418 "2025-01-15 12:00:00", 419 "2025-01-15 12:00:00", 420 "bafytest123", 421 ]) 422}