import { assert } from "chai" import _ from "lodash" import { default as ExprValidator } from "../src/ExprValidator" import * as fixtures from "./fixtures" import { setupTestExtension } from "./extensionSetup" import { CaseExpr, ScoreExpr, SubqueryExpr, Variable } from "../src" setupTestExtension() const variables: Variable[] = [ { id: "varenum", name: { _base: "en", en: "Varenum" }, type: "enum", enumValues: [ { id: "a", name: { _base: "en", en: "A" } }, { id: "b", name: { _base: "en", en: "B" } } ] }, { id: "varnumber", name: { _base: "en", en: "Varnumber" }, type: "number" }, { id: "varnumberexpr", name: { _base: "en", en: "Varnumberexpr" }, type: "number", table: "t1" }, { id: "varid", name: { _base: "en", en: "Varid" }, type: "id", idTable: "t1" } ] describe("ExprValidator", function () { beforeEach(function () { this.schema = fixtures.simpleSchema().addVariables(variables, {}) this.exprValidator = new ExprValidator(this.schema) this.isValid = (expr: any, options: any) => { const error = this.exprValidator.validateExpr(expr, options) assert.isNull(error, "Expected to be valid: " + error) } this.notValid = (expr: any, options: any) => { assert(this.exprValidator.validateExpr(expr, options), "Expected to be invalid") } }) it("invalid if wrong table", function () { this.notValid({ type: "field", table: "t1", column: "text" }, { table: "t2" }) }) it("invalid if wrong type", function () { this.notValid({ type: "field", table: "t1", column: "enum" }, { types: ["text"] }) }) it("invalid if wrong idTable", function () { const field = { type: "id", table: "t1" } this.isValid(field, { types: ["id"], idTable: "t1" }) this.notValid(field, { types: ["id"], idTable: "t2" }) }) it("invalid if wrong enums", function () { const field = { type: "field", table: "t1", column: "enum" } this.isValid(field, { enumValueIds: ["a", "b", "c"] }) this.notValid(field, { enumValueIds: ["a"] }) }) it("invalid if wrong enums expression", function () { const field = { type: "field", table: "t1", column: "expr_enum" } this.isValid(field, { enumValueIds: ["a", "b", "c"] }) this.notValid(field, { enumValueIds: ["a"] }) }) it("valid if ok", function () { this.isValid({ type: "field", table: "t1", column: "text" }) }) it("invalid if missing field", function () { this.notValid({ type: "field", table: "t1", column: "xyz" }) }) it("invalid if field expr invalid", function () { let table = this.schema.getTable("t1") table = _.cloneDeep(table) table.contents.push({ id: "expr_invalid", name: { _base: "en", en: "Expr Invalid" }, type: "expr", expr: { type: "field", table: "t1", column: "xyz" } }) const schema = this.schema.addTable(table) const exprValidator = new ExprValidator(schema) assert(exprValidator.validateExpr({ type: "field", table: "t1", column: "expr_invalid" })) }) it("handles recursive field expr", function () { let table = this.schema.getTable("t1") table = _.cloneDeep(table) table.contents.push({ id: "expr_recursive", name: { _base: "en", en: "Expr Recursive" }, type: "expr", expr: { type: "field", table: "t1", column: "expr_recursive" } }) const schema = this.schema.addTable(table) const exprValidator = new ExprValidator(schema) assert(exprValidator.validateExpr({ type: "field", table: "t1", column: "expr_recursive" })) }) it("valid if op with literal", function () { const exprValidator = new ExprValidator(this.schema) assert.isNull(exprValidator.validateExpr({ type: "op", table: "t1", op: "+", exprs: [ { type: "field", table: "t1", column: "number" }, { type: "literal", valueType: "number", value: 3 } ] }, { aggrStatuses: ["individual"] })) }) describe("scalar", function () { it("valid", function () { const expr = { type: "scalar", table: "t2", joins: ["2-1"], expr: { type: "field", table: "t1", column: "number" } } this.isValid(expr) }) it("bad join", function () { const expr = { type: "scalar", table: "t2", joins: ["xyz"], expr: { type: "field", table: "t1", column: "number" } } this.notValid(expr) }) it("bad expr", function () { const expr = { type: "scalar", table: "t2", joins: ["2-1"], expr: { type: "field", table: "t1", column: "xyz" } } this.notValid(expr) }) it("valid aggr", function () { const expr = { type: "scalar", table: "t1", joins: ["1-2"], expr: { type: "op", table: "t2", op: "avg", exprs: [{ type: "field", table: "t2", column: "number" }] } } this.isValid(expr) }) }) describe("op", function () { it("invalid if mixed aggregate and individual", function () { const expr = { type: "op", table: "t1", op: "+", exprs: [ { type: "field", table: "t1", column: "number" }, { type: "op", op: "sum", exprs: [{ type: "field", table: "t1", column: "number" }] } ] } this.notValid(expr, { aggrStatuses: ["individual", "literal", "aggregate"] }) }) it("valid", function () { const expr = { type: "op", table: "t1", op: "+", exprs: [{ type: "field", table: "t1", column: "number" }] } this.isValid(expr) }) it("invalid if expr invalid", function () { const expr = { type: "op", table: "t1", op: "+", exprs: [{ type: "field", table: "t1", column: "xyz" }] } this.notValid(expr) }) it("invalid if wrong expr types", function () { const expr = { type: "op", table: "t1", op: "+", exprs: [{ type: "field", table: "t1", column: "text" }] } this.notValid(expr) }) it("valid if incomplete", function () { const expr = { type: "op", table: "t1", op: "+", exprs: [] } this.isValid(expr) }) it("valid if null exprs", function () { const expr = { type: "op", table: "t1", op: "+", exprs: [null, null] } this.isValid(expr) }) it("invalid if ordering field is not date/datetime", function () { const expr = { type: "op", table: "t1", op: "last", exprs: [ { type: "field", table: "t1", column: "number" }, { type: "field", table: "t1", column: "text" } ] } this.notValid(expr, { aggrStatuses: ["aggregate"] }) }) it("valid if ordering field is date/datetime", function () { const expr = { type: "op", table: "t1", op: "last", exprs: [ { type: "field", table: "t1", column: "number" }, { type: "field", table: "t1", column: "datetime" } ] } this.isValid(expr, { aggrStatuses: ["aggregate"] }) }) it("valid to compare date/datetime", function () { const expr = { type: "op", table: "t1", op: ">", exprs: [{ type: "field", table: "t1", column: "date" }, { type: "field", table: "t1", column: "datetime" }] } this.isValid(expr) }) }) describe("case", function () { it("validates else", function () { let expr: CaseExpr = { type: "case", table: "t1", cases: [ { when: { type: "literal", valueType: "boolean", value: true }, then: { type: "literal", valueType: "text", value: "def" } } ], else: { type: "literal", valueType: "text", value: "abc" } } this.isValid(expr) expr = _.cloneDeep(expr) expr.else = { type: "field", table: "t1", column: "xyz" } this.notValid(expr) }) it("validates cases whens boolean", function () { let expr: CaseExpr = { type: "case", table: "t1", cases: [ { when: { type: "literal", valueType: "boolean", value: true }, then: { type: "literal", valueType: "text", value: "def" } } ], else: { type: "literal", valueType: "text", value: "abc" } } this.isValid(expr) expr = _.cloneDeep(expr) expr.cases[0].when = { type: "field", table: "t1", column: "text" } this.notValid(expr) }) it("validates cases thens", function () { let expr: CaseExpr = { type: "case", table: "t1", cases: [ { when: { type: "literal", valueType: "boolean", value: true }, then: { type: "literal", valueType: "text", value: "def" } } ], else: { type: "literal", valueType: "text", value: "abc" } } this.isValid(expr) expr = _.cloneDeep(expr) expr.cases[0].then = { type: "field", table: "t1", column: "xyz" } this.notValid(expr) }) it("validates that all thens are of same type", function () { const expr = { type: "case", table: "t1", cases: [ { when: { type: "literal", valueType: "boolean", value: true }, then: { type: "literal", valueType: "text", value: "abc" } }, { when: { type: "literal", valueType: "boolean", value: false }, then: { type: "literal", valueType: "number", value: 123 } } ] } this.notValid(expr) }) it("invalid if mixing aggregate and individual in then/else", function () { const expr: CaseExpr = { type: "case", table: "t1", cases: [ { when: { type: "literal", valueType: "boolean", value: true }, then: { type: "field", table: "t1", column: "number" } // individual } ], else: { type: "op", op: "sum", table: "t1", exprs: [{ type: "field", table: "t1", column: "number" }] } // aggregate } // Should be invalid because mixing individual (then) and aggregate (else) this.notValid(expr, { aggrStatuses: ["individual", "literal", "aggregate"] }) }) }) describe("score", function () { it("validates input", function () { let expr = { type: "score", table: "t1", input: { type: "field", table: "t1", column: "enum" }, scores: {} } this.isValid(expr) expr = { type: "score", table: "t1", input: { type: "field", table: "t1", column: "text" }, scores: {} } this.notValid(expr) expr = { type: "score", table: "t1", input: { type: "field", table: "t1", column: "xyz" }, scores: {} } this.notValid(expr) }) it("validates score keys", function () { let expr: ScoreExpr = { type: "score", table: "t1", input: { type: "field", table: "t1", column: "enum" }, scores: { a: { type: "field", table: "t1", column: "number" } } } this.isValid(expr) expr = { type: "score", table: "t1", input: { type: "field", table: "t1", column: "enum" }, scores: { xyz: { type: "field", table: "t1", column: "number" } } } this.notValid(expr) }) it("validates score values", function () { let expr = { type: "score", table: "t1", input: { type: "field", table: "t1", column: "enum" }, scores: { a: { type: "field", table: "t1", column: "number" } } } this.isValid(expr) expr = { type: "score", table: "t1", input: { type: "field", table: "t1", column: "enum" }, scores: { a: { type: "field", table: "t1", column: "text" } } } this.notValid(expr) }) }) describe("variable", function () { it("fails if non-existent", function () { this.notValid({ type: "variable", variableId: "varxyz" }) }) it("success if exists", function () { this.isValid({ type: "variable", variableId: "varnumber" }) }) it("checks idTable", function () { this.isValid({ type: "variable", variableId: "varid" }) this.isValid({ type: "variable", variableId: "varid" }, { table: "t2" }) this.isValid({ type: "variable", variableId: "varid" }, { table: "t2", idTable: "t1" }) this.notValid({ type: "variable", variableId: "varid" }, { table: "t2", idTable: "t2" }) }) }) describe("subquery", function () { it("success if valid", function () { const expr: SubqueryExpr = { type: "subquery", table: "t1", from: "t2", select: { type: "field", table: "t2", column: "number" }, outerRefs: [{ id: "var1", expr: { type: "field", table: "t1", column: "number" } }], orderBys: [{ expr: { type: "field", table: "t2", column: "number" }, dir: "desc" }], where: { type: "op", op: ">", table: "t2", exprs: [ { type: "field", table: "t2", column: "number" }, { type: "variable", variableId: "var1" } ] }, } this.isValid(expr) }) it("fails if invalid", function () { const expr = { type: "subquery", table: "t1", from: "nonsuch", expr: { type: "field", table: "t1", column: "nonsuch" }, orderBys: [], where: null, outerRefs: [], } this.notValid(expr) }) }) it("validates extension", function () { const schema = fixtures.simpleSchema() const exprValidator = new ExprValidator(schema) assert.equal(exprValidator.validateExpr({ type: "extension", extension: "test" }), "test") }) })