feat: add cursor pagination to global session list
This commit is contained in:
parent
b9ec4e6b97
commit
34e69735cb
3 changed files with 42 additions and 3 deletions
|
|
@ -135,6 +135,10 @@ export const GlobalRoutes = lazy(() =>
|
||||||
.number()
|
.number()
|
||||||
.optional()
|
.optional()
|
||||||
.meta({ description: "Filter sessions updated on or after this timestamp (milliseconds since epoch)" }),
|
.meta({ description: "Filter sessions updated on or after this timestamp (milliseconds since epoch)" }),
|
||||||
|
cursor: z.coerce
|
||||||
|
.number()
|
||||||
|
.optional()
|
||||||
|
.meta({ description: "Return sessions updated before this timestamp (milliseconds since epoch)" }),
|
||||||
search: z.string().optional().meta({ description: "Filter sessions by title (case-insensitive)" }),
|
search: z.string().optional().meta({ description: "Filter sessions by title (case-insensitive)" }),
|
||||||
limit: z.coerce.number().optional().meta({ description: "Maximum number of sessions to return" }),
|
limit: z.coerce.number().optional().meta({ description: "Maximum number of sessions to return" }),
|
||||||
archived: z.coerce.boolean().optional().meta({ description: "Include archived sessions (default false)" }),
|
archived: z.coerce.boolean().optional().meta({ description: "Include archived sessions (default false)" }),
|
||||||
|
|
@ -142,18 +146,25 @@ export const GlobalRoutes = lazy(() =>
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const query = c.req.valid("query")
|
const query = c.req.valid("query")
|
||||||
|
const limit = query.limit ?? 100
|
||||||
const sessions: Session.GlobalInfo[] = []
|
const sessions: Session.GlobalInfo[] = []
|
||||||
for await (const session of Session.listGlobal({
|
for await (const session of Session.listGlobal({
|
||||||
directory: query.directory,
|
directory: query.directory,
|
||||||
roots: query.roots,
|
roots: query.roots,
|
||||||
start: query.start,
|
start: query.start,
|
||||||
|
cursor: query.cursor,
|
||||||
search: query.search,
|
search: query.search,
|
||||||
limit: query.limit,
|
limit: limit + 1,
|
||||||
archived: query.archived,
|
archived: query.archived,
|
||||||
})) {
|
})) {
|
||||||
sessions.push(session)
|
sessions.push(session)
|
||||||
}
|
}
|
||||||
return c.json(sessions)
|
const hasMore = sessions.length > limit
|
||||||
|
const list = hasMore ? sessions.slice(0, limit) : sessions
|
||||||
|
if (hasMore && list.length > 0) {
|
||||||
|
c.header("x-next-cursor", String(list[list.length - 1].time.updated))
|
||||||
|
}
|
||||||
|
return c.json(list)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { Flag } from "../flag/flag"
|
||||||
import { Identifier } from "../id/id"
|
import { Identifier } from "../id/id"
|
||||||
import { Installation } from "../installation"
|
import { Installation } from "../installation"
|
||||||
|
|
||||||
import { Database, NotFoundError, eq, and, or, gte, isNull, desc, like, inArray } from "../storage/db"
|
import { Database, NotFoundError, eq, and, or, gte, isNull, desc, like, inArray, lt } from "../storage/db"
|
||||||
import type { SQL } from "../storage/db"
|
import type { SQL } from "../storage/db"
|
||||||
import { SessionTable, MessageTable, PartTable } from "./session.sql"
|
import { SessionTable, MessageTable, PartTable } from "./session.sql"
|
||||||
import { ProjectTable } from "../project/project.sql"
|
import { ProjectTable } from "../project/project.sql"
|
||||||
|
|
@ -568,6 +568,7 @@ export namespace Session {
|
||||||
directory?: string
|
directory?: string
|
||||||
roots?: boolean
|
roots?: boolean
|
||||||
start?: number
|
start?: number
|
||||||
|
cursor?: number
|
||||||
search?: string
|
search?: string
|
||||||
limit?: number
|
limit?: number
|
||||||
archived?: boolean
|
archived?: boolean
|
||||||
|
|
@ -583,6 +584,9 @@ export namespace Session {
|
||||||
if (input?.start) {
|
if (input?.start) {
|
||||||
conditions.push(gte(SessionTable.time_updated, input.start))
|
conditions.push(gte(SessionTable.time_updated, input.start))
|
||||||
}
|
}
|
||||||
|
if (input?.cursor) {
|
||||||
|
conditions.push(lt(SessionTable.time_updated, input.cursor))
|
||||||
|
}
|
||||||
if (input?.search) {
|
if (input?.search) {
|
||||||
conditions.push(like(SessionTable.title, `%${input.search}%`))
|
conditions.push(like(SessionTable.title, `%${input.search}%`))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,4 +62,28 @@ describe("Session.listGlobal", () => {
|
||||||
|
|
||||||
expect(allIds).toContain(archived.id)
|
expect(allIds).toContain(archived.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("supports cursor pagination", async () => {
|
||||||
|
await using tmp = await tmpdir({ git: true })
|
||||||
|
|
||||||
|
const first = await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => Session.create({ title: "page-one" }),
|
||||||
|
})
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5))
|
||||||
|
const second = await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => Session.create({ title: "page-two" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const page = [...Session.listGlobal({ directory: tmp.path, limit: 1 })]
|
||||||
|
expect(page.length).toBe(1)
|
||||||
|
expect(page[0].id).toBe(second.id)
|
||||||
|
|
||||||
|
const next = [...Session.listGlobal({ directory: tmp.path, limit: 10, cursor: page[0].time.updated })]
|
||||||
|
const ids = next.map((session) => session.id)
|
||||||
|
|
||||||
|
expect(ids).toContain(first.id)
|
||||||
|
expect(ids).not.toContain(second.id)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue