Filters and Pagination
Master advanced querying patterns including filtering, pagination, and ordering.
Table of Contents
Section titled “Table of Contents”Filter Types
Section titled “Filter Types”Sai Keeper supports various filter types for different data types.
String Filters
Section titled “String Filters”StringFilter supports equality and pattern matching.
input StringFilter { eq: String # Exact match like: String # Pattern match}Example - Exact Match:
query SearchToken { oracle { tokens(where: { name: { eq: "Bitcoin" } }) { id symbol name } }}Example - Pattern Match:
query SearchByPattern { oracle { tokens(where: { name: { like: "%coin%" } }) { symbol name } }}Usage in JavaScript:
// Exact matchconst { data } = await client.query({ query: gql` query SearchToken($name: String!) { oracle { tokens(where: { name: $name }) { symbol name } } } `, variables: { name: "Bitcoin" }})
// Pattern searchconst searchResults = await client.query({ query: gql` query SearchTokens($pattern: String!) { oracle { tokens(where: { name: $pattern }) { symbol name } } } `, variables: { pattern: "%BTC%" }})Integer Filters
Section titled “Integer Filters”IntFilter supports comparison operations.
input IntFilter { eq: Int # Equal to gt: Int # Greater than gte: Int # Greater than or equal lt: Int # Less than lte: Int # Less than or equal}Example - Fee Amount Range:
query GetLargeFees { fee { feeTransactions( filter: { minAmount: 1000000 # >= 1,000,000 maxAmount: 10000000 # <= 10,000,000 } ) { id totalFeeCharged traderAddress } }}Usage in JavaScript:
const { data } = await client.query({ query: gql` query GetFeesInRange($min: Int!, $max: Int!) { fee { feeTransactions( filter: { minAmount: $min maxAmount: $max } ) { id totalFeeCharged } } } `, variables: { min: 1000000, max: 10000000 }})Float Filters
Section titled “Float Filters”FloatFilter for floating-point comparisons.
input FloatFilter { eq: Float # Equal to gt: Float # Greater than gte: Float # Greater than or equal lt: Float # Less than lte: Float # Less than or equal}Example - Filter by Leverage:
// Note: Direct float filters not used in current schema,// but filter objects use similar patternsconst highLeverageTrades = await client.query({ query: gql` query GetHighLeverageTrades($trader: String!) { perp { trades(where: { trader: $trader }) { id leverage isOpen } } } `, variables: { trader: "nibi1abc..." }})
// Filter client-sideconst filtered = highLeverageTrades.data.perp.trades.filter( trade => trade.leverage > 10)Time Filters
Section titled “Time Filters”TimeFilter for date/time range queries.
input TimeFilter { eq: Time # Exact time gt: Time # After gte: Time # After or at lt: Time # Before lte: Time # Before or at}Example - Filter by Date Range:
query GetFeesLastMonth { fee { feeTransactions( filter: { fromDate: "2024-10-01T00:00:00Z" toDate: "2024-10-31T23:59:59Z" } limit: 100 ) { id totalFeeCharged blockTime } }}Usage in JavaScript:
// Get fees for last 30 daysconst thirtyDaysAgo = new Date()thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
const { data } = await client.query({ query: gql` query GetRecentFees($fromDate: Time!, $toDate: Time!) { fee { feeTransactions( filter: { fromDate: $fromDate toDate: $toDate } limit: 100 ) { id totalFeeCharged blockTime } } } `, variables: { fromDate: thirtyDaysAgo.toISOString(), toDate: new Date().toISOString() }})Helper Function:
function getDateRange(days) { const toDate = new Date() const fromDate = new Date() fromDate.setDate(fromDate.getDate() - days)
return { fromDate: fromDate.toISOString(), toDate: toDate.toISOString() }}
// Usageconst last7Days = getDateRange(7)const last30Days = getDateRange(30)const last90Days = getDateRange(90)Enum Filters
Section titled “Enum Filters”Filter by enum values.
Example - Fee Type Filter:
query GetOpeningFees($trader: String!) { fee { feeTransactions( filter: { traderAddress: $trader feeType: OPENING } limit: 50 ) { id feeType totalFeeCharged } }}Example - Trade Type Filter:
// Get only limit ordersconst { data } = await client.query({ query: gql` query GetLimitOrders($trader: String!) { perp { tradeHistory(where: { trader: $trader }, limit: 100) { id tradeChangeType trade { id tradeType } } } } `, variables: { trader: "nibi1abc..." }})
// Filter for limit_order_created eventsconst limitOrders = data.perp.tradeHistory.filter( h => h.tradeChangeType === 'limit_order_created')Pagination
Section titled “Pagination”Basic Pagination
Section titled “Basic Pagination”Use limit and offset for pagination.
query GetTradesPaginated($trader: String!, $limit: Int!, $offset: Int!) { perp { trades( where: { trader: $trader } limit: $limit offset: $offset ) { id isOpen leverage } }}JavaScript Implementation:
async function fetchPage(trader, page = 0, pageSize = 20) { const { data } = await client.query({ query: GET_TRADES_PAGINATED, variables: { trader, limit: pageSize, offset: page * pageSize } })
return data.perp.trades}
// Get first pageconst firstPage = await fetchPage("nibi1abc...", 0, 20)
// Get second pageconst secondPage = await fetchPage("nibi1abc...", 1, 20)
// Get third pageconst thirdPage = await fetchPage("nibi1abc...", 2, 20)Fetch All Pattern
Section titled “Fetch All Pattern”Fetch all results across multiple pages.
async function fetchAllTrades(trader, pageSize = 100) { let allTrades = [] let page = 0 let hasMore = true
while (hasMore) { const { data } = await client.query({ query: gql` query GetTrades($trader: String!, $limit: Int!, $offset: Int!) { perp { trades( where: { trader: $trader } limit: $limit offset: $offset ) { id isOpen leverage } } } `, variables: { trader, limit: pageSize, offset: page * pageSize } })
const trades = data.perp.trades allTrades = allTrades.concat(trades)
hasMore = trades.length === pageSize page++
console.log(`Fetched page ${page}, total: ${allTrades.length}`) }
return allTrades}
// Usageconst allTrades = await fetchAllTrades("nibi1abc...")console.log(`Total trades: ${allTrades.length}`)Cursor-Based Pagination Alternative
Section titled “Cursor-Based Pagination Alternative”While Sai Keeper uses offset pagination, you can implement cursor-based logic:
async function fetchTradesAfterBlock(trader, afterBlock = 0, limit = 100) { const { data } = await client.query({ query: gql` query GetTrades($trader: String!, $limit: Int!) { perp { trades( where: { trader: $trader } limit: $limit order_desc: false ) { id openBlock { block } } } } `, variables: { trader, limit } })
// Filter trades after cursor block const filtered = data.perp.trades.filter( trade => trade.openBlock.block > afterBlock )
return filtered}
// Get first batchconst firstBatch = await fetchTradesAfterBlock("nibi1abc...", 0)const lastBlock = firstBatch[firstBatch.length - 1].openBlock.block
// Get next batchconst nextBatch = await fetchTradesAfterBlock("nibi1abc...", lastBlock)Infinite Scroll Pattern
Section titled “Infinite Scroll Pattern”Implement infinite scroll for UI:
class TradesPaginator { constructor(client, trader, pageSize = 20) { this.client = client this.trader = trader this.pageSize = pageSize this.currentPage = 0 this.allTrades = [] this.hasMore = true }
async loadMore() { if (!this.hasMore) { return [] }
const { data } = await this.client.query({ query: gql` query GetTrades($trader: String!, $limit: Int!, $offset: Int!) { perp { trades( where: { trader: $trader } limit: $limit offset: $offset order_desc: true ) { id isOpen leverage } } } `, variables: { trader: this.trader, limit: this.pageSize, offset: this.currentPage * this.pageSize } })
const trades = data.perp.trades this.allTrades = this.allTrades.concat(trades) this.hasMore = trades.length === this.pageSize this.currentPage++
return trades }
reset() { this.currentPage = 0 this.allTrades = [] this.hasMore = true }
getAll() { return this.allTrades }}
// Usageconst paginator = new TradesPaginator(client, "nibi1abc...", 20)
// Load first pageconst firstPage = await paginator.loadMore()
// Load more on scrollwindow.addEventListener('scroll', async () => { if (isNearBottom() && paginator.hasMore) { const nextPage = await paginator.loadMore() appendToUI(nextPage) }})Ordering
Section titled “Ordering”Order Direction
Section titled “Order Direction”Most queries support order_by and order_desc parameters.
query GetTradesOrdered($trader: String!) { perp { trades( where: { trader: $trader } order_by: sequence order_desc: true # Descending (newest first) ) { id openBlock { block_ts } } }}Order Options by Domain:
Perp Trades:
sequence- Order by sequence number
Trade History:
sequence- Order by sequencetrade_id- Order by trade ID
LP Deposits:
depositor- Order by depositor addressvault- Order by vault address
LP Deposit History:
depositor- Order by depositorsequence- Order by sequencevault- Order by vault
LP Withdraw Requests:
depositor- Order by depositorunlock_epoch- Order by unlock epochvault- Order by vault
Oracle Tokens:
id- Order by token IDname- Order by token namepermission_group- Order by permission group
Fee Transactions:
- No explicit ordering parameter, results ordered by ID
Sorting Examples
Section titled “Sorting Examples”Newest First:
const { data } = await client.query({ query: gql` query GetRecentTrades($trader: String!) { perp { trades( where: { trader: $trader } order_desc: true limit: 20 ) { id openBlock { block_ts } } } } `, variables: { trader: "nibi1abc..." }})Oldest First:
const { data } = await client.query({ query: gql` query GetOldestTrades($trader: String!) { perp { trades( where: { trader: $trader } order_desc: false limit: 20 ) { id openBlock { block_ts } } } } `, variables: { trader: "nibi1abc..." }})By Token Name:
const { data } = await client.query({ query: gql` query GetTokensAlphabetically { oracle { tokens( order_by: name order_desc: false ) { symbol name } } } `})Advanced Patterns
Section titled “Advanced Patterns”Combined Filters
Section titled “Combined Filters”Combine multiple filters for precise queries.
const { data } = await client.query({ query: gql` query GetFilteredFees( $trader: String! $feeType: FeeType! $fromDate: Time! $toDate: Time! $minAmount: Int! ) { fee { feeTransactions( filter: { traderAddress: $trader feeType: $feeType fromDate: $fromDate toDate: $toDate minAmount: $minAmount } limit: 100 ) { id totalFeeCharged blockTime } } } `, variables: { trader: "nibi1abc...", feeType: "OPENING", fromDate: "2024-01-01T00:00:00Z", toDate: "2024-12-31T23:59:59Z", minAmount: 1000000 }})Client-Side Post-Filtering
Section titled “Client-Side Post-Filtering”When server-side filters aren’t available, filter client-side:
// Fetch all tradesconst { data } = await client.query({ query: gql` query GetAllTrades($trader: String!) { perp { trades(where: { trader: $trader }, limit: 1000) { id leverage isLong isOpen collateralAmount state { pnlPct } perpBorrowing { baseToken { symbol } } } } } `, variables: { trader: "nibi1abc..." }})
// Filter for high-leverage profitable longsconst filtered = data.perp.trades.filter(trade => trade.isLong && trade.leverage > 10 && trade.state.pnlPct > 0.1 && // 10% profit trade.isOpen)
// Group by tokenconst byToken = filtered.reduce((acc, trade) => { const symbol = trade.perpBorrowing.baseToken.symbol if (!acc[symbol]) acc[symbol] = [] acc[symbol].push(trade) return acc}, {})
console.log('Profitable high-leverage longs by token:', byToken)Search Implementation
Section titled “Search Implementation”Build a search feature:
async function searchTrades(trader, searchTerm) { // Fetch all trades const { data } = await client.query({ query: gql` query GetTrades($trader: String!) { perp { trades(where: { trader: $trader }) { id isLong leverage perpBorrowing { baseToken { symbol name } marketId } } } } `, variables: { trader } })
// Search by token symbol or name const searchLower = searchTerm.toLowerCase() return data.perp.trades.filter(trade => { const symbol = trade.perpBorrowing.baseToken.symbol.toLowerCase() const name = trade.perpBorrowing.baseToken.name.toLowerCase() return symbol.includes(searchLower) || name.includes(searchLower) })}
// Usageconst btcTrades = await searchTrades("nibi1abc...", "bitcoin")const ethTrades = await searchTrades("nibi1abc...", "eth")Best Practices
Section titled “Best Practices”1. Use Appropriate Page Sizes
Section titled “1. Use Appropriate Page Sizes”// Good: Reasonable page sizeconst PAGE_SIZE = 50
// Bad: Too large, slow queriesconst PAGE_SIZE = 10000
// Bad: Too small, too many requestsconst PAGE_SIZE = 5Recommended page sizes:
- Trades: 20-100
- Fee transactions: 50-100
- Trade history: 50-200
- Tokens: 100-500
2. Cache Paginated Results
Section titled “2. Cache Paginated Results”class CachedPaginator { constructor(client, query, variables) { this.client = client this.query = query this.variables = variables this.cache = new Map() }
async getPage(page, pageSize = 20) { const cacheKey = `${page}-${pageSize}`
if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey) }
const { data } = await this.client.query({ query: this.query, variables: { ...this.variables, limit: pageSize, offset: page * pageSize } })
this.cache.set(cacheKey, data) return data }
clearCache() { this.cache.clear() }}3. Handle Empty Results
Section titled “3. Handle Empty Results”async function fetchTradesWithEmptyCheck(trader, page = 0) { const { data } = await client.query({ query: GET_TRADES, variables: { trader, limit: 20, offset: page * 20 } })
const trades = data.perp.trades
if (trades.length === 0) { if (page === 0) { console.log('No trades found for this trader') } else { console.log('No more trades to load') } }
return trades}4. Debounce Filter Changes
Section titled “4. Debounce Filter Changes”import { debounce } from 'lodash'
const debouncedSearch = debounce(async (searchTerm) => { const results = await searchTrades(trader, searchTerm) updateUI(results)}, 300) // Wait 300ms after user stops typing
// Usage in Reactfunction SearchInput() { const handleChange = (e) => { debouncedSearch(e.target.value) }
return <input onChange={handleChange} />}5. Show Loading States
Section titled “5. Show Loading States”async function loadTradesWithUI(trader, page) { try { showLoadingSpinner()
const trades = await fetchPage(trader, page)
if (trades.length === 0) { showEmptyState() } else { renderTrades(trades) } } catch (error) { showErrorState(error) } finally { hideLoadingSpinner() }}6. Validate Filter Inputs
Section titled “6. Validate Filter Inputs”function validateDateRange(fromDate, toDate) { const from = new Date(fromDate) const to = new Date(toDate)
if (isNaN(from.getTime()) || isNaN(to.getTime())) { throw new Error('Invalid date format') }
if (from > to) { throw new Error('From date must be before to date') }
const maxRange = 90 * 24 * 60 * 60 * 1000 // 90 days if (to - from > maxRange) { throw new Error('Date range too large (max 90 days)') }
return { fromDate, toDate }}
// Usagetry { const { fromDate, toDate } = validateDateRange( userInputFrom, userInputTo )
const { data } = await client.query({ query: GET_FEES, variables: { fromDate, toDate } })} catch (error) { showError(error.message)}