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

feat(core-api): search delegates by usernames #2143

Merged
merged 11 commits into from
Feb 26, 2019
20 changes: 20 additions & 0 deletions packages/core-api/__tests__/v2/handlers/delegates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ const delegate = {
publicKey: "0377f81a18d25d77b100cb17e829a72259f08334d064f6c887298917a04df8f647",
};

const delegate2 = {
username: "genesis_10",
address: "AFyf2qVpX2JbpKcy29XbusedCpFDeYFX8Q",
publicKey: "02f7acb179ddfddb2e220aa600921574646ac59fd3f1ae6255ada40b9a7fab75fd",
};

beforeAll(async () => {
await setUp();
await calculateRanks();
Expand Down Expand Up @@ -172,6 +178,20 @@ describe("API 2.0 - Delegates", () => {

utils.expectDelegate(response.data.data[0], delegate);
});

it("should POST a search for delegates with any of the specified usernames", async () => {
const response = await utils[request]("POST", "delegates/search", {
usernames: [delegate.username, delegate2.username],
});
expect(response).toBeSuccessfulResponse();
expect(response.data.data).toBeArray();

expect(response.data.data).toHaveLength(2);

for (const delegate of response.data.data) {
utils.expectDelegate(delegate);
}
});
},
);
});
Expand Down
15 changes: 14 additions & 1 deletion packages/core-api/src/versions/2/delegates/schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import * as Joi from "joi";
import { app } from "@arkecosystem/core-container";
import { pagination } from "../shared/schemas/pagination";

const config = app.getConfig();

const schemaIdentifier = Joi.string()
.regex(/^[a-zA-Z0-9!@$&_.]+$/)
.min(1)
Expand Down Expand Up @@ -52,9 +55,19 @@ export const show: object = {
};

export const search: object = {
query: pagination,
query: {
...pagination,
...{
orderBy: Joi.string(),
},
},
payload: {
username: schemaUsername,
usernames: Joi.array()
.unique()
.min(1)
.max(config.getMilestone().activeDelegates)
.items(schemaUsername),
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,114 @@ describe("Delegate Repository", () => {
});
});

describe("by `usernames`", () => {
it("should search by exact match", () => {
const usernames = [
"username-APnhwwyTbMiykJwYbGhYjNgtHiVJDSEhSn",
"username-AG8kwwk4TsYfA2HdwaWBVAJQBj6VhdcpMo",
"username-AHXtmB84sTZ9Zd35h9Y1vfFvPE2Xzqj8ri"
];
const { count, rows } = repository.search({ usernames });

expect(count).toBe(3);
expect(rows).toHaveLength(3);

rows.forEach(row => {
expect(usernames.includes(row.username)).toBeTrue();
});
});

describe('when a username is "undefined"', () => {
it("should return it", () => {
// Index a wallet with username "undefined"
walletManager.allByAddress()[0].username = "undefined";

const usernames = ["undefined"];
const { count, rows } = repository.search({ usernames });

expect(count).toBe(1);
expect(rows).toHaveLength(1);
expect(rows[0].username).toEqual(usernames[0]);
});
});

describe("when the username does not exist", () => {
it("should return no results", () => {
const { count, rows } = repository.search({
usernames: ["unknown-dummy-username"],
});

expect(count).toBe(0);
expect(rows).toHaveLength(0);
});
});

it("should be ok with params", () => {
const { count, rows } = repository.search({
usernames: [
"username-APnhwwyTbMiykJwYbGhYjNgtHiVJDSEhSn",
"username-AG8kwwk4TsYfA2HdwaWBVAJQBj6VhdcpMo"
],
offset: 1,
limit: 10,
});
expect(count).toBe(2);
expect(rows).toHaveLength(1);
});

it("should be ok with params (no offset)", () => {
const { count, rows } = repository.search({
usernames: [
"username-APnhwwyTbMiykJwYbGhYjNgtHiVJDSEhSn",
"username-AG8kwwk4TsYfA2HdwaWBVAJQBj6VhdcpMo"
],
limit: 1,
});
expect(count).toBe(2);
expect(rows).toHaveLength(1);
});

it("should be ok with params (offset = 0)", () => {
const { count, rows } = repository.search({
usernames: [
"username-APnhwwyTbMiykJwYbGhYjNgtHiVJDSEhSn",
"username-AG8kwwk4TsYfA2HdwaWBVAJQBj6VhdcpMo"
],
offset: 0,
limit: 2,
});
expect(count).toBe(2);
expect(rows).toHaveLength(2);
});

it("should be ok with params (no limit)", () => {
const { count, rows } = repository.search({
usernames: [
"username-APnhwwyTbMiykJwYbGhYjNgtHiVJDSEhSn",
"username-AG8kwwk4TsYfA2HdwaWBVAJQBj6VhdcpMo"
],
offset: 1,
});
expect(count).toBe(2);
expect(rows).toHaveLength(1);
});
});

