diff --git a/src/adapter/postgres.spec.ts b/src/adapter/postgres.spec.ts index 18583960504ee4918d1b68060a6b78c77c82a870..85687ae05e9c39882bce0ff22e6fe86dcb848746 100644 --- a/src/adapter/postgres.spec.ts +++ b/src/adapter/postgres.spec.ts @@ -228,7 +228,45 @@ describe("postgres adapter", () => { adapter.getDataFromView(view, (err, result) => { expect(err).to.be.a("null"); expect(result).to.be.an("array"); - expect(result).to.have.length(4); + expect(result).to.have.length(2); + expect(result[0]).to.be.an("object"); + let keys: string[] = []; + keys = keys.concat(view.metrics.map((item) => item.name)); + keys = keys.concat(view.dimensions.map((item) => item.name)); + result.forEach((row) => { + expect(row).to.be.an("object"); + expect(row).to.have.all.keys(keys); + }); + + done(); + }); + }); + + it("should get data from view, using > AND < operators on filters", (done) => { + let view = adapterScenario.gtltView; + adapter.getDataFromView(view, (err, result) => { + expect(err).to.be.a("null"); + expect(result).to.be.an("array"); + expect(result).to.have.length(1); + expect(result[0]).to.be.an("object"); + let keys: string[] = []; + keys = keys.concat(view.metrics.map((item) => item.name)); + keys = keys.concat(view.dimensions.map((item) => item.name)); + result.forEach((row) => { + expect(row).to.be.an("object"); + expect(row).to.have.all.keys(keys); + }); + + done(); + }); + }); + + it("should get data from view, using >= AND <= operators on filters", (done) => { + let view = adapterScenario.geleView; + adapter.getDataFromView(view, (err, result) => { + expect(err).to.be.a("null"); + expect(result).to.be.an("array"); + expect(result).to.have.length(3); expect(result[0]).to.be.an("object"); let keys: string[] = []; keys = keys.concat(view.metrics.map((item) => item.name)); diff --git a/src/adapter/postgres.ts b/src/adapter/postgres.ts index df8623e22312a8740a489fb76e6158ace1c86792..2e5cd81248dff79dcd5cbf29a039a27c72309e3e 100644 --- a/src/adapter/postgres.ts +++ b/src/adapter/postgres.ts @@ -268,6 +268,14 @@ export class PostgresAdapter extends Adapter { return " = "; case FilterOperator.NOTEQUAL: return " != "; + case FilterOperator.GREATER: + return " > "; + case FilterOperator.LOWER: + return " < "; + case FilterOperator.GREATEREQ: + return " >= "; + case FilterOperator.LOWEREQ: + return " <= "; default: return ""; } @@ -280,6 +288,8 @@ export class PostgresAdapter extends Adapter { return "::DATE"; case "integer": return "::INTEGER"; + case "boolean": + return "::BOOLEAN"; default: return ""; } diff --git a/src/core/engine.spec.ts b/src/core/engine.spec.ts index 00ad8be38c2f1bb66950dd2143e295ba5f4ccc39..a1dde4b77635165cbf237348538cdb94823813e6 100644 --- a/src/core/engine.spec.ts +++ b/src/core/engine.spec.ts @@ -270,4 +270,65 @@ describe("engine class", () => { } expect(error).to.be.true; }); + + it("should throw an exception, when a operator does not suit", () => { + const operators = [">", "<", "<=", ">="]; + for (let i = 0; i < operators.length; ++i) { + let error: boolean = false; + let strFilter = "dim:3" + operators[i] + "joao"; + let exeption = "Filter could not be created: Operator \"" + operators[i] + "\" is invalid for target \"dim:3\""; + try { + engine.parseClause(strFilter); + } + catch (e){ + error = true; + expect(e.message).to.be.equal(exeption); + + } + } + }); + + it("should parse clauses with several operators for dates and integers", () => { + const operators: {[key: string]: FilterOperator} = { + ">": FilterOperator.GREATER, + "<": FilterOperator.LOWER, + "<=": FilterOperator.LOWEREQ, + ">=": FilterOperator.GREATEREQ, + "==": FilterOperator.EQUAL, + "!=": FilterOperator.NOTEQUAL + }; + for (let op in operators) { + const strFilter = "dim:0" + op + "0"; + const clause = engine.parseClause(strFilter); + expect(clause).to.be.an("object"); + expect(clause).to.have.property("filters"); + expect(clause).to.have.property("id"); + expect(clause.filters).to.be.an("array"); + expect(clause.filters).to.have.length(1); + expect(clause.filters[0]).to.have.property("id"); + expect(clause.filters[0]).to.have.property("target"); + expect(clause.filters[0]).to.have.property("operator"); + expect(clause.filters[0]).to.have.property("value"); + expect(clause.filters[0].target).to.be.equal(dim[0]); + expect(clause.filters[0].value).to.be.equal("0"); + expect(clause.filters[0].operator).to.be.equal(operators[op]); + } + + for (let op in operators) { + const strFilter = "dim:2" + op + "0"; + const clause = engine.parseClause(strFilter); + expect(clause).to.be.an("object"); + expect(clause).to.have.property("filters"); + expect(clause).to.have.property("id"); + expect(clause.filters).to.be.an("array"); + expect(clause.filters).to.have.length(1); + expect(clause.filters[0]).to.have.property("id"); + expect(clause.filters[0]).to.have.property("target"); + expect(clause.filters[0]).to.have.property("operator"); + expect(clause.filters[0]).to.have.property("value"); + expect(clause.filters[0].target).to.be.equal(dim[2]); + expect(clause.filters[0].value).to.be.equal("0"); + expect(clause.filters[0].operator).to.be.equal(operators[op]); + } + }); }); diff --git a/src/core/engine.ts b/src/core/engine.ts index 44fcf68eb7353bc8cefa4bd87153c2f8c63dbccb..a0209b5eb61de40f4969aa252dd9f2f78eecd729 100644 --- a/src/core/engine.ts +++ b/src/core/engine.ts @@ -124,11 +124,17 @@ export class Engine { throw new Error("Filter could not be created: \"" + segment.target + "\" was not found"); } - return new Filter({ + const filter = new Filter({ target: target, operator: op, value: segment.value }); + + if (!filter.isValid) { + throw new Error("Filter could not be created: Operator \"" + segment.operator + "\" is invalid for target \"" + segment.target + "\""); + } + + return filter; } else { throw new Error("Filter could not be created: Operator on \"" + strFilter + "\" could not be extracted"); diff --git a/src/core/filter.spec.ts b/src/core/filter.spec.ts index e109d2352318cacb616025b0b062d77074c03959..6b6b4e05b59e122a46f042e5197f31bab9be040c 100644 --- a/src/core/filter.spec.ts +++ b/src/core/filter.spec.ts @@ -26,6 +26,10 @@ describe("filter class", () => { it("should correctly parse the operators", () => { expect(Filter.parseOperator("==")).to.be.equal(FilterOperator.EQUAL); expect(Filter.parseOperator("!=")).to.be.equal(FilterOperator.NOTEQUAL); + expect(Filter.parseOperator(">")).to.be.equal(FilterOperator.GREATER); + expect(Filter.parseOperator("<")).to.be.equal(FilterOperator.LOWER); + expect(Filter.parseOperator(">=")).to.be.equal(FilterOperator.GREATEREQ); + expect(Filter.parseOperator("<=")).to.be.equal(FilterOperator.LOWEREQ); expect(Filter.parseOperator("?=")).to.be.equal(FilterOperator.NONE); }); }); diff --git a/src/core/filter.ts b/src/core/filter.ts index cffa555af42aa8939bf51ff82e1f0b96dfc0992b..9dff0e9046dc2cb72faa3868197513d4e3eae15f 100644 --- a/src/core/filter.ts +++ b/src/core/filter.ts @@ -37,7 +37,11 @@ export interface StrFilterOptions { export enum FilterOperator { NONE, EQUAL, - NOTEQUAL + NOTEQUAL, + GREATER, + LOWER, + GREATEREQ, + LOWEREQ } export class Filter { @@ -45,12 +49,14 @@ export class Filter { public readonly target: Metric|Dimension; public readonly operator: FilterOperator; public readonly value: string; + public readonly isValid: boolean; constructor (options: FilterOptions) { this.target = options.target; this.operator = options.operator; this.value = options.value; this.id = Hash.sha1(options.target.name + options.operator + options.value); + this.isValid = Filter.isTypeValid(options); } public static parseOperator(op: string): FilterOperator { @@ -59,6 +65,14 @@ export class Filter { return FilterOperator.EQUAL; case "!=": return FilterOperator.NOTEQUAL; + case ">": + return FilterOperator.GREATER; + case "<": + return FilterOperator.LOWER; + case ">=": + return FilterOperator.GREATEREQ; + case "<=": + return FilterOperator.LOWEREQ; default: return FilterOperator.NONE; } @@ -66,30 +80,80 @@ export class Filter { public static segment(strFilter: string): StrFilterOptions { for (let i = 0; i < strFilter.length; ++i) { - if (strFilter[i] === "=") { - if (strFilter[i + 1] === "=") { - return { - target: strFilter.slice(0, i), - operator: "==", - value: strFilter.slice(i + 2) - }; - } - + switch (strFilter[i]){ + case "=": + if (strFilter[i + 1] === "=") { + return { + target: strFilter.slice(0, i), + operator: "==", + value: strFilter.slice(i + 2) + }; + } + break; + case "!": + if (strFilter[i + 1] === "=") { + return { + target: strFilter.slice(0, i), + operator: "!=", + value: strFilter.slice(i + 2) + }; + } + break; + case ">": + if (strFilter[i + 1] === "=") { + return { + target: strFilter.slice(0, i), + operator: ">=", + value: strFilter.slice(i + 2) + }; + } + else { + return { + target: strFilter.slice(0, i), + operator: ">", + value: strFilter.slice(i + 1) + }; + } + case "<": + if (strFilter[i + 1] === "=") { + return { + target: strFilter.slice(0, i), + operator: "<=", + value: strFilter.slice(i + 2) + }; + } + else { + return { + target: strFilter.slice(0, i), + operator: "<", + value: strFilter.slice(i + 1) + }; + } + default: + break; } + } + + return null; + } - if (strFilter[i] === "!") { - if (strFilter[i + 1] === "=") { - return { - target: strFilter.slice(0, i), - operator: "!=", - value: strFilter.slice(i + 2) - }; - } + private static isTypeValid(op: FilterOptions): boolean { + if (op.operator === FilterOperator.GREATER || + op.operator === FilterOperator.LOWER || + op.operator === FilterOperator.GREATEREQ || + op.operator === FilterOperator.LOWEREQ) { + if (op.target.dataType === "date" || op.target.dataType === "integer") { + return true; + } + else { + return false; } } - return null; + else { + return true; + } } } diff --git a/test/scenario.ts b/test/scenario.ts index 04861a044df7ec3f7c4cad1ad7001aa9fb4b681a..a26bb2ab5e423571a5d3c756de9dd798c8615b59 100644 --- a/test/scenario.ts +++ b/test/scenario.ts @@ -48,6 +48,8 @@ interface AdapterScenario { multiFilterView: View; multiClauseView: View; notEqualView: View; + gtltView: View; + geleView: View; } interface DataCtrlScenario { @@ -74,10 +76,30 @@ const filters: { [key: string]: Filter } = { operator: FilterOperator.EQUAL, value: "2017-01-03" }), - "dim:3" : new Filter({ - target: dims[3], + "dim:0:gt" : new Filter({ + target: dims[0], + operator: FilterOperator.GREATER, + value: "2017-01-02" + }), + "dim:0:lt" : new Filter({ + target: dims[0], + operator: FilterOperator.LOWER, + value: "2017-01-04" + }), + "dim:0:ge" : new Filter({ + target: dims[0], + operator: FilterOperator.GREATEREQ, + value: "2017-01-02" + }), + "dim:0:le" : new Filter({ + target: dims[0], + operator: FilterOperator.LOWEREQ, + value: "2017-01-04" + }), + "dim:5" : new Filter({ + target: dims[5], operator: FilterOperator.NOTEQUAL, - value: "dim:3:1" + value: "true" }), "dim:7" : new Filter({ target: dims[7], @@ -88,8 +110,12 @@ const filters: { [key: string]: Filter } = { const clauses: { [key: string]: Clause } = { "view0dim7": new Clause({filters: [filters["dim:7"]]}), + "view0gt": new Clause({filters: [filters["dim:0:gt"]]}), + "view0lt": new Clause({filters: [filters["dim:0:lt"]]}), + "view0ge": new Clause({filters: [filters["dim:0:ge"]]}), + "view0le": new Clause({filters: [filters["dim:0:le"]]}), "view0dim0": new Clause({filters: [filters["dim:0:0"], filters["dim:0:1"]]}), - "view3dim3": new Clause({filters: [filters["dim:3"]]}) + "view7dim5": new Clause({filters: [filters["dim:5"]]}) }; const wrongMet = new Metric({ @@ -206,11 +232,29 @@ const multiClauseView = new View({ const notEqualView = new View({ metrics: [], - dimensions: [dims[3]], + dimensions: [dims[4], dims[5]], + materialized: false, + origin: false, + childViews: [views[7]], + clauses: [clauses.view7dim5] +}); + +const gtltView = new View({ + metrics: [], + dimensions: [dims[0]], + materialized: false, + origin: false, + childViews: [views[0]], + clauses: [clauses.view0gt, clauses.view0lt] +}); + +const geleView = new View({ + metrics: [], + dimensions: [dims[0]], materialized: false, origin: false, - childViews: [views[3]], - clauses: [clauses.view3dim3] + childViews: [views[0]], + clauses: [clauses.view0ge, clauses.view0le] }); const subDimView = new View({ @@ -265,7 +309,9 @@ export const adapterScenario: AdapterScenario = { clauseView: clauseView, multiFilterView: multiFilterView, multiClauseView: multiClauseView, - notEqualView: notEqualView + notEqualView: notEqualView, + gtltView: gtltView, + geleView: geleView }; export const dataCtrlScenario: DataCtrlScenario = {