Skip to content

Commit

Permalink
Add pagination improvements (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
foyarash authored Sep 28, 2021
1 parent d14eca9 commit c0a3918
Show file tree
Hide file tree
Showing 24 changed files with 553 additions and 1,253 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ node_modules/
.eslintcache
dist
coverage/
*.db
*.db
.DS_Store
24 changes: 24 additions & 0 deletions __tests__/adapters/prisma/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,30 @@ describe('Prisma interraction', () => {
expect(res.send).toHaveBeenCalledWith(expectedResult)
})

it('should get a page based paginated users list', async () => {
const { res } = getMockRes()
const req = getMockReq({
url: '/api/users?page=2&limit=2',
method: 'GET',
})

const expectedResult = await prisma.user.findMany({
skip: 2,
take: 2,
})

await handler(req, res)

expect(res.send).toHaveBeenCalledWith({
data: expectedResult,
pagination: {
total: 4,
pageCount: 2,
page: 2,
},
})
})

it('should get the user with first id', async () => {
const user = await prisma.user.findFirst()

Expand Down
57 changes: 54 additions & 3 deletions __tests__/handler.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
import { getMockReq, getMockRes } from '@jest-mock/express'
import * as http from 'http'
import NextCrud from '../src/handler'
import { IAdapter, IParsedQueryParams, RouteType } from '../src/types'
import {
IAdapter,
IParsedQueryParams,
RouteType,
TPaginationData,
} from '../src/types'
import HttpError from '../src/httpError'

class NoopAdapter implements IAdapter<unknown, unknown> {
async getPaginationData(
query: unknown,
lastElement?: unknown
): Promise<TPaginationData> {
return {
total: 1,
pageCount: 1,
page: 1,
}
}
parseQuery(query?: IParsedQueryParams): unknown {
return {}
}
async getAll(query?: unknown): Promise<unknown> {
return {}
async getAll(query?: unknown): Promise<unknown[]> {
return []
}
async getOne(resourceId: string | number, query?: unknown): Promise<unknown> {
return {}
Expand Down Expand Up @@ -553,4 +568,40 @@ describe('Handler', () => {
expect(res.status).toHaveBeenCalledWith(404)
})
})

describe('Pagination', () => {
it('should get page based pagination data', async () => {
const mockResources = [{ id: 1 }]
const getAll = jest.fn(() => {
return mockResources
})
const getPaginationData = jest.fn(() => {
return {
total: mockResources.length,
pageCount: 1,
}
})
const adapter = generateNoopAdapter({ getAll, getPaginationData })

const handler = NextCrud({
adapter,
resourceName: 'foo',
})

const { res } = getMockRes()
const req = getMockReq({
url: '/api/foo?page=1',
method: 'GET',
})

await handler(req, res)
expect(res.send).toHaveBeenCalledWith({
data: mockResources,
pagination: {
total: 1,
pageCount: 1,
},
})
})
})
})
77 changes: 76 additions & 1 deletion __tests__/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { getMockReq, getMockRes } from '@jest-mock/express'
import { RouteType } from '../src/types'
import { RouteType, TPaginationOptions } from '../src/types'
import {
applyPaginationOptions,
executeMiddlewares,
formatResourceId,
getPaginationOptions,
getRouteType,
GetRouteType,
isPrimitive,
Expand Down Expand Up @@ -232,6 +234,34 @@ describe('Middlewares', () => {
expect(fn1).toHaveBeenCalled()
expect(fn2).toHaveBeenCalled()
})

it('should run correctly an async middleware', async () => {
const fn1 = jest.fn(async (ctx, next) => {
await new Promise((resolve) => setTimeout(resolve, 200))
ctx.result = {
customKey: ctx.result,
}
next()
})
const fn2 = jest.fn()
const { res } = getMockRes()
const req = getMockReq({
url: '/api/foo/bar',
method: 'GET',
})

const result = {
data: 1,
}

await executeMiddlewares([fn1, fn2], { req, res, result })
expect(fn1).toHaveBeenCalled()
expect(fn2.mock.calls[0][0]).toEqual({
req,
res,
result: { customKey: result },
})
})
})

describe('Primitives', () => {
Expand Down Expand Up @@ -265,3 +295,48 @@ describe('Format resource', () => {
expect(formatResourceId('some-slug')).toBe('some-slug')
})
})

describe('Pagination options', () => {
it('should throw an error with non strictly positive page query param', () => {
expect(() =>
getPaginationOptions(
{
page: 0,
},
{
perPage: 30,
}
)
).toThrow('page query must be a strictly positive number')
})

it('should return a number page based pagination options object with perPage based on limit', () => {
expect(
getPaginationOptions(
{
page: 1,
limit: 50,
},
{
perPage: 30,
}
)
).toEqual<TPaginationOptions>({ page: 1, perPage: 50 })
})

it('should apply the page based pagination options in the query', () => {
const query = {}

const paginationOptions: TPaginationOptions = {
page: 1,
perPage: 10,
}

applyPaginationOptions(query, paginationOptions)

expect(query).toEqual({
skip: 0,
limit: 10,
})
})
})
1 change: 1 addition & 0 deletions docs/pages/api-docs/adapters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Out of the box, `next-crud` provides an adapter for [Prisma](https://www.prisma.
export interface IAdapter<T, Q = IParsedQueryParams> {
parseQuery(query?: IParsedQueryParams): Q
getAll(query?: Q): Promise<T> // GET /api/modelName
getPaginationData(query: Q): Promise<TPaginationData> // Used for pagination
getOne(resourceId: string | number, query?: Q): Promise<T> // GET /api/modelname/:id
create(data: any, query?: Q): Promise<T> // POST /api/modelName
update(resourceId: string | number, data: any, query?: Q): Promise<T> // PUT/PATCH /api/modelName/:id
Expand Down
20 changes: 20 additions & 0 deletions docs/pages/api-docs/options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,23 @@ interface ICustomHandler<T, Q> {
methods?: string[] // request methods, defaults to ['GET']
}
```

#### config

You can pass a config object with the following shape:

```typescript
interface IHandlerConfig {
pagination?: IPaginationConfig
}
```

##### pagination

The pagination config accepts the following shape:

```typescript
interface IPaginationConfig {
perPage: number // default number of elements to display on each page
}
```
1 change: 1 addition & 0 deletions docs/pages/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"index": "Next Crud",
"getting-started": "Getting started",
"query-params": "Query params",
"pagination": "Pagination",
"api-docs": "API"
}
47 changes: 47 additions & 0 deletions docs/pages/pagination.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Link from 'next/link'

# Pagination

Pagination can be achieved by passing down the `page` query param at least. It should be a strictly positive number.

Example:

`/api/users?page=10`

## Adapter

To make the pagination work with your adapter, it needs to implement the following method:

```typescript
getPaginationData(query: Q): Promise<TPaginationData>
```

where `query` is the result of the `parseQuery` function of your adapter. It needs to return a Promise with the following shape:

```typescript
type TPaginationDataPageBased = {
total: number // total number of elements in the dataset, independent of pages
pageCount: number // number of pages
page: number // current page
}

type TPaginationData = TPaginationDataPageBased
```
## Pages size
There are two ways to set a page size :
- pass a `limit` query param in the URL, eg: `/api/users?page=1&limit=10`
- add a pagination config to the <Link href="/api-docs/options#pagination">NextCrud config</Link>
---
The result of a page request is the following:
```typescript
type TPaginationResult<T> = {
data: T[] // Page dataset
pagination: TPaginationData
}
```
4 changes: 2 additions & 2 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
"@emotion/react": "^11.1.1",
"@emotion/styled": "^11.0.0",
"@hookform/resolvers": "^1.0.1",
"@premieroctet/next-crud": "file:../dist",
"@prisma/client": "^2.11.0",
"framer-motion": "^2.9.4",
"next": "latest",
"@premieroctet/next-crud": "latest",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-hook-form": "^6.11.5",
"swr": "^0.3.9",
"react-query": "^3.24.4",
"yup": "^0.31.0"
},
"devDependencies": {
Expand Down
11 changes: 8 additions & 3 deletions example/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import React from 'react'
import type { AppProps } from 'next/app'
import { ChakraProvider } from '@chakra-ui/react'
import { QueryClient, QueryClientProvider } from 'react-query'

const queryClient = new QueryClient()

const App = ({ Component, pageProps }: AppProps) => {
return (
<ChakraProvider resetCSS>
<Component {...pageProps} />
</ChakraProvider>
<QueryClientProvider client={queryClient}>
<ChakraProvider resetCSS>
<Component {...pageProps} />
</ChakraProvider>
</QueryClientProvider>
)
}

Expand Down
19 changes: 5 additions & 14 deletions example/pages/api/users/[[...users]].ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,6 @@ const handler = NextCrud({
onError: (req, res, error) => {
console.log('error during request', error)
},
middlewares: [
(ctx, next) => {
console.log('first middleware', ctx.result)
ctx.result = {
// @ts-ignore
myCustomKey: ctx.result,
}
next()
},
(ctx, next) => {
console.log('second middleware', ctx.result)
next()
},
],
customHandlers: [
{
path: '/(.*)/users/custom',
Expand All @@ -40,6 +26,11 @@ const handler = NextCrud({
},
},
],
config: {
pagination: {
perPage: 2,
},
},
})

export default handler
Loading

0 comments on commit c0a3918

Please sign in to comment.