import * as Cursor from './Cursor.js' describe('Cursor', () => { test('round-trips an int tuple through an opaque, URL-safe token', () => { const token = Cursor.encode([123456, 7]) expect(token).toMatch(/^[A-Za-z0-9_-]+$/) expect(Cursor.decode(token, ['int', 'int'])).toEqual([123456, 7]) }) test('round-trips a balance/address tuple', () => { const token = Cursor.encode([ '1000000000000000000', '0xBE058E1C4DF8A4366A387BF595B284246A93039E', ]) expect(Cursor.decode(token, ['uint', 'address'])).toEqual([ '1000000000000000000', '0xBE058E1C4DF8A4366A387BF595B284246A93039E', ]) }) test('round-trips a timestamp/id tuple', () => { const token = Cursor.encode(['2026-01-01T00:00:00.000Z', 'source:transaction-0']) expect(Cursor.decode(token, ['timestamp', 'id'])).toEqual([ '2026-01-01T00:00:00.000Z', 'source:transaction-0', ]) }) test('returns undefined for malformed, mis-typed, or wrong-arity tokens', () => { expect(Cursor.decode('not-base64!!', ['int', 'int'])).toBeUndefined() expect(Cursor.decode(btoa('{"oops":true}'), ['int', 'int'])).toBeUndefined() expect(Cursor.decode(btoa('[1]'), ['int', 'int'])).toBeUndefined() expect(Cursor.decode(btoa('[1,2,3]'), ['int', 'int'])).toBeUndefined() expect(Cursor.decode(btoa('[1.5,2]'), ['int', 'int'])).toBeUndefined() expect(Cursor.decode(btoa('[-1,2]'), ['int', 'int'])).toBeUndefined() expect(Cursor.decode(btoa('["x",2]'), ['uint', 'int'])).toBeUndefined() expect(Cursor.decode(btoa('["0xnotanaddress",2]'), ['address', 'int'])).toBeUndefined() expect(Cursor.decode(btoa('["org_abc\' OR 1=1"]'), ['id'])).toBeUndefined() }) test('renders injection-safe SQL literals per field kind', () => { expect(Cursor.literal(42, 'int')).toBe('42') expect(Cursor.literal('1000', 'uint')).toBe('1000') expect(Cursor.literal('0xAbC0000000000000000000000000000000000000', 'address')).toBe( "'0xabc0000000000000000000000000000000000000'", ) expect(Cursor.literal('org_abc123', 'id')).toBe("'org_abc123'") expect(Cursor.literal('2024-01-01T00:00:00Z', 'timestamp')).toBe("'2024-01-01T00:00:00Z'") }) test('paginates a keyset result, deriving hasMore and nextCursor from the extra row', () => { const rows = [ { block: 10, idx: 2 }, { block: 10, idx: 1 }, { block: 9, idx: 0 }, ] // Fetched `limit + 1` rows, so the trailing row signals another page. const page = Cursor.paginate({ rows, limit: 2, key: (row) => [row.block, row.idx] }) expect(page.rows).toEqual([ { block: 10, idx: 2 }, { block: 10, idx: 1 }, ]) expect(page.hasMore).toBe(true) expect(Cursor.decode(page.nextCursor!, ['int', 'int'])).toEqual([10, 1]) }) test('paginate reports the last page with a null nextCursor', () => { const rows = [{ block: 9, idx: 0 }] const page = Cursor.paginate({ rows, limit: 2, key: (row) => [row.block, row.idx] }) expect(page.rows).toEqual(rows) expect(page.hasMore).toBe(false) expect(page.nextCursor).toBeNull() }) test('paginate yields a null nextCursor when the last row has no sort-key', () => { const rows = [{ ok: true }, { ok: true }, { ok: false }] const page = Cursor.paginate({ rows, limit: 2, key: (row) => (row.ok ? [1, 2] : undefined) }) expect(page.hasMore).toBe(true) // The boundary row (last of the page) is `{ ok: true }`, so a cursor exists. expect(page.nextCursor).not.toBeNull() }) test('builds a row-value tuple predicate with a prunable first-column bound', () => { expect( Cursor.keyset([ { literal: '100', name: 'block_num', order: 'desc' }, { literal: '3', name: 'idx', order: 'desc' }, ]), ).toBe('(block_num <= 100 AND (block_num, idx) < (100, 3))') expect( Cursor.keyset([ { literal: '100', name: 'block_num', order: 'asc' }, { literal: '3', name: 'idx', order: 'asc' }, ]), ).toBe('(block_num >= 100 AND (block_num, idx) > (100, 3))') }) test('omits the redundant bound for a single-column keyset', () => { expect(Cursor.keyset([{ literal: '100', name: 'num', order: 'desc' }])).toBe('((num) < (100))') }) test('falls back to an OR expansion for a mixed-direction keyset', () => { const holder = `'0x${'ab'.repeat(20)}'` expect( Cursor.keyset([ { literal: '1000', name: 'balance', order: 'desc' }, { literal: holder, name: 'holder', order: 'asc' }, ]), ).toBe(`(balance <= 1000 AND ((balance < 1000) OR (balance = 1000 AND holder > ${holder})))`) }) })