Add project
This commit is contained in:
parent
e83619baaf
commit
d7670f0a10
14
.editorconfig
Normal file
14
.editorconfig
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{json,js,yml,md}]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
24
.eslintrc.json
Normal file
24
.eslintrc.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "tsconfig.json",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint/eslint-plugin"],
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier",
|
||||
"prettier/@typescript-eslint"
|
||||
],
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true,
|
||||
"jest": true
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
}
|
||||
}
|
26
.github/workflows/main.yml
vendored
Normal file
26
.github/workflows/main.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
name: Main CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run test:cov
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
lib/
|
||||
coverage/
|
||||
.DS_Store
|
||||
npm-debug.log
|
15
.prettierrc
Normal file
15
.prettierrc
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"printWidth": 120,
|
||||
"semi": false,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "{*.json,*.md,.prettierrc,.*.yml}",
|
||||
"options": {
|
||||
"tabWidth": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
}
|
22
LICENSE
Normal file
22
LICENSE
Normal file
@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Philipp Petzold (https://github.com/ppetzold)
|
||||
Copyright (c) 2020 jyutzio (https://github.com/jyutzio)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
170
README.md
170
README.md
@ -1 +1,169 @@
|
||||
# nestjs-paginate
|
||||
# Nest.js Paginate
|
||||
|
||||
![Version](https://img.shields.io/github/package-json/v/ppetzold/nestjs-paginate)
|
||||
![Main CI](https://github.com/ppetzold/nestjs-paginate/workflows/Main%20CI/badge.svg)
|
||||
[![Coverage](https://img.shields.io/codecov/c/github/ppetzold/nestjs-paginate/master.svg)](https://codecov.io/gh/ppetzold/nestjs-paginate)
|
||||
[![GitHub license](https://img.shields.io/github/license/ppetzold/nestjs-paginate.svg)](https://github.com/ppetzold/nestjs-paginate/blob/master/LICENSE)
|
||||
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
|
||||
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
|
||||
|
||||
Pagination and filtering helper method for TypeORM repositories or query builders using [Nest.js](https://nestjs.com/) framework.
|
||||
|
||||
- Pagination conforms to [JSON:API](https://jsonapi.org/)
|
||||
- Sort by multiple columns
|
||||
- Filter by multiple columns using operators
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
npm install nestjs-paginate
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Example example
|
||||
|
||||
The following code exposes a route that can be utilized like so:
|
||||
|
||||
#### Endpoint
|
||||
|
||||
```url
|
||||
http://localhost:3000/cats?limit=10&page=2&sortBy=createdAt:DESC&sortBy=color:ASC
|
||||
```
|
||||
|
||||
#### Result
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 4,
|
||||
"name": "George",
|
||||
"color": "white"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Leche",
|
||||
"color": "white"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Garfield",
|
||||
"color": "ginger"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Milo",
|
||||
"color": "brown"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Shadow",
|
||||
"color": "black"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"itemsPerPage": 2,
|
||||
"totalItems": 5,
|
||||
"currentPage": 2,
|
||||
"totalPages": 3,
|
||||
"sortBy": [["color", "DESC"]]
|
||||
},
|
||||
"links": {
|
||||
"first": "http://localhost:3000/cats?limit=2&page=1&sortBy=color:DESC",
|
||||
"previous": "http://localhost:3000/cats?&limit=2&page=1&sortBy=color:DESC",
|
||||
"next": "http://localhost:3000/cats?&limit=2&page=3&sortBy=color:DESC",
|
||||
"last": "http://localhost:3000/cats?&limit=2&page=3&sortBy=color:DESC"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Code
|
||||
|
||||
```ts
|
||||
import { Controller, Injectable, Get } from '@nestjs/common'
|
||||
import { InjectRepository } from '@nestjs/typeorm'
|
||||
import { Repository, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
|
||||
import { Paginate, PaginateQuery, paginate, Paginated } from 'nestjs-paginate'
|
||||
|
||||
@Entity()
|
||||
export class CatEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column('text')
|
||||
name: string
|
||||
|
||||
@Column('text')
|
||||
color: string
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CatsService {
|
||||
constructor(
|
||||
@InjectRepository(CatEntity)
|
||||
private readonly catsRepository: Repository<CatEntity>
|
||||
) {}
|
||||
|
||||
public findAll(query: PaginateQuery): Promise<Paginated<CatEntity>> {
|
||||
return paginate(query, this.catsRepository, {
|
||||
sortableColumns: ['name', 'color'],
|
||||
defaultOrderby: [['color', 'DESC']],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('cats')
|
||||
export class CatsController {
|
||||
constructor(private readonly catsService: CatsService) {}
|
||||
|
||||
@Get()
|
||||
public findAll(@Paginate() query: PaginateQuery): Promise<Paginated<CatEntity>> {
|
||||
return this.catsService.findAll(query)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
```ts
|
||||
const paginateConfig: PaginateConfig<CatEntity> {
|
||||
/**
|
||||
* Required: true (must have a minimum of one column)
|
||||
* Type: keyof CatEntity
|
||||
* Description: These are the columns that are valid to be sorted by.
|
||||
*/
|
||||
sortableColumns: ['id', 'name', 'color'],
|
||||
|
||||
/**
|
||||
* Required: false
|
||||
* Type: number
|
||||
* Default: 100
|
||||
* Description: The maximum amount of entities to return per page.
|
||||
*/
|
||||
maxLimit: 20,
|
||||
|
||||
/**
|
||||
* Required: false
|
||||
* Type: [string, 'ASC' | 'DESC'][]
|
||||
* Default: [sortableColumns[0], 'ASC]]
|
||||
* Description: The order to display the sorted entities.
|
||||
*/
|
||||
defaultSortBy: [['name', 'DESC']],
|
||||
|
||||
/**
|
||||
* Required: false
|
||||
* Type: number
|
||||
* Default: 20
|
||||
*/
|
||||
defaultLimit: 50,
|
||||
|
||||
/**
|
||||
* Required: false
|
||||
* Type: TypeORM find options
|
||||
* Default: None
|
||||
* https://typeorm.io/#/find-optionsfind-options.md
|
||||
*/
|
||||
where: { color: 'ginger' }
|
||||
}
|
||||
```
|
||||
|
7507
package-lock.json
generated
Normal file
7507
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
88
package.json
Normal file
88
package.json
Normal file
@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "nestjs-paginate",
|
||||
"version": "0.1.0",
|
||||
"author": "Philipp Petzold <ppetzold@protonmail.com>",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
],
|
||||
"description": "Pagination and filtering helper method for TypeORM repostiories or query builders using Nest.js framework.",
|
||||
"keywords": [
|
||||
"nestjs",
|
||||
"typeorm",
|
||||
"express",
|
||||
"pagination",
|
||||
"paginate",
|
||||
"filtration",
|
||||
"filter"
|
||||
],
|
||||
"scripts": {
|
||||
"prebuild": "rimraf lib",
|
||||
"build": "tsc",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"lint": "eslint \"src/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
|
||||
},
|
||||
"dependencies": {
|
||||
"rxjs": "^6.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/common": "^7.2.0",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/jest": "^26.0.3",
|
||||
"@types/node": "^14.0.14",
|
||||
"@typescript-eslint/eslint-plugin": "^3.4.0",
|
||||
"@typescript-eslint/parser": "^3.4.0",
|
||||
"eslint": "^7.3.1",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"jest": "^26.1.0",
|
||||
"prettier": "^2.0.5",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sqlite3": "^4.2.0",
|
||||
"ts-jest": "^26.1.1",
|
||||
"ts-node": "^8.10.2",
|
||||
"typeorm": "^0.2.25",
|
||||
"typescript": "^3.9.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^7.2.0",
|
||||
"express": "^4.17.1",
|
||||
"typeorm": "^0.2.25"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ppetzold/nestjs-paginate.git"
|
||||
},
|
||||
"homepage": "https://github.com/ppetzold/nestjs-paginate#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ppetzold/nestjs-paginate/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"release": {
|
||||
"branches": [
|
||||
"master"
|
||||
]
|
||||
}
|
||||
}
|
67
src/decorator.spec.ts
Normal file
67
src/decorator.spec.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'
|
||||
import { HttpArgumentsHost, CustomParamFactory, ExecutionContext } from '@nestjs/common/interfaces'
|
||||
import { Request } from 'express'
|
||||
import { Paginate, PaginateQuery } from './decorator'
|
||||
|
||||
function getParamDecoratorFactory<T>(decorator: Function): CustomParamFactory {
|
||||
class Test {
|
||||
public test(@decorator() _value: T): void {
|
||||
//
|
||||
}
|
||||
}
|
||||
const args = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test')
|
||||
return args[Object.keys(args)[0]].factory
|
||||
}
|
||||
const decoratorfactory = getParamDecoratorFactory<PaginateQuery>(Paginate)
|
||||
|
||||
function contextFactory(query: Request['query']): Partial<ExecutionContext> {
|
||||
const mockContext: Partial<ExecutionContext> = {
|
||||
switchToHttp: (): HttpArgumentsHost =>
|
||||
Object({
|
||||
getRequest: (): Partial<Request> =>
|
||||
Object({
|
||||
protocol: 'http',
|
||||
get: () => 'localhost',
|
||||
baseUrl: '/items',
|
||||
path: '/all',
|
||||
query: query,
|
||||
}),
|
||||
}),
|
||||
}
|
||||
return mockContext
|
||||
}
|
||||
|
||||
describe('Decorator', () => {
|
||||
it('should handle undefined query fields', () => {
|
||||
const context = contextFactory({})
|
||||
|
||||
const result: PaginateQuery = decoratorfactory(null, context)
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
page: undefined,
|
||||
limit: undefined,
|
||||
sortBy: undefined,
|
||||
path: 'http://localhost/items/all',
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle defined query fields', () => {
|
||||
const context = contextFactory({
|
||||
page: '1',
|
||||
limit: '20',
|
||||
sortBy: ['id:ASC', 'createdAt:DESC'],
|
||||
})
|
||||
|
||||
const result: PaginateQuery = decoratorfactory(null, context)
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
sortBy: [
|
||||
['id', 'ASC'],
|
||||
['createdAt', 'DESC'],
|
||||
],
|
||||
path: 'http://localhost/items/all',
|
||||
})
|
||||
})
|
||||
})
|
44
src/decorator.ts
Normal file
44
src/decorator.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
|
||||
import { Request } from 'express'
|
||||
|
||||
export interface PaginateQuery {
|
||||
page?: number
|
||||
limit?: number
|
||||
sortBy?: [string, string][]
|
||||
path: string
|
||||
}
|
||||
|
||||
export const Paginate = createParamDecorator(
|
||||
(_data: unknown, ctx: ExecutionContext): PaginateQuery => {
|
||||
const request: Request = ctx.switchToHttp().getRequest()
|
||||
const { query } = request
|
||||
const path = request.protocol + '://' + request.get('host') + request.baseUrl + request.path
|
||||
|
||||
function readParamAsArray(param: unknown): string[] {
|
||||
const result = typeof param === 'string' ? [param] : param
|
||||
if (Array.isArray(result) && result.every((value) => typeof value === 'string')) {
|
||||
return result
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
let sortBy: [string, string][] | undefined = undefined
|
||||
if (query.sortBy) {
|
||||
const params = readParamAsArray(query.sortBy)
|
||||
for (const param of params) {
|
||||
const items = param.split(':')
|
||||
if (items.length === 2) {
|
||||
if (!sortBy) sortBy = []
|
||||
sortBy.push(items as [string, string])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
page: query.page ? parseInt(query.page.toString(), 10) : undefined,
|
||||
limit: query.limit ? parseInt(query.limit.toString(), 10) : undefined,
|
||||
sortBy,
|
||||
path,
|
||||
}
|
||||
}
|
||||
)
|
2
src/index.ts
Normal file
2
src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './decorator'
|
||||
export * from './paginate'
|
110
src/paginate.spec.ts
Normal file
110
src/paginate.spec.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { createConnection, Repository } from 'typeorm'
|
||||
import { Paginated, paginate, PaginateConfig } from './paginate'
|
||||
import { PaginateQuery } from './decorator'
|
||||
import { Entity, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm'
|
||||
import { HttpException } from '@nestjs/common'
|
||||
|
||||
@Entity()
|
||||
export class CatEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
describe('paginate', () => {
|
||||
let repo: Repository<CatEntity>
|
||||
|
||||
beforeAll(async () => {
|
||||
const connection = await createConnection({
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
synchronize: true,
|
||||
logging: false,
|
||||
entities: [CatEntity],
|
||||
})
|
||||
repo = connection.getRepository(CatEntity)
|
||||
await repo.save([repo.create(), repo.create(), repo.create(), repo.create(), repo.create()])
|
||||
})
|
||||
|
||||
it('should return an instance of Paginated', async () => {
|
||||
const config: PaginateConfig<CatEntity> = {
|
||||
sortableColumns: ['id'],
|
||||
defaultSortBy: [['createdAt', 'DESC']], // Should fall back to id
|
||||
defaultLimit: 1,
|
||||
}
|
||||
const query: PaginateQuery = {
|
||||
path: '',
|
||||
page: 30, // will fallback to last available page
|
||||
limit: 2,
|
||||
sortBy: [['id', 'ASC']],
|
||||
}
|
||||
|
||||
const results = await paginate<CatEntity>(query, repo, config)
|
||||
|
||||
expect(results).toBeInstanceOf(Paginated)
|
||||
})
|
||||
|
||||
it('should default to index 0 of sortableColumns, when no other are given', async () => {
|
||||
const config: PaginateConfig<CatEntity> = {
|
||||
sortableColumns: ['id'],
|
||||
}
|
||||
const query: PaginateQuery = {
|
||||
path: '',
|
||||
page: 0,
|
||||
}
|
||||
|
||||
const results = await paginate<CatEntity>(query, repo, config)
|
||||
|
||||
expect(results).toBeInstanceOf(Paginated)
|
||||
})
|
||||
|
||||
it('should return correct links', async () => {
|
||||
const config: PaginateConfig<CatEntity> = {
|
||||
sortableColumns: ['id'],
|
||||
}
|
||||
const query: PaginateQuery = {
|
||||
path: '',
|
||||
page: 2,
|
||||
limit: 2,
|
||||
}
|
||||
|
||||
const { links } = await paginate<CatEntity>(query, repo, config)
|
||||
|
||||
expect(links.first).toBe('?page=1&limit=2&sortBy=id:ASC')
|
||||
expect(links.previous).toBe('?page=1&limit=2&sortBy=id:ASC')
|
||||
expect(links.current).toBe('?page=2&limit=2&sortBy=id:ASC')
|
||||
expect(links.next).toBe('?page=3&limit=2&sortBy=id:ASC')
|
||||
expect(links.last).toBe('?page=3&limit=2&sortBy=id:ASC')
|
||||
})
|
||||
|
||||
it('should default to defaultOrderby if query sortBy does not exist', async () => {
|
||||
const config: PaginateConfig<CatEntity> = {
|
||||
sortableColumns: ['id', 'createdAt'],
|
||||
defaultSortBy: [['createdAt', 'DESC']],
|
||||
}
|
||||
const query: PaginateQuery = {
|
||||
path: '',
|
||||
}
|
||||
|
||||
const results = await paginate<CatEntity>(query, repo, config)
|
||||
|
||||
expect(results.meta.sortBy).toStrictEqual([['createdAt', 'DESC']])
|
||||
})
|
||||
|
||||
it('should throw an error when no sortableColumns', async () => {
|
||||
const config: PaginateConfig<CatEntity> = {
|
||||
sortableColumns: [],
|
||||
}
|
||||
const query: PaginateQuery = {
|
||||
path: '',
|
||||
}
|
||||
|
||||
try {
|
||||
await paginate<CatEntity>(query, repo, config)
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(HttpException)
|
||||
}
|
||||
})
|
||||
})
|
116
src/paginate.ts
Normal file
116
src/paginate.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { Repository, FindConditions, SelectQueryBuilder } from 'typeorm'
|
||||
import { PaginateQuery } from './decorator'
|
||||
import { ServiceUnavailableException } from '@nestjs/common'
|
||||
|
||||
type Column<T> = Extract<keyof T, string>
|
||||
type Order<T> = [Column<T>, 'ASC' | 'DESC']
|
||||
type SortBy<T> = Order<T>[]
|
||||
|
||||
export class Paginated<T> {
|
||||
data: T[]
|
||||
meta: {
|
||||
itemsPerPage: number
|
||||
totalItems: number
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
sortBy: SortBy<T>
|
||||
}
|
||||
links: {
|
||||
first?: string
|
||||
previous?: string
|
||||
current: string
|
||||
next?: string
|
||||
last?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface PaginateConfig<T> {
|
||||
sortableColumns: Column<T>[]
|
||||
maxLimit?: number
|
||||
defaultSortBy?: SortBy<T>
|
||||
defaultLimit?: number
|
||||
where?: FindConditions<T>
|
||||
queryBuilder?: SelectQueryBuilder<T>
|
||||
}
|
||||
|
||||
export async function paginate<T>(
|
||||
query: PaginateQuery,
|
||||
repo: Repository<T> | SelectQueryBuilder<T>,
|
||||
config: PaginateConfig<T>
|
||||
): Promise<Paginated<T>> {
|
||||
let page = query.page || 1
|
||||
const limit = query.limit || config.defaultLimit || 20
|
||||
const path = query.path
|
||||
|
||||
function isEntityKey(sortableColumns: Column<T>[], column: string): column is Column<T> {
|
||||
return !!sortableColumns.find((c) => c === column)
|
||||
}
|
||||
|
||||
const { sortableColumns } = config
|
||||
if (config.sortableColumns.length < 1) throw new ServiceUnavailableException()
|
||||
|
||||
let sortBy: SortBy<T> = []
|
||||
if (query.sortBy) {
|
||||
for (const order of query.sortBy) {
|
||||
if (isEntityKey(sortableColumns, order[0]) && ['ASC', 'DESC'].includes(order[1])) {
|
||||
sortBy.push(order as Order<T>)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!sortBy.length) {
|
||||
sortBy = sortBy.concat(config.defaultSortBy || [[sortableColumns[0], 'ASC']])
|
||||
}
|
||||
|
||||
let [items, totalItems]: [T[], number] = [[], 0]
|
||||
|
||||
if (repo instanceof Repository) {
|
||||
const query = repo
|
||||
.createQueryBuilder('e')
|
||||
.take(limit)
|
||||
.skip((page - 1) * limit)
|
||||
|
||||
for (const order of sortBy) {
|
||||
query.addOrderBy('e.' + order[0], order[1])
|
||||
}
|
||||
|
||||
;[items, totalItems] = await query.where(config.where || {}).getManyAndCount()
|
||||
} else {
|
||||
const query = repo.take(limit).skip((page - 1) * limit)
|
||||
|
||||
for (const order of sortBy) {
|
||||
query.addOrderBy(repo.alias + '.' + order[0], order[1])
|
||||
}
|
||||
|
||||
;[items, totalItems] = await query.getManyAndCount()
|
||||
}
|
||||
|
||||
let totalPages = totalItems / limit
|
||||
if (totalItems % limit) totalPages = Math.ceil(totalPages)
|
||||
|
||||
if (page > totalPages) page = totalPages
|
||||
if (page < 1) page = 1
|
||||
|
||||
const options = `&limit=${limit}${sortBy.map((order) => `&sortBy=${order.join(':')}`).join('')}`
|
||||
|
||||
const buildLink = (p: number): string => path + '?page=' + p + options
|
||||
|
||||
const results: Paginated<T> = {
|
||||
data: items,
|
||||
meta: {
|
||||
itemsPerPage: limit,
|
||||
totalItems,
|
||||
currentPage: page,
|
||||
totalPages: totalPages,
|
||||
sortBy,
|
||||
},
|
||||
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<T>(), results)
|
||||
}
|
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2017",
|
||||
"sourceMap": true,
|
||||
"outDir": "./lib",
|
||||
"baseUrl": "./",
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/__test__/*"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user