跳转到内容
本页面的翻译由 LLM 生成。如果你发现翻译有误,请在 GitHub 上提交 issue 告诉我们。

筛选与分页

学习筛选、分页、排序等进阶查询模式。


Keeper 为各数据类型提供不同筛选器。

StringFilter 支持相等和模式匹配。

input StringFilter {
eq: String # Exact match
like: String # Pattern match
}

示例:精确匹配

query SearchToken {
oracle {
tokens(where: { name: { eq: "Bitcoin" } }) {
id
symbol
name
}
}
}

示例:模式匹配

query SearchByPattern {
oracle {
tokens(where: { name: { like: "%coin%" } }) {
symbol
name
}
}
}

JavaScript 用法:

// Exact match
const { data } = await client.query({
query: gql`
query SearchToken($name: String!) {
oracle {
tokens(where: { name: $name }) {
symbol
name
}
}
}
`,
variables: { name: "Bitcoin" }
})
// Pattern search
const searchResults = await client.query({
query: gql`
query SearchTokens($pattern: String!) {
oracle {
tokens(where: { name: $pattern }) {
symbol
name
}
}
}
`,
variables: { pattern: "%BTC%" }
})

IntFilter 支持比较运算。

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
}

示例:费用金额范围

query GetLargeFees {
fee {
feeTransactions(
filter: {
minAmount: 1000000 # >= 1,000,000
maxAmount: 10000000 # <= 10,000,000
}
) {
id
totalFeeCharged
traderAddress
}
}
}

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
}
})

FloatFilter 用于浮点比较。

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
}

示例:按杠杆筛选

// Note: Direct float filters not used in current schema,
// but filter objects use similar patterns
const highLeverageTrades = await client.query({
query: gql`
query GetHighLeverageTrades($trader: String!) {
perp {
trades(where: { trader: $trader }) {
id
leverage
isOpen
}
}
}
`,
variables: { trader: "nibi1abc..." }
})
// Filter client-side
const filtered = highLeverageTrades.data.perp.trades.filter(
trade => trade.leverage > 10
)

TimeFilter 用于日期时间范围查询。

input TimeFilter {
eq: Time # Exact time
gt: Time # After
gte: Time # After or at
lt: Time # Before
lte: Time # Before or at
}

示例:按日期范围筛选

query GetFeesLastMonth {
fee {
feeTransactions(
filter: {
fromDate: "2024-10-01T00:00:00Z"
toDate: "2024-10-31T23:59:59Z"
}
limit: 100
) {
id
totalFeeCharged
blockTime
}
}
}

JavaScript 用法:

// Get fees for last 30 days
const 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()
}
})

辅助函数:

function getDateRange(days) {
const toDate = new Date()
const fromDate = new Date()
fromDate.setDate(fromDate.getDate() - days)
return {
fromDate: fromDate.toISOString(),
toDate: toDate.toISOString()
}
}
// Usage
const last7Days = getDateRange(7)
const last30Days = getDateRange(30)
const last90Days = getDateRange(90)

按枚举值筛选。

示例:费用类型筛选

query GetOpeningFees($trader: String!) {
fee {
feeTransactions(
filter: {
traderAddress: $trader
feeType: OPENING
}
limit: 50
) {
id
feeType
totalFeeCharged
}
}
}

示例:交易类型筛选

// Get only limit orders
const { 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 events
const limitOrders = data.perp.tradeHistory.filter(
h => h.tradeChangeType === 'limit_order_created'
)

使用 limit 和 offset 分页。

query GetTradesPaginated($trader: String!, $limit: Int!, $offset: Int!) {
perp {
trades(
where: { trader: $trader }
limit: $limit
offset: $offset
) {
id
isOpen
leverage
}
}
}

JavaScript 实现:

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 page
const firstPage = await fetchPage("nibi1abc...", 0, 20)
// Get second page
const secondPage = await fetchPage("nibi1abc...", 1, 20)
// Get third page
const thirdPage = await fetchPage("nibi1abc...", 2, 20)

跨多个页面获取全部结果。

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
}
// Usage
const allTrades = await fetchAllTrades("nibi1abc...")
console.log(`Total trades: ${allTrades.length}`)

Keeper 使用偏移量分页,但客户端可实现游标逻辑。

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 batch
const firstBatch = await fetchTradesAfterBlock("nibi1abc...", 0)
const lastBlock = firstBatch[firstBatch.length - 1].openBlock.block
// Get next batch
const nextBatch = await fetchTradesAfterBlock("nibi1abc...", lastBlock)

在界面实现无限滚动。

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
}
}
// Usage
const paginator = new TradesPaginator(client, "nibi1abc...", 20)
// Load first page
const firstPage = await paginator.loadMore()
// Load more on scroll
window.addEventListener('scroll', async () => {
if (isNearBottom() && paginator.hasMore) {
const nextPage = await paginator.loadMore()
appendToUI(nextPage)
}
})

多数查询支持 order_by 和 order_desc。

query GetTradesOrdered($trader: String!) {
perp {
trades(
where: { trader: $trader }
order_by: sequence
order_desc: true # Descending (newest first)
) {
id
openBlock { block_ts }
}
}
}

各领域排序选项:

合约交易:

  • sequence:按序号排序。

交易历史:

  • sequence:按序号。
  • trade_id:按交易 ID。

LP 存款:

  • depositor:按存款人地址。
  • vault:按金库地址。

LP 存款历史:

  • depositor:按存款人。
  • sequence:按序号。
  • vault:按金库。

LP 提现申请:

  • depositor:按存款人。
  • unlock_epoch:按解锁周期。
  • vault:按金库。

预言机代币:

  • id:按代币 ID。
  • name:按名称。
  • permission_group:按权限组。

费用交易:

  • 无显式排序参数,按 ID 排序。

最新优先:

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..." }
})

最早优先:

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..." }
})

按代币名称:

const { data } = await client.query({
query: gql`
query GetTokensAlphabetically {
oracle {
tokens(
order_by: name
order_desc: false
) {
symbol
name
}
}
}
`
})

组合多个筛选条件进行精确查询。

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
}
})

服务端不支持的筛选可在客户端执行。

// Fetch all trades
const { 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 longs
const filtered = data.perp.trades.filter(trade =>
trade.isLong &&
trade.leverage > 10 &&
trade.state.pnlPct > 0.1 && // 10% profit
trade.isOpen
)
// Group by token
const 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)

构建搜索功能。

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)
})
}
// Usage
const btcTrades = await searchTrades("nibi1abc...", "bitcoin")
const ethTrades = await searchTrades("nibi1abc...", "eth")

// Good: Reasonable page size
const PAGE_SIZE = 50
// Bad: Too large, slow queries
const PAGE_SIZE = 10000
// Bad: Too small, too many requests
const PAGE_SIZE = 5

建议分页大小:

  • 交易:20 至 100。
  • 费用交易:50 至 100。
  • 历史:50 至 200。
  • 代币:100 至 500。

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()
}
}

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
}

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 React
function SearchInput() {
const handleChange = (e) => {
debouncedSearch(e.target.value)
}
return <input onChange={handleChange} />
}

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()
}
}

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 }
}
// Usage
try {
const { fromDate, toDate } = validateDateRange(
userInputFrom,
userInputTo
)
const { data } = await client.query({
query: GET_FEES,
variables: { fromDate, toDate }
})
} catch (error) {
showError(error.message)
}