Last active
December 4, 2020 07:51
-
-
Save Romakita/2dd599b6905a08ab0301414599c3858a to your computer and use it in GitHub Desktop.
Pagination example with Ts.ED
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Controller, Get, PlatformTest, QueryParams, PlatformTest} from "@tsed/common"; | |
import { | |
getSpec, | |
Returns, | |
SpecTypes | |
} from "@tsed/schema"; | |
import {expect} from "chai"; | |
import * as qs from "querystring"; | |
import * as SuperTest from "supertest"; | |
import {PlatformExpress} from "@tsed/platform-express"; | |
import {Server} from "./app/Server"; | |
import {Pagination} from "./app/models/Pagination"; | |
import {Product} from "./app/models/Product"; | |
import {PaginationFilter} from "./app/filters/PaginationFilter"; | |
@Controller("/pageable") | |
class TestPageableCtrl { | |
@Get("/") | |
@Returns(206, Pagination).Of(Product).Title("PaginatedProduct") | |
@Returns(200, Pagination).Of(Product).Title("PaginatedProduct") | |
async get(@QueryParams() pageableOptions: Pageable, @QueryParams("all") all: boolean) { | |
return new Pagination<Product>({ | |
data: [new Product({ | |
id: "100", | |
title: "CANON D3000" | |
})], | |
totalCount: all ? 1 : 100, // just for test, | |
pageable: pageableOptions | |
}); | |
} | |
} | |
describe("Pageable", () => { | |
let request: SuperTest.SuperTest<SuperTest.Test>; | |
before( | |
PlatformTest.bootstrap(Server, { | |
mount: { | |
"/rest": [TestPageableCtrl] | |
}, | |
responseFilters: [ | |
PaginationFilter | |
] | |
}) | |
); | |
after(utils.reset); | |
before(() => { | |
request = SuperTest(PlatformTest.callback()); | |
}); | |
it("should generate spec", () => { | |
const spec = getSpec(TestPageableCtrl, {specType: SpecTypes.OPENAPI}); | |
expect(spec).to.deep.eq({ | |
"paths": { | |
"/pageable": { | |
"get": { | |
"operationId": "testPageableCtrlGet", | |
"parameters": [{ | |
"in": "query", | |
"required": false, | |
"name": "page", | |
"schema": {"type": "integer", "description": "Page number.", "default": 0, "minimum": 0, "multipleOf": 1} | |
}, { | |
"in": "query", | |
"required": false, | |
"name": "size", | |
"schema": { | |
"type": "integer", | |
"description": "Number of objects per page.", | |
"default": 20, | |
"minimum": 1, | |
"multipleOf": 1 | |
} | |
}, { | |
"in": "query", | |
"required": false, | |
"name": "sort", | |
"schema": { | |
"type": "array", | |
"description": "Sorting criteria: property(,asc|desc). Default sort order is ascending. Multiple sort criteria are supported.", | |
"maxItems": 2, | |
"items": {"type": "string"} | |
} | |
}, {"in": "query", "name": "all", "required": false, "schema": {"type": "boolean"}}], | |
"responses": { | |
"200": { | |
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaginatedProduct"}}}, | |
"description": "Success" | |
}, | |
"206": { | |
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaginatedProduct"}}}, | |
"description": "Partial Content" | |
} | |
}, | |
"tags": ["TestPageableCtrl"] | |
} | |
} | |
}, | |
"tags": [{"name": "TestPageableCtrl"}], | |
"components": { | |
"schemas": { | |
"Product": { | |
"type": "object", | |
"properties": {"id": {"type": "string"}, "title": {"type": "string"}} | |
}, | |
"PaginatedProduct": { | |
"type": "object", | |
"properties": { | |
"page": { | |
"type": "integer", | |
"description": "Page number.", | |
"default": 0, | |
"minimum": 0, | |
"multipleOf": 1 | |
}, | |
"size": { | |
"type": "integer", | |
"description": "Number of objects per page.", | |
"default": 20, | |
"minimum": 1, | |
"multipleOf": 1 | |
}, | |
"sort": { | |
"type": "array", | |
"description": "Sorting criteria: property(,asc|desc). Default sort order is ascending. Multiple sort criteria are supported.", | |
"maxItems": 2, | |
"items": {"type": "string"} | |
}, | |
"data": {"type": "array", "items": {"$ref": "#/components/schemas/Product"}}, | |
"totalCount": {"type": "integer", "minLength": 0, "multipleOf": 1} | |
} | |
} | |
} | |
} | |
}); | |
}); | |
it("should get paginated products with a status 206 Partial Content", async () => { | |
const options = { | |
page: 1, | |
size: 10, | |
sort: ["field", "asc"] | |
}; | |
const {body} = await request.get("/rest/pageable?" + qs.stringify(options)).expect(206); | |
expect(body).to.deep.eq({ | |
"data": [ | |
{ | |
"id": "100", | |
"title": "CANON D3000" | |
} | |
], | |
"page": 1, | |
"size": 10, | |
"sort": [ | |
"asc", | |
"field" | |
], | |
"totalCount": 100 | |
}); | |
}); | |
it("should get all products with a status 216", async () => { | |
const options = { | |
all: true | |
}; | |
const {body} = await request.get("/rest/pageable?" + qs.stringify(options)).expect(200); | |
expect(body).to.deep.eq({ | |
data: [ { id: '100', title: 'CANON D3000' } ], | |
totalCount: 1, | |
page: 0, | |
size: 20 | |
}); | |
}); | |
it("should not return a bad request when sort is not given", async () => { | |
const options = { | |
page: 1, | |
size: 10 | |
}; | |
const {body} = await request.get("/rest/pageable?" + qs.stringify(options)).expect(216); | |
expect(body).to.deep.eq({ | |
"data": [ | |
{ | |
"id": "100", | |
"title": "CANON D3000" | |
} | |
], | |
"page": 1, | |
"size": 10, | |
"totalCount": 100 | |
}); | |
}); | |
it("should apply default pagination parameters", async () => { | |
const options = {}; | |
const {body} = await request.get("/rest/pageable?" + qs.stringify(options)).expect(216); | |
expect(body).to.deep.eq({ | |
data: [{id: "100", title: "CANON D3000"}], | |
totalCount: 100, | |
page: 0, | |
size: 20 | |
}); | |
}); | |
it("should throw bad request when options isn\'t correct", async () => { | |
const options = { | |
page: -1 | |
}; | |
const {body} = await request.get("/rest/pageable?" + qs.stringify(options)).expect(400); | |
expect(body).to.deep.eq({ | |
"errors": [ | |
{ | |
"data": -1, | |
"dataPath": ".page", | |
"keyword": "minimum", | |
"message": "should be >= 0", | |
"modelName": "Pageable", | |
"params": { | |
"comparison": ">=", | |
"exclusive": false, | |
"limit": 0 | |
}, | |
"schemaPath": "#/properties/page/minimum" | |
} | |
], | |
"message": "Bad request on parameter \"request.query\".\nPageable.page should be >= 0. Given value: -1", | |
"name": "AJV_VALIDATION_ERROR", | |
"status": 400 | |
}); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
CollectionOf, | |
Default, | |
Description, | |
Integer, | |
MaxItems, | |
Min, | |
MinLength, | |
Property, | |
Required, | |
For, | |
SpecTypes, | |
oneOf, | |
string, | |
array | |
} from "@tsed/schema"; | |
import {OnDeserialize} from "@tsed/json-mapper"; | |
import {isArray} from "@tsed/core"; | |
class Pageable { | |
@Integer() | |
@Min(0) | |
@Default(0) | |
@Description("Page number.") | |
page: number = 0; | |
@Integer() | |
@Min(1) | |
@Default(20) | |
@Description("Number of objects per page.") | |
size: number = 20; | |
@For(SpecTypes.JSON, oneOf(string(), array().items(string()).maxItems(2))) // v6.11.0 | |
@For(SpecTypes.OPENAPI, array().items(string()).maxItems(2)) | |
@For(SpecTypes.SWAGGER, array().items(string()).maxItems(2)) | |
@OnDeserialize((value: string | string[]) => isString(value) ? value.split(",") : value) | |
@Description("Sorting criteria: property(,asc|desc). Default sort order is ascending. Multiple sort criteria are supported.") | |
sort: string | string[]; | |
constructor(options: Partial<Pageable>) { | |
options.page && (this.page = options.page); | |
options.size && (this.size = options.size); | |
options.sort && (this.sort = options.sort); | |
} | |
get offset() { | |
return this.page ? this.page * this.limit : 0; | |
} | |
get limit() { | |
return this.size; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
CollectionOf, | |
Default, | |
Generics, | |
Integer, | |
MaxItems, | |
Min, | |
MinLength, | |
Property, | |
Required | |
} from "@tsed/schema"; | |
import { Pageable } from "./Pageable"; | |
@Generics("T") | |
class Pagination<T> extends Pageable { | |
@CollectionOf("T") | |
data: T[]; | |
@Integer() | |
@MinLength(0) | |
@Default(0) | |
totalCount: number = 0; | |
constructor({data, totalCount, pageable}: Partial<Pagination<T>> & { pageable: Pageable }) { | |
super(pageable); | |
data && (this.data = data); | |
totalCount && (this.totalCount = totalCount); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ResponseFilter, ResponseFilterMethods, PlatformContext } from "@tsed/common"; | |
@ResponseFilter("application/json") | |
class PaginationFilter implements ResponseFilterMethods { | |
transform(data: unknown, ctx: PlatformContext): any { | |
if (ctx.data instanceof Pagination) {// get the unserialized data | |
if (ctx.data.isPaginated) { | |
ctx.response.status(206); | |
} | |
} | |
return data; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Property } from "@tsed/schema"; | |
class Product { | |
@Property() | |
id: string; | |
@Property() | |
title: string; | |
constructor({id, title}: Partial<Product> = {}) { | |
id && (this.id = id); | |
title && (this.title = title); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
AWESOME, thx. I am done with pagination/sorting now. I hope :)