@emeryld/keyset
v0.1.1
Published
Small, reusable keyset cursor utilities with a Prisma adapter.
Downloads
4
Readme
@emeryld/keyset
Small, reusable keyset cursor utilities with a Prisma adapter.
Core concepts
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
- validates & decodes an incoming cursor for a given
Prisma adapter:
- default
whereWithCursorimplements:- asc: (field > value) OR (field = value AND id > cursorId)
- desc: (field < value) OR (field = value AND id < cursorId)
- you can substitute
whereWithCursorviacreatePrismaKeysetAdapter({ whereWithCursor })
- default
Installation (monorepo)
Add the package under packages/keyset, then:
pnpm -C packages/keyset buildConsume it from your app:
import { createKeyset } from '@emeryld/keyset/core'
import { createPrismaKeysetAdapter } from '@emeryld/keyset/adapters/prisma'Usage
Define a keyset spec per feed (domain-owned, close to the repo code):
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:
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)Customizing the Prisma where builder
You can override the default whereWithCursor for special cases (nullable sort fields, computed fields, non-standard tie-breakers, etc.).
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
},
})Notes
- Cursor format is base64url JSON with
v: 1for versioning. You can bumpvlater. nextCursorassumes each row contains anidproperty.- For raw SQL / computed sorts, keep the special-case branch in your repo or implement a custom
whereWithCursorin the adapter.
