Skip to content

Commit 3a453a2

Browse files
committed
fix: apply cursor to pagination queries (addressing review feedback)
- Parse base64 cursor containing {sortOrder, createdAt, id} - Build keyset pagination condition for composite ORDER BY - Encode nextCursor as composite value matching sort order - Remove unused 'gt' import
1 parent cb99aa7 commit 3a453a2

File tree

1 file changed

+47
-5
lines changed

1 file changed

+47
-5
lines changed

apps/sim/app/api/workflows/route.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { db } from '@sim/db'
22
import { permissions, workflow, workflowFolder } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
4-
import { and, asc, eq, gt, inArray, isNull, min } from 'drizzle-orm'
4+
import { and, asc, eq, gt, gte, inArray, isNull, lt, min, or } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
77
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
@@ -72,11 +72,43 @@ export async function GET(request: NextRequest) {
7272
// Fetch limit+1 to detect if there are more pages
7373
const fetchLimit = limit + 1
7474

75+
// Build cursor condition for keyset pagination
76+
// Cursor is base64-encoded JSON: { s: sortOrder, c: createdAt, i: id }
77+
let cursorCondition = null
78+
if (cursor) {
79+
try {
80+
const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString())
81+
const cursorSortOrder = decoded.s
82+
const cursorCreatedAt = new Date(decoded.c)
83+
const cursorId = decoded.i
84+
// Keyset pagination for ORDER BY sortOrder ASC, createdAt ASC, id ASC:
85+
// (sortOrder > cursorSortOrder) OR
86+
// (sortOrder = cursorSortOrder AND createdAt > cursorCreatedAt) OR
87+
// (sortOrder = cursorSortOrder AND createdAt = cursorCreatedAt AND id > cursorId)
88+
cursorCondition = or(
89+
gt(workflow.sortOrder, cursorSortOrder),
90+
and(
91+
eq(workflow.sortOrder, cursorSortOrder),
92+
or(
93+
gt(workflow.createdAt, cursorCreatedAt),
94+
and(eq(workflow.createdAt, cursorCreatedAt), gt(workflow.id, cursorId))
95+
)
96+
)
97+
)
98+
} catch {
99+
// Invalid cursor - ignore and return first page
100+
}
101+
}
102+
75103
if (workspaceId) {
104+
const whereClause = cursorCondition
105+
? and(eq(workflow.workspaceId, workspaceId), cursorCondition)
106+
: eq(workflow.workspaceId, workspaceId)
107+
76108
workflows = await db
77109
.select()
78110
.from(workflow)
79-
.where(eq(workflow.workspaceId, workspaceId))
111+
.where(whereClause)
80112
.orderBy(...orderByClause)
81113
.limit(fetchLimit)
82114
} else {
@@ -88,18 +120,28 @@ export async function GET(request: NextRequest) {
88120
if (workspaceIds.length === 0) {
89121
return NextResponse.json({ data: [] }, { status: 200 })
90122
}
123+
const whereClause = cursorCondition
124+
? and(inArray(workflow.workspaceId, workspaceIds), cursorCondition)
125+
: inArray(workflow.workspaceId, workspaceIds)
126+
91127
workflows = await db
92128
.select()
93129
.from(workflow)
94-
.where(inArray(workflow.workspaceId, workspaceIds))
130+
.where(whereClause)
95131
.orderBy(...orderByClause)
96132
.limit(fetchLimit)
97133
}
98134

99-
// Determine if there are more results and set cursor
135+
// Determine if there are more results and compute next cursor
100136
const hasMore = workflows.length > limit
101137
const data = hasMore ? workflows.slice(0, limit) : workflows
102-
const nextCursor = hasMore ? data[data.length - 1]?.id : null
138+
let nextCursor = null
139+
if (hasMore && data.length > 0) {
140+
const last = data[data.length - 1]
141+
nextCursor = Buffer.from(
142+
JSON.stringify({ s: last.sortOrder, c: last.createdAt, i: last.id })
143+
).toString('base64')
144+
}
103145

104146
return NextResponse.json({ data, nextCursor }, { status: 200 })
105147
} catch (error: any) {

0 commit comments

Comments
 (0)