diff --git a/lib/data/odata-filter.js b/lib/data/odata-filter.js index b9b80756e..397749c47 100644 --- a/lib/data/odata-filter.js +++ b/lib/data/odata-filter.js @@ -24,13 +24,18 @@ const odataFilter = (expr, odataToColumnMap) => { // I don't want to pass it to all of them. const extractFunctions = ['year', 'month', 'day', 'hour', 'minute', 'second']; - const methodCall = (fn, params) => { + const methodCall = (node) => { // n.b. odata-v4-parser appears to already validate function name and arity. + const fn = node.value.method; const lowerName = fn.toLowerCase(); - if (extractFunctions.includes(lowerName)) + const params = node.value.parameters; + if (extractFunctions.includes(lowerName)) { return sql`extract(${raw(lowerName)} from ${op(params[0])})`; // eslint-disable-line no-use-before-define - else if (fn === 'now') + } else if (fn === 'now') { return sql`now()`; + } else { + throw Problem.internal.unsupportedODataExpression({ at: node.position, type: node.type, text: node.raw }); + } }; const binaryOp = (left, right, operator) => // always use parens to ensure the original AST op precedence. @@ -51,7 +56,7 @@ const odataFilter = (expr, odataToColumnMap) => { : (/^'.*'$/.test(node.raw)) ? node.raw.slice(1, node.raw.length - 1) : node.raw; // eslint-disable-line indent } else if (node.type === 'MethodCallExpression') { - return methodCall(node.value.method, node.value.parameters); + return methodCall(node); } else if (node.type === 'EqualsExpression') { return binaryOp(node.value.left, node.value.right, 'is not distinct from'); } else if (node.type === 'NotEqualsExpression') { diff --git a/test/assertions.js b/test/assertions.js index 0ba7db4f5..facce83d9 100644 --- a/test/assertions.js +++ b/test/assertions.js @@ -227,6 +227,14 @@ should.Assertion.add('FormAttachment', function() { if (this.obj.updatedAt != null) this.obj.updatedAt.should.be.an.isoDate(); }); +should.Assertion.add('Problem', function() { + this.params = { operator: 'to be a Problem' }; + + this.obj.should.be.a.Error(); + Object.keys(this.obj).should.containDeep([ 'problemCode', 'problemDetails' ]); + this.obj.problemCode.should.be.a.Number(); +}); + should.Assertion.add('Project', function() { this.params = { operator: 'to be a Project' }; diff --git a/test/unit/data/odata-filter.js b/test/unit/data/odata-filter.js index 2aae786da..120eea6a9 100644 --- a/test/unit/data/odata-filter.js +++ b/test/unit/data/odata-filter.js @@ -74,6 +74,15 @@ describe('OData filter query transformer', () => { return true; }); }); + + it('should reject unrecognized function names', () => { + assert.throws(() => { odataFilter('123 eq trim(\' 123 \')'); }, (err) => { + err.should.be.a.Problem(); + err.problemCode.should.equal(501.4); + err.message.should.equal('The given OData filter expression uses features not supported by this server: MethodCallExpression at 7 ("trim(\' 123 \')")'); + return true; + }); + }); }); describe('OData orderby/sort query transformer', () => {