Small, reusable keyset cursor utilities with a Prisma adapter.
npm install @emeryld/keysetSmall, reusable keyset cursor utilities with a Prisma adapter.
- createKeyset(spec):
- validates & decodes an incoming cursor for a given sortBy + dir
- generates the next cursor from the last row
- exposes field(sortBy) for adapters
- Prisma adapter:
- default whereWithCursor implements:
- asc: (field > value) OR (field = value AND id > cursorId)
- desc: (field < value) OR (field = value AND id < cursorId)
- you can substitute whereWithCursor via createPrismaKeysetAdapter({ whereWithCursor })
Add the package under packages/keyset, then:
``bash`
pnpm -C packages/keyset build
Consume it from your app:
`ts`
import { createKeyset } from '@emeryld/keyset/core'
import { createPrismaKeysetAdapter } from '@emeryld/keyset/adapters/prisma'
Define a keyset spec per feed (domain-owned, close to the repo code):
`ts
import { createKeyset } from '@emeryld/keyset/core'
export const orgKeyset = createKeyset({
name: {
field: 'name',
cursorValueKey: 'name',
rowValue: (r: { name: string }) => r.name,
isRaw: (v): v is string => typeof v === 'string',
fromRaw: (v: string) => v,
toRaw: (v: string) => v,
},
createdAt: {
field: 'createdAt',
cursorValueKey: 'dt',
rowValue: (r: { createdAt: Date }) => r.createdAt,
isRaw: (v): v is string => typeof v === 'string',
fromRaw: (v: string) => new Date(v),
toRaw: (d: Date) => d.toISOString(),
},
} as const)
`
Use it in your repository with the Prisma adapter:
`ts
import { createPrismaKeysetAdapter } from '@emeryld/keyset/adapters/prisma'
const prismaKeyset = createPrismaKeysetAdapter()
const cursor = orgKeyset.decode(query.cursor, sortBy, sortDir)
const where = prismaKeyset.whereWithCursor({
baseWhere,
field: orgKeyset.field(sortBy),
dir: sortDir,
cursor,
})
const orderBy = prismaKeyset.orderBy({
field: orgKeyset.field(sortBy),
dir: sortDir,
})
const rows = await ctx.prisma.organization.findMany({
where,
orderBy,
select: { id: true, name: true, createdAt: true },
take: limit + 1,
})
const page = rows.slice(0, limit)
const hasMore = rows.length > limit
const nextCursor = orgKeyset.nextCursor(page, hasMore, sortBy, sortDir)
`
You can override the default whereWithCursor for special cases (nullable sort fields, computed fields, non-standard tie-breakers, etc.).
`ts
import {
createPrismaKeysetAdapter,
prismaWhereWithCursorDefault,
} from '@emeryld/keyset/adapters/prisma'
const prismaKeyset = createPrismaKeysetAdapter({
whereWithCursor: (args) => {
// delegate for most fields
const where = prismaWhereWithCursorDefault(args)
// example: require non-null values for a nullable sort field
if (args.field === 'someNullableColumn') {
return { ...where, [args.field]: { not: null } }
}
return where
},
})
`
- Cursor format is base64url JSON with v: 1 for versioning. You can bump v later.nextCursor
- assumes each row contains an id property.whereWithCursor` in the adapter.
- For raw SQL / computed sorts, keep the special-case branch in your repo or implement a custom