Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core-api): integrate hapi-pagination to replace fork #2994

Merged
merged 7 commits into from
Oct 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/core-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"@hapi/joi": "^15.1.0",
"ajv": "^6.10.2",
"dayjs": "^1.8.15",
"hapi-pagination": "https://github.com/faustbrian/hapi-pagination#f4991348ca779b68b8e7139cfcbc601e6d496612",
"hapi-rate-limit": "^4.0.0",
"ip": "^1.1.5",
"lodash.groupby": "^4.6.0",
Expand Down
30 changes: 0 additions & 30 deletions packages/core-api/src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,36 +38,6 @@ export const defaults = {
// @see https://github.com/fknop/hapi-pagination
pagination: {
limit: 100,
include: [
"/api/blocks",
"/api/blocks/{id}/transactions",
"/api/blocks/search",
"/api/bridgechains",
"/api/bridgechains/search",
"/api/businesses",
"/api/businesses/{id}/bridgechains",
"/api/businesses/search",
"/api/delegates",
"/api/delegates/{id}/blocks",
"/api/delegates/{id}/voters",
"/api/delegates/search",
"/api/locks",
"/api/locks/search",
"/api/locks/unlocked",
"/api/peers",
"/api/transactions",
"/api/transactions/search",
"/api/transactions/unconfirmed",
"/api/votes",
"/api/wallets",
"/api/wallets/top",
"/api/wallets/{id}/locks",
"/api/wallets/{id}/transactions",
"/api/wallets/{id}/transactions/received",
"/api/wallets/{id}/transactions/sent",
"/api/wallets/{id}/votes",
"/api/wallets/search",
],
},
whitelist: ["*"],
plugins: [],
Expand Down
3 changes: 0 additions & 3 deletions packages/core-api/src/handlers/transactions/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ export const registerRoutes = (server: Hapi.Server): void => {
handler: controller.store,
options: {
plugins: {
pagination: {
enabled: false,
},
"hapi-ajv": {
payloadSchema: Schema.store,
},
Expand Down
18 changes: 18 additions & 0 deletions packages/core-api/src/plugins/pagination/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Based on https://github.com/fknop/hapi-pagination

import Joi from "@hapi/joi";

export const getConfig = options => {
const { error, value } = Joi.validate(options, {
query: Joi.object({
limit: Joi.object({
default: Joi.number()
.integer()
.positive()
.default(100),
}),
}),
});

return { error: error || undefined, config: error ? undefined : value };
};
37 changes: 37 additions & 0 deletions packages/core-api/src/plugins/pagination/decorate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Based on https://github.com/fknop/hapi-pagination

import { internal } from "@hapi/boom";

export const decorate = () => {
return {
paginate(response, totalCount, options) {
options = options || {};

const key = options.key;

if (Array.isArray(response) && key) {
throw internal("Object required with results key");
}

if (!Array.isArray(response) && !key) {
throw internal("Missing results key");
}

if (key && !response[key]) {
throw internal(`key: ${key} does not exists on response`);
}

const results = key ? response[key] : response;

if (key) {
delete response[key];
}

return this.response({
results,
totalCount,
response: Array.isArray(response) ? undefined : response,
});
},
};
};
157 changes: 157 additions & 0 deletions packages/core-api/src/plugins/pagination/ext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Based on https://github.com/fknop/hapi-pagination

import Hoek from "@hapi/hoek";
import { get } from "dottie";
import Qs from "querystring";

interface IRoute {
method: string;
path: string;
}

export class Ext {
private readonly routes: IRoute[] = [
{ method: "get", path: "/api/blocks" },
{ method: "get", path: "/api/blocks/{id}/transactions" },
{ method: "post", path: "/api/blocks/search" },
{ method: "get", path: "/api/bridgechains" },
{ method: "post", path: "/api/bridgechains/search" },
{ method: "get", path: "/api/businesses" },
{ method: "get", path: "/api/businesses/{id}/bridgechains" },
{ method: "post", path: "/api/businesses/search" },
{ method: "get", path: "/api/delegates" },
{ method: "get", path: "/api/delegates/{id}/blocks" },
{ method: "get", path: "/api/delegates/{id}/voters" },
{ method: "post", path: "/api/delegates/search" },
{ method: "get", path: "/api/locks" },
{ method: "post", path: "/api/locks/search" },
{ method: "post", path: "/api/locks/unlocked" },
{ method: "get", path: "/api/peers" },
{ method: "get", path: "/api/transactions" },
{ method: "post", path: "/api/transactions/search" },
{ method: "get", path: "/api/transactions/unconfirmed" },
{ method: "get", path: "/api/votes" },
{ method: "get", path: "/api/wallets" },
{ method: "get", path: "/api/wallets/top" },
{ method: "get", path: "/api/wallets/{id}/locks" },
{ method: "get", path: "/api/wallets/{id}/transactions" },
{ method: "get", path: "/api/wallets/{id}/transactions/received" },
{ method: "get", path: "/api/wallets/{id}/transactions/sent" },
{ method: "get", path: "/api/wallets/{id}/votes" },
{ method: "post", path: "/api/wallets/search" },
];

constructor(private readonly config) {}

public isValidRoute(request) {
if (!this.hasPagination(request)) {
return false;
}

const { method, path } = request.route;

return this.routes.find(route => route.method === method && route.path === path) !== undefined;
}

public onPreHandler(request, h) {
if (this.isValidRoute(request)) {
const setParam = (name, defaultValue) => {
let value;

if (request.query[name]) {
value = parseInt(request.query[name]);

if (Number.isNaN(value)) {
value = defaultValue;
}
}

request.query[name] = value || defaultValue;

return undefined;
};

setParam("page", 1);
setParam("limit", get(this.config, "query.limit.default", 100));
}

return h.continue;
}

public onPostHandler(request, h) {
const { statusCode } = request.response;
const processResponse: boolean =
this.isValidRoute(request) && statusCode >= 200 && statusCode <= 299 && this.hasPagination(request);

if (!processResponse) {
return h.continue;
}

const { source } = request.response;
const results = Array.isArray(source) ? source : source.results;

Hoek.assert(Array.isArray(results), "The results must be an array");

const baseUri = request.url.pathname + "?";
const { query } = request;
const currentPage = query.page;
const currentLimit = query.limit;

const { totalCount } = !!source.totalCount ? source : request;

let pageCount: number;
if (totalCount) {
pageCount = Math.trunc(totalCount / currentLimit) + (totalCount % currentLimit === 0 ? 0 : 1);
}

const getUri = (page: number | null): string =>
// tslint:disable-next-line: no-null-keyword
page ? baseUri + Qs.stringify(Hoek.applyToDefaults({ ...query, ...request.orig.query }, { page })) : null;

const newSource = {
meta: {
...(source.meta || {}),
...{
count: results.length,
pageCount: pageCount || 1,
totalCount: totalCount ? totalCount : 0,

// tslint:disable-next-line: no-null-keyword
next: totalCount && currentPage < pageCount ? getUri(currentPage + 1) : null,
previous:
// tslint:disable-next-line: no-null-keyword
totalCount && currentPage > 1 && currentPage <= pageCount + 1 ? getUri(currentPage - 1) : null,

self: getUri(currentPage),
first: getUri(1),
last: getUri(pageCount),
},
},
data: results,
};

if (source.response) {
const keys = Object.keys(source.response);

for (const key of keys) {
if (key !== "meta" && key !== "data") {
newSource[key] = source.response[key];
}
}
}

request.response.source = newSource;

return h.continue;
}

public hasPagination(request) {
const routeOptions = this.getRouteOptions(request);

return Object.prototype.hasOwnProperty.call(routeOptions, "pagination") ? routeOptions.pagination : true;
}

private getRouteOptions(request) {
return request.route.settings.plugins.pagination || {};
}
}
28 changes: 28 additions & 0 deletions packages/core-api/src/plugins/pagination/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Based on https://github.com/fknop/hapi-pagination

import { getConfig } from "./config";
import { decorate } from "./decorate";
import { Ext } from "./ext";

exports.plugin = {
name: "hapi-pagination",
version: "1.0.0",
register(server, options) {
const { error, config } = getConfig(options);

if (error) {
throw error;
}

try {
server.decorate("toolkit", "paginate", decorate().paginate);
} catch {
//
}

const ext = new Ext(config);

server.ext("onPreHandler", (request, h) => ext.onPreHandler(request, h));
server.ext("onPostHandler", (request, h) => ext.onPostHandler(request, h));
},
};
15 changes: 3 additions & 12 deletions packages/core-api/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { app } from "@arkecosystem/core-container";
import { createServer, mountServer, plugins } from "@arkecosystem/core-http-utils";
import { Logger } from "@arkecosystem/core-interfaces";
import Hapi from "@hapi/hapi";
import { get } from "dottie";

export class Server {
private logger = app.resolvePlugin<Logger.ILogger>("logger");
Expand Down Expand Up @@ -90,23 +91,13 @@ export class Server {
});

await server.register({
plugin: require("hapi-pagination"),
plugin: require("./plugins/pagination"),
options: {
meta: {
baseUri: "",
},
query: {
limit: {
default: this.config.pagination.limit,
default: get(this.config, "pagination.limit", 100),
},
},
results: {
name: "data",
},
routes: {
include: this.config.pagination.include,
exclude: ["*"],
},
},
});

Expand Down
Loading