import { type SelectQueryBuilder, type ReferenceExpression, type OperandValueExpression, sql } from 'kysely' type ColumnsOf = T extends any ? keyof DB[T] & string : never type AppearsIn = { [T in TB]: Col extends ColumnsOf ? T : never }[TB] type IsUnion = T extends any ? ([U] extends [T] ? false : true) : never type UniqueColumn = { [C in ColumnsOf]: IsUnion> extends true ? never : C }[ColumnsOf] type PrefixedColumn = TB extends any ? `${TB & string}.${keyof DB[TB] & string}` : never type UnambiguousColumn = PrefixedColumn | UniqueColumn type WhereInput = Partial<{ [C in UnambiguousColumn]: | OperandValueExpression | readonly OperandValueExpression[] | null }> type WhereCapable = { where(lhs: ReferenceExpression, op: any, rhs: any): Self } export function applyWhere>( builder: QB, where: WhereInput, ): QB { return Object.entries(where).reduce((builder, [key, value]) => { const column = key as ReferenceExpression if (value === null) { return builder.where(column, 'is', null) } if (Array.isArray(value)) { return builder.where(column, 'in', value) } if (value === undefined) { return builder } return builder.where(column, '=', value) }, builder) } interface PaginationInput { where: WhereInput limit: number offset: number sort?: UnambiguousColumn | `-${UnambiguousColumn}` } export function applyPagination( builder: SelectQueryBuilder, { limit, offset, sort }: PaginationInput, ): SelectQueryBuilder { let qb = builder.limit(limit).offset(offset) if (sort) { const columnName = (sort.startsWith('-') ? sort.slice(1) : sort) as UnambiguousColumn qb = qb.orderBy(columnName, columnName === sort ? 'asc' : 'desc') } return qb } export async function paginate( baseDataQuery: SelectQueryBuilder, baseCountQuery: SelectQueryBuilder, input: PaginationInput, ) { const dataQuery = applyPagination(input.where ? applyWhere(baseDataQuery, input.where) : baseDataQuery, input) let countQuery = baseCountQuery.select(sql`count(*)::int`.as('totalCount')) if (input.where) { countQuery = applyWhere(countQuery, input.where) } const [countRow, data] = await Promise.all([countQuery.executeTakeFirstOrThrow(), dataQuery.execute()]) return { data, meta: { totalCount: (countRow as { totalCount: number }).totalCount, limit: input.limit, offset: input.offset, count: data.length, }, } }