Compare commits
24 Commits
containmen
...
odata
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddb962dcc3 | ||
|
|
5e52d4633a | ||
|
|
24dd1164cc | ||
|
|
f3f554396c | ||
|
|
a11aadc8f0 | ||
|
|
cfc5a56d5a | ||
|
|
098b27330a | ||
|
|
683b785ac5 | ||
|
|
2782cf0d6d | ||
|
|
fd97e3bda9 | ||
|
|
6a4af929f1 | ||
|
|
5b966c503c | ||
|
|
75628b6096 | ||
|
|
c12e516f5d | ||
|
|
01073fd6a5 | ||
|
|
dc72442764 | ||
|
|
7e04f50852 | ||
|
|
ea6e274810 | ||
|
|
86e5c429bd | ||
|
|
f32398ba8d | ||
|
|
684c2d53f1 | ||
|
|
b4594e23c5 | ||
|
|
b6028721af | ||
|
|
e15a6192b6 |
@@ -21,6 +21,7 @@
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"require-atomic-updates": "off"
|
||||
"require-atomic-updates": "off",
|
||||
"require-await":"warn"
|
||||
}
|
||||
}
|
||||
|
||||
26
README.md
26
README.md
@@ -5,16 +5,20 @@ Find here a collection of samples for the [SAP Cloud Application Programming Mod
|
||||

|
||||
[](https://api.reuse.software/info/github.com/SAP-samples/cloud-cap-samples)
|
||||
|
||||
|
||||
### Preliminaries
|
||||
|
||||
1. [Install @sap/cds-dk](https://cap.cloud.sap/docs/get-started/) as documented in [capire](https://cap.cloud.sap)
|
||||
2. _Optional:_ [Use Visual Studio Code](https://cap.cloud.sap/docs/get-started/in-vscode)
|
||||
1. Install [**@sap/cds-dk**](https://cap.cloud.sap/docs/get-started/) globally:
|
||||
|
||||
```sh
|
||||
npm i -g @sap/cds-dk
|
||||
```
|
||||
|
||||
2. _Optional:_ [Use Visual Studio Code](https://cap.cloud.sap/docs/get-started/tools#vscode)
|
||||
|
||||
### Download
|
||||
|
||||
Clone this repo as shown below, if you have [git](https://git-scm.com/downloads) installed,
|
||||
otherwise [download as zip file](archive/master.zip).
|
||||
If you have [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/master.zip).
|
||||
|
||||
```sh
|
||||
git clone https://github.com/sap-samples/cloud-cap-samples samples
|
||||
@@ -39,26 +43,30 @@ cds watch bookshop
|
||||
|
||||
After that open this link in your browser: [http://localhost:4004](http://localhost:4004)
|
||||
|
||||
When asked to log in, type `alice` as user and leave the password field blank, which is the [default user](https://cap.cloud.sap/docs/node.js/authentication#mocked).
|
||||
|
||||
### Testing
|
||||
|
||||
Run the provided tests with [_jest_](http://jestjs.io) or [_mocha_](http://mochajs.org), for example:
|
||||
|
||||
```sh
|
||||
npx jest
|
||||
```
|
||||
> While mocha is a bit smaller and faster, jest runs tests in parallel and isolation, which allows to run all tests.
|
||||
|
||||
|
||||
### Serve `npm`
|
||||
### Serve `npm`
|
||||
|
||||
We've simple npm registry mock included which allows you to do an `npm install @capire/<package>` anywhere locally. Use it as follows:
|
||||
We've included a simple npm registry mock, which allows you to do an `npm install @capire/<package>` locally. Use it as follows:
|
||||
|
||||
1. Start the @capire registry:
|
||||
```sh
|
||||
npm run registry
|
||||
```
|
||||
> While running this will have `@capire:registry=http://localhost:4444` set with npmrc.
|
||||
> While running this will have `@capire:registry=http://localhost:4444` set with npmrc.
|
||||
|
||||
2. Install one of the @capire packages wherever you like, for example:
|
||||
|
||||
2. Install one of the @capire packages wherever you like, e.g.:
|
||||
```sh
|
||||
npm add @capire/common @capire/bookshop
|
||||
```
|
||||
@@ -72,4 +80,4 @@ In case you have a question, find a bug, or otherwise need support, please use o
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSES/Apache-2.0.txt) file.
|
||||
Copyright (c) 2021 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSES/Apache-2.0.txt) file.
|
||||
|
||||
@@ -5,10 +5,10 @@ This stand-alone sample introduces the essential tasks in the development of CAP
|
||||
## Hypothetical Use Cases
|
||||
|
||||
1. Build a service that allows to browse _Books_ and _Authors_.
|
||||
2. Books have assigned _Genres_ which are organized hierarchically.
|
||||
2. Books have assigned _Genres_, which are organized hierarchically.
|
||||
3. All users may browse books without login.
|
||||
4. All entries are maintained by Administrators.
|
||||
5. End users may order books (the actual order mgmt being out of scope)
|
||||
5. End users may order books (the actual order mgmt being out of scope).
|
||||
|
||||
## Running the Sample
|
||||
|
||||
@@ -20,12 +20,12 @@ npm run watch
|
||||
|
||||
| Links to capire | Sample files / folders |
|
||||
| --------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
||||
| [Project Setup and Layouts](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) | [`./`](./) |
|
||||
| [Defining Domain Models](https://cap.cloud.sap/docs/guides/domain-models) | [`./db/schema.cds`](./db/schema.cds) |
|
||||
| [Defining Services](https://cap.cloud.sap/docs/guides/providing-services) | [`./srv/*.cds`](./srv) |
|
||||
| [Single-purposed Services](https://cap.cloud.sap/docs/guides/providing-services#single-purposed-services) | [`./srv/*.cds`](./srv) |
|
||||
| [Generic Providers](https://cap.cloud.sap/docs/guides/providing-services) | http://localhost:4004 |
|
||||
| Using Databases | [`./db/data/*.csv`](./db/data) |
|
||||
| [Project Setup & Layouts](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) | [`./`](./) |
|
||||
| [Domain Modeling with CDS](https://cap.cloud.sap/docs/guides/domain-models) | [`./db/schema.cds`](./db/schema.cds) |
|
||||
| [Defining Services](https://cap.cloud.sap/docs/guides/services#defining-services) | [`./srv/*.cds`](./srv) |
|
||||
| [Single-purposed Services](https://cap.cloud.sap/docs/guides/services#single-purposed-services) | [`./srv/*.cds`](./srv) |
|
||||
| [Providing & Consuming Providers](https://cap.cloud.sap/docs/guides/providing-services) | http://localhost:4004 |
|
||||
| [Using Databases](https://cap.cloud.sap/docs/guides/databases) | [`./db/data/*.csv`](./db/data) |
|
||||
| [Adding Custom Logic](https://cap.cloud.sap/docs/guides/service-impl) | [`./srv/*.js`](./srv) |
|
||||
| Adding Tests | [`./test`](./test) |
|
||||
| [Sharing for Reuse](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) | [`./index.cds`](./index.cds) |
|
||||
| Adding Tests | [`./test`](./test) |
|
||||
| [Sharing for Reuse](https://cap.cloud.sap/docs/guides/reuse-and-compose) | [`./index.cds`](./index.cds) |
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using { sap.capire.bookshop as my } from '../db/schema';
|
||||
service CatalogService @(path:'/browse') {
|
||||
|
||||
@readonly entity Books as SELECT from my.Books {*,
|
||||
@readonly entity Books as SELECT from my.Books { *,
|
||||
author.name as author
|
||||
} excluding { createdBy, modifiedBy };
|
||||
|
||||
@readonly entity ListOfBooks as SELECT from Books
|
||||
excluding { descr, stock };
|
||||
excluding { descr };
|
||||
|
||||
@requires: 'authenticated-user'
|
||||
action submitOrder ( book: Books:ID, amount: Integer ) returns { stock: Integer };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const cds = require('@sap/cds')
|
||||
const { Books } = cds.entities ('sap.capire.bookshop')
|
||||
|
||||
class CatalogService extends cds.ApplicationService { async init(){
|
||||
class CatalogService extends cds.ApplicationService { init(){
|
||||
|
||||
// Reduce stock of ordered books if available stock suffices
|
||||
this.on ('submitOrder', async req => {
|
||||
@@ -9,7 +9,7 @@ class CatalogService extends cds.ApplicationService { async init(){
|
||||
let {stock} = await tx.read('stock').from(Books,book)
|
||||
if (stock >= amount) {
|
||||
await tx.update (Books,book).with ({ stock: stock -= amount })
|
||||
this.emit ('OrderedBook', { book, amount, buyer:req.user.id })
|
||||
await this.emit ('OrderedBook', { book, amount, buyer:req.user.id })
|
||||
return { stock }
|
||||
}
|
||||
else return req.error (409,`${amount} exceeds stock for book #${book}`)
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"name": "@capire/common",
|
||||
"version": "1.0.0"
|
||||
"description": "Provides a pre-built extension package for std @sap/cds/common",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@sap/cds": "latest"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
applications: {
|
||||
"browse-books": {
|
||||
title: "Browse Books",
|
||||
description: "... testing FE v42",
|
||||
description: "w/ SAP Fiori Elements",
|
||||
additionalInformation: "SAPUI5.Component=bookshop",
|
||||
applicationType : "URL",
|
||||
url: "/browse/webapp",
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"manage-books": {
|
||||
title: "Manage Books",
|
||||
description: "... testing FE v42",
|
||||
description: "w/ SAP Fiori Elements",
|
||||
additionalInformation: "SAPUI5.Component=admin",
|
||||
applicationType : "URL",
|
||||
url: "/admin/webapp",
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"manage-orders": {
|
||||
title: "Manage Orders",
|
||||
description: "... testing FE v42",
|
||||
description: "w/ SAP Fiori Elements",
|
||||
additionalInformation: "SAPUI5.Component=orders",
|
||||
applicationType : "URL",
|
||||
url: "/orders/webapp",
|
||||
@@ -40,8 +40,7 @@
|
||||
</script>
|
||||
|
||||
<script id="sap-ushell-bootstrap" src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
|
||||
<!-- <script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js" -->
|
||||
<script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/1.78.6/resources/sap-ui-core.js"
|
||||
<script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
|
||||
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
|
||||
data-sap-ui-compatVersion="edge"
|
||||
data-sap-ui-theme="sap_fiori_3"
|
||||
|
||||
@@ -19,7 +19,7 @@ extend Books with {
|
||||
// Extend Orders with Books as Products
|
||||
//
|
||||
|
||||
using { sap.capire.orders.OrderItems } from '@capire/orders';
|
||||
extend OrderItems with {
|
||||
using { sap.capire.orders.Orders_Items } from '@capire/orders';
|
||||
extend Orders_Items with {
|
||||
book : Association to Books on product.ID = book.ID
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ module.exports = async()=>{ // called by server.js
|
||||
//
|
||||
// Reduce stock of ordered books for orders are created from Orders admin UI
|
||||
//
|
||||
OrdersService.on ('OrderChanged', async (msg) => {
|
||||
OrdersService.on ('OrderChanged', (msg) => {
|
||||
console.debug ('> received:', msg.event, msg.data)
|
||||
const { product, deltaAmount } = msg.data
|
||||
return UPDATE (Books) .where ('ID =', product)
|
||||
|
||||
@@ -42,7 +42,24 @@ GET {{bookshop}}/browse/Books(201)?
|
||||
|
||||
#################################################
|
||||
#
|
||||
# Orders Service
|
||||
# Orders Service, incl. draft choreography
|
||||
#
|
||||
@newOrderID = e939604c-ab83-4d4f-bdb6-95fe30b3773e
|
||||
|
||||
GET {{bookshop}}/orders/Orders
|
||||
|
||||
### Create order, still inactive
|
||||
POST {{bookshop}}/orders/Orders
|
||||
Content-Type: application/json
|
||||
|
||||
{"ID": "{{newOrderID}}"}
|
||||
|
||||
### Get inactive order. We have to specify `IsActiveEntity`.
|
||||
GET {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=false)
|
||||
|
||||
### Activate order using `.../<servicename>.draftActivate`
|
||||
POST {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=false)/OrdersService.draftActivate
|
||||
Content-Type: application/json
|
||||
|
||||
### Get active order
|
||||
GET {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=true)
|
||||
|
||||
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'`))
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -68,7 +68,7 @@ annotate OrdersService.Orders with @(
|
||||
|
||||
|
||||
|
||||
annotate OrdersService.OrderItems with @(
|
||||
annotate OrdersService.Orders_Items with @(
|
||||
UI: {
|
||||
LineItem: [
|
||||
{Value: product_ID, Label:'Product ID'},
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"name": "sap.fe.templates.ObjectPage",
|
||||
"options": {
|
||||
"settings" : {
|
||||
"entitySet": "OrderItems"
|
||||
"entitySet": "Orders_Items"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ID;order_ID;amount;product_ID;title;price
|
||||
ID;up__ID;amount;product_ID;title;price
|
||||
58040e66-1dcd-4ffb-ab10-fdce32028b79;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;201;Wuthering Heights;11.11
|
||||
64e718c9-ff99-47f1-8ca3-950c850777d4;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;271;Catweazle;15
|
||||
e9641166-e050-4261-bfee-d1e797e6cb7f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Eleonora;28
|
||||
|
@@ -1,26 +1,26 @@
|
||||
using { Currency, User, managed, cuid } from '@sap/cds/common';
|
||||
using from '@capire/common';
|
||||
namespace sap.capire.orders;
|
||||
|
||||
entity Orders : cuid, managed {
|
||||
OrderNo : String @title:'Order Number'; //> readable key
|
||||
Items : Composition of many OrderItems on Items.order = $self;
|
||||
Items : Composition of many Orders_Items on Items.up_ = $self;
|
||||
buyer : User;
|
||||
currency : Currency;
|
||||
}
|
||||
|
||||
entity OrderItems {
|
||||
entity Orders_Items {
|
||||
key ID : UUID;
|
||||
order : Association to Orders;
|
||||
@assert.integrity:false // REVISIT: this is a temporary workaround for a glitch in cds-runtime
|
||||
product : Association to Products;
|
||||
up_ : Association to Orders;
|
||||
product : Association to Products @assert.integrity:false; // REVISIT: this is a temporary workaround for a glitch in cds-runtime
|
||||
amount : Integer;
|
||||
title : String;
|
||||
price : Double;
|
||||
}
|
||||
|
||||
/** This is a stand-in for arbitrary ordered Products */
|
||||
@cds.persistence.skip:'always'
|
||||
entity Products {
|
||||
entity Products @(cds.persistence.skip:'always') {
|
||||
key ID : String;
|
||||
}
|
||||
|
||||
// Activate extension package
|
||||
using from '@capire/common';
|
||||
|
||||
@@ -4,10 +4,5 @@
|
||||
"dependencies": {
|
||||
"@capire/common": "*",
|
||||
"@sap/cds": "^4.3.0"
|
||||
},
|
||||
"cds": {
|
||||
"odata": {
|
||||
"containment": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,24 +3,24 @@ class OrdersService extends cds.ApplicationService {
|
||||
|
||||
/** register custom handlers */
|
||||
init(){
|
||||
const { OrderItems } = this.entities
|
||||
const { Orders_Items:OrderItems } = this.entities
|
||||
|
||||
this.before ('UPDATE', 'Orders', async function(req) {
|
||||
const { ID, Items } = req.data
|
||||
if (Items) for (let { product_ID, amount } of Items) {
|
||||
const { amount:before } = await cds.tx(req).run (
|
||||
SELECT.one.from (OrderItems, oi => oi.amount) .where ({order_ID:ID, product_ID})
|
||||
SELECT.one.from (OrderItems, oi => oi.amount) .where ({up__ID:ID, product_ID})
|
||||
)
|
||||
if (amount != before) this.orderChanged (product_ID, amount-before)
|
||||
if (amount != before) await this.orderChanged (product_ID, amount-before)
|
||||
}
|
||||
})
|
||||
|
||||
this.before ('DELETE', 'Orders', async function(req) {
|
||||
const { ID } = req.data
|
||||
const Items = await cds.tx(req).run (
|
||||
SELECT.from (OrderItems, oi => { oi.product_ID, oi.amount }) .where ({order_ID:ID})
|
||||
SELECT.from (OrderItems, oi => { oi.product_ID, oi.amount }) .where ({up__ID:ID})
|
||||
)
|
||||
if (Items) for (let it of Items) this.orderChanged (it.product_ID, -it.amount)
|
||||
if (Items) await Promise.all (Items.map(it => this.orderChanged (it.product_ID, -it.amount)))
|
||||
})
|
||||
|
||||
return super.init()
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@capire/reviews": "./reviews"
|
||||
},
|
||||
"devDependencies": {
|
||||
"pegjs": "^0.10.0",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-subset": "^1.6.0",
|
||||
|
||||
@@ -37,7 +37,7 @@ const reviews = new Vue ({
|
||||
reviews.message = {}
|
||||
},
|
||||
|
||||
async newReview () {
|
||||
newReview () {
|
||||
reviews.review = {}
|
||||
reviews.message = {}
|
||||
setTimeout (()=> $('form > input').focus(), 111)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const cds = require ('@sap/cds')
|
||||
module.exports = cds.service.impl (async function(){
|
||||
module.exports = cds.service.impl (function(){
|
||||
|
||||
// Get the CSN definition for Reviews from the db schema for sub-sequent queries
|
||||
// ( Note: we explicitly specify the namespace to support embedded reuse )
|
||||
@@ -16,7 +16,7 @@ module.exports = cds.service.impl (async function(){
|
||||
SELECT.one (['round(avg(rating),2) as rating']) .from (Reviews) .where ({subject})
|
||||
)
|
||||
global.it || console.log ('< emitting:', 'reviewed', { subject, rating })
|
||||
this.emit ('reviewed', { subject, rating })
|
||||
await this.emit ('reviewed', { subject, rating })
|
||||
})
|
||||
|
||||
// Increment counter for reviews considered helpful
|
||||
|
||||
20
samples.md
20
samples.md
@@ -1,7 +1,7 @@
|
||||
# Overview of Samples
|
||||
|
||||
The list below gives an overview of the samples provided in subdirectories.
|
||||
Each sub directory essentially is a individual npm package arranged in an [all-in-one monorepo](all-in-one-monorepo) umbrella setup.
|
||||
The following list gives an overview of the samples provided in subdirectories.
|
||||
Each sub directory essentially is an individual npm package arranged in an [all-in-one monorepo](all-in-one-monorepo) umbrella setup.
|
||||
|
||||
|
||||
## [@capire/hello-world](hello)
|
||||
@@ -13,7 +13,7 @@ Each sub directory essentially is a individual npm package arranged in an [all-i
|
||||
|
||||
- [Getting Started](https://cap.cloud.sap/docs/get-started/in-a-nutshell) with CAP, briefly introducing:
|
||||
- [Project Setup](https://cap.cloud.sap/docs/get-started/) and [Layouts](https://cap.cloud.sap/docs/get-started/projects)
|
||||
- [Domain Modelling](https://cap.cloud.sap/docs/guides/domain-models)
|
||||
- [Domain Modeling](https://cap.cloud.sap/docs/guides/domain-models)
|
||||
- [Defining Services](https://cap.cloud.sap/docs/guides/providing-services)
|
||||
- [Generic Providers](https://cap.cloud.sap/docs/guides/generic-providers)
|
||||
- [Adding Custom Logic](https://cap.cloud.sap/docs/guides/service-impl)
|
||||
@@ -22,7 +22,7 @@ Each sub directory essentially is a individual npm package arranged in an [all-i
|
||||
|
||||
## [@capire/common](common)
|
||||
|
||||
- Showcases how to extend [@sap/cds/common](https://cap.cloud.sap/docs/cds/common) thereby covering...
|
||||
- Showcases how to extend [@sap/cds/common](https://cap.cloud.sap/docs/cds/common) thereby covering:
|
||||
- Building [extension packages](https://cap.cloud.sap/docs/guides/domain-models#aspects-extensibility)
|
||||
- Providing [reuse packages](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content)
|
||||
- [Verticalization](https://cap.cloud.sap/docs/cds/common#adapting-to-your-needs)
|
||||
@@ -32,22 +32,22 @@ Each sub directory essentially is a individual npm package arranged in an [all-i
|
||||
|
||||
## [@capire/orders](orders)
|
||||
|
||||
- A standalone orders mgmt service, demonstrating...
|
||||
- A standalone orders management service, demonstrating:
|
||||
- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with
|
||||
- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)
|
||||
|
||||
|
||||
## [@capire/reviews](reviews)
|
||||
|
||||
- Shows how to implement a modular service to manage product reviews, including...
|
||||
- Shows how to implement a modular service to manage product reviews, including:
|
||||
- Consuming other services synchronously and asynchronously
|
||||
- Serving requests synchronously
|
||||
- Emitting events asynchronously
|
||||
- Grow as you go, with...
|
||||
- Grow as you go, with:
|
||||
- Mocking app services
|
||||
- Running service meshes
|
||||
- Late-cut Micro Services
|
||||
- As well as managed data, input validations and authorization
|
||||
- As well as managed data, input validations, and authorization
|
||||
|
||||
|
||||
## [@capire/fiori](fiori)
|
||||
@@ -57,11 +57,11 @@ Each sub directory essentially is a individual npm package arranged in an [all-i
|
||||
- [@capire/reviews](reviews)
|
||||
- [@capire/orders](orders)
|
||||
- [@capire/common](common)
|
||||
- [Adds a Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookshop, thereby introducing to...
|
||||
- [Adds a SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookshop, thereby introducing to:
|
||||
- [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files
|
||||
- Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)
|
||||
- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)
|
||||
- Serving Fiori apps locally
|
||||
- Serving SAP Fiori apps locally
|
||||
- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { expect } = require('../test')
|
||||
const cds = require('@sap/cds/lib')
|
||||
const { expect } = cds.test
|
||||
const CQL = ([cql]) => cds.parse.cql(cql)
|
||||
const Foo = { name: 'Foo' }
|
||||
const Books = { name: 'capire.bookshop.Books' }
|
||||
@@ -325,7 +325,26 @@ describe('cds.ql → cqn', () => {
|
||||
})
|
||||
|
||||
// using CQL fragments -> uses cds.parse.expr
|
||||
expect((cqn = CQL`SELECT from Foo where ID=11 and x in ( foo, 'bar', 3)`)).to.eql({
|
||||
const is_v2 = !!cds.parse.expr('(1,2)').list
|
||||
if (is_v2) expect((cqn = CQL`SELECT from Foo where ID=11 and x in ( foo, 'bar', 3)`)).to.eql({
|
||||
SELECT: {
|
||||
from: { ref: ['Foo'] },
|
||||
where: [
|
||||
{ ref: ['ID'] },
|
||||
'=',
|
||||
{ val: ID },
|
||||
'and',
|
||||
{ ref: ['x'] },
|
||||
'in',
|
||||
{list:[
|
||||
{ ref: ['foo'] },
|
||||
{ val: 'bar' },
|
||||
{ val: 3 },
|
||||
]}
|
||||
],
|
||||
},
|
||||
})
|
||||
else expect((cqn = CQL`SELECT from Foo where ID=11 and x in ( foo, 'bar', 3)`)).to.eql({
|
||||
SELECT: {
|
||||
from: { ref: ['Foo'] },
|
||||
where: [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const cds = require('@sap/cds/lib')
|
||||
const { expect } = cds.test (
|
||||
const { expect } = require('../test') .run (
|
||||
'serve', 'AdminService', '--from', '@capire/bookshop,@capire/common', '--in-memory'
|
||||
).in(__dirname)
|
||||
)
|
||||
const cds = require('@sap/cds/lib')
|
||||
|
||||
describe('Consuming Services locally', () => {
|
||||
//
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const cds = require('@sap/cds/lib'); cds.User = cds.User.Privileged // skip auth
|
||||
const { GET, POST, expect } = cds.test('bookshop').in(__dirname,'..')
|
||||
const { GET, POST, expect } = require('../test') .run ('bookshop')
|
||||
const cds = require('@sap/cds/lib')
|
||||
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch
|
||||
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
|
||||
|
||||
describe('Custom Handlers', () => {
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const cds = require('@sap/cds/lib')
|
||||
const { GET, expect } = cds.test('serve','hello/world.cds').in(__dirname,'..')
|
||||
const { GET, expect } = require('../test') .run ('serve','hello/world.cds')
|
||||
|
||||
describe('Hello world!', () => {
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const cwd = process.cwd(); process.chdir (__dirname) //> only for internal CI/CD@SAP
|
||||
const {expect} = require('../test')
|
||||
const cds = require('@sap/cds/lib')
|
||||
const {expect} = cds.test
|
||||
|
||||
// monkey patching older releases:
|
||||
if (!cds.compile.cdl) cds.compile.cdl = cds.parse
|
||||
@@ -25,8 +24,6 @@ describe('Hierarchical Data', ()=>{
|
||||
expect (cds.db.model) .to.exist
|
||||
})
|
||||
|
||||
after(()=> process.chdir(cwd))
|
||||
|
||||
it ('supports deeply nested inserts', ()=> INSERT.into (Cats,
|
||||
{ ID:100, name:'Some Cats...', children:[
|
||||
{ ID:101, name:'Cat', children:[
|
||||
|
||||
6
test/index.js
Normal file
6
test/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
const test = require('@sap/cds/lib/utils/tests').in(__dirname,'..')
|
||||
module.exports = Object.assign(test,{run:test})
|
||||
|
||||
// REVISIT: With upcoming release of @sap/cds this should become:
|
||||
// module.exports = require('@sap/cds/tests').in(__dirname,'..')
|
||||
@@ -1,5 +1,7 @@
|
||||
const cds = require('@sap/cds/lib'); cds.User = cds.User.Privileged // skip auth
|
||||
const { GET, expect } = cds.test ('serve', __dirname+'/localized-data.cds', '--in-memory')
|
||||
const { GET, expect } = require('../test') .run ('serve', 'test/localized-data.cds', '--in-memory')
|
||||
const cds = require('@sap/cds/lib')
|
||||
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch
|
||||
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
|
||||
|
||||
describe('Localized Data', () => {
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
const { expect } = require('../test')
|
||||
const cds = require('@sap/cds/lib')
|
||||
const cwd = process.cwd(); process.chdir (__dirname) //> only for internal CI/CD@SAP
|
||||
const {expect} = cds.test
|
||||
const _model = '@capire/reviews'
|
||||
cds.User = cds.User.Privileged // hard core monkey patch
|
||||
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch
|
||||
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
|
||||
|
||||
describe('Messaging', ()=>{
|
||||
|
||||
after(()=> process.chdir(cwd))
|
||||
|
||||
it ('should bootstrap sqlite in-memory db', async()=>{
|
||||
const db = await cds.deploy (_model) .to ('sqlite::memory:')
|
||||
await db.delete('Reviews')
|
||||
@@ -44,16 +42,16 @@ describe('Messaging', ()=>{
|
||||
// { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N },
|
||||
// ),
|
||||
srv.create ('Reviews') .entries (
|
||||
{ ID: 111 + (++N), subject: "201", title: "Captivating", rating: N }
|
||||
{ ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N }
|
||||
),
|
||||
srv.create ('Reviews') .entries (
|
||||
{ ID: 111 + (++N), subject: "201", title: "Captivating", rating: N }
|
||||
{ ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N }
|
||||
),
|
||||
srv.create ('Reviews') .entries (
|
||||
{ ID: 111 + (++N), subject: "201", title: "Captivating", rating: N }
|
||||
{ ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N }
|
||||
),
|
||||
srv.create ('Reviews') .entries (
|
||||
{ ID: 111 + (++N), subject: "201", title: "Captivating", rating: N }
|
||||
{ ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N }
|
||||
),
|
||||
]))
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
const cds = require('@sap/cds/lib'); cds.User = cds.User.Privileged // skip auth
|
||||
const { GET, expect } = cds.test('bookshop').in(__dirname,'..')
|
||||
const { GET, expect } = require('../test') .run ('bookshop')
|
||||
const cds = require('@sap/cds/lib')
|
||||
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch
|
||||
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
|
||||
|
||||
describe('OData Protocol', () => {
|
||||
|
||||
|
||||
it('serves $metadata documents in v4', async () => {
|
||||
const { headers, status, data } = await GET `/browse/$metadata`
|
||||
expect(status).to.equal(200)
|
||||
|
||||
Reference in New Issue
Block a user