Skip to content

Commit

Permalink
refactor(core-api): pagination configuration through joi context (#3955)
Browse files Browse the repository at this point in the history
  • Loading branch information
rainydio authored Aug 13, 2020
1 parent 39894a4 commit 8ebf821
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 151 deletions.
2 changes: 1 addition & 1 deletion __tests__/integration/core-api/handlers/wallets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ describe("API 2.0 - Wallets", () => {

it("should fail to GET a wallet by the given invalid identifier", async () => {
for (const value of invalidIdentifiers) {
api.expectError(await api.request("GET", `wallets/${value}`), 400);
api.expectError(await api.request("GET", `wallets/${value}`), 422);
}
});

Expand Down
229 changes: 102 additions & 127 deletions packages/core-api/src/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,138 +1,113 @@
import Joi from "@hapi/joi";

type SchemaSettings = {
pagination: {
limit: number;
};
export const pagination = {
page: Joi.number().integer().positive().default(1),
offset: Joi.number().integer().min(0),
limit: Joi.number().integer().min(1).default(100).max(Joi.ref("$configuration.plugins.pagination.limit")),
};

export const createSchemas = (settings: SchemaSettings) => {
const pagination = {
page: Joi.number().integer().positive().default(1),
offset: Joi.number().integer().min(0),
limit: Joi.number().integer().min(1).default(100).max(settings.pagination.limit),
};

const blockId = Joi.alternatives().try(
Joi.string()
.min(1)
.max(20)
.regex(/^[0-9]+$/, "decimal non-negative integer"),
Joi.string().length(64).hex(),
);

const address = Joi.string().alphanum().length(34);

const delegateIdentifier = Joi.string()
.regex(/^[a-zA-Z0-9!@$&_.]+$/)
.min(1)
.max(66);

const username = Joi.string()
.regex(/^[a-z0-9!@$&_.]+$/)
export const blockId = Joi.alternatives().try(
Joi.string()
.min(1)
.max(20);

const integerBetween = Joi.object().keys({
.max(20)
.regex(/^[0-9]+$/, "decimal non-negative integer"),
Joi.string().length(64).hex(),
);

export const address = Joi.string().alphanum().length(34);

export const delegateIdentifier = Joi.string()
.regex(/^[a-zA-Z0-9!@$&_.]+$/)
.min(1)
.max(66);

export const username = Joi.string()
.regex(/^[a-z0-9!@$&_.]+$/)
.min(1)
.max(20);

export const integerBetween = Joi.object().keys({
from: Joi.number().integer().min(0),
to: Joi.number().integer().min(0),
});

export const percentage = Joi.object().keys({
from: Joi.number().precision(2).min(0).max(100),
to: Joi.number().precision(2).min(0).max(100),
});

export const numberFixedOrBetween = Joi.alternatives().try(
Joi.number().integer().min(0),
Joi.object().keys({
from: Joi.number().integer().min(0),
to: Joi.number().integer().min(0),
});

const percentage = Joi.object().keys({
from: Joi.number().precision(2).min(0).max(100),
to: Joi.number().precision(2).min(0).max(100),
});

const numberFixedOrBetween = Joi.alternatives().try(
Joi.number().integer().min(0),
Joi.object().keys({
from: Joi.number().integer().min(0),
to: Joi.number().integer().min(0),
}),
);

const walletId = Joi.alternatives().try(
Joi.string()
.regex(/^[a-z0-9!@$&_.]+$/)
.min(1)
.max(20),
Joi.string().alphanum().length(34),
Joi.string().hex().length(66),
);
}),
);

const orderBy = Joi.string().regex(
/^[a-z._]{1,40}:(asc|desc)$/i,
"orderBy query parameter (<iteratee>:<direction>)",
export const walletId = Joi.alternatives().try(
Joi.string()
.regex(/^[a-z0-9!@$&_.]+$/)
.min(1)
.max(20),
Joi.string().alphanum().length(34),
Joi.string().hex().length(66),
);

export const orderBy = Joi.string().regex(
/^[a-z._]{1,40}:(asc|desc)$/i,
"orderBy query parameter (<iteratee>:<direction>)",
);

export const blocksOrderBy = orderBy.default("height:desc");
export const transactionsOrderBy = orderBy.default("timestamp:desc,sequence:desc");

const equalCriteria = (value: any) => value;
const numericCriteria = (value: any) =>
Joi.alternatives().try(
value,
Joi.object().keys({ from: value }),
Joi.object().keys({ to: value }),
Joi.object().keys({ from: value, to: value }),
);
const likeCriteria = (value: any) => value;
const containsCriteria = (value: any) => value;
const orCriteria = (criteria: any) => Joi.alternatives().try(criteria, Joi.array().items(criteria));
const orEqualCriteria = (value: any) => orCriteria(equalCriteria(value));
const orNumericCriteria = (value: any) => orCriteria(numericCriteria(value));
const orLikeCriteria = (value: any) => orCriteria(likeCriteria(value));
const orContainsCriteria = (value: any) => orCriteria(containsCriteria(value));

export const blockCriteriaSchemas = {
id: orEqualCriteria(blockId),
version: orEqualCriteria(Joi.number().integer().min(0)),
timestamp: orNumericCriteria(Joi.number().integer().min(0)),
previousBlock: orEqualCriteria(blockId),
height: orNumericCriteria(Joi.number().integer().min(0)),
numberOfTransactions: orNumericCriteria(Joi.number().integer().min(0)),
totalAmount: orNumericCriteria(Joi.number().integer().min(0)),
totalFee: orNumericCriteria(Joi.number().integer().min(0)),
reward: orNumericCriteria(Joi.number().integer().min(0)),
payloadLength: orNumericCriteria(Joi.number().integer().min(0)),
payloadHash: orEqualCriteria(Joi.string().hex()),
generatorPublicKey: orEqualCriteria(Joi.string().hex().length(66)),
blockSignature: orEqualCriteria(Joi.string().hex()),
};

const blocksOrderBy = orderBy.default("height:desc");
const transactionsOrderBy = orderBy.default("timestamp:desc,sequence:desc");

const equalCriteria = (value: any) => value;
const numericCriteria = (value: any) =>
Joi.alternatives().try(
value,
Joi.object().keys({ from: value }),
Joi.object().keys({ to: value }),
Joi.object().keys({ from: value, to: value }),
);
const likeCriteria = (value: any) => value;
const containsCriteria = (value: any) => value;
const orCriteria = (criteria: any) => Joi.alternatives().try(criteria, Joi.array().items(criteria));
const orEqualCriteria = (value: any) => orCriteria(equalCriteria(value));
const orNumericCriteria = (value: any) => orCriteria(numericCriteria(value));
const orLikeCriteria = (value: any) => orCriteria(likeCriteria(value));
const orContainsCriteria = (value: any) => orCriteria(containsCriteria(value));

const blockCriteriaSchemas = {
id: orEqualCriteria(blockId),
version: orEqualCriteria(Joi.number().integer().min(0)),
timestamp: orNumericCriteria(Joi.number().integer().min(0)),
previousBlock: orEqualCriteria(blockId),
height: orNumericCriteria(Joi.number().integer().min(0)),
numberOfTransactions: orNumericCriteria(Joi.number().integer().min(0)),
totalAmount: orNumericCriteria(Joi.number().integer().min(0)),
totalFee: orNumericCriteria(Joi.number().integer().min(0)),
reward: orNumericCriteria(Joi.number().integer().min(0)),
payloadLength: orNumericCriteria(Joi.number().integer().min(0)),
payloadHash: orEqualCriteria(Joi.string().hex()),
generatorPublicKey: orEqualCriteria(Joi.string().hex().length(66)),
blockSignature: orEqualCriteria(Joi.string().hex()),
};

const transactionCriteriaSchemas = {
address: orEqualCriteria(address),
senderId: orEqualCriteria(address),
recipientId: orEqualCriteria(address),
id: orEqualCriteria(Joi.string().hex().length(64)),
version: orEqualCriteria(Joi.number().integer().positive()),
blockId: orEqualCriteria(blockId),
sequence: orNumericCriteria(Joi.number().integer().positive()),
timestamp: orNumericCriteria(Joi.number().integer().min(0)),
nonce: orNumericCriteria(Joi.number().integer().positive()),
senderPublicKey: orEqualCriteria(Joi.string().hex().length(66)),
type: orEqualCriteria(Joi.number().integer().min(0)),
typeGroup: orEqualCriteria(Joi.number().integer().min(0)),
vendorField: orLikeCriteria(Joi.string().max(255, "utf8")),
amount: orNumericCriteria(Joi.number().integer().min(0)),
fee: orNumericCriteria(Joi.number().integer().min(0)),
asset: orContainsCriteria(Joi.object()),
};

return {
pagination,
blockId,
address,
delegateIdentifier,
username,
integerBetween,
percentage,
numberFixedOrBetween,
walletId,
orderBy,
blocksOrderBy,
transactionsOrderBy,
blockCriteriaSchemas,
transactionCriteriaSchemas,
};
export const transactionCriteriaSchemas = {
address: orEqualCriteria(address),
senderId: orEqualCriteria(address),
recipientId: orEqualCriteria(address),
id: orEqualCriteria(Joi.string().hex().length(64)),
version: orEqualCriteria(Joi.number().integer().positive()),
blockId: orEqualCriteria(blockId),
sequence: orNumericCriteria(Joi.number().integer().positive()),
timestamp: orNumericCriteria(Joi.number().integer().min(0)),
nonce: orNumericCriteria(Joi.number().integer().positive()),
senderPublicKey: orEqualCriteria(Joi.string().hex().length(66)),
type: orEqualCriteria(Joi.number().integer().min(0)),
typeGroup: orEqualCriteria(Joi.number().integer().min(0)),
vendorField: orLikeCriteria(Joi.string().max(255, "utf8")),
amount: orNumericCriteria(Joi.number().integer().min(0)),
fee: orNumericCriteria(Joi.number().integer().min(0)),
asset: orContainsCriteria(Joi.object()),
};
55 changes: 32 additions & 23 deletions packages/core-api/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Container, Contracts, Providers, Types } from "@arkecosystem/core-kernel";
import { Container, Contracts, Providers, Types, Utils } from "@arkecosystem/core-kernel";
import { badData } from "@hapi/boom";
import { Server as HapiServer, ServerInjectOptions, ServerInjectResponse, ServerRoute } from "@hapi/hapi";
import { readFileSync } from "fs";

import { createSchemas } from "./schemas";
import * as Schemas from "./schemas";

// todo: review the implementation
@Container.injectable()
Expand Down Expand Up @@ -63,11 +63,7 @@ export class Server {
this.server.listener.headersTimeout = timeout;

this.server.app.app = this.app;
this.server.app.schemas = createSchemas({
pagination: {
limit: this.configuration.getRequired<number>("plugins.pagination.limit"),
},
});
this.server.app.schemas = Schemas;

this.server.ext("onPreHandler", (request, h) => {
request.headers["content-type"] = "application/json";
Expand Down Expand Up @@ -166,27 +162,40 @@ export class Server {
options.tls.cert = readFileSync(options.tls.cert).toString();
}

return {
...{
router: {
stripTrailingSlash: true,
const validateContext = {
configuration: {
plugins: {
pagination: {
limit: this.configuration.getRequired<number>("plugins.pagination.limit"),
},
},
routes: {
payload: {
/* istanbul ignore next */
async failAction(request, h, err) {
return badData(err.message);
},
},
};

const defaultOptions = {
router: {
stripTrailingSlash: true,
},
routes: {
payload: {
/* istanbul ignore next */
async failAction(request, h, err) {
return badData(err.message);
},
validate: {
/* istanbul ignore next */
async failAction(request, h, err) {
return badData(err.message);
},
},
validate: {
options: {
context: validateContext,
},

/* istanbul ignore next */
async failAction(request, h, err) {
return badData(err.message);
},
},
},
...options,
};

return Utils.merge(defaultOptions, options);
}
}

0 comments on commit 8ebf821

Please sign in to comment.