New home of odata2cqn
This commit is contained in:
1
odata/.npmrc
Normal file
1
odata/.npmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@sap:registry=http://nexus.wdf.sap.corp:8081/nexus/repository/build.milestones.npm/
|
||||||
15
odata/.vscode/extensions.json
vendored
Normal file
15
odata/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
|
||||||
|
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
|
||||||
|
|
||||||
|
// List of extensions which should be recommended for users of this workspace.
|
||||||
|
"recommendations": [
|
||||||
|
"SirTobi.pegjs-language",
|
||||||
|
"tamuratak.vscode-pegjs",
|
||||||
|
"joeandaverde.vscode-pegjs-live"
|
||||||
|
],
|
||||||
|
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
1189
odata/etc/odata-url.abnf
Normal file
1189
odata/etc/odata-url.abnf
Normal file
File diff suppressed because it is too large
Load Diff
1162
odata/etc/odata-url.pegjs
Normal file
1162
odata/etc/odata-url.pegjs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
/**
|
/** ------------------------------------------
|
||||||
* Odata Spec http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/abnf/odata-abnf-construction-rules.txt
|
* 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
|
* Future test cases http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/abnf/odata-abnf-testcases.xml
|
||||||
*
|
*
|
||||||
@@ -12,546 +12,178 @@
|
|||||||
* Books/201
|
* Books/201
|
||||||
* Books?$select=ID,title&$expand=author($select=name)&$filter=stock gt 1&$orderby=title
|
* 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) => {
|
// ---------- JavaScript Helpers -------------
|
||||||
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: '>=',
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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 /
|
|
||||||
"$filter=" filter /
|
|
||||||
"$orderby=" orderby
|
|
||||||
)( o'&'o QueryOptions )?
|
|
||||||
|
|
||||||
|
|
||||||
filter
|
|
||||||
= (o { SELECT.where = [] })
|
|
||||||
FilterExprSequence
|
|
||||||
|
|
||||||
|
|
||||||
// ---------- Grouped $filter expression ----------
|
|
||||||
// ----------
|
|
||||||
|
|
||||||
FilterExprSequence = (Expr (SP logicalOperator SP Expr)*)
|
|
||||||
GroupedExpr = (startGroup FilterExprSequence closeGroup)
|
|
||||||
Expr = (
|
|
||||||
(notOperator SP)? ( boolFunc / GroupedExpr )
|
|
||||||
) / ( commonExp )
|
|
||||||
startGroup
|
|
||||||
= OPEN
|
|
||||||
{ SELECT.where.push('(') }
|
|
||||||
closeGroup
|
|
||||||
= CLOSE
|
|
||||||
{ SELECT.where.push(')') }
|
|
||||||
|
|
||||||
|
|
||||||
// ---------- Function expressions ----------
|
|
||||||
// ----------
|
|
||||||
commonExp = val:(
|
|
||||||
timeExpr /
|
|
||||||
secondExpr /
|
|
||||||
minuteExpr /
|
|
||||||
hourExpr /
|
|
||||||
dayExpr /
|
|
||||||
monthExpr /
|
|
||||||
yearExpr /
|
|
||||||
compStrExpr /
|
|
||||||
compareNumExpr
|
|
||||||
)
|
|
||||||
{
|
|
||||||
const res = val.filter(cur => cur !== ' ');
|
|
||||||
SELECT.where.push(...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
|
|
||||||
value:strArg
|
|
||||||
CLOSE
|
|
||||||
{
|
|
||||||
const args = {
|
|
||||||
contains: [ "'%'", value, "'%'" ],
|
|
||||||
endswith: [ "'%'", value ],
|
|
||||||
startswith: [ value, "'%'" ]
|
|
||||||
}[funcName]
|
|
||||||
SELECT.where.push(
|
|
||||||
fieldRef, 'like', {func:'concat', args }, '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 }
|
|
||||||
|
|
||||||
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);
|
const stack=[]
|
||||||
if(!!matches) {
|
let SELECT
|
||||||
const out = matches[0].split(/\s/g);
|
|
||||||
SELECT.orderby = [
|
|
||||||
{ ref: [out[0]], sort: out[1] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Entity Paths ---------------
|
||||||
|
|
||||||
// Primitive literals
|
ODataRelativeURI // Note: case-sensitive!
|
||||||
//
|
= (p:path { SELECT = { from:p } })
|
||||||
// date
|
( o"?"o QueryOption ( o'&'o QueryOption )* )? {
|
||||||
dateValue "Edm.Date" = val:$( year "-" month "-" day )
|
if (SELECT.expand) {
|
||||||
{ return { val } }
|
SELECT.columns = SELECT.expand
|
||||||
year = "-"? ( "0" DIGIT DIGIT DIGIT / oneToNine DIGIT DIGIT DIGIT )
|
delete SELECT.expand
|
||||||
yearVal = val:$year { return { val } }
|
}
|
||||||
month = "0" oneToNine
|
return { SELECT }
|
||||||
/ "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
|
|
||||||
) {
|
|
||||||
return { val: Number(val) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IEEE 754 binary64 floating-point number (15-17 decimal digits)
|
path
|
||||||
doubleValue "Edm.Double" = decimalValue ( "e" SIGN? DIGIT+ )? / nanInfinity
|
= crv:$("$count"/"$ref"/"$value") {return {ref:[crv]}}
|
||||||
decimalValue "Edm.Decimal" = SIGN? DIGIT+ ("." DIGIT+)?
|
/ head:identifier filter:(OPEN args CLOSE)? tail:( '/' p:path {return p} )? {
|
||||||
// IEEE 754 binary32 floating-point number (6-9 decimal digits)
|
const ref = [ filter ? { id:head, where:filter[1] } : head ]
|
||||||
singleValue "Edm.Single" = doubleValue
|
if (tail) ref.push (...tail.ref)
|
||||||
// numbers in the range from -9223372036854775808 to 9223372036854775807
|
return {ref}
|
||||||
// contains 1-19 digits
|
}
|
||||||
int64Value "Edm.Int64" = SIGN? DIGIT DIGIT? DIGIT? DIGIT? DIGIT?
|
|
||||||
DIGIT? DIGIT? DIGIT? DIGIT? DIGIT?
|
args
|
||||||
DIGIT? DIGIT? DIGIT? DIGIT? DIGIT?
|
= val:( number / integer / string ) {return [{val}]}
|
||||||
DIGIT? DIGIT? DIGIT? DIGIT?
|
/ ref:identifier o"="o val:( number / integer / string ) more:( COMMA args )? {
|
||||||
// numbers in the range from -2147483648 to 2147483647
|
const args = [ {ref}, '=', {val} ]
|
||||||
// contains 1-15 digits
|
if (more) args.push ('and', ...more[1])
|
||||||
int32Value "Emd.Int32"
|
return args
|
||||||
= val:(SIGN? DIGIT DIGIT? DIGIT? DIGIT? DIGIT?
|
}
|
||||||
DIGIT? DIGIT? DIGIT? DIGIT? DIGIT?
|
|
||||||
DIGIT? DIGIT? DIGIT? DIGIT? DIGIT?)
|
ref "a reference"
|
||||||
{ return parseInt(val); }
|
= head:identifier tail:( '/' identifier )* {
|
||||||
// numbers in the range from -32768 to 32767
|
return { ref:[ head, ...tail ] }
|
||||||
// contains 1-5 digits
|
}
|
||||||
int16Value "Edm.Int16" = SIGN? DIGIT DIGIT? DIGIT? DIGIT? DIGIT?
|
|
||||||
nanInfinity = 'NaN' / '-INF' / 'INF'
|
//
|
||||||
|
// ---------- Query Options ------------
|
||||||
|
|
||||||
|
QueryOption = ExpandOption
|
||||||
|
ExpandOption =
|
||||||
|
"$select=" o select ( COMMA select )* /
|
||||||
|
"$expand=" o expand ( COMMA expand )* /
|
||||||
|
"$filter=" o filter /
|
||||||
|
"$orderby=" o orderby /
|
||||||
|
"$top=" o top /
|
||||||
|
"$skip=" o skip /
|
||||||
|
"$search=" o search /
|
||||||
|
"$count=" o count
|
||||||
|
|
||||||
|
select
|
||||||
|
= col:ref {
|
||||||
|
(SELECT.expand || (SELECT.expand = [])).push(col)
|
||||||
|
return col
|
||||||
|
}
|
||||||
|
|
||||||
|
expand =
|
||||||
|
( c:select {c.expand='*'} )
|
||||||
|
( // --- nested query options, if any
|
||||||
|
(OPEN {
|
||||||
|
stack.push (SELECT)
|
||||||
|
SELECT = SELECT.expand[SELECT.expand.length-1]
|
||||||
|
SELECT.expand = []
|
||||||
|
})
|
||||||
|
ExpandOption ( o";"o ExpandOption )*
|
||||||
|
(CLOSE {
|
||||||
|
SELECT = stack.pop()
|
||||||
|
})
|
||||||
|
)? // --- end of nested query options
|
||||||
|
( COMMA expand )?
|
||||||
|
|
||||||
|
top
|
||||||
|
= val:integer {
|
||||||
|
(SELECT.limit || (SELECT.limit={})).rows = {val}
|
||||||
|
}
|
||||||
|
|
||||||
|
skip
|
||||||
|
= val:integer {
|
||||||
|
(SELECT.limit || (SELECT.limit={})).offset = {val}
|
||||||
|
}
|
||||||
|
|
||||||
|
search
|
||||||
|
= p:search_clause {SELECT.search = p}
|
||||||
|
search_clause = p:( n:NOT? {return n?[n]:[]} )(
|
||||||
|
OPEN xpr:search_clause CLOSE {p.push({xpr})}
|
||||||
|
/ val:(identifier/string) {p.push({val})}
|
||||||
|
)( ao:(AND/OR) more:search_clause {p.push(ao,...more)} )*
|
||||||
|
{return p}
|
||||||
|
|
||||||
|
filter
|
||||||
|
= p:where_clause {SELECT.where = p}
|
||||||
|
where_clause = p:( n:NOT? {return n?[n]:[]} )(
|
||||||
|
OPEN xpr:where_clause CLOSE {p.push({xpr})}
|
||||||
|
/ comp:comparison {p.push(...comp)}
|
||||||
|
/ func:boolish {p.push(func)}
|
||||||
|
)( ao:(AND/OR) more:where_clause {p.push(ao,...more)} )*
|
||||||
|
{return p}
|
||||||
|
|
||||||
|
orderby
|
||||||
|
= ref:ref sort:( _ s:$("asc"/"desc") {return s})? {
|
||||||
|
SELECT.orderby = $(ref, sort && {sort})
|
||||||
|
}
|
||||||
|
|
||||||
|
count
|
||||||
|
= c:$[^,?&()]+ { SELECT.count = true }
|
||||||
|
|
||||||
|
//
|
||||||
|
// ---------- Expressions ------------
|
||||||
|
|
||||||
|
|
||||||
// ---------- URI sintax ----------
|
comparison "a comparison"
|
||||||
//
|
= a:operand _ o:$("eq"/"ne"/"lt"/"gt"/"le"/"ge") _ b:operand {
|
||||||
otherDelims = "!" / "(" / ")" / "*" / "+" / "," / ";"
|
const op = { eq:'=', ne:'!=', lt:'<', gt:'>', le:'<=', ge:'>=' }[o]||o
|
||||||
unreserved = [A-Za-z] / [0-9] / " " / "-" / "." / "_" / "~"
|
return [ a, op, b ]
|
||||||
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
|
|
||||||
|
|
||||||
|
operand "an operand"
|
||||||
|
= val:number {return {val}}
|
||||||
|
/ val:string {return {val}}
|
||||||
|
/ function
|
||||||
|
/ ref
|
||||||
|
|
||||||
|
function "a function call"
|
||||||
|
= func:$[a-z]+ OPEN a:operand more:( COMMA o:operand {return o} )* CLOSE
|
||||||
|
{ return { func, args:[a,...more] }}
|
||||||
|
|
||||||
|
boolish "a boolean function"
|
||||||
|
= func:("contains"/"endswith"/"startswith") OPEN a:operand COMMA b:operand CLOSE
|
||||||
|
{ return { func, args:[a,b] }}
|
||||||
|
|
||||||
|
NOT = o "not"i _ {return 'not'}
|
||||||
|
AND = _ "and"i _ {return 'and'}
|
||||||
|
OR = _ "or"i _ {return 'or'}
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// ---------- Literals -----------
|
||||||
|
|
||||||
|
string "Edm.String"
|
||||||
|
= "'" s:$("''"/[^'])* "'"
|
||||||
|
{return s.replace(/''/g,"'")}
|
||||||
|
|
||||||
|
number
|
||||||
|
= x:$( [+-]? [0-9]+ ("."[0-9]+)? ("e"[0-9]+)? )
|
||||||
|
{return Number(x)}
|
||||||
|
|
||||||
|
integer
|
||||||
|
= x:$( [+-]? [0-9]+ )
|
||||||
|
{return parseInt(x)}
|
||||||
|
|
||||||
|
identifier
|
||||||
|
= $([a-zA-Z][_a-zA-Z0-9]*)
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
// ---------- Punctuation ----------
|
// ---------- Punctuation ----------
|
||||||
//
|
|
||||||
WS "whitespace" = ( SP / HTAB )*
|
|
||||||
|
|
||||||
AT = "@"
|
COLON = o":"o
|
||||||
COLON = ":"
|
COMMA = o","o
|
||||||
COMMA = ","
|
SEMI = o";"o
|
||||||
EQ = "="
|
OPEN = o"("o
|
||||||
SIGN = "+" / "-"
|
CLOSE = o")"
|
||||||
SEMI = ";"
|
|
||||||
STAR = "*"
|
|
||||||
SQUOTE = "'"
|
|
||||||
OPEN = "("
|
|
||||||
CLOSE = ")"
|
|
||||||
|
|
||||||
// ---------- Operators ----------
|
//
|
||||||
//
|
// ---------- Whitespaces -----------
|
||||||
eqOperator
|
|
||||||
= operatorVal:("eq" / "ne")
|
|
||||||
{ return compOperators[operatorVal]; }
|
|
||||||
numCompOperator
|
|
||||||
= operatorVal:("lt" / "gt" / "le" / "ge")
|
|
||||||
{ return compOperators[operatorVal]; }
|
|
||||||
logicalOperator = operator:$("and" / "or")
|
|
||||||
{ SELECT.where.push(operator); }
|
|
||||||
notOperator
|
|
||||||
= "not"
|
|
||||||
{ SELECT.where.push('not') }
|
|
||||||
|
|
||||||
|
o "optional whitespaces" = $[ \t\n]*
|
||||||
|
_ "mandatory whitespaces" = $[ \t\n]+
|
||||||
|
|
||||||
//
|
//
|
||||||
// ---------- 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]*
|
|
||||||
_ "mandatory whitespaces" = $[ \t\n]+
|
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
"name": "@sap/cds-odata",
|
"name": "@sap/cds-odata",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/bookshop": "*"
|
"@sap/cds-compiler": "latest",
|
||||||
|
"@sap/cds": "^4.4.10"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "rm -fr node_modules/@sap/cds/node_modules"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"pegjs": "^0.10.0"
|
"pegjs": "^0.10.0"
|
||||||
|
|||||||
@@ -2,19 +2,13 @@ const { parse:{cql:CQL}, test:{expect}} = require("@sap/cds/lib")
|
|||||||
const { parser:{parse:OData} } = require("../lib/odata2cqn")
|
const { parser:{parse:OData} } = require("../lib/odata2cqn")
|
||||||
|
|
||||||
describe("$filter", () => {
|
describe("$filter", () => {
|
||||||
// logger can be omitted
|
|
||||||
// afterEach(function () {
|
|
||||||
// logParserResult(newParserRes, `${this.currentTest.title}-new`);
|
|
||||||
// logParserResult(currentParserRes, `${this.currentTest.title}-cur`);
|
|
||||||
// });
|
|
||||||
|
|
||||||
describe("comparing expressions", () => {
|
describe("comparing expressions", () => {
|
||||||
|
|
||||||
const types = {
|
const types = {
|
||||||
strings: "'some string'",
|
strings: "'some string'",
|
||||||
'safe integers': 11,
|
integers: 11,
|
||||||
'unsafe integers': 2e53,
|
// decimals: 0.99, //> REVISIT: wait for compiler v2.0.4 ?
|
||||||
decimals: 0.99,
|
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
it.each(Object.keys(types))("should support expressions with %s", (t) => {
|
it.each(Object.keys(types))("should support expressions with %s", (t) => {
|
||||||
@@ -41,20 +35,21 @@ describe("$filter", () => {
|
|||||||
describe("logical expressions", () => {
|
describe("logical expressions", () => {
|
||||||
|
|
||||||
it.each(['and','or'])("should support '%s'", (t) => {
|
it.each(['and','or'])("should support '%s'", (t) => {
|
||||||
expect (OData(`Foo?$filter=bar lt 11 and name eq 'some name'`))
|
expect (OData(`Foo?$filter=bar lt 11 ${t} name eq 'some name'`))
|
||||||
.to.eql (CQL(`SELECT from Foo where bar < 11 and name = 'some name'`))
|
.to.eql (CQL(`SELECT from Foo where bar < 11 ${t} name = 'some name'`))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should support 'not'", () => {
|
it("should support 'not'", () => {
|
||||||
// REVISIT: We need to check with the Node.js team why they translated that to the equivalent of:
|
// REVISIT: We need to check with the Node.js team why they translated that to the equivalent of:
|
||||||
// not name like concat('%','sunny','%') escape '^'
|
// not name like concat('%','sunny','%') escape '^'
|
||||||
expect (OData(`Foo?$filter= not contains(name,'sunny')`))
|
expect (OData(`Foo?$filter= not contains(name,'sunny')`))
|
||||||
.to.eql (CQL(`SELECT from Foo where not name like '%sunny%'`))
|
.to.eql (CQL(`SELECT from Foo where not contains(name,'sunny')`))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// REVISIT: wait for compiler v2
|
||||||
it("should support group expr", () => {
|
it("should support group expr", () => {
|
||||||
expect (OData(`Foo?$filter= (unitPrice lt 11 and length(name) eq 12) or name eq 'Restless and Wild'`))
|
expect (OData(`Foo?$filter= (unitPrice gt 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'`))
|
.to.eql (CQL(`SELECT from Foo where (unitPrice > 11 and length(name) = 12) or name = 'Restless and Wild'`))
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,17 +57,17 @@ describe("$filter", () => {
|
|||||||
|
|
||||||
it("should support contains", () => {
|
it("should support contains", () => {
|
||||||
expect (OData(`Foo?$filter= contains(name,'sunny')`))
|
expect (OData(`Foo?$filter= contains(name,'sunny')`))
|
||||||
.to.eql (CQL(`SELECT from Foo where name like '%sunny%'`))
|
.to.eql (CQL(`SELECT from Foo where contains(name,'sunny')`))
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support startswith", () => {
|
it("should support startswith", () => {
|
||||||
expect (OData(`Foo?$filter= startswith(name,'sunny')`))
|
expect (OData(`Foo?$filter= startswith(name,'sunny')`))
|
||||||
.to.eql (CQL(`SELECT from Foo where name like 'sunny%'`))
|
.to.eql (CQL(`SELECT from Foo where startswith(name,'sunny')`))
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support endswith", () => {
|
it("should support endswith", () => {
|
||||||
expect (OData(`Foo?$filter= endswith(name,'sunny')`))
|
expect (OData(`Foo?$filter= endswith(name,'sunny')`))
|
||||||
.to.eql (CQL(`SELECT from Foo where name like '%sunny'`))
|
.to.eql (CQL(`SELECT from Foo where endswith(name,'sunny')`))
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support length", () => {
|
it("should support length", () => {
|
||||||
@@ -82,7 +77,7 @@ describe("$filter", () => {
|
|||||||
|
|
||||||
it("should support indexof", () => {
|
it("should support indexof", () => {
|
||||||
expect (OData(`Foo?$filter= indexof(name,'x') eq 11`))
|
expect (OData(`Foo?$filter= indexof(name,'x') eq 11`))
|
||||||
.to.eql (CQL(`SELECT from Foo where locate(name,'x') = 11`))
|
.to.eql (CQL(`SELECT from Foo where indexof(name,'x') = 11`))
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support substring", () => {
|
it("should support substring", () => {
|
||||||
@@ -92,12 +87,12 @@ describe("$filter", () => {
|
|||||||
|
|
||||||
it.each(['tolower','toupper','trim'])("should support '%s'", (fn) => {
|
it.each(['tolower','toupper','trim'])("should support '%s'", (fn) => {
|
||||||
expect (OData(`Foo?$filter= ${fn}(name) eq 'foo'`))
|
expect (OData(`Foo?$filter= ${fn}(name) eq 'foo'`))
|
||||||
.to.eql (CQL(`SELECT from Foo where ${fn.replace(/^to/,'')}(name) = 'foo'`))
|
.to.eql (CQL(`SELECT from Foo where ${fn}(name) = 'foo'`))
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support 'day'", () => {
|
it("should support 'day'", () => {
|
||||||
expect (OData(`Foo?$filter= day(name) eq 11`))
|
expect (OData(`Foo?$filter= day(name) eq 11`))
|
||||||
.to.eql (CQL(`SELECT from Foo where dayofmonth(name) = 11`))
|
.to.eql (CQL(`SELECT from Foo where day(name) = 11`))
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support concat", () => {
|
it("should support concat", () => {
|
||||||
@@ -106,4 +101,5 @@ describe("$filter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user