import { Repository, FindConditions, SelectQueryBuilder, Like, ObjectLiteral } from 'typeorm' import { PaginateQuery } from './decorator' import { ServiceUnavailableException } from '@nestjs/common' type Column = Extract type Order = [Column, 'ASC' | 'DESC'] type SortBy = Order[] export class Paginated { data: T[] meta: { itemsPerPage: number totalItems: number currentPage: number totalPages: number sortBy: SortBy search: string } links: { first?: string previous?: string current: string next?: string last?: string } } export interface PaginateConfig { sortableColumns: Column[] searchableColumns?: Column[] maxLimit?: number defaultSortBy?: SortBy defaultLimit?: number where?: FindConditions queryBuilder?: SelectQueryBuilder } export async function paginate( query: PaginateQuery, repo: Repository | SelectQueryBuilder, config: PaginateConfig ): Promise> { let page = query.page || 1 const limit = query.limit || config.defaultLimit || 20 const sortBy = [] as SortBy const search = query.search const path = query.path function isEntityKey(sortableColumns: Column[], column: string): column is Column { return !!sortableColumns.find((c) => c === column) } const { sortableColumns } = config if (config.sortableColumns.length < 1) throw new ServiceUnavailableException() if (query.sortBy) { for (const order of query.sortBy) { if (isEntityKey(sortableColumns, order[0]) && ['ASC', 'DESC'].includes(order[1])) { sortBy.push(order as Order) } } } if (!sortBy.length) { sortBy.push(...(config.defaultSortBy || [[sortableColumns[0], 'ASC']])) } if (page < 1) page = 1 let [items, totalItems]: [T[], number] = [[], 0] let queryBuilder: SelectQueryBuilder if (repo instanceof Repository) { queryBuilder = repo .createQueryBuilder('e') .take(limit) .skip((page - 1) * limit) for (const order of sortBy) { queryBuilder.addOrderBy('e.' + order[0], order[1]) } } else { queryBuilder = repo.take(limit).skip((page - 1) * limit) for (const order of sortBy) { queryBuilder.addOrderBy(repo.alias + '.' + order[0], order[1]) } } const where: ObjectLiteral[] = [] if (search && config.searchableColumns) { for (const column of config.searchableColumns) { where.push({ [column]: Like(`%${search}%`), ...config.where }) } } ;[items, totalItems] = await queryBuilder.where(where.length ? where : config.where || {}).getManyAndCount() let totalPages = totalItems / limit if (totalItems % limit) totalPages = Math.ceil(totalPages) const options = `&limit=${limit}${sortBy.map((order) => `&sortBy=${order.join(':')}`).join('')}${ search ? `&search=${search}` : '' }` const buildLink = (p: number): string => path + '?page=' + p + options const results: Paginated = { data: items, meta: { itemsPerPage: limit, totalItems, currentPage: page, totalPages: totalPages, sortBy, search, }, links: { first: page == 1 ? undefined : buildLink(1), previous: page - 1 < 1 ? undefined : buildLink(page - 1), current: buildLink(page), next: page + 1 > totalPages ? undefined : buildLink(page + 1), last: page == totalPages ? undefined : buildLink(totalPages), }, } return Object.assign(new Paginated(), results) }