Compare commits
10 Commits
trying-esm
...
odata
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddb962dcc3 | ||
|
|
5e52d4633a | ||
|
|
24dd1164cc | ||
|
|
f3f554396c | ||
|
|
a11aadc8f0 | ||
|
|
cfc5a56d5a | ||
|
|
098b27330a | ||
|
|
683b785ac5 | ||
|
|
2782cf0d6d | ||
|
|
fd97e3bda9 |
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
26
odata/lib/index.js
Normal file
26
odata/lib/index.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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 = {
|
||||||
|
parse: {
|
||||||
|
url: parser.parse,
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
cqn: parser.parse,
|
||||||
|
url: (cqn) => pending(cqn)
|
||||||
|
},
|
||||||
|
serialize: (data) => pending(data),
|
||||||
|
deserialize: (body) => pending(body),
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = ()=>{
|
||||||
|
throw new Error ('Not yet implemented')
|
||||||
|
}
|
||||||
200
odata/lib/odata2cqn.pegjs
Normal file
200
odata/lib/odata2cqn.pegjs
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
/** ------------------------------------------
|
||||||
|
* This is a peg.js adaptation of the https://github.com/oasis-tcs/odata-abnf/blob/master/abnf/odata-abnf-construction-rules.txt
|
||||||
|
* which directly constructs CQN out of parsed sources.
|
||||||
|
*
|
||||||
|
* NOTE:
|
||||||
|
* In contrast to the OData ABNF source, which uses very detailedsemantic rules,
|
||||||
|
* this adaptation uses rather generic syntactic rules only, e.g. not distinguishing
|
||||||
|
* betwenn Collection Navigation or not knowing individual function names.
|
||||||
|
* This is to be open to future enhancements of the OData standard, as well as
|
||||||
|
* to improve error messages. For example a typo in a function name could be
|
||||||
|
* reported specifically instead of throwing a generic parser error.
|
||||||
|
*
|
||||||
|
* See also: https://docs.microsoft.com/en-us/odata/concepts/queryoptions-overview
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ---------- JavaScript Helpers -------------
|
||||||
|
{
|
||||||
|
const stack=[]
|
||||||
|
let SELECT
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Entity Paths ---------------
|
||||||
|
|
||||||
|
ODataRelativeURI // Note: case-sensitive!
|
||||||
|
= (p:path { SELECT = { from:p } })
|
||||||
|
( o"?"o QueryOption ( o'&'o QueryOption )* )? {
|
||||||
|
if (SELECT.expand) {
|
||||||
|
SELECT.columns = SELECT.expand
|
||||||
|
delete SELECT.expand
|
||||||
|
}
|
||||||
|
return { SELECT }
|
||||||
|
}
|
||||||
|
|
||||||
|
path
|
||||||
|
= crv:$("$count"/"$ref"/"$value") {return {ref:[crv]}}
|
||||||
|
/ head:identifier filter:(OPEN args CLOSE)? tail:( '/' p:path {return p} )? {
|
||||||
|
const ref = [ filter ? { id:head, where:filter[1] } : head ]
|
||||||
|
if (tail) ref.push (...tail.ref)
|
||||||
|
return {ref}
|
||||||
|
}
|
||||||
|
|
||||||
|
args
|
||||||
|
= val:( number / integer / string ) {return [{val}]}
|
||||||
|
/ ref:identifier o"="o val:( number / integer / string ) more:( COMMA args )? {
|
||||||
|
const args = [ {ref}, '=', {val} ]
|
||||||
|
if (more) args.push ('and', ...more[1])
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
ref "a reference"
|
||||||
|
= head:identifier tail:( '/' identifier )* {
|
||||||
|
return { ref:[ head, ...tail ] }
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// ---------- 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 ------------
|
||||||
|
|
||||||
|
|
||||||
|
comparison "a comparison"
|
||||||
|
= a:operand _ o:$("eq"/"ne"/"lt"/"gt"/"le"/"ge") _ b:operand {
|
||||||
|
const op = { eq:'=', ne:'!=', lt:'<', gt:'>', le:'<=', ge:'>=' }[o]||o
|
||||||
|
return [ a, op, b ]
|
||||||
|
}
|
||||||
|
|
||||||
|
operand "an operand"
|
||||||
|
= val:number {return Number.isSafeInteger(val) ? {val} : { val:String(val), literal:'number' }}
|
||||||
|
/ 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 ----------
|
||||||
|
|
||||||
|
COLON = o":"o
|
||||||
|
COMMA = o","o
|
||||||
|
SEMI = o";"o
|
||||||
|
OPEN = o"("o
|
||||||
|
CLOSE = o")"
|
||||||
|
|
||||||
|
//
|
||||||
|
// ---------- Whitespaces -----------
|
||||||
|
|
||||||
|
o "optional whitespaces" = $[ \t\n]*
|
||||||
|
_ "mandatory whitespaces" = $[ \t\n]+
|
||||||
|
|
||||||
|
//
|
||||||
|
// ------------------------------------
|
||||||
15
odata/package.json
Normal file
15
odata/package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "@sap/cds-odata",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@sap/cds-compiler": "latest",
|
||||||
|
"@sap/cds": "^4.4.10"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "rm -fr node_modules/@sap/cds/node_modules"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"pegjs": "^0.10.0"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
105
odata/test/odata2cqn.test.js
Normal file
105
odata/test/odata2cqn.test.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
const cds = require("@sap/cds/lib"), {expect} = cds.test
|
||||||
|
cds.odata = require("../lib")
|
||||||
|
|
||||||
|
describe("$filter", () => {
|
||||||
|
|
||||||
|
describe("comparing expressions", () => {
|
||||||
|
|
||||||
|
const types = {
|
||||||
|
strings: "'some string'",
|
||||||
|
integers: 11,
|
||||||
|
decimals: 0.99,
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
it.each(Object.keys(types))("should support expressions with %s", (t) => {
|
||||||
|
expect (cds.odata.parse.url(`Foo?$filter=bar eq ${types[t]}`))
|
||||||
|
.to.eql (cds.parse.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 (cds.odata.parse.url(`Foo?$filter=bar ${op} 11`))
|
||||||
|
.to.eql (cds.parse.cql(`SELECT from Foo where bar ${operators[op]} 11`))
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("logical expressions", () => {
|
||||||
|
|
||||||
|
it.each(['and','or'])("should support '%s'", (t) => {
|
||||||
|
expect (cds.odata.parse.url(`Foo?$filter=bar lt 11 ${t} name eq 'some name'`))
|
||||||
|
.to.eql (cds.parse.cql(`SELECT from Foo where bar < 11 ${t} 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 (cds.odata.parse.url(`Foo?$filter= not contains(name,'sunny')`))
|
||||||
|
.to.eql (cds.parse.cql(`SELECT from Foo where not contains(name,'sunny')`))
|
||||||
|
});
|
||||||
|
|
||||||
|
// REVISIT: wait for compiler v2
|
||||||
|
it("should support group expr", () => {
|
||||||
|
expect (cds.odata.parse.url(`Foo?$filter= (unitPrice gt 11 and length(name) eq 12) or name eq 'Restless and Wild'`))
|
||||||
|
.to.eql (cds.parse.cql(`SELECT from Foo where (unitPrice > 11 and length(name) = 12) or name = 'Restless and Wild'`))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("function expressions", () => {
|
||||||
|
|
||||||
|
it("should support contains", () => {
|
||||||
|
expect (cds.odata.parse.url(`Foo?$filter= contains(name,'sunny')`))
|
||||||
|
.to.eql (cds.parse.cql(`SELECT from Foo where contains(name,'sunny')`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support startswith", () => {
|
||||||
|
expect (cds.odata.parse.url(`Foo?$filter= startswith(name,'sunny')`))
|
||||||
|
.to.eql (cds.parse.cql(`SELECT from Foo where startswith(name,'sunny')`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support endswith", () => {
|
||||||
|
expect (cds.odata.parse.url(`Foo?$filter= endswith(name,'sunny')`))
|
||||||
|
.to.eql (cds.parse.cql(`SELECT from Foo where endswith(name,'sunny')`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support length", () => {
|
||||||
|
expect (cds.odata.parse.url(`Foo?$filter= length(name) lt 11`))
|
||||||
|
.to.eql (cds.parse.cql(`SELECT from Foo where length(name) < 11`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support indexof", () => {
|
||||||
|
expect (cds.odata.parse.url(`Foo?$filter= indexof(name,'x') eq 11`))
|
||||||
|
.to.eql (cds.parse.cql(`SELECT from Foo where indexof(name,'x') = 11`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support substring", () => {
|
||||||
|
expect (cds.odata.parse.url(`Foo?$filter= substring(name,1) eq 'foo'`))
|
||||||
|
.to.eql (cds.parse.cql(`SELECT from Foo where substring(name,1) = 'foo'`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(['tolower','toupper','trim'])("should support '%s'", (fn) => {
|
||||||
|
expect (cds.odata.parse.url(`Foo?$filter= ${fn}(name) eq 'foo'`))
|
||||||
|
.to.eql (cds.parse.cql(`SELECT from Foo where ${fn}(name) = 'foo'`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support 'day'", () => {
|
||||||
|
expect (cds.odata.parse.url(`Foo?$filter= day(name) eq 11`))
|
||||||
|
.to.eql (cds.parse.cql(`SELECT from Foo where day(name) = 11`))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support concat", () => {
|
||||||
|
expect (cds.odata.parse.url(`Foo?$filter= concat(name,'o') eq 'foo'`))
|
||||||
|
.to.eql (cds.parse.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