|
|
@ -226,65 +226,68 @@ export default { |
|
|
|
parseAndValidateFormula(formula) { |
|
|
|
parseAndValidateFormula(formula) { |
|
|
|
try { |
|
|
|
try { |
|
|
|
const pt = jsep(formula) |
|
|
|
const pt = jsep(formula) |
|
|
|
const err = this.validateAgainstMeta(pt) |
|
|
|
const metaErrors = this.validateAgainstMeta(pt) |
|
|
|
if (err.size) { |
|
|
|
if (metaErrors.size) { |
|
|
|
return [...err].join(', ') |
|
|
|
return [...metaErrors].join(', ') |
|
|
|
} |
|
|
|
} |
|
|
|
return true |
|
|
|
return true |
|
|
|
} catch (e) { |
|
|
|
} catch (e) { |
|
|
|
return e.message |
|
|
|
return e.message |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
validateAgainstMeta(pt, arr = new Set()) { |
|
|
|
validateAgainstMeta(pt, errors = new Set(), typeErrors = new Set()) { |
|
|
|
if (pt.type === jsep.CALL_EXP) { |
|
|
|
if (pt.type === jsep.CALL_EXP) { |
|
|
|
|
|
|
|
// validate function name |
|
|
|
if (!this.availableFunctions.includes(pt.callee.name)) { |
|
|
|
if (!this.availableFunctions.includes(pt.callee.name)) { |
|
|
|
arr.add(`'${pt.callee.name}' function is not available`) |
|
|
|
errors.add(`'${pt.callee.name}' function is not available`) |
|
|
|
} |
|
|
|
} |
|
|
|
const validation = formulas[pt.callee.name] && formulas[pt.callee.name].validation |
|
|
|
|
|
|
|
// validate arguments |
|
|
|
// validate arguments |
|
|
|
|
|
|
|
const validation = formulas[pt.callee.name] && formulas[pt.callee.name].validation |
|
|
|
if (validation && validation.args) { |
|
|
|
if (validation && validation.args) { |
|
|
|
if (validation.args.rqd !== undefined && validation.args.rqd !== pt.arguments.length) { |
|
|
|
if (validation.args.rqd !== undefined && validation.args.rqd !== pt.arguments.length) { |
|
|
|
arr.add(`'${pt.callee.name}' required ${validation.args.rqd} arguments`) |
|
|
|
errors.add(`'${pt.callee.name}' required ${validation.args.rqd} arguments`) |
|
|
|
} else if (validation.args.min !== undefined && validation.args.min > pt.arguments.length) { |
|
|
|
} else if (validation.args.min !== undefined && validation.args.min > pt.arguments.length) { |
|
|
|
arr.add(`'${pt.callee.name}' required minimum ${validation.args.min} arguments`) |
|
|
|
errors.add(`'${pt.callee.name}' required minimum ${validation.args.min} arguments`) |
|
|
|
} else if (validation.args.max !== undefined && validation.args.max < pt.arguments.length) { |
|
|
|
} else if (validation.args.max !== undefined && validation.args.max < pt.arguments.length) { |
|
|
|
arr.add(`'${pt.callee.name}' required maximum ${validation.args.max} arguments`) |
|
|
|
errors.add(`'${pt.callee.name}' required maximum ${validation.args.max} arguments`) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
pt.arguments.map(arg => this.validateAgainstMeta(arg, errors)) |
|
|
|
|
|
|
|
|
|
|
|
// validate data type |
|
|
|
// validate data type |
|
|
|
const type = formulas[pt.callee.name].type |
|
|
|
const type = formulas[pt.callee.name].type |
|
|
|
if (type === formulaTypes.NUMERIC) { |
|
|
|
if ( |
|
|
|
for (const arg of pt.arguments) { |
|
|
|
type === formulaTypes.NUMERIC || |
|
|
|
if (arg.value && typeof arg.value !== 'number') { |
|
|
|
type === formulaTypes.STRING |
|
|
|
arr.add(`Value '${arg.value}' should have a numeric type`) |
|
|
|
) { |
|
|
|
} |
|
|
|
pt.arguments.map(arg => this.validateAgainstType(arg, type, func, typeErrors)) |
|
|
|
if (arg.name) { |
|
|
|
|
|
|
|
// TODO: handle jsep.IDENTIFIER case |
|
|
|
|
|
|
|
// arr.add(`Column '${arg.name}' should have a numeric type`) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else if (type === formulaTypes.STRING) { |
|
|
|
|
|
|
|
for (const arg of pt.arguments) { |
|
|
|
|
|
|
|
if (arg.value && typeof arg.value !== 'string') { |
|
|
|
|
|
|
|
arr.add(`Value '${arg.value}' should have a string type`) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (arg.name) { |
|
|
|
|
|
|
|
// TODO: handle jsep.IDENTIFIER case |
|
|
|
|
|
|
|
// arr.add(`Column '${arg.name}' should have a string type`) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else if (type === formulaTypes.DATE) { |
|
|
|
} else if (type === formulaTypes.DATE) { |
|
|
|
if (pt.callee.name === 'DATEADD') { |
|
|
|
if (pt.callee.name === 'DATEADD') { |
|
|
|
// pt.arguments[0] = date type |
|
|
|
// pt.arguments[0] = date type |
|
|
|
|
|
|
|
this.validateAgainstType(pt.arguments[0], formulaTypes.DATE, (v) => { |
|
|
|
|
|
|
|
if (!(v instanceof Date)) { |
|
|
|
|
|
|
|
typeErrors.add('The first parameter of DATEADD() should have date value') |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
// pt.arguments[1] = numeric |
|
|
|
// pt.arguments[1] = numeric |
|
|
|
|
|
|
|
this.validateAgainstType(pt.arguments[1], formulaTypes.NUMERIC, (v) => { |
|
|
|
|
|
|
|
if (typeof v !== 'number') { |
|
|
|
|
|
|
|
typeErrors.add('The second parameter of DATEADD() should have numeric value') |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
// pt.arguments[2] = ["day" | "week" | "month" | "year"] |
|
|
|
// pt.arguments[2] = ["day" | "week" | "month" | "year"] |
|
|
|
// TODO: write a dry-run function to validate each segment |
|
|
|
this.validateAgainstType(pt.arguments[2], formulaTypes.STRING, (v) => { |
|
|
|
|
|
|
|
if (!['day', 'week', 'month', 'year'].includes(v)) { |
|
|
|
|
|
|
|
typeErrors.add('The third parameter of DATEADD() should have the value either "day", "week", "month" or "year"') |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// NOW()? |
|
|
|
} |
|
|
|
} |
|
|
|
pt.arguments.map(arg => this.validateAgainstMeta(arg, arr)) |
|
|
|
errors = new Set([...errors, ...typeErrors]) |
|
|
|
} else if (pt.type === jsep.IDENTIFIER) { |
|
|
|
} else if (pt.type === jsep.IDENTIFIER) { |
|
|
|
if (this.meta.columns.filter(c => !this.column || this.column.id !== c.id).every(c => c.title !== pt.name)) { |
|
|
|
if (this.meta.columns.filter(c => !this.column || this.column.id !== c.id).every(c => c.title !== pt.name)) { |
|
|
|
arr.add(`Column '${pt.name}' is not available`) |
|
|
|
errors.add(`Column '${pt.name}' is not available`) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// check circular reference |
|
|
|
// check circular reference |
|
|
@ -351,25 +354,52 @@ export default { |
|
|
|
} |
|
|
|
} |
|
|
|
// vertices not same as visited = cycle found |
|
|
|
// vertices not same as visited = cycle found |
|
|
|
if (vertices !== visited) { |
|
|
|
if (vertices !== visited) { |
|
|
|
arr.add('Can’t save field because it causes a circular reference') |
|
|
|
errors.add('Can’t save field because it causes a circular reference') |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} else if (pt.type === jsep.BINARY_EXP) { |
|
|
|
} else if (pt.type === jsep.BINARY_EXP) { |
|
|
|
if (!this.availableBinOps.includes(pt.operator)) { |
|
|
|
if (!this.availableBinOps.includes(pt.operator)) { |
|
|
|
arr.add(`'${pt.operator}' operation is not available`) |
|
|
|
errors.add(`'${pt.operator}' operation is not available`) |
|
|
|
} |
|
|
|
} |
|
|
|
this.validateAgainstMeta(pt.left, arr) |
|
|
|
this.validateAgainstMeta(pt.left, errors) |
|
|
|
this.validateAgainstMeta(pt.right, arr) |
|
|
|
this.validateAgainstMeta(pt.right, errors) |
|
|
|
} else if (pt.type === jsep.LITERAL) { |
|
|
|
} else if (pt.type === jsep.LITERAL) { |
|
|
|
// do nothing |
|
|
|
// do nothing |
|
|
|
} else if (pt.type === jsep.COMPOUND) { |
|
|
|
} else if (pt.type === jsep.COMPOUND) { |
|
|
|
if (pt.body.length) { |
|
|
|
if (pt.body.length) { |
|
|
|
arr.add('Can’t save field because the formula is invalid') |
|
|
|
errors.add('Can’t save field because the formula is invalid') |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
arr.add('Can’t save field because the formula is invalid') |
|
|
|
errors.add('Can’t save field because the formula is invalid') |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return errors |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
validateAgainstType(pt, type, func, typeErrors = new Set()) { |
|
|
|
|
|
|
|
if (pt === false || typeof pt === 'undefined') { return typeErrors } |
|
|
|
|
|
|
|
if (pt.type === jsep.LITERAL) { |
|
|
|
|
|
|
|
if (typeof func === 'function') { |
|
|
|
|
|
|
|
func(pt.value) |
|
|
|
|
|
|
|
} else if (type === formulaTypes.NUMERIC) { |
|
|
|
|
|
|
|
if (typeof pt.value !== 'number') { |
|
|
|
|
|
|
|
typeErrors.add('Numeric type is expected') |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else if (type === formulaTypes.STRING) { |
|
|
|
|
|
|
|
if (typeof pt.value !== 'string') { |
|
|
|
|
|
|
|
typeErrors.add('string type is expected') |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else if (pt.type == jsep.IDENTIFIER) { |
|
|
|
|
|
|
|
// TODO: |
|
|
|
|
|
|
|
} else if (pt.type === jsep.UNARY_EXP || pt.type === jsep.BINARY_EXP) { |
|
|
|
|
|
|
|
if (type !== formulaTypes.NUMERIC) { |
|
|
|
|
|
|
|
typeErrors.add('Numeric type is expected') |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else if (pt.type === jsep.CALL_EXP) { |
|
|
|
|
|
|
|
if (type !== formulas[pt.callee.name].type) { |
|
|
|
|
|
|
|
typeErrors.add(`${type} not matched with ${formulas[pt.callee.name].type}`) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return arr |
|
|
|
return typeErrors |
|
|
|
}, |
|
|
|
}, |
|
|
|
appendText(it) { |
|
|
|
appendText(it) { |
|
|
|
const text = it.text |
|
|
|
const text = it.text |
|
|
|