Skip to content

Commit

Permalink
feat(core-manager): implement database query support (#3780)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastijankuzner committed Jun 8, 2020
1 parent 7cb916a commit 8a2033a
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 20 deletions.
108 changes: 105 additions & 3 deletions __tests__/unit/core-manager/database-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ describe("DatabaseService", () => {
});

it("should return limit 10 with offset", async () => {
const result = database.queryEvents({ offset: 10 });
const result = database.queryEvents({ $offset: 10 });

expect(result.total).toBe(200);
expect(result.limit).toBe(10);
Expand All @@ -113,7 +113,7 @@ describe("DatabaseService", () => {
});

it("should return limit 20", async () => {
const result = database.queryEvents({ limit: 20 });
const result = database.queryEvents({ $limit: 20 });

expect(result.total).toBe(200);
expect(result.limit).toBe(20);
Expand All @@ -123,7 +123,7 @@ describe("DatabaseService", () => {
});

it("should return events with name", async () => {
const result = database.queryEvents({ limit: 1000, event: "dummy_event" });
const result = database.queryEvents({ $limit: 1000, event: "dummy_event" });

expect(result.total).toBe(100);
expect(result.limit).toBe(1000);
Expand All @@ -132,4 +132,106 @@ describe("DatabaseService", () => {
expect(result.data.length).toBe(100);
});
});

describe("Query JSON", () => {
beforeEach(() => {
database.boot();

database.addEvent("dummy_event", { size: 1, name: "1_dummy_event" });
database.addEvent("dummy_event", { size: 2, name: "2_dummy_event" });
database.addEvent("dummy_event", { size: 3, name: "3_dummy_event" });
database.addEvent("dummy_event", { size: 4, name: "4_dummy_event" });
database.addEvent("dummy_event", { size: 5, name: "5_dummy_event" });
});

it("should chose $eq by default", async () => {
const result = database.queryEvents({ data: { size: 1 } });

expect(result.total).toBe(1);
expect(result.limit).toBe(10);
expect(result.offset).toBe(0);
expect(result.data).toBeArray();
expect(result.data.length).toBe(1);
});

it("should use $eq on string", async () => {
const result = database.queryEvents({ data: { name: { $eq: "1_dummy_event" } } });

expect(result.total).toBe(1);
expect(result.limit).toBe(10);
expect(result.offset).toBe(0);
expect(result.data).toBeArray();
expect(result.data.length).toBe(1);
});

it("should use $ne", async () => {
const result = database.queryEvents({ data: { size: { $ne: 3 } } });

expect(result.total).toBe(4);
expect(result.limit).toBe(10);
expect(result.offset).toBe(0);
expect(result.data).toBeArray();
expect(result.data.length).toBe(4);
});

it("should use $like on string", async () => {
const result = database.queryEvents({ data: { name: { $like: "1_%" } } });

expect(result.total).toBe(1);
expect(result.limit).toBe(10);
expect(result.offset).toBe(0);
expect(result.data).toBeArray();
expect(result.data.length).toBe(1);
});

it("should use $lt", async () => {
const result = database.queryEvents({ data: { size: { $lt: 2 } } });

expect(result.total).toBe(1);
expect(result.limit).toBe(10);
expect(result.offset).toBe(0);
expect(result.data).toBeArray();
expect(result.data.length).toBe(1);
});

it("should use $lte", async () => {
const result = database.queryEvents({ data: { size: { $lte: 2 } } });

expect(result.total).toBe(2);
expect(result.limit).toBe(10);
expect(result.offset).toBe(0);
expect(result.data).toBeArray();
expect(result.data.length).toBe(2);
});

it("should use $gt", async () => {
const result = database.queryEvents({ data: { size: { $gt: 4 } } });

expect(result.total).toBe(1);
expect(result.limit).toBe(10);
expect(result.offset).toBe(0);
expect(result.data).toBeArray();
expect(result.data.length).toBe(1);
});

it("should use $gte", async () => {
const result = database.queryEvents({ data: { size: { $gte: 4 } } });

expect(result.total).toBe(2);
expect(result.limit).toBe(10);
expect(result.offset).toBe(0);
expect(result.data).toBeArray();
expect(result.data.length).toBe(2);
});

it("should use $gte an $lte", async () => {
const result = database.queryEvents({ data: { size: { $gte: 2, $lte: 4 } } });

expect(result.total).toBe(3);
expect(result.limit).toBe(10);
expect(result.offset).toBe(0);
expect(result.data).toBeArray();
expect(result.data.length).toBe(3);
});
});
});
15 changes: 6 additions & 9 deletions packages/core-manager/src/actions/watcher-get-events.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Container } from "@arkecosystem/core-kernel";
import { DatabaseService } from "../database-service";

import { Actions } from "../contracts";
import { DatabaseService } from "../database-service";

@Container.injectable()
export class Action implements Actions.Action {
Expand All @@ -13,18 +13,15 @@ export class Action implements Actions.Action {
query: {
type: "object",
properties: {
limit: {
$limit: {
type: "number",
minimum: 0
minimum: 0,
},
offset: {
$offset: {
type: "number",
minimum: 0
minimum: 0,
},
event: {
type: "string",
}
}
},
},
},
required: ["query"],
Expand Down
112 changes: 104 additions & 8 deletions packages/core-manager/src/database-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ import { Container, Providers } from "@arkecosystem/core-kernel";
import BetterSqlite3 from "better-sqlite3";
import { ensureFileSync } from "fs-extra";

interface ConditionLine {
property: string;
condition: string;
value: string;
}

const conditions = new Map<string, string>([
["$eq", "="],
["$ne", "!="],
["$lt", "<"],
["$lte", "<="],
["$gt", ">"],
["$gte", ">="],
["$like", "LIKE"],
]);

@Container.injectable()
export class DatabaseService {
@Container.inject(Container.Identifiers.PluginConfiguration)
Expand Down Expand Up @@ -62,7 +78,11 @@ export class DatabaseService {
limit,
offset,
data: this.database
.prepare(`SELECT * FROM events ${this.prepareWhere(conditions)} LIMIT ${limit} OFFSET ${offset}`)
.prepare(
`SELECT events.id, events.event, events.data, events.timestamp FROM events ${this.prepareWhere(
conditions,
)} LIMIT ${limit} OFFSET ${offset}`,
)
.pluck(false)
.all()
.map((x) => {
Expand All @@ -73,16 +93,16 @@ export class DatabaseService {
}

private prepareLimit(conditions?: any): number {
if (conditions?.limit && typeof conditions.limit === "number" && conditions.limit <= 1000) {
return conditions.limit;
if (conditions?.$limit && typeof conditions.$limit === "number" && conditions.$limit <= 1000) {
return conditions.$limit;
}

return 10;
}

private prepareOffset(conditions?: any): number {
if (conditions?.offset && typeof conditions.offset === "number") {
return conditions.offset;
if (conditions?.$offset && typeof conditions.$offset === "number") {
return conditions.$offset;
}

return 0;
Expand All @@ -91,16 +111,92 @@ export class DatabaseService {
private prepareWhere(conditions?: any): string {
let query = "";

const extractedConditions = this.extractWhereConditions(conditions);

if (extractedConditions.length > 0) {
query += "WHERE " + extractedConditions[0];
}

for (let i = 1; i < extractedConditions.length; i++) {
query += " AND " + extractedConditions[i];
}

console.log(query);

return query;
}

private extractWhereConditions(conditions?: any): string[] {
let result: string[] = [];

if (!conditions) {
return query;
return [];
}

for (const key of Object.keys(conditions)) {
if (key === "event") {
query += `WHERE event LIKE '${conditions[key]}%'`;
result = [
...result,
...this.extractConditions(conditions[key], key).map((x) => this.conditionLineToSQLCondition(x)),
];
}
if (key === "data") {
result = [
...result,
...this.extractConditions(conditions[key], "$").map((x) =>
this.conditionLineToSQLCondition(x, key),
),
];
}
}

return query;
return result;
}

private conditionLineToSQLCondition(conditionLine: ConditionLine, jsonExtractProperty?: string): string {
const useQuote = typeof conditionLine.value !== "number";

if (jsonExtractProperty) {
// Example: json_extract(data, '$.publicKey') = '0377f81a18d25d77b100cb17e829a72259f08334d064f6c887298917a04df8f647'
// prettier-ignore
return `json_extract(${jsonExtractProperty}, '${conditionLine.property}') ${conditions.get(conditionLine.condition)} ${useQuote ? "'" : ""}${conditionLine.value}${useQuote ? "'" : ""}`;
}

// Example: event LIKE 'wallet'
// prettier-ignore
return `${conditionLine.property} ${conditions.get(conditionLine.condition)} ${useQuote ? "'" : ""}${conditionLine.value}${useQuote ? "'" : ""}`;
}

private extractConditions(data: any, property: string): ConditionLine[] {
let result: ConditionLine[] = [];

if (!data) {
/* istanbul ignore next */
return [];
}

if (typeof data !== "object") {
result.push({
property: `${property}`,
condition: "$eq",
value: data,
});

return result;
}

for (const key of Object.keys(data)) {
if (key.startsWith("$")) {
result.push({
property: property,
condition: key,
value: data[key],
});
} else {
result = [...result, ...this.extractConditions(data[key], `${property}.${key}`)];
}
}

return result;
}
}

0 comments on commit 8a2033a

Please sign in to comment.