Skip to content

Instantly share code, notes, and snippets.

@Romakita
Last active December 4, 2020 07:51
Show Gist options
  • Save Romakita/2dd599b6905a08ab0301414599c3858a to your computer and use it in GitHub Desktop.
Save Romakita/2dd599b6905a08ab0301414599c3858a to your computer and use it in GitHub Desktop.
Pagination example with Ts.ED
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
});
});
});
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;
}
}
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);
}
}
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;
}
}
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);
}
}
@Romakita
Copy link
Author

Ok the description doesn’t appear on the right level. I’ll fix it tommorrow ;). My bad...

@arturrozputnii
Copy link

yep, saw the same. Many thx

@arturrozputnii
Copy link

Screenshot 2020-11-24 at 10 08 08

AWESOME, thx. I am done with pagination/sorting now. I hope :)

@Romakita
Copy link
Author

Great :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment