odata-v4-inmemory
Advanced tools
Comparing version 0.1.3 to 0.1.4
@@ -15,2 +15,21 @@ import { Token } from 'odata-v4-parser/lib/lexer'; | ||
substring: (v: any, i: any) => any; | ||
contains: (v: any, i: any) => any; | ||
endswith: (v: any, i: any) => any; | ||
startswith: (v: any, i: any) => any; | ||
length: (v: any) => any; | ||
tolower: (v: any) => any; | ||
toupper: (v: any) => any; | ||
trim: (v: any) => any; | ||
concat: (v: any, i: any) => any; | ||
year: (v: any) => any; | ||
month: (v: any) => any; | ||
day: (v: any) => any; | ||
hour: (v: any) => any; | ||
minute: (v: any) => any; | ||
second: (v: any) => any; | ||
now: () => Date; | ||
maxdatetime: () => Date; | ||
mindatetime: () => Date; | ||
floor: (v: any) => number; | ||
ceiling: (v: any) => number; | ||
}; | ||
@@ -28,8 +47,16 @@ export declare class FilterVisitor implements VisitorMap { | ||
protected VisitFilter(node: Token, context: any): (a: any) => boolean; | ||
protected VisitNotExpression(node: Token, context: any): (a: any) => boolean; | ||
protected VisitEqualsExpression(node: Token, context: any): (a: any) => boolean; | ||
protected VisitNotEqualsExpression(node: Token, context: any): (a: any) => boolean; | ||
protected VisitGreaterThanExpression(node: Token, context: any): (a: any) => boolean; | ||
protected VisitLesserThanExpression(node: Token, context: any): (a: any) => boolean; | ||
protected VisitLesserOrEqualsExpression(node: Token, context: any): (a: any) => boolean; | ||
protected VisitGreaterOrEqualsExpression(node: Token, context: any): (a: any) => boolean; | ||
protected VisitImplicitVariableExpression(node: Token, context: any): (a: any) => any; | ||
protected VisitAndExpression(node: Token, context: any): (a: any) => any; | ||
protected VisitAddExpression(node: Token, context: any): (a: any) => any; | ||
protected VisitSubExpression(node: Token, context: any): (a: any) => number; | ||
protected VisitMulExpression(node: Token, context: any): (a: any) => number; | ||
protected VisitDivExpression(node: Token, context: any): (a: any) => number; | ||
protected VisitModExpression(node: Token, context: any): (a: any) => number; | ||
protected VisitParenExpression(node: Token, context: any): (a: any) => any; | ||
@@ -40,3 +67,8 @@ protected getLiteral(node: Token): any; | ||
protected VisitODataIdentifier(node: Token, context: any): (a: any) => any; | ||
protected VisitIsOfExpression(node: Token, context: any): (a: any) => boolean; | ||
protected VisitOrExpression(node: Token, context: any): (a: any) => any; | ||
private resolveIdentifier(node); | ||
protected VisitIdentifier(node: any, context: any): (a: any) => any; | ||
protected VisitArray(node: Token, context: any): (a: any) => any[]; | ||
protected VisitNegateExpression(node: Token, context: any): (a: any) => number; | ||
} |
"use strict"; | ||
var lexer_1 = require('odata-v4-parser/lib/lexer'); | ||
var minDateTime = new Date(-8640000000000000); | ||
var maxDateTime = new Date(8640000000000000); | ||
exports.ODataMethodMap = { | ||
round: function (v) { return Math.round(v); }, | ||
indexof: function (v, i) { return v.indexOf && v.indexOf(i); }, | ||
substring: function (v, i) { return v.substr(i - 1); } | ||
substring: function (v, i) { return v.substr(i - 1); }, | ||
contains: function (v, i) { return v.includes(i); }, | ||
endswith: function (v, i) { return v.endsWith(i); }, | ||
startswith: function (v, i) { return v.startsWith(i); }, | ||
length: function (v) { return v.length; }, | ||
tolower: function (v) { return v.toLowerCase(); }, | ||
toupper: function (v) { return v.toUpperCase(); }, | ||
trim: function (v) { return v.trim(); }, | ||
concat: function (v, i) { return v.concat(i); }, | ||
year: function (v) { return v.getFullYear(); }, | ||
month: function (v) { return v.getMonth() + 1; }, | ||
day: function (v) { return v.getDate(); }, | ||
hour: function (v) { return v.getHours(); }, | ||
minute: function (v) { return v.getMinutes(); }, | ||
second: function (v) { return v.getSeconds(); }, | ||
now: function () { return new Date(); }, | ||
maxdatetime: function () { return maxDateTime; }, | ||
mindatetime: function () { return minDateTime; }, | ||
floor: function (v) { return Math.floor(v); }, | ||
ceiling: function (v) { return Math.ceil(v); } | ||
}; | ||
@@ -13,2 +34,5 @@ var FilterVisitor = (function () { | ||
//console.log("Visiting: ", node.type) | ||
if (!node) { | ||
throw new Error("Node cannot be empty"); | ||
} | ||
switch (node.type) { | ||
@@ -22,2 +46,3 @@ //these are auto handled by visitor bubbling | ||
case lexer_1.TokenType.CommonExpression: | ||
case lexer_1.TokenType.ArrayOrObject: | ||
case undefined: | ||
@@ -67,2 +92,6 @@ break; | ||
}; | ||
FilterVisitor.prototype.VisitNotExpression = function (node, context) { | ||
var expression = this.Visit(node.value, context); | ||
return function (a) { return !expression(a); }; | ||
}; | ||
FilterVisitor.prototype.VisitEqualsExpression = function (node, context) { | ||
@@ -72,2 +101,6 @@ var _a = this.VisitBinaryExpression(node, context), left = _a[0], right = _a[1]; | ||
}; | ||
FilterVisitor.prototype.VisitNotEqualsExpression = function (node, context) { | ||
var _a = this.VisitBinaryExpression(node, context), left = _a[0], right = _a[1]; | ||
return function (a) { return left(a) !== right(a); }; | ||
}; | ||
FilterVisitor.prototype.VisitGreaterThanExpression = function (node, context) { | ||
@@ -81,2 +114,10 @@ var _a = this.VisitBinaryExpression(node, context), left = _a[0], right = _a[1]; | ||
}; | ||
FilterVisitor.prototype.VisitLesserOrEqualsExpression = function (node, context) { | ||
var _a = this.VisitBinaryExpression(node, context), left = _a[0], right = _a[1]; | ||
return function (a) { return left(a) <= right(a); }; | ||
}; | ||
FilterVisitor.prototype.VisitGreaterOrEqualsExpression = function (node, context) { | ||
var _a = this.VisitBinaryExpression(node, context), left = _a[0], right = _a[1]; | ||
return function (a) { return left(a) >= right(a); }; | ||
}; | ||
FilterVisitor.prototype.VisitImplicitVariableExpression = function (node, context) { | ||
@@ -93,2 +134,18 @@ return function (a) { return a; }; | ||
}; | ||
FilterVisitor.prototype.VisitSubExpression = function (node, context) { | ||
var _a = this.VisitBinaryExpression(node, context), left = _a[0], right = _a[1]; | ||
return function (a) { return left(a) - right(a); }; | ||
}; | ||
FilterVisitor.prototype.VisitMulExpression = function (node, context) { | ||
var _a = this.VisitBinaryExpression(node, context), left = _a[0], right = _a[1]; | ||
return function (a) { return left(a) * right(a); }; | ||
}; | ||
FilterVisitor.prototype.VisitDivExpression = function (node, context) { | ||
var _a = this.VisitBinaryExpression(node, context), left = _a[0], right = _a[1]; | ||
return function (a) { return left(a) / right(a); }; | ||
}; | ||
FilterVisitor.prototype.VisitModExpression = function (node, context) { | ||
var _a = this.VisitBinaryExpression(node, context), left = _a[0], right = _a[1]; | ||
return function (a) { return left(a) % right(a); }; | ||
}; | ||
FilterVisitor.prototype.VisitParenExpression = function (node, context) { | ||
@@ -100,5 +157,41 @@ var fn = this.Visit(node.value, context); | ||
switch (node.value) { | ||
case 'null': return null; | ||
case "Edm.SByte": return parseInt(node.raw); | ||
case "Edm.Decimal": return parseFloat(node.raw); | ||
case "Edm.Double": switch (node.raw) { | ||
case 'INF': return Infinity; | ||
default: return parseFloat(node.raw); | ||
} | ||
case "Edm.Boolean": return node.raw === "true"; | ||
case "Edm.String": return node.raw.replace(/'/g, ''); | ||
//todo: check if string is actually a valid literal type | ||
case "string": | ||
case "Edm.String": | ||
return node.raw.replace(/'/g, '').replace(/\"/g, ""); | ||
case "Edm.DateTimeOffset": | ||
case "Edm.Date": | ||
return new Date(node.raw); | ||
case "Edm.TimeOfDay": | ||
return new Date("1970-01-01T" + node.raw + "Z"); | ||
case "Edm.Duration": | ||
var m = node.raw.match(/P([0-9]*D)?T?([0-9]{1,2}H)?([0-9]{1,2}M)?([\.0-9]*S)?/); | ||
if (m) { | ||
var d = new Date(0); | ||
for (var i = 1; i < m.length; i++) { | ||
switch (m[i].slice(-1)) { | ||
case 'D': | ||
d.setDate(parseInt(m[i])); | ||
continue; | ||
case 'H': | ||
d.setHours(parseInt(m[i])); | ||
continue; | ||
case 'M': | ||
d.setMinutes(parseInt(m[i])); | ||
continue; | ||
case 'S': | ||
d.setSeconds(parseInt(m[i])); | ||
continue; | ||
} | ||
} | ||
return d; | ||
} | ||
default: | ||
@@ -116,3 +209,7 @@ console.log("unknown value type:" + node.value); | ||
var method = exports.ODataMethodMap[node.value.method]; | ||
var params = node.value.parameters.map(function (p) { return _this.Visit(p, context); }); | ||
var params = (node.value.parameters || []).map(function (p) { return _this.Visit(p, context); }); | ||
if (!method) { | ||
console.log("Unknown method " + node.value.method); | ||
return function (a) { return a; }; | ||
} | ||
return function (a) { return method.apply(_this, params.map(function (p) { return p(a); })); }; | ||
@@ -128,2 +225,12 @@ }; | ||
}; | ||
FilterVisitor.prototype.VisitIsOfExpression = function (node, context) { | ||
var type = this.Visit(node.value.typename, context); | ||
if (!node.value.target) { | ||
return function (a) { | ||
return a.constructor.name === type(a); | ||
}; | ||
} | ||
var target = this.Visit(node.value.target, context); | ||
return function (a) { return target(a).constructor.name === type(a); }; | ||
}; | ||
FilterVisitor.prototype.VisitOrExpression = function (node, context) { | ||
@@ -133,4 +240,24 @@ var _a = this.VisitBinaryExpression(node, context), left = _a[0], right = _a[1]; | ||
}; | ||
FilterVisitor.prototype.resolveIdentifier = function (node) { | ||
switch (node.value) { | ||
case 'EntityTypeName': | ||
return node.raw.split(".").slice(-1)[0]; | ||
} | ||
}; | ||
//this needs double check. why is model type model as 'identifier' | ||
FilterVisitor.prototype.VisitIdentifier = function (node, context) { | ||
var _this = this; | ||
return function (a) { return _this.resolveIdentifier(node); }; | ||
}; | ||
FilterVisitor.prototype.VisitArray = function (node, context) { | ||
var _this = this; | ||
var items = (node.value.items || []).map(function (item) { return _this.Visit(item, context); }); | ||
return function (a) { return items.map(function (item) { return item(a); }).slice(); }; | ||
}; | ||
FilterVisitor.prototype.VisitNegateExpression = function (node, context) { | ||
var exp = this.Visit(node.value, context); | ||
return function (a) { return -exp(a); }; | ||
}; | ||
return FilterVisitor; | ||
}()); | ||
exports.FilterVisitor = FilterVisitor; |
import { Token } from 'odata-v4-parser/lib/lexer'; | ||
export { Token } from 'odata-v4-parser/lib/lexer'; | ||
export interface ExpressionFunction { | ||
(entity: any): any; | ||
} | ||
export interface FilterFunction { | ||
@@ -14,3 +17,2 @@ (entity: any): boolean; | ||
* @example | ||
* //return true | ||
* const filterFn = createFilter("Size eq 4 and startswith(Name,'Ch')") | ||
@@ -22,1 +24,12 @@ * const items = [{Size:1, Name:'Chai'}, {Size:4, Name:'Childrens book' }] | ||
export declare function createFilter(odataFilter: string): FilterFunction; | ||
/** | ||
* Compiles a value returning function from an OData expression string | ||
* @param {string} odataExpression - An expression in OData expression format | ||
* @return {ExpressionFunction} JavaScript function that implements the expression | ||
* @example | ||
* const expression = compileExpression("concat((Size add 12) mul 3,Name)") | ||
* const item = {Size:1, Name:'Chai'} | ||
* console.log(expression(item)) | ||
* >> 39Chai | ||
*/ | ||
export declare function compileExpression(odataExpression: string): ExpressionFunction; |
@@ -22,3 +22,2 @@ "use strict"; | ||
* @example | ||
* //return true | ||
* const filterFn = createFilter("Size eq 4 and startswith(Name,'Ch')") | ||
@@ -33,1 +32,15 @@ * const items = [{Size:1, Name:'Chai'}, {Size:4, Name:'Childrens book' }] | ||
exports.createFilter = createFilter; | ||
/** | ||
* Compiles a value returning function from an OData expression string | ||
* @param {string} odataExpression - An expression in OData expression format | ||
* @return {ExpressionFunction} JavaScript function that implements the expression | ||
* @example | ||
* const expression = compileExpression("concat((Size add 12) mul 3,Name)") | ||
* const item = {Size:1, Name:'Chai'} | ||
* console.log(expression(item)) | ||
* >> 39Chai | ||
*/ | ||
function compileExpression(odataExpression) { | ||
return filterVisitor.Visit(infrastructure.createFilterAst(odataExpression), {}); | ||
} | ||
exports.compileExpression = compileExpression; |
{ | ||
"name": "odata-v4-inmemory", | ||
"version": "0.1.3", | ||
"version": "0.1.4", | ||
"description": "Service OData requests from an inmemory data store", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
@@ -21,3 +21,3 @@ # OData V4 Service modules - InMemory Connector | ||
## Usage TS | ||
## Usage as server - TypeScript | ||
```javascript | ||
@@ -45,17 +45,41 @@ import { createFilter } from 'odata-v4-inmemory' | ||
## Using as expression engine | ||
```javascript | ||
import { compileExpression } from 'odata-v4-inmemory' | ||
const expression = compileExpression("concat((Size add 12) mul 3,Name)") | ||
const item = {Size:1, Name:'Chai'} | ||
console.log(expression(item)) | ||
>> 39Chai | ||
``` | ||
## Supported OData segments | ||
For now **$filter**, support for **$select** and **$expand** is next. | ||
For now **$filter** | ||
Support for **$select** and **$expand** is next. | ||
### Supported $filter expressions | ||
The [OData v4 Parser](https://www.npmjs.com/package/odata-v4-parser) layer supports 100% of the specification. | ||
The InMemory Connector is about 80% ready. | ||
The Connector is about 90% ready on filters. Except for date arithmetic, and geo.* everything is supported. | ||
*We are into creating a comprehensive feature availability chart for V1 release* | ||
✓ expression: 1 eq 1 | ||
✓ expression: A eq 1 | ||
✓ expression 5.1.1.6.3: null | ||
✓ expression 5.1.1.6.1: NullValue eq null | ||
✓ expression 5.1.1.6.1: duration'P12DT23H59M59.999999999999S' | ||
✓ expression 5.1.1.6.1: A eq INF | ||
✓ expression 5.1.1.6.1: A eq 0.31415926535897931e1 | ||
✓ expression 5.1.1.6.1: A add B eq hour(07:59:59.999) | ||
✓ expression 5.1.1.6.2: ["a","b"] | ||
✓ expression 5.1.1.1.1: 1 eq 1 | ||
✓ expression 5.1.1.1.1: null eq null | ||
✓ expression 5.1.1.1.2: A ne 1 | ||
✓ expression 5.1.1.1.3: A gt 2 | ||
✓ expression 5.1.1.1.4: A ge 3 | ||
✓ expression 5.1.1.1.5: A lt 2 | ||
✓ expression 5.1.1.1.6: A le 2 | ||
✓ expression 5.1.1.2.3: -A eq 1 | ||
✓ expression: A | ||
@@ -65,11 +89,26 @@ ✓ expression: A/b | ||
✓ expression: A/b eq A/b | ||
✓ expression: (A/b eq B/a) or (B/c lt 4) and ((E add 2) gt B add A) | ||
✓ expression: A/$count | ||
✓ expression 5.1.1.3: (A/b eq B/a) or (B/c lt 4) and ((E add 2) gt B add A) | ||
✓ expression 5.1.1.1.9: not A ne 1 | ||
- expression 5.1.1.1.10: A has enum'b | ||
✓ expression 4.8: A/$count | ||
✓ expression: A/$count eq 3 | ||
✓ expression: A/$count gt 2 | ||
✓ expression: A and B | ||
✓ expression: A le B | ||
✓ expression: A ge B | ||
✓ expression 5.1.1.1.7: A and B | ||
✓ expression 5.1.1.1.8: A or B | ||
✓ expression: (A and B) | ||
✓ expression: A/$count gt 2 and A/$count lt 4 | ||
✓ expression: (A/$count gt 2) and A/$count lt 3 | ||
✓ expression: A add B | ||
✓ expression 5.1.1.2.1: A add B | ||
- expression 5.1.1.2.1: '2016-01-01' add 'P4DT15H' | ||
- expression 5.1.1.2.1: '2016-01-01T12:00:00Z' add 'P4DT15H' | ||
- expression 5.1.1.2.1: duration'P4DT15H' add duration'P4DT15H' | ||
✓ expression 5.1.1.2.1: A sub B | ||
- expression 5.1.1.2.1: '2016-01-01T12:00:00Z' sub 'P4DT15H' | ||
✓ expression: 5 sub 10 | ||
✓ expression 5.1.1.2.4: A mul B | ||
✓ expression 5.1.1.2.4: 5 mul 10 | ||
✓ expression 5.1.1.2.5: 10 div 3 | ||
✓ expression 5.1.1.2.5: (A mul B) div 10 | ||
✓ expression 5.1.1.2.6: (A mod B) div 10 | ||
✓ expression: A add 'B' | ||
@@ -83,7 +122,39 @@ ✓ expression: 'A' add 'B' | ||
✓ expression: A/all(i:$it eq 1) | ||
✓ expression: A/all(i: i/a eq 1) | ||
✓ expression: A/any(i: i/a eq 1) | ||
✓ expression: substring(A, 2) eq 'BC' | ||
✓ expression 5.1.1.5.2: A/all(i: i/a eq 1) | ||
✓ expression 5.1.1.5.1: A/any(i: i/a eq 1) | ||
✓ expression 5.1.1.4.6: substring(A, 2) eq 'BC' | ||
✓ expression: substring('ABC', D) eq 'BC' | ||
✓ expression: substring('ABC', 2) eq 'BC' | ||
✓ expression: A eq 1.5 | ||
✓ expression: A eq year(2016-01-01) | ||
✓ expression 5.1.1.4.1: contains(A, 'BC') | ||
✓ expression 5.1.1.4.2: endswith(A, 'CD') | ||
✓ expression 5.1.1.4.3: startswith(A, 'CD') | ||
✓ expression 5.1.1.4.4: length(A) eq 3 | ||
✓ expression 5.1.1.4.5: indexof(A, 'DE') | ||
✓ expression 5.1.1.4.7: tolower(A) eq 'abc' | ||
✓ expression 5.1.1.4.8: toupper(A) eq 'ABC' | ||
✓ expression 5.1.1.4.9: trim(A) eq 'abc' | ||
✓ expression 5.1.1.4.10: concat(A,B) eq 'fubar' | ||
✓ expression 5.1.1.4.10: concat(A,concat(B,C)) eq 'fubardoh' | ||
✓ expression 5.1.1.4.11: A eq year(2016-01-01T13:00Z) | ||
✓ expression 5.1.1.4.12: A eq month(2016-01-01T13:00Z) | ||
✓ expression 5.1.1.4.13: A eq day(2016-01-01T13:00Z) | ||
✓ expression 5.1.1.4.14: A eq hour(2016-01-01T13:00Z) | ||
✓ expression 5.1.1.4.15: A eq minute(2016-01-01T13:00Z) | ||
✓ expression 5.1.1.4.16: A eq second(2016-01-01T13:00:02Z) | ||
✓ expression 5.1.1.4.21: year(now()) | ||
✓ expression 5.1.1.4.22: year(maxdatetime()) | ||
✓ expression 5.1.1.4.23: year(mindatetime()) | ||
- expression 5.1.1.4.24: totalseconds(A) | ||
✓ expression 5.1.1.4.25: round(A) eq 42 | ||
✓ expression 5.1.1.4.26: floor(A) eq 42 | ||
✓ expression 5.1.1.4.27: ceiling(A) eq 42 | ||
✓ expression 5.1.1.4.28: isof(Model.Order) | ||
✓ expression 5.1.1.4.28: isof($it, Model.Order) | ||
✓ expression 5.1.1.4.28: Orders/all(item: isof(item, Model.Order)) | ||
- expression 5.1.1.4.29: cast(A, Edm.String) | ||
- expression 5.1.1.4.30: geo.distance(A, B) | ||
- expression 5.1.1.4.31: geo.intersects(A, B) | ||
- expression 5.1.1.4.31: geo.length(A) | ||
@@ -93,1 +164,3 @@ | ||
@@ -17,6 +17,28 @@ import { TokenType, Token } from 'odata-v4-parser/lib/lexer' | ||
const minDateTime = new Date(-8640000000000000) | ||
const maxDateTime = new Date(8640000000000000) | ||
export const ODataMethodMap = { | ||
round: v => Math.round(v), | ||
indexof: (v, i) => v.indexOf && v.indexOf(i), | ||
substring: (v, i) => v.substr(i - 1) | ||
substring: (v, i) => v.substr(i - 1), | ||
contains: (v, i) => v.includes(i), | ||
endswith: (v, i) => v.endsWith(i), | ||
startswith: (v, i) => v.startsWith(i), | ||
length: v => v.length, | ||
tolower: v => v.toLowerCase(), | ||
toupper: v => v.toUpperCase(), | ||
trim: v => v.trim(), | ||
concat: (v,i) => v.concat(i), | ||
year: v => v.getFullYear(), | ||
month: v => v.getMonth() + 1, | ||
day: v => v.getDate(), | ||
hour: v => v.getHours(), | ||
minute: v => v.getMinutes(), | ||
second: v => v.getSeconds(), | ||
now: () => new Date(), | ||
maxdatetime: () => maxDateTime, | ||
mindatetime: () => minDateTime, | ||
floor: v => Math.floor(v), | ||
ceiling: v => Math.ceil(v), | ||
//isof: (v, i) => return v | ||
} | ||
@@ -30,2 +52,5 @@ | ||
//console.log("Visiting: ", node.type) | ||
if (!node) { | ||
throw new Error("Node cannot be empty") | ||
} | ||
switch (node.type) { | ||
@@ -39,2 +64,3 @@ //these are auto handled by visitor bubbling | ||
case TokenType.CommonExpression: | ||
case TokenType.ArrayOrObject: | ||
case undefined: | ||
@@ -94,2 +120,7 @@ break; | ||
protected VisitNotExpression(node: Token, context: any) { | ||
var expression = this.Visit(node.value, context) | ||
return a => !expression(a) | ||
} | ||
protected VisitEqualsExpression(node: Token, context: any) { | ||
@@ -100,2 +131,7 @@ var [left, right] = this.VisitBinaryExpression(node, context) | ||
protected VisitNotEqualsExpression(node: Token, context: any) { | ||
var [left, right] = this.VisitBinaryExpression(node, context) | ||
return a => left(a) !== right(a) | ||
} | ||
protected VisitGreaterThanExpression(node: Token, context: any) { | ||
@@ -111,2 +147,12 @@ var [left, right] = this.VisitBinaryExpression(node, context) | ||
protected VisitLesserOrEqualsExpression(node: Token, context: any) { | ||
var [left, right] = this.VisitBinaryExpression(node, context) | ||
return a => left(a) <= right(a) | ||
} | ||
protected VisitGreaterOrEqualsExpression(node: Token, context: any) { | ||
var [left, right] = this.VisitBinaryExpression(node, context) | ||
return a => left(a) >= right(a) | ||
} | ||
protected VisitImplicitVariableExpression(node: Token, context: any) { | ||
@@ -124,2 +170,18 @@ return a => a | ||
} | ||
protected VisitSubExpression(node: Token, context: any) { | ||
var [left, right] = this.VisitBinaryExpression(node, context) | ||
return a => left(a) - right(a) | ||
} | ||
protected VisitMulExpression(node: Token, context: any) { | ||
var [left, right] = this.VisitBinaryExpression(node, context) | ||
return a => left(a) * right(a) | ||
} | ||
protected VisitDivExpression(node: Token, context: any) { | ||
var [left, right] = this.VisitBinaryExpression(node, context) | ||
return a => left(a) / right(a) | ||
} | ||
protected VisitModExpression(node: Token, context: any) { | ||
var [left, right] = this.VisitBinaryExpression(node, context) | ||
return a => left(a) % right(a) | ||
} | ||
@@ -130,7 +192,36 @@ protected VisitParenExpression(node: Token, context: any) { | ||
} | ||
protected getLiteral(node: Token): any { | ||
switch(node.value) { | ||
case 'null': return null | ||
case "Edm.SByte": return parseInt(node.raw) | ||
case "Edm.Decimal": return parseFloat(node.raw) | ||
case "Edm.Double": switch(node.raw) { | ||
case 'INF': return Infinity | ||
default: return parseFloat(node.raw) | ||
} | ||
case "Edm.Boolean": return node.raw === "true" | ||
case "Edm.String": return node.raw.replace(/'/g,'') | ||
//todo: check if string is actually a valid literal type | ||
case "string": | ||
case "Edm.String": | ||
return node.raw.replace(/'/g,'').replace(/\"/g,"") | ||
case "Edm.DateTimeOffset": | ||
case "Edm.Date": | ||
return new Date(node.raw) | ||
case "Edm.TimeOfDay": | ||
return new Date(`1970-01-01T${node.raw}Z`) | ||
case "Edm.Duration": | ||
var m = node.raw.match(/P([0-9]*D)?T?([0-9]{1,2}H)?([0-9]{1,2}M)?([\.0-9]*S)?/) | ||
if (m) { | ||
var d = new Date(0); | ||
for(var i = 1; i < m.length; i++) { | ||
switch(m[i].slice(-1)) { | ||
case 'D': d.setDate(parseInt(m[i])); continue; | ||
case 'H': d.setHours(parseInt(m[i])); continue; | ||
case 'M': d.setMinutes(parseInt(m[i])); continue; | ||
case 'S': d.setSeconds(parseInt(m[i])); continue; | ||
} | ||
} | ||
return d; | ||
} | ||
default: | ||
@@ -147,3 +238,7 @@ console.log("unknown value type:" + node.value) | ||
var method = ODataMethodMap[node.value.method] | ||
var params = node.value.parameters.map(p => this.Visit(p, context)) | ||
var params = (node.value.parameters || []).map(p => this.Visit(p, context)) | ||
if (!method) { | ||
console.log(`Unknown method ${node.value.method}`) | ||
return a => a | ||
} | ||
return a => method.apply(this, params.map(p => p(a))) | ||
@@ -161,2 +256,12 @@ } | ||
protected VisitIsOfExpression(node: Token, context: any) { | ||
var type = this.Visit(node.value.typename, context) | ||
if (!node.value.target) { | ||
return a => { | ||
return a.constructor.name === type(a) | ||
} | ||
} | ||
var target = this.Visit(node.value.target, context) | ||
return a => target(a).constructor.name === type(a) | ||
} | ||
protected VisitOrExpression(node: Token, context: any) { | ||
@@ -167,3 +272,23 @@ var [left, right] = this.VisitBinaryExpression(node, context) | ||
private resolveIdentifier(node) { | ||
switch(node.value) { | ||
case 'EntityTypeName': | ||
return node.raw.split(".").slice(-1)[0] | ||
} | ||
} | ||
//this needs double check. why is model type model as 'identifier' | ||
protected VisitIdentifier(node, context) { | ||
return a => this.resolveIdentifier(node) | ||
} | ||
protected VisitArray(node: Token, context: any) { | ||
var items = (node.value.items || []).map( item => this.Visit(item, context)) | ||
return a => [...items.map(item => item(a))] | ||
} | ||
protected VisitNegateExpression(node: Token, context: any) { | ||
var exp = this.Visit(node.value, context) | ||
return a => -exp(a) | ||
} | ||
} | ||
@@ -170,0 +295,0 @@ |
@@ -6,3 +6,3 @@ import { FilterVisitor } from './FilterVisitor' | ||
interface ExpressionFunction { | ||
export interface ExpressionFunction { | ||
(entity: any): any | ||
@@ -32,3 +32,2 @@ } | ||
* @example | ||
* //return true | ||
* const filterFn = createFilter("Size eq 4 and startswith(Name,'Ch')") | ||
@@ -43,1 +42,14 @@ * const items = [{Size:1, Name:'Chai'}, {Size:4, Name:'Childrens book' }] | ||
/** | ||
* Compiles a value returning function from an OData expression string | ||
* @param {string} odataExpression - An expression in OData expression format | ||
* @return {ExpressionFunction} JavaScript function that implements the expression | ||
* @example | ||
* const expression = compileExpression("concat((Size add 12) mul 3,Name)") | ||
* const item = {Size:1, Name:'Chai'} | ||
* console.log(expression(item)) | ||
* >> 39Chai | ||
*/ | ||
export function compileExpression(odataExpression: string): ExpressionFunction { | ||
return filterVisitor.Visit(infrastructure.createFilterAst(odataExpression), {}) | ||
} |
@@ -6,19 +6,74 @@ var createFilter = require('../lib').createFilter | ||
var f | ||
var e | ||
beforeEach(function() { | ||
var match | ||
if (match = this.currentTest.title.match(/expression\: ?(.*)/)) { | ||
f = createFilter(match[1]) | ||
if (match = this.currentTest.title.match(/expression[^\:]*\: ?(.*)/)) { | ||
e = f = createFilter(match[1]) | ||
} | ||
}) | ||
it("expression: 1 eq 1", () => { | ||
//all numbers are referencing this: | ||
//http://docs.oasis-open.org/odata/odata/v4.0/errata02/os/complete/part2-url-conventions/odata-v4.0-errata02-os-part2-url-conventions-complete.html#_Toc406398116 | ||
it("expression 5.1.1.6.3: null", () => { | ||
expect(e()).to.be.null | ||
}) | ||
it("expression 5.1.1.6.1: NullValue eq null", () => { | ||
expect(f({NullValue: null})).to.be.true | ||
}) | ||
it("expression 5.1.1.6.1: duration'P12DT23H59M59.999999999999S'", () => { | ||
expect(e({}).getTime()).to.eql(1036799000) | ||
}) | ||
it("expression 5.1.1.6.1: A eq INF", () => { | ||
expect(f({A: Infinity})).to.be.true | ||
}) | ||
it("expression 5.1.1.6.1: A eq 0.31415926535897931e1", () => { | ||
expect(f({A: 0.31415926535897931e1})).to.be.true | ||
}) | ||
it("expression 5.1.1.6.1: A add B eq hour(07:59:59.999)", () => { | ||
expect(f({A: 3, B: 4})).to.be.true | ||
}) | ||
it('expression 5.1.1.6.2: ["a","b"]', () => { | ||
expect(e({A: 3, B: 4})).to.eql(['a','b']) | ||
}) | ||
it("expression 5.1.1.1.1: 1 eq 1", () => { | ||
expect(f()).to.be.true | ||
}) | ||
it("expression: A eq 1", () => { | ||
expect(f({A:1})).to.be.true | ||
it("expression 5.1.1.1.1: null eq null", () => { | ||
expect(f()).to.be.true | ||
}) | ||
it("expression 5.1.1.1.2: A ne 1", () => { | ||
expect(f({A:1})).to.equal(false) | ||
expect(f({A:2})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.1.3: A gt 2", () => { | ||
expect(f({A: 3})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.1.4: A ge 3", () => { | ||
expect(f({A: 3})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.1.5: A lt 2", () => { | ||
expect(f({A: 1})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.1.6: A le 2", () => { | ||
expect(f({A: 2})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.2.3: -A eq 1", () => { | ||
expect(f({A:-1})).to.be.true | ||
}) | ||
it("expression: A", () => { | ||
@@ -41,7 +96,16 @@ expect(f({A:1})).to.equal(1) | ||
it("expression: (A/b eq B/a) or (B/c lt 4) and ((E add 2) gt B add A)", () => { | ||
it("expression 5.1.1.3: (A/b eq B/a) or (B/c lt 4) and ((E add 2) gt B add A)", () => { | ||
expect(f({A:{b:1}})).to.equal(false) | ||
}) | ||
it("expression: A/$count", () => { | ||
it("expression 5.1.1.1.9: not A ne 1", () => { | ||
expect(f({A:1})).to.equal(true) | ||
expect(f({A:2})).to.equal(false) | ||
}) | ||
xit("expression 5.1.1.1.10: A has enum'b", () => { | ||
}) | ||
it("expression 4.8: A/$count", () => { | ||
expect(f({A:[1,2,3]})).to.equal(3) | ||
@@ -54,10 +118,22 @@ }) | ||
it("expression: A/$count gt 2", () => { | ||
expect(f({A:[1,2,3]})).to.equal(true) | ||
it("expression: A le B", () => { | ||
expect(f({A:1, B:2})).to.equal(true) | ||
expect(f({A:2, B:2})).to.equal(true) | ||
expect(f({A:3, B:2})).to.equal(false) | ||
}) | ||
it("expression: A and B", () => { | ||
it("expression: A ge B", () => { | ||
expect(f({A:1, B:2})).to.equal(false) | ||
expect(f({A:2, B:2})).to.equal(true) | ||
expect(f({A:3, B:2})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.1.7: A and B", () => { | ||
expect(f({A:1, B:2})).to.equal(2) | ||
}) | ||
it("expression 5.1.1.1.8: A or B", () => { | ||
expect(e({A:1, B:2})).to.equal(1) | ||
}) | ||
it("expression: (A and B)", () => { | ||
@@ -76,6 +152,53 @@ expect(f({A:1, B:2})).to.equal(true) | ||
it("expression: A add B", () => { | ||
it("expression 5.1.1.2.1: A add B", () => { | ||
expect(f({A:1, B:2})).to.equal(3) | ||
}) | ||
//undone | ||
xit("expression 5.1.1.2.1: '2016-01-01' add 'P4DT15H'", () => { | ||
expect(f({A:1, B:2})).to.equal(3) | ||
}) | ||
//undone | ||
xit("expression 5.1.1.2.1: '2016-01-01T12:00:00Z' add 'P4DT15H'", () => { | ||
expect(f({A:1, B:2})).to.equal(3) | ||
}) | ||
//undone | ||
xit("expression 5.1.1.2.1: duration'P4DT15H' add duration'P4DT15H'", () => { | ||
expect(f({A:1, B:2})).to.equal(3) | ||
}) | ||
it("expression 5.1.1.2.1: A sub B", () => { | ||
expect(f({A:1, B:2})).to.equal(-1) | ||
}) | ||
xit("expression 5.1.1.2.1: '2016-01-01T12:00:00Z' sub 'P4DT15H'", () => { | ||
expect(f({A:1, B:2})).to.equal(3) | ||
}) | ||
it("expression: 5 sub 10", () => { | ||
expect(f({A:1, B:2})).to.equal(-5) | ||
}) | ||
it("expression 5.1.1.2.4: A mul B", () => { | ||
expect(f({A:1, B:2})).to.equal(2) | ||
}) | ||
it("expression 5.1.1.2.4: 5 mul 10", () => { | ||
expect(f({A:1, B:2})).to.equal(50) | ||
}) | ||
it("expression 5.1.1.2.5: 10 div 3", () => { | ||
expect(f({A:1, B:2})).to.equal(3.3333333333333335) | ||
}) | ||
it("expression 5.1.1.2.5: (A mul B) div 10", () => { | ||
expect(f({A:1, B:2})).to.equal(0.2) | ||
}) | ||
it("expression 5.1.1.2.6: (A mod B) div 10", () => { | ||
expect(f({A:3, B:2})).to.equal(0.1) | ||
}) | ||
it("expression: A add 'B'", () => { | ||
@@ -114,3 +237,3 @@ expect(f({A:1, B:2})).to.equal('1B') | ||
it("expression: A/all(i: i/a eq 1)", () => { | ||
it("expression 5.1.1.5.2: A/all(i: i/a eq 1)", () => { | ||
expect(f({A: [{a:1},{a:1}]})).to.equal(true) | ||
@@ -120,3 +243,3 @@ expect(f({A: [{a:1},{a:2}]})).to.equal(false) | ||
it("expression: A/any(i: i/a eq 1)", () => { | ||
it("expression 5.1.1.5.1: A/any(i: i/a eq 1)", () => { | ||
expect(f({A: [{a:1},{a:2}]})).to.equal(true) | ||
@@ -126,3 +249,3 @@ expect(f({A: [{a:3},{a:2}]})).to.equal(false) | ||
it("expression: substring(A, 2) eq 'BC'", () => { | ||
it("expression 5.1.1.4.6: substring(A, 2) eq 'BC'", () => { | ||
expect(f({A:'ABC'})).to.equal(true) | ||
@@ -138,2 +261,153 @@ }) | ||
}) | ||
it("expression: A eq 1.5", () => { | ||
expect(f({A: 1.5})).to.equal(true) | ||
}) | ||
it("expression: A eq year(2016-01-01)", () => { | ||
expect(f({A: 2016})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.4.1: contains(A, 'BC')", () => { | ||
expect(f({A: 'ABCD'})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.4.2: endswith(A, 'CD')", () => { | ||
expect(f({A: 'ABCD'})).to.equal(true) | ||
expect(f({A: 'ABCDE'})).to.equal(false) | ||
}) | ||
it("expression 5.1.1.4.3: startswith(A, 'CD')", () => { | ||
expect(f({A: 'CDE'})).to.equal(true) | ||
expect(f({A: 'ABCDE'})).to.equal(false) | ||
}) | ||
it("expression 5.1.1.4.4: length(A) eq 3", () => { | ||
expect(f({A: 'CDE'})).to.equal(true) | ||
expect(f({A: 'ABCDE'})).to.equal(false) | ||
}) | ||
it("expression 5.1.1.4.5: indexof(A, 'DE')", () => { | ||
expect(f({A: 'CDE'})).to.equal(1) | ||
expect(f({A: 'ABCDE'})).to.equal(3) | ||
}) | ||
it("expression 5.1.1.4.7: tolower(A) eq 'abc'", () => { | ||
expect(f({A: 'ABC'})).to.equal(true) | ||
expect(f({A: 'DEF'})).to.equal(false) | ||
}) | ||
it("expression 5.1.1.4.8: toupper(A) eq 'ABC'", () => { | ||
expect(f({A: 'abc'})).to.equal(true) | ||
expect(f({A: 'def'})).to.equal(false) | ||
}) | ||
it("expression 5.1.1.4.9: trim(A) eq 'abc'", () => { | ||
expect(f({A: ' abc '})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.4.10: concat(A,B) eq 'fubar'", () => { | ||
expect(f({A:'fu',B:'bar'})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.4.10: concat(A,concat(B,C)) eq 'fubardoh'", () => { | ||
expect(f({A:'fu',B:'bar', C:'doh'})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.4.11: A eq year(2016-01-01T13:00Z)", () => { | ||
expect(f({A: 2016})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.4.12: A eq month(2016-01-01T13:00Z)", () => { | ||
expect(f({A: 1})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.4.13: A eq day(2016-01-01T13:00Z)", () => { | ||
expect(f({A: 1})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.4.14: A eq hour(2016-01-01T13:00Z)", () => { | ||
expect(f({A: 13})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.4.15: A eq minute(2016-01-01T13:00Z)", () => { | ||
expect(f({A: 0})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.4.16: A eq second(2016-01-01T13:00:02Z)", () => { | ||
expect(f({A: 02})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.4.21: year(now())", () => { | ||
expect(f({})).to.equal(new Date().getFullYear()) | ||
}) | ||
it("expression 5.1.1.4.22: year(maxdatetime())", () => { | ||
expect(f({})).to.equal(275760) | ||
}) | ||
it("expression 5.1.1.4.23: year(mindatetime())", () => { | ||
expect(f({})).to.equal(-271821) | ||
}) | ||
//parser ok | ||
//undone | ||
xit("expression 5.1.1.4.24: totalseconds(A)", () => { | ||
expect(f({A: 'P6DT23H59M59.9999S'})).to.equal(-271821) | ||
}) | ||
it("expression 5.1.1.4.25: round(A) eq 42", () => { | ||
expect(f({A: 41.9})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.4.26: floor(A) eq 42", () => { | ||
expect(f({A: 42.9})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.4.27: ceiling(A) eq 42", () => { | ||
expect(f({A: 41.3})).to.equal(true) | ||
}) | ||
it("expression 5.1.1.4.28: isof(Model.Order)", () => { | ||
function Order() { } | ||
expect(f(new Order())).to.equal(true) | ||
}) | ||
it("expression 5.1.1.4.28: isof($it, Model.Order)", () => { | ||
function Order() { } | ||
expect(f(new Order())).to.equal(true) | ||
}) | ||
it("expression 5.1.1.4.28: Orders/all(item: isof(item, Model.Order))", () => { | ||
function Order() { } | ||
expect(f({Orders: [new Order()]})).to.equal(true) | ||
expect(f({Orders: [new Order(),{}]})).to.equal(false) | ||
}) | ||
//parser error | ||
//undone | ||
xit("expression 5.1.1.4.29: cast(A, Edm.String)", () => { | ||
expect(f({A: 41.3})).to.equal(true) | ||
}) | ||
//parser ok | ||
//undone | ||
xit("expression 5.1.1.4.30: geo.distance(A, B)", () => { | ||
expect(f({A: 41.3})).to.equal(true) | ||
}) | ||
//parser ok | ||
//undone | ||
xit("expression 5.1.1.4.31: geo.intersects(A, B)", () => { | ||
expect(f({A: 41.3})).to.equal(true) | ||
}) | ||
//parser ok | ||
//undone | ||
xit("expression 5.1.1.4.31: geo.length(A)", () => { | ||
expect(f({A: 41.3})).to.equal(true) | ||
}) | ||
}) |
54807
1012
162