describe("when searching by `username` and `usernames`", () => {
it("should search delegates only by `username`", () => {
const username = "username-APnhwwyTbMiykJwYbGhYjNgtHiVJDSEhSn";
const usernames = [
"username-AG8kwwk4TsYfA2HdwaWBVAJQBj6VhdcpMo",
"username-AHXtmB84sTZ9Zd35h9Y1vfFvPE2Xzqj8ri"
]

const { count, rows } = repository.search({ username, usernames });

expect(count).toBe(1);
expect(rows).toHaveLength(1);
});
});

describe("when searching without params", () => {
it("should return all results", () => {
const { count, rows } = repository.search({});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Database } from "@arkecosystem/core-interfaces";
import { delegateCalculator } from "@arkecosystem/core-utils";
import { orderBy } from "@arkecosystem/utils";
import filterRows from "./utils/filter-rows";
import limitRows from "./utils/limit-rows";
import { sortEntries } from "./utils/sort-entries";

Expand Down Expand Up @@ -42,29 +43,26 @@ export class DelegatesBusinessRepository implements Database.IDelegatesBusinessR
* TODO Currently it searches by username only
* @param {Object} [params]
* @param {String} [params.username] - Search by username
* @param {Array} [params.usernames] - Search by usernames
*/
public search(params: Database.IParameters) {
let delegates = this.getLocalDelegates();
if (params.hasOwnProperty("username")) {
delegates = delegates.filter(delegate => delegate.username.indexOf(params.username as string) > -1);
}

if (params.orderBy) {
const orderByField = params.orderBy.split(":")[0];
const orderByDirection = params.orderBy.split(":")[1] || "desc";
const query: any = {
like: ["username"],
};

delegates = delegates.sort((a, b) => {
if (orderByDirection === "desc" && a[orderByField] < b[orderByField]) {
return -1;
}
if (params.usernames) {
if (!params.username) {
params.username = params.usernames;
query.like.shift();
query.in = ["username"];
}
delete params.usernames;
}

if (orderByDirection === "asc" && a[orderByField] > b[orderByField]) {
return 1;
}
this.applyOrder(params);

return 0;
});
}
let delegates = filterRows(this.getLocalDelegates(), params, query);
delegates = sortEntries(params, delegates, ["rate", "asc"]);

return {
rows: limitRows(delegates, params),
Expand Down Expand Up @@ -124,6 +122,8 @@ export class DelegatesBusinessRepository implements Database.IDelegatesBusinessR
return "rate";
case "productivity":
return delegateCalculator.calculateProductivity;
case "votes":
return "voteBalance";
case "approval":
return delegateCalculator.calculateApproval;
default:
Expand Down
8 changes: 8 additions & 0 deletions packages/core-database/src/repositories/utils/filter-rows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ export = <T>(rows: T[], params, filters) =>
}
}

if (filters.hasOwnProperty("like")) {
for (const elem of filters.like) {
if (params[elem] && !item[elem].includes(params[elem])) {
return false;
}
}
}

if (filters.hasOwnProperty("between")) {
for (const elem of filters.between) {
if (!params[elem]) {
Expand Down