@azhulin/pagination-typeorm
v1.2.0
Published
TypeORM query-plan renderer for @azhulin/pagination-core (cursor + offset).
Maintainers
Readme
Pagination (TypeORM)
Runs a @azhulin/pagination-core QueryPlan on a TypeORM SelectQueryBuilder — keyset bounds as WHERE,
the resolved order, and take/skip.
npm i @azhulin/pagination-typeormtypeorm is a peer dependency, so this adapter never pins your version — pass any real SelectQueryBuilder.
applyQueryPlan
applyQueryPlan(query: PaginatableQueryBuilder, plan: QueryPlan): voidPass your query builder (SelectQueryBuilder<T> satisfies PaginatableQueryBuilder structurally). It mutates query
in place: adds each ORDER BY key (with NULLS FIRST/LAST), applies keyset WHERE bounds (cursor mode) or
skip(offset) (offset mode), and take(limit) unless unbounded. Only cursor values are parameterized; sort-key
field identifiers are interpolated verbatim, so keep them trusted/whitelisted.
End to end
Build your base query (filters, joins), then let the paginator plan, render, and run:
import { PaginationMode } from '@azhulin/pagination-core'
import type { Connection, ConnectionArgs, ConnectionEdge } from '@azhulin/pagination-core'
import { applyQueryPlan } from '@azhulin/pagination-typeorm'
import { UserPaginator } from './user.paginator' // the subclass from @azhulin/pagination-core's README
async function paginateUsersByGroup(args: ConnectionArgs, groupId: string): Promise<Connection<ConnectionEdge<User>>> {
const paginator = new UserPaginator(args, { maxLimit: 50, defaultLimit: 20 })
const plan = paginator.plan()
const query = dataSource.getRepository(User).createQueryBuilder('user').where('user.groupId = :groupId', { groupId })
applyQueryPlan(query, plan)
const rows = await query.getMany()
const totalCount = PaginationMode.Offset === plan.mode ? await query.getCount() : undefined // cursor mode runs no count
return paginator.connection({ rows, totalCount })
}Generated SQL
For sort keys email ASC, id ASC and a forward after cursor, the adapter applies the tuple keyset comparison, order,
and limit — shown here simplified:
WHERE user.groupId = $1
AND (user.email > $2 OR (user.email = $2 AND user.id > $3)) -- keyset tuple comparison (simplified)
ORDER BY user.email ASC NULLS LAST, user.id ASC NULLS LAST
LIMIT 21 -- requested 20 + 1 probe rowThe emitted WHERE is more verbose: values use named :cursor_* parameters, and each key's NULLS FIRST/LAST is
spelled out so nullable columns page correctly. Backward pages (last/before) flip both the direction and null
placement, and connection() reverses the rows back to natural order for you.
License
MIT © 2022–2026 Alex Zhulin
