Moved to main repo
This commit is contained in:
12
odata/lib/odata2cqn.js
Normal file
12
odata/lib/odata2cqn.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const peg = require("pegjs");
|
||||||
|
const pegGrammarPath = path.join(__dirname, "/odata2cqn.pegjs");
|
||||||
|
|
||||||
|
const odataPegGrammar = fs.readFileSync(pegGrammarPath, {
|
||||||
|
encoding: "utf8",
|
||||||
|
flag: "r",
|
||||||
|
});
|
||||||
|
const parser = peg.generate(odataPegGrammar);
|
||||||
|
|
||||||
|
module.exports = { parser };
|
||||||
589
odata/lib/odata2cqn.pegjs
Normal file
589
odata/lib/odata2cqn.pegjs
Normal file
@@ -0,0 +1,589 @@
|
|||||||
|
/**
|
||||||
|
* Odata Spec http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/abnf/odata-abnf-construction-rules.txt
|
||||||
|
* Future test cases http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/abnf/odata-abnf-testcases.xml
|
||||||
|
*
|
||||||
|
* Limitations: Type, Geo functions are not supported,
|
||||||
|
* maxdatetime, mindatetime, fractionalseconds,
|
||||||
|
* totaloffsetminutes, date, totalseconds,
|
||||||
|
* floor, ceiling also are not supported by CAP
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* Books
|
||||||
|
* Books/201
|
||||||
|
* Books?$select=ID,title&$expand=author($select=name)&$filter=stock gt 1&$orderby=title
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
const $=Object.assign
|
||||||
|
const SELECT={};
|
||||||
|
const stack=[];
|
||||||
|
let columns=[];
|
||||||
|
let filterExpr;
|
||||||
|
|
||||||
|
const select = (col) => {
|
||||||
|
if (!SELECT.columns) SELECT.columns = columns
|
||||||
|
columns.push(col)
|
||||||
|
}
|
||||||
|
const expand = (col) => {
|
||||||
|
select (col)
|
||||||
|
stack.push (columns)
|
||||||
|
columns = col.expand = []
|
||||||
|
}
|
||||||
|
const end = ()=> columns = stack.pop()
|
||||||
|
const compOperators = {
|
||||||
|
eq: '=',
|
||||||
|
ne: '!=',
|
||||||
|
lt: '<',
|
||||||
|
gt: '>',
|
||||||
|
le: '<=',
|
||||||
|
ge: '>=',
|
||||||
|
}
|
||||||
|
|
||||||
|
function FilterExpr() {
|
||||||
|
let parsedWhereClause = [];
|
||||||
|
|
||||||
|
function appendWhereClause(body) {
|
||||||
|
if(!parsedWhereClause) {
|
||||||
|
parsedWhereClause = body;
|
||||||
|
} else {
|
||||||
|
parsedWhereClause = [...parsedWhereClause, ...body];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getParsedWhereClause() {
|
||||||
|
return parsedWhereClause;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
appendWhereClause,
|
||||||
|
getParsedWhereClause,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start = ODataRelativeURI
|
||||||
|
|
||||||
|
ODataRelativeURI // Note: case-sensitive!
|
||||||
|
= (p:ref { SELECT.from = p }) ( o"?"o QueryOptions )?
|
||||||
|
{return { SELECT }}
|
||||||
|
|
||||||
|
QueryOptions = (
|
||||||
|
"$expand=" expand /
|
||||||
|
"$select=" select /
|
||||||
|
"$top=" top /
|
||||||
|
"$skip=" skip /
|
||||||
|
"$count=" count /
|
||||||
|
"$orderby=" orderby /
|
||||||
|
(beforeFilter FilterExprSequence aflterFilter)
|
||||||
|
)( o'&'o QueryOptions )?
|
||||||
|
|
||||||
|
|
||||||
|
// ---------- Grouped $filter expression ----------
|
||||||
|
// ----------
|
||||||
|
beforeFilter = "$filter=" {
|
||||||
|
console.log('starting $filter');
|
||||||
|
filterExpr = new FilterExpr();
|
||||||
|
}
|
||||||
|
aflterFilter = "" {
|
||||||
|
console.log('end of $filter');
|
||||||
|
SELECT.where = filterExpr.getParsedWhereClause();
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterExprSequence = (Expr (SP logicalOperator SP Expr)*)
|
||||||
|
GroupedExpr = (startGroup FilterExprSequence closeGroup)
|
||||||
|
Expr = (
|
||||||
|
(notOperator SP)? ( boolFunc / GroupedExpr )
|
||||||
|
) / ( commonExp )
|
||||||
|
startGroup
|
||||||
|
= OPEN
|
||||||
|
{ filterExpr.appendWhereClause(['(']); }
|
||||||
|
closeGroup
|
||||||
|
= CLOSE
|
||||||
|
{ filterExpr.appendWhereClause([')']); }
|
||||||
|
|
||||||
|
|
||||||
|
// ---------- Function expressions ----------
|
||||||
|
// ----------
|
||||||
|
commonExp = val:(
|
||||||
|
timeExpr /
|
||||||
|
secondExpr /
|
||||||
|
minuteExpr /
|
||||||
|
hourExpr /
|
||||||
|
dayExpr /
|
||||||
|
monthExpr /
|
||||||
|
yearExpr /
|
||||||
|
compStrExpr /
|
||||||
|
compareNumExpr
|
||||||
|
)
|
||||||
|
{
|
||||||
|
const res = val.filter(cur => cur !== ' ');
|
||||||
|
filterExpr.appendWhereClause([...res]);
|
||||||
|
}
|
||||||
|
|
||||||
|
compStrExpr
|
||||||
|
= firstArgObj:strArg
|
||||||
|
SP operatorVal:eqOperator SP
|
||||||
|
secondArgObj:strArg
|
||||||
|
compareNumExpr
|
||||||
|
= firstArgObj:numberArg
|
||||||
|
SP operatorVal:( eqOperator / numCompOperator ) SP
|
||||||
|
secondArgObj:numberArg
|
||||||
|
dayExpr
|
||||||
|
= firstArg:(dayFunc / dayVal)
|
||||||
|
SP operatorVal:( eqOperator / numCompOperator ) SP
|
||||||
|
secondArg:(dayFunc / dayVal)
|
||||||
|
hourExpr
|
||||||
|
= firstArg:(hourFunc / hourVal)
|
||||||
|
SP operatorVal:( eqOperator / numCompOperator ) SP
|
||||||
|
secondArg:(hourFunc / hourVal)
|
||||||
|
minuteExpr
|
||||||
|
= firstArg:( minuteFunc / minuteVal )
|
||||||
|
SP operatorVal:( eqOperator / numCompOperator ) SP
|
||||||
|
secondArg:( minuteFunc / minuteVal )
|
||||||
|
monthExpr
|
||||||
|
= firstArg:(monthFunc / monthVal)
|
||||||
|
SP operatorVal:( eqOperator / numCompOperator ) SP
|
||||||
|
secondArg:(monthFunc / monthVal)
|
||||||
|
secondExpr
|
||||||
|
= firstArg:(secondFunc / secondVal)
|
||||||
|
SP operatorVal:( eqOperator / numCompOperator ) SP
|
||||||
|
secondArg:(secondFunc / secondVal)
|
||||||
|
yearExpr
|
||||||
|
= firstArg:(yearFunc / yearVal)
|
||||||
|
SP operatorVal:( eqOperator / numCompOperator ) SP
|
||||||
|
secondArg:(yearFunc / yearVal)
|
||||||
|
timeExpr
|
||||||
|
= firstArg:(timeFunc / timeOfDayValue)
|
||||||
|
SP operatorVal:( numCompOperator / eqOperator ) SP
|
||||||
|
secondArg:(timeFunc / timeOfDayValue)
|
||||||
|
|
||||||
|
strArg = (
|
||||||
|
substringFunc /
|
||||||
|
tolowerFunc /
|
||||||
|
toupperFunc /
|
||||||
|
trimFunc /
|
||||||
|
concatFunc /
|
||||||
|
strLiteral /
|
||||||
|
field
|
||||||
|
)
|
||||||
|
numberArg = (
|
||||||
|
lengthFunc /
|
||||||
|
indexofFunc /
|
||||||
|
roundFunc /
|
||||||
|
number /
|
||||||
|
field
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------- Functions ----------
|
||||||
|
//
|
||||||
|
// ---------- "contains" / "endswith" / "startswith" ----------
|
||||||
|
boolFunc
|
||||||
|
= funcName:( "contains" / "endswith" / "startswith" )
|
||||||
|
OPEN
|
||||||
|
fieldRef:strArg COMMA
|
||||||
|
containsStrArg:strArg
|
||||||
|
CLOSE
|
||||||
|
{
|
||||||
|
function getLikeArgs (value) {
|
||||||
|
const funcArgs = {
|
||||||
|
contains: [ "'%'", value, "'%'" ],
|
||||||
|
endswith: [ "'%'", value ],
|
||||||
|
startswith: [ value, "'%'" ]
|
||||||
|
};
|
||||||
|
return funcArgs[funcName];
|
||||||
|
};
|
||||||
|
filterExpr.appendWhereClause([
|
||||||
|
fieldRef,
|
||||||
|
'like',
|
||||||
|
{
|
||||||
|
func: 'concat',
|
||||||
|
args: getLikeArgs(containsStrArg)
|
||||||
|
},
|
||||||
|
'escape',
|
||||||
|
"'^'"
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// ---------- "length" ----------
|
||||||
|
lengthFunc
|
||||||
|
= "length"
|
||||||
|
OPEN
|
||||||
|
fieldRef:strArg
|
||||||
|
CLOSE
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
func: 'length',
|
||||||
|
args: [ fieldRef ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---------- "indexof" ----------
|
||||||
|
indexofFunc
|
||||||
|
= "indexof"
|
||||||
|
OPEN
|
||||||
|
fieldRef:strArg COMMA
|
||||||
|
strArgVal:strArg
|
||||||
|
CLOSE
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
func: 'locate',
|
||||||
|
args: [ fieldRef, strArgVal ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---------- "substring" ----------
|
||||||
|
substringFunc
|
||||||
|
= "substring"
|
||||||
|
OPEN
|
||||||
|
fieldRef:strArg COMMA
|
||||||
|
arg:(
|
||||||
|
(numberArg COMMA numberArg) /
|
||||||
|
numberArg
|
||||||
|
)
|
||||||
|
CLOSE
|
||||||
|
{
|
||||||
|
const args = Array.isArray(arg) ?
|
||||||
|
[
|
||||||
|
fieldRef,
|
||||||
|
...arg.filter(cur => cur !== ',')
|
||||||
|
] :
|
||||||
|
[fieldRef, arg];
|
||||||
|
return {
|
||||||
|
func: 'substring',
|
||||||
|
args: args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---------- "tolower" ----------
|
||||||
|
tolowerFunc
|
||||||
|
= "tolower"
|
||||||
|
OPEN
|
||||||
|
fieldRef:strArg
|
||||||
|
CLOSE
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
func: 'lower',
|
||||||
|
args: [ fieldRef ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---------- "toupper" ----------
|
||||||
|
toupperFunc
|
||||||
|
= "toupper"
|
||||||
|
OPEN
|
||||||
|
fieldRef:strArg
|
||||||
|
CLOSE
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
func: 'upper',
|
||||||
|
args: [ fieldRef ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---------- "trim" ----------
|
||||||
|
trimFunc
|
||||||
|
= "trim"
|
||||||
|
OPEN
|
||||||
|
fieldRef:strArg
|
||||||
|
CLOSE
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
func: 'trim',
|
||||||
|
args: [ fieldRef ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---------- "concat" ----------
|
||||||
|
concatFunc
|
||||||
|
= "concat"
|
||||||
|
OPEN
|
||||||
|
fieldRef:strArg COMMA
|
||||||
|
strArgVal:strArg
|
||||||
|
CLOSE
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
func: 'concat',
|
||||||
|
args: [ fieldRef, strArgVal ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ---------- "day" ----------
|
||||||
|
dayFunc
|
||||||
|
= "day"
|
||||||
|
OPEN
|
||||||
|
fieldRef:(dateTimeOffsetValue / dateValue / field)
|
||||||
|
CLOSE
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
func: 'dayofmonth',
|
||||||
|
args: [ fieldRef ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- "hour" ----------
|
||||||
|
hourFunc
|
||||||
|
= "hour"
|
||||||
|
OPEN
|
||||||
|
fieldRef:(dateTimeOffsetValue / timeOfDayValue / field)
|
||||||
|
CLOSE
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
func: 'hour',
|
||||||
|
args: [ fieldRef ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- "minute" ----------
|
||||||
|
minuteFunc
|
||||||
|
= "minute"
|
||||||
|
OPEN
|
||||||
|
fieldRef:(dateTimeOffsetValue / timeOfDayValue / field)
|
||||||
|
CLOSE
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
func: 'minute',
|
||||||
|
args: [ fieldRef ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- "month" ----------
|
||||||
|
monthFunc
|
||||||
|
= "month"
|
||||||
|
OPEN
|
||||||
|
fieldRef:(dateTimeOffsetValue / dateValue / field)
|
||||||
|
CLOSE
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
func: 'month',
|
||||||
|
args: [ fieldRef ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- "second" ----------
|
||||||
|
secondFunc
|
||||||
|
= "second"
|
||||||
|
OPEN
|
||||||
|
fieldRef:(dateTimeOffsetValue / timeOfDayValue / field)
|
||||||
|
CLOSE
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
func: 'second',
|
||||||
|
args: [ fieldRef ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- "year" ----------
|
||||||
|
yearFunc
|
||||||
|
= "year"
|
||||||
|
OPEN
|
||||||
|
fieldRef:(dateTimeOffsetValue / dateValue / field)
|
||||||
|
CLOSE
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
func: 'year',
|
||||||
|
args: [ fieldRef ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- "time" ----------
|
||||||
|
timeFunc
|
||||||
|
= "time"
|
||||||
|
OPEN
|
||||||
|
fieldRef:(dateTimeOffsetValue / field)
|
||||||
|
CLOSE
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
func: 'to_time',
|
||||||
|
args: [ fieldRef ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- "round" ----------
|
||||||
|
roundFunc
|
||||||
|
= "round"
|
||||||
|
OPEN
|
||||||
|
fieldRef:(dateTimeOffsetValue / field)
|
||||||
|
CLOSE
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
func: 'round',
|
||||||
|
args: [ fieldRef ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
expand
|
||||||
|
= ((c:ref {expand(c)}) (o'('o QueryOptions o')'o)? {end()})
|
||||||
|
(o','o expand)?
|
||||||
|
|
||||||
|
select
|
||||||
|
= (c:ref {select(c)})
|
||||||
|
(o','o select)?
|
||||||
|
|
||||||
|
top
|
||||||
|
= n:$[0-9]+
|
||||||
|
{ (SELECT.limit || (SELECT.limit={})).rows = { val: parseInt(n) } }
|
||||||
|
|
||||||
|
skip
|
||||||
|
= n:$[0-9]+
|
||||||
|
{ (SELECT.limit || (SELECT.limit={})).offset = { val: parseInt(n) } }
|
||||||
|
|
||||||
|
other "other query options"
|
||||||
|
= o:$([^=]+) "=" x:todo
|
||||||
|
{ SELECT[o.slice(1)] = x; console.log('another option was called') }
|
||||||
|
|
||||||
|
ref "a reference"
|
||||||
|
= p:$[^,?&()]+ {
|
||||||
|
const reffs = p.split('/');
|
||||||
|
if(reffs.length === 2 && reffs[reffs.length-1] === '$count') {
|
||||||
|
SELECT.count = true;
|
||||||
|
}
|
||||||
|
return { ref: [reffs[0]] };
|
||||||
|
}
|
||||||
|
|
||||||
|
todo = $[^,?&]+
|
||||||
|
|
||||||
|
count = c:$[^,?&()]+
|
||||||
|
{ if(c === 'true') SELECT.count = true }
|
||||||
|
|
||||||
|
orderby = o:$[^,?&()]+
|
||||||
|
{
|
||||||
|
const matches = o.match(/\w+\s(asc|desc)/g);
|
||||||
|
if(!!matches) {
|
||||||
|
const out = matches[0].split(/\s/g);
|
||||||
|
SELECT.orderby = [
|
||||||
|
{ ref: [out[0]], sort: out[1] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Primitive literals
|
||||||
|
//
|
||||||
|
// date
|
||||||
|
dateValue "Edm.Date" = val:$( year "-" month "-" day )
|
||||||
|
{ return { val } }
|
||||||
|
year = "-"? ( "0" DIGIT DIGIT DIGIT / oneToNine DIGIT DIGIT DIGIT )
|
||||||
|
yearVal = val:$year { return { val } }
|
||||||
|
month = "0" oneToNine
|
||||||
|
/ "1" ( "0" / "1" / "2" )
|
||||||
|
monthVal = val:$month { return { val } }
|
||||||
|
day = "0" oneToNine
|
||||||
|
/ ( "1" / "2" ) DIGIT
|
||||||
|
/ "3" ( "0" / "1" )
|
||||||
|
dayVal = val:$day { return { val } }
|
||||||
|
oneToNine = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9"
|
||||||
|
|
||||||
|
// datetime offset
|
||||||
|
dateTimeOffsetValue "Edm.DateTimeOffset" = val:$( year "-" month "-" day "T" hour ":" minute ( ":" second ( "." fractionalSeconds )? )? ( "Z" / SIGN hour ":" minute ))
|
||||||
|
{ return { val: (new Date(val)).toISOString() } }
|
||||||
|
hour = ( "0" / "1" ) DIGIT
|
||||||
|
/ "2" ( "0" / "1" / "2" / "3" )
|
||||||
|
hourVal = val:$hour { return { val } }
|
||||||
|
minute = zeroToFiftyNine
|
||||||
|
minuteVal = val:$minute { return { val } }
|
||||||
|
second = zeroToFiftyNine
|
||||||
|
secondVal = val:$second { return { val: parseInt(val).toFixed(3) } }
|
||||||
|
zeroToFiftyNine = ( "0" / "1" / "2" / "3" / "4" / "5" ) DIGIT
|
||||||
|
fractionalSeconds = DIGIT DIGIT? DIGIT? DIGIT?
|
||||||
|
DIGIT? DIGIT? DIGIT? DIGIT?
|
||||||
|
DIGIT? DIGIT? DIGIT? DIGIT?
|
||||||
|
|
||||||
|
// time of day value
|
||||||
|
timeOfDayValue "Edm.TimeOfDay" = val:$( hour ":" minute ( ":" second ( "." fractionalSeconds )? )? )
|
||||||
|
{ return { val } }
|
||||||
|
|
||||||
|
// string
|
||||||
|
strLiteral "Edm.String" = SQUOTE strArgVal:strEntry SQUOTE
|
||||||
|
{ return strArgVal; }
|
||||||
|
strEntry = symbols:( ( SQUOTEInString / pcharNoSQUOTE )* )
|
||||||
|
{ return { val: symbols.join('') }; }
|
||||||
|
|
||||||
|
// field name
|
||||||
|
field "field name"
|
||||||
|
= field:$([a-zA-Z] [_a-zA-Z0-9]*)
|
||||||
|
{ return { ref: [field] }; }
|
||||||
|
|
||||||
|
// number
|
||||||
|
number = val:$(
|
||||||
|
doubleValue /
|
||||||
|
singleValue /
|
||||||
|
decimalValue /
|
||||||
|
int64Value /
|
||||||
|
int32Value /
|
||||||
|
int16Value
|
||||||
|
) {
|
||||||
|
console.log('number', val)
|
||||||
|
return { val: Number(val) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// IEEE 754 binary64 floating-point number (15-17 decimal digits)
|
||||||
|
doubleValue "Edm.Double" = decimalValue ( "e" SIGN? DIGIT+ )? / nanInfinity
|
||||||
|
decimalValue "Edm.Decimal" = SIGN? DIGIT+ ("." DIGIT+)?
|
||||||
|
// IEEE 754 binary32 floating-point number (6-9 decimal digits)
|
||||||
|
singleValue "Edm.Single" = doubleValue
|
||||||
|
// numbers in the range from -9223372036854775808 to 9223372036854775807
|
||||||
|
// contains 1-19 digits
|
||||||
|
int64Value "Edm.Int64" = SIGN? DIGIT DIGIT? DIGIT? DIGIT? DIGIT?
|
||||||
|
DIGIT? DIGIT? DIGIT? DIGIT? DIGIT?
|
||||||
|
DIGIT? DIGIT? DIGIT? DIGIT? DIGIT?
|
||||||
|
DIGIT? DIGIT? DIGIT? DIGIT?
|
||||||
|
// numbers in the range from -2147483648 to 2147483647
|
||||||
|
// contains 1-15 digits
|
||||||
|
int32Value "Emd.Int32"
|
||||||
|
= val:(SIGN? DIGIT DIGIT? DIGIT? DIGIT? DIGIT?
|
||||||
|
DIGIT? DIGIT? DIGIT? DIGIT? DIGIT?
|
||||||
|
DIGIT? DIGIT? DIGIT? DIGIT? DIGIT?)
|
||||||
|
{ return parseInt(val); }
|
||||||
|
// numbers in the range from -32768 to 32767
|
||||||
|
// contains 1-5 digits
|
||||||
|
int16Value "Edm.Int16" = SIGN? DIGIT DIGIT? DIGIT? DIGIT? DIGIT?
|
||||||
|
nanInfinity = 'NaN' / '-INF' / 'INF'
|
||||||
|
|
||||||
|
|
||||||
|
// ---------- URI sintax ----------
|
||||||
|
//
|
||||||
|
otherDelims = "!" / "(" / ")" / "*" / "+" / "," / ";"
|
||||||
|
unreserved = [A-Za-z] / [0-9] / " " / "-" / "." / "_" / "~"
|
||||||
|
pcharNoSQUOTE = val:$(unreserved / otherDelims / "$" / "&" / "=" / ":" / "@")
|
||||||
|
{ return val; }
|
||||||
|
SQUOTEInString = SQUOTE SQUOTE // two consecutive single quotes represent one within a string literal
|
||||||
|
{ return "'"; } // convert double quotation mark to single like in current CAP implementaion
|
||||||
|
|
||||||
|
// ---------- Punctuation ----------
|
||||||
|
//
|
||||||
|
WS "whitespace" = ( SP / HTAB )*
|
||||||
|
|
||||||
|
AT = "@"
|
||||||
|
COLON = ":"
|
||||||
|
COMMA = ","
|
||||||
|
EQ = "="
|
||||||
|
SIGN = "+" / "-"
|
||||||
|
SEMI = ";"
|
||||||
|
STAR = "*"
|
||||||
|
SQUOTE = "'"
|
||||||
|
OPEN = "("
|
||||||
|
CLOSE = ")"
|
||||||
|
|
||||||
|
// ---------- Operators ----------
|
||||||
|
//
|
||||||
|
eqOperator
|
||||||
|
= operatorVal:("eq" / "ne")
|
||||||
|
{ return compOperators[operatorVal]; }
|
||||||
|
numCompOperator
|
||||||
|
= operatorVal:("lt" / "gt" / "le" / "ge")
|
||||||
|
{ return compOperators[operatorVal]; }
|
||||||
|
logicalOperator = operator:$("and" / "or")
|
||||||
|
{ filterExpr.appendWhereClause([operator]); }
|
||||||
|
notOperator
|
||||||
|
= notOperator:$("not")
|
||||||
|
{ filterExpr.appendWhereClause([notOperator]); }
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// ---------- ABNF core definitions ----------
|
||||||
|
//
|
||||||
|
ALPHA "letter" = [A-Za-z]
|
||||||
|
AtoF "A to F" = "A" / "B" / "C" / "D" / "E" / "F"
|
||||||
|
DIGIT "digit" = [0-9]
|
||||||
|
HEXDIG "hexing" = DIGIT / AtoF
|
||||||
|
DQUOTE "double quote" = '"'
|
||||||
|
SP "space" = ' '
|
||||||
|
HTAB "horizontal tab" = ' '
|
||||||
|
WSP "white space" = SP / HTAB
|
||||||
|
BIT = "0" / "1"
|
||||||
|
RWS = ( SP / HTAB )+ // "required" whitespace
|
||||||
|
|
||||||
|
|
||||||
|
//-- Whitespaces
|
||||||
|
o "optional whitespaces" = $[ \t\n]*
|
||||||
11
odata/package.json
Normal file
11
odata/package.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@sap/cds-odata",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@capire/bookshop": "*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"pegjs": "^0.10.0"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
109
odata/test/odata2cqn.test.js
Normal file
109
odata/test/odata2cqn.test.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
const { parse:{cql:CQL}, test:{expect}} = require("@sap/cds/lib")
|
||||||
|
const { parser:{parse:OData} } = require("../lib/odata2cqn")
|
||||||
|
|
||||||
|
describe("$filter", () => {
|
||||||
|
// logger can be omitted
|
||||||
|
// afterEach(function () {
|
||||||
|
// logParserResult(newParserRes, `${this.currentTest.title}-new`);
|
||||||
|
// logParserResult(currentParserRes, `${this.currentTest.title}-cur`);
|
||||||
|
// });
|
||||||
|
|
||||||
|
describe("comparing expressions", () => {
|
||||||
|
|
||||||
|
const types = {
|
||||||
|
strings: "'some string'",
|
||||||
|
'safe integers': 11,
|
||||||
|
'unsafe integers': 2e53,
|
||||||
|
decimals: 0.99,
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
it.each(Object.keys(types))("should support expressions with %s", (t) => {
|
||||||
|
expect (OData(`Foo?$filter=bar eq ${types[t]}`))
|
||||||
|
.to.eql (CQL(`SELECT from Foo where bar = ${types[t]}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
const operators = {
|
||||||
|
eq: '=',
|
||||||
|
lt: '<',
|
||||||
|
le: '<=',
|
||||||
|
gt: '>',
|
||||||
|
ge: '>=',
|
||||||
|
ne: '!=',
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
it.each(Object.keys(operators))("should support comparison operator '%s'", (op) => {
|
||||||
|
expect (OData(`Foo?$filter=bar ${op} 11`))
|
||||||
|
.to.eql (CQL(`SELECT from Foo where bar ${operators[op]} 11`))
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("logical expressions", () => {
|
||||||
|
|
||||||
|
it.each(['and','or'])("should support '%s'", (t) => {
|
||||||
|
expect (OData(`Foo?$filter=bar lt 11 and name eq 'some name'`))
|
||||||
|
.to.eql (CQL(`SELECT from Foo where bar < 11 and name = 'some name'`))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should support 'not'", () => {
|
||||||
|
// REVISIT: We need to check with the Node.js team why they translated that to the equivalent of:
|
||||||
|
// not name like concat('%','sunny','%') escape '^'
|
||||||
|
expect (OData(`Foo?$filter = not contains(name,'sunny')`))
|
||||||
|
.to.eql (CQL(`SELECT from Foo where not name like '%sunny%'`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support group expr", () => {
|
||||||
|
expect (OData(`Foo?$filter = (unitPrice lt 11 and length(name) eq 12) or name eq 'Restless and Wild'`))
|
||||||
|
.to.eql (CQL(`SELECT from Foo where (unitPrice < 11 and length(name) = 12) or name = 'Restless and Wild'`))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("function expressions", () => {
|
||||||
|
|
||||||
|
it("should support contains", () => {
|
||||||
|
expect (OData(`Foo?$filter = contains(name,'sunny')`))
|
||||||
|
.to.eql (CQL(`SELECT from Foo where name like '%sunny%'`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support startswith", () => {
|
||||||
|
expect (OData(`Foo?$filter = startswith(name,'sunny')`))
|
||||||
|
.to.eql (CQL(`SELECT from Foo where name like 'sunny%'`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support endswith", () => {
|
||||||
|
expect (OData(`Foo?$filter = endswith(name,'sunny')`))
|
||||||
|
.to.eql (CQL(`SELECT from Foo where name like '%sunny'`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support length", () => {
|
||||||
|
expect (OData(`Foo?$filter = length(name) lt 11`))
|
||||||
|
.to.eql (CQL(`SELECT from Foo where length(name) < 11`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support indexof", () => {
|
||||||
|
expect (OData(`Foo?$filter = indexof(name,'x') eq 11`))
|
||||||
|
.to.eql (CQL(`SELECT from Foo where locate(name,'x') = 12`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support substring", () => {
|
||||||
|
expect (OData(`Foo?$filter = substring(name,1) eq 'foo'`))
|
||||||
|
.to.eql (CQL(`SELECT from Foo where substring(name,2) = 'foo'`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(['tolower','toupper','trim'])("should support '%s'", (fn) => {
|
||||||
|
expect (OData(`Foo?$filter = ${fn}(name) eq 'foo'`))
|
||||||
|
.to.eql (CQL(`SELECT from Foo where ${fn.replace(/^to/,'')}(name) = 'foo'`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support 'day'", () => {
|
||||||
|
expect (OData(`Foo?$filter = day(name) eq 11`))
|
||||||
|
.to.eql (CQL(`SELECT from Foo where dayofmonth(name) = 11`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support concat", () => {
|
||||||
|
expect (OData(`Foo?$filter = concat(name,'o') eq 'foo'`))
|
||||||
|
.to.eql (CQL(`SELECT from Foo where concat(name,'o') = 'foo'`))
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"@capire/reviews": "./reviews"
|
"@capire/reviews": "./reviews"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"pegjs": "^0.10.0",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"chai-subset": "^1.6.0",
|
"chai-subset": "^1.6.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user