Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddb962dcc3 | ||
|
|
5e52d4633a | ||
|
|
24dd1164cc | ||
|
|
f3f554396c | ||
|
|
a11aadc8f0 | ||
|
|
cfc5a56d5a | ||
|
|
098b27330a | ||
|
|
683b785ac5 | ||
|
|
2782cf0d6d | ||
|
|
fd97e3bda9 |
2
.github/workflows/node.js.yml
vendored
2
.github/workflows/node.js.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x, 14.x]
|
node-version: [10.x, 12.x, 14.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|||||||
1
.registry/.gitignore
vendored
1
.registry/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
*.tgz
|
|
||||||
@@ -5,9 +5,9 @@ const app = express()
|
|||||||
|
|
||||||
const { PORT=4444 } = process.env
|
const { PORT=4444 } = process.env
|
||||||
const [,,port=PORT] = process.argv
|
const [,,port=PORT] = process.argv
|
||||||
process.chdir(__dirname)
|
|
||||||
|
|
||||||
app.use('/-/:tarball', (req,res,next) => {
|
app.use('/-/:tarball', (req,res,next) => {
|
||||||
|
const url = decodeURIComponent(req.url)
|
||||||
console.debug ('GET', req.params)
|
console.debug ('GET', req.params)
|
||||||
try {
|
try {
|
||||||
const { tarball } = req.params
|
const { tarball } = req.params
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://aka.ms/codetour-schema",
|
|
||||||
"title": "Database Functions",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"title": "Introduction",
|
|
||||||
"description": "### Database Functions in CDS Models\n\nIn this tour, you'll learn how to add database-specific functions to CDS models in your application."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "bookshop/db/schema.cds",
|
|
||||||
"description": "#### Basic Schema\n\nWe want to add two fields to the `Authors` entity, one for the author's age and one for the span of years that she or he lived.\n\nThese two fields can be computed out of the existing `dateOfBirth` and `dateOfDeath` fields.",
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 19,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 21,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Base fields in Author"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "bookshop/srv/admin-service.cds",
|
|
||||||
"description": "This is how the `Authors` entity gets exposed in an OData or REST service.\n\nIn the next step, you'll see how we extend this projection.",
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 4,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 5,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Authors service"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/db/sqlite/index.cds",
|
|
||||||
"description": "#### SQLite Implementation\n\nHere's the first implementation for SQLite. It computes the two fields `age` and `lifetime` through SQLite's [strftime](https://sqlite.org/lang_datefunc.html) function.\n\nThrough the [`extend projection`](https://cap.cloud.sap/docs/cds/cdl#extend-view) clause you can add additional fields to projection entities. These are deployed as database views, which is why we can integrate the database functions in the first place.\n",
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 7,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 11,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "SQLite implementation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/db/hana/index.cds",
|
|
||||||
"description": "#### SAP HANA Implementation\n\nThis is the second implementation for SAP HANA. It computes the same two fields `age` and `lifetime` through the [YEARS_BETWEEN](https://help.sap.com/viewer/7c78579ce9b14a669c1f3295b0d8ca16/Cloud/en-US/7c0d2c161ea34def86de3f5eadd6a0af.html) and [YEAR](https://help.sap.com/viewer/7c78579ce9b14a669c1f3295b0d8ca16/Cloud/en-US/20f5fac6751910148dabd3c6821f907d.html) functions of SAP HANA.\n\n#### File Layout and Code Structure\n\nNote the path of the `.cds` file we are in: it's in a subfolder of `db`, so that it's _not_ automatically picked up when we start the application. The same is true for the SQLite implementation: it's in a separate `db/sqlite/` folder as well. In the next step, you'll see how these files are loaded.\n\nAlso, we choose to implement all of that as an extension of the original bookshop here in the _fiori_ package. See the first [CAP Samples] code tour for more details on the different packages of this repository.",
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 7,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 11,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "SAP HANA implementation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/package.json",
|
|
||||||
"description": "#### Configuration\n\nThe `cds` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile.",
|
|
||||||
"line": 17,
|
|
||||||
"title": "Configuration"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/package.json",
|
|
||||||
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch fiori\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
|
|
||||||
"line": 28,
|
|
||||||
"title": "Run with SQLite"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/package.json",
|
|
||||||
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd fiori; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd fiori; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
|
|
||||||
"line": 31,
|
|
||||||
"title": "Run with SAP HANA"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/test/requests.http",
|
|
||||||
"description": "### Add More Data\n\nOptionally you can add some `Authors` data by clicking on the _Send Request_ link (provided by the [REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension).",
|
|
||||||
"line": 68,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 67,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 73,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Add Data"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Wrap-up",
|
|
||||||
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"ref": "master"
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://aka.ms/codetour-schema",
|
|
||||||
"title": "CAP Samples",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"title": "Welcome",
|
|
||||||
"file": "README.md",
|
|
||||||
"description": "### Welcome to CAP Samples!\n\nThis tour leads you through a collection of samples for the [SAP Cloud Application Programming Model (CAP)](https://cap.cloud.sap).\nYou will learn which features of the programming model are demonstrated in which sample.\n\nLet's start!",
|
|
||||||
"line": 2,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 1,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 3,
|
|
||||||
"character": 108
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "hello/world.cds",
|
|
||||||
"description": "### Hello World!\n\nThis is a simplistic [Hello World](https://cap.cloud.sap/docs/get-started/hello-world) service using [CDS](https://cap.cloud.sap/docs/cds/) and [cds.services](https://cap.cloud.sap/docs/node.js/api#services-api).",
|
|
||||||
"line": 2,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 1,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 4,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Hello World!"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "bookshop/db/schema.cds",
|
|
||||||
"description": "### A Bookshop!\n\nIntroduces:\n- [Project Setup](https://cap.cloud.sap/docs/get-started/) and [Layouts](https://cap.cloud.sap/docs/get-started/projects)\n- [Domain Modeling](https://cap.cloud.sap/docs/guides/domain-models)\n- [Defining Services](https://cap.cloud.sap/docs/guides/providing-services)\n- [Generic Providers](https://cap.cloud.sap/docs/guides/generic-providers)\n- [Adding Custom Logic](https://cap.cloud.sap/docs/guides/service-impl)\n- [Using Databases](https://cap.cloud.sap/docs/guides/databases)\n",
|
|
||||||
"line": 1,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 1,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 32,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Bookshop"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "common/index.cds",
|
|
||||||
"description": "### Extend and Reuse\n\nShowcases how to extend [@sap/cds/common](https://cap.cloud.sap/docs/cds/common) thereby covering:\n- Building [extension packages](https://cap.cloud.sap/docs/guides/domain-models#aspects-extensibility)\n- Providing [reuse packages](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content)\n- [Verticalization](https://cap.cloud.sap/docs/cds/common#adapting-to-your-needs)\n- Using [Aspects](https://cap.cloud.sap/docs/cds/cdl#aspects)\n- Used in the [fiori app sample](#fiori)\n",
|
|
||||||
"line": 1,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 1,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 46,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Common"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "orders/db/schema.cds",
|
|
||||||
"description": "### Compositions and Serving Documents\n\nA standalone orders management service, demonstrating:\n- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with\n- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)\n",
|
|
||||||
"line": 1,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 1,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 27,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Orders"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "reviews/db/schema.cds",
|
|
||||||
"description": "### More Modularity\n\nShows how to implement a modular service to manage product reviews, including:\n- Consuming other services synchronously and asynchronously\n- Serving requests synchronously\n- Emitting events asynchronously\n- Grow as you go, with:\n- Mocking app services\n- Running service meshes\n- Late-cut Micro Services\n- As well as managed data, input validations, and authorization\n",
|
|
||||||
"line": 1,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 1,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 39,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Reviews"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/app/index.cds",
|
|
||||||
"description": "### Annotations for SAP Fiori Elements\n\nA [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:\n - [@capire/bookshop](bookshop)\n - [@capire/reviews](reviews)\n - [@capire/orders](orders)\n - [@capire/common](common)\n\n[Adds a SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookshop, thereby introducing to:\n - [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files\n - Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)\n - Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)\n - Serving SAP Fiori apps locally\n\n[The Vue.js app](bookshop/app/vue) imported from bookshop is served as well.\n",
|
|
||||||
"line": 1,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 1,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 13,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Fiori"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "package.json",
|
|
||||||
"description": "### All-in-one Monorepo\n\nEach sample sub directory essentially is a standard npm package, some with standard npm dependencies to other samples. The root folder's [package.json](package.json) has local links to the sub folders, such that an `npm install` populates a local `node_modules` folder acts like a local npm registry to the individual sample packages.\n",
|
|
||||||
"line": 8,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 8,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 15,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Packages"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isPrimary": true,
|
|
||||||
"description": "Overview of CAP Samples for Node.js"
|
|
||||||
}
|
|
||||||
5
.vscode/extensions.json
vendored
5
.vscode/extensions.json
vendored
@@ -4,15 +4,14 @@
|
|||||||
|
|
||||||
// List of extensions which should be recommended for users of this workspace.
|
// List of extensions which should be recommended for users of this workspace.
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"sapse.vscode-cds",
|
"SAPSE.vscode-cds",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"mechatroner.rainbow-csv",
|
"mechatroner.rainbow-csv",
|
||||||
"humao.rest-client",
|
"humao.rest-client",
|
||||||
"alexcvzz.vscode-sqlite",
|
"alexcvzz.vscode-sqlite",
|
||||||
"hbenl.vscode-mocha-test-adapter",
|
"hbenl.vscode-mocha-test-adapter",
|
||||||
"sdras.night-owl",
|
"sdras.night-owl"
|
||||||
"vsls-contrib.codetour"
|
|
||||||
],
|
],
|
||||||
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
||||||
"unwantedRecommendations": [
|
"unwantedRecommendations": [
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -3,7 +3,7 @@
|
|||||||
Find here a collection of samples for the [SAP Cloud Application Programming Model](https://cap.cloud.sap) organized in a simplistic [monorepo setup](samples.md#all-in-one-monorepo). → See [**Overview** of contained samples](samples.md)
|
Find here a collection of samples for the [SAP Cloud Application Programming Model](https://cap.cloud.sap) organized in a simplistic [monorepo setup](samples.md#all-in-one-monorepo). → See [**Overview** of contained samples](samples.md)
|
||||||
|
|
||||||

|

|
||||||
<!--[](https://api.reuse.software/info/github.com/SAP-samples/cloud-cap-samples)-->
|
[](https://api.reuse.software/info/github.com/SAP-samples/cloud-cap-samples)
|
||||||
|
|
||||||
|
|
||||||
### Preliminaries
|
### Preliminaries
|
||||||
@@ -18,7 +18,7 @@ Find here a collection of samples for the [SAP Cloud Application Programming Mod
|
|||||||
|
|
||||||
### Download
|
### Download
|
||||||
|
|
||||||
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, 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
|
```sh
|
||||||
git clone https://github.com/sap-samples/cloud-cap-samples samples
|
git clone https://github.com/sap-samples/cloud-cap-samples samples
|
||||||
@@ -72,14 +72,10 @@ npm add @capire/common @capire/bookshop
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Code Tours
|
|
||||||
|
|
||||||
Take one of the [guided tours](.tours) in VS Code through our CAP samples and learn which CAP features are showcased by the different parts of the repository. Just install the [CodeTour extension](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.codetour) for VS Code. We'll add more code tours in the future. Stay tuned!
|
|
||||||
|
|
||||||
## Get Support
|
## Get Support
|
||||||
|
|
||||||
Check out the documentation at [https://cap.cloud.sap](https://cap.cloud.sap). <br>
|
Check out the documentation at [https://cap.cloud.sap](https://cap.cloud.sap). <br>
|
||||||
In case you've a question, find a bug, or otherwise need support, use our [community](https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce) to get more visibility.
|
In case you have a question, find a bug, or otherwise need support, please use our [community](https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce).
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<td class="rating-stars">
|
<td class="rating-stars">
|
||||||
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
|
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
<td>{{ book.currency.symbol }} {{ book.price }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -44,11 +44,11 @@
|
|||||||
<img v-bind:src="book.image" alt=""/>
|
<img v-bind:src="book.image" alt=""/>
|
||||||
<label style="text-align:right">
|
<label style="text-align:right">
|
||||||
<span class="succeeded"> {{ order.succeeded }} </span>
|
<span class="succeeded"> {{ order.succeeded }} </span>
|
||||||
<span class="failed"> {{ order.failed }} </span>
|
<span class="failed"> {{ order.failed }} </span>
|
||||||
<span id="stock"> {{ book.stock }} in stock </span>
|
{{ book.stock }} in stock
|
||||||
</label>
|
</label>
|
||||||
<form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
|
<form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
|
||||||
<input id="amount" type="number" v-model="order.amount" v-bind:class="{ failed: order.failed }" style="width:5em">
|
<input type="number" v-model="order.amount" v-bind:class="{ failed: order.failed }" style="width:5em">
|
||||||
<input type="submit" value="Order:" class="muted-button">
|
<input type="submit" value="Order:" class="muted-button">
|
||||||
</form>
|
</form>
|
||||||
<h4> {{ book.title }} </h4>
|
<h4> {{ book.title }} </h4>
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
"description": "Provides a pre-built extension package for std @sap/cds/common",
|
"description": "Provides a pre-built extension package for std @sap/cds/common",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sap/cds": "*"
|
"@sap/cds": "latest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ DateOfBirth = Date of Birth
|
|||||||
DateOfDeath = Date of Death
|
DateOfDeath = Date of Death
|
||||||
PlaceOfBirth = Place of Birth
|
PlaceOfBirth = Place of Birth
|
||||||
PlaceOfDeath = Place of Death
|
PlaceOfDeath = Place of Death
|
||||||
Age = Age
|
|
||||||
Authors = Authors
|
Authors = Authors
|
||||||
Order = Order
|
Order = Order
|
||||||
Orders = Orders
|
Orders = Orders
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ Authors = Autoren
|
|||||||
Author = Autor
|
Author = Autor
|
||||||
AuthorID = ID des Autors
|
AuthorID = ID des Autors
|
||||||
AuthorName = Name des Autors
|
AuthorName = Name des Autors
|
||||||
Age = Alter
|
|
||||||
Name = Name
|
Name = Name
|
||||||
Stock = Bestand
|
Stock = Bestand
|
||||||
Order = Bestellung
|
Order = Bestellung
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using { AdminService } from '../../db';
|
using AdminService from '@capire/bookshop';
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
@@ -39,27 +39,6 @@ annotate AdminService.Books with @(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
annotate AdminService.Authors with @(
|
|
||||||
UI: {
|
|
||||||
HeaderInfo: {
|
|
||||||
Description: {Value: lifetime}
|
|
||||||
},
|
|
||||||
Facets: [
|
|
||||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
|
||||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Books}', Target: 'books/@UI.LineItem'},
|
|
||||||
],
|
|
||||||
FieldGroup#Details: {
|
|
||||||
Data: [
|
|
||||||
{Value: placeOfBirth},
|
|
||||||
{Value: placeOfDeath},
|
|
||||||
{Value: dateOfBirth},
|
|
||||||
{Value: dateOfDeath},
|
|
||||||
{Value: age, Label: '{i18n>Age}'},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta http-equiv="refresh" content="0;url=bookshop/index.html">
|
<meta http-equiv="refresh" content="0;url=vue/bookshop/index.html">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta http-equiv="refresh" content="0;url=reviews/index.html">
|
<meta http-equiv="refresh" content="0;url=vue/reviews/index.html">
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
//
|
|
||||||
// Add Author.age and .lifetime with a DB-specific function
|
|
||||||
//
|
|
||||||
|
|
||||||
using { AdminService } from '..';
|
|
||||||
|
|
||||||
extend projection AdminService.Authors with {
|
|
||||||
YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer,
|
|
||||||
YEAR(dateOfBirth) || ' – ' || YEAR(dateOfDeath) as lifetime : String
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using { sap.capire.bookshop } from '@capire/bookshop';
|
|
||||||
|
|
||||||
// Forward-declare calculated fields to be filled in database-specific ways
|
|
||||||
// TODO find a better way to have 'default' fields that still can be overwritten.
|
|
||||||
extend bookshop.Authors with {
|
|
||||||
virtual age: Integer;
|
|
||||||
virtual lifetime: String;
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
//
|
|
||||||
// Add Author.age and .lifetime with a DB-specific function
|
|
||||||
//
|
|
||||||
|
|
||||||
using { AdminService } from '..';
|
|
||||||
|
|
||||||
extend projection AdminService.Authors with {
|
|
||||||
strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer,
|
|
||||||
strftime('%Y',dateOfBirth) || ' – ' || strftime('%Y',dateOfDeath) as lifetime : String
|
|
||||||
}
|
|
||||||
@@ -8,36 +8,22 @@
|
|||||||
"@capire/common": "*",
|
"@capire/common": "*",
|
||||||
"@sap/cds": "^4",
|
"@sap/cds": "^4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"passport": "^0.4.1"
|
"passport": "0.4.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cds run --in-memory?",
|
"start": "cds run --in-memory?",
|
||||||
"watch": "cds watch"
|
"watch": "cds watch"
|
||||||
},
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
"hana": {
|
|
||||||
"deploy-format": "hdbtable"
|
|
||||||
},
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"auth": {
|
|
||||||
"strategy": "dummy"
|
|
||||||
},
|
|
||||||
"ReviewsService": {
|
"ReviewsService": {
|
||||||
"kind": "odata",
|
"kind": "odata", "model": "@capire/reviews"
|
||||||
"model": "@capire/reviews"
|
|
||||||
},
|
},
|
||||||
"OrdersService": {
|
"OrdersService": {
|
||||||
"kind": "odata",
|
"kind": "odata", "model": "@capire/orders"
|
||||||
"model": "@capire/orders"
|
|
||||||
},
|
},
|
||||||
"db": {
|
"db": {
|
||||||
"kind": "sql",
|
"kind": "sql"
|
||||||
"[development]": {
|
|
||||||
"model": "db/sqlite"
|
|
||||||
},
|
|
||||||
"[production]": {
|
|
||||||
"model": "db/hana"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
|
const express = require ('express')
|
||||||
const cds = require ('@sap/cds')
|
const cds = require ('@sap/cds')
|
||||||
|
|
||||||
cds.once('bootstrap',(app)=>{
|
cds.once('bootstrap',(app)=>{
|
||||||
app.use ('/orders/webapp', _from('@capire/orders/app/orders/webapp/manifest.json'))
|
const {dirname} = require ('path')
|
||||||
app.use ('/bookshop', _from('@capire/bookshop/app/vue/index.html'))
|
// serving the orders app imported from @capire/orders
|
||||||
app.use ('/reviews', _from('@capire/reviews/app/vue/index.html'))
|
const orders_app = dirname (require.resolve('@capire/orders/app/orders/webapp/manifest.json'))
|
||||||
|
app.use ('/orders/webapp', express.static(orders_app))
|
||||||
|
// serving the vue.js app imported from @capire/bookshop
|
||||||
|
const bookshop_app = dirname (require.resolve('@capire/bookshop/app/vue/index.html'))
|
||||||
|
app.use ('/vue/bookshop', express.static(bookshop_app))
|
||||||
|
// serving the vue.js app imported from @capire/reviews
|
||||||
|
const reviews_app = dirname (require.resolve('@capire/reviews/app/vue/index.html'))
|
||||||
|
app.use ('/vue/reviews', express.static(reviews_app))
|
||||||
})
|
})
|
||||||
|
|
||||||
cds.once('served', require('./srv/mashup'))
|
cds.once('served', require('./srv/mashup'))
|
||||||
|
|
||||||
module.exports = cds.server
|
module.exports = cds.server
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Helper for serving static content from npm-installed packages
|
|
||||||
const {static} = require('express')
|
|
||||||
const {dirname} = require('path')
|
|
||||||
const _from = target => static (dirname (require.resolve(target)))
|
|
||||||
|
|||||||
@@ -63,15 +63,3 @@ Content-Type: application/json
|
|||||||
|
|
||||||
### Get active order
|
### Get active order
|
||||||
GET {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=true)
|
GET {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=true)
|
||||||
|
|
||||||
### Create author
|
|
||||||
POST {{bookshop}}/admin/Authors
|
|
||||||
Content-Type: application/json
|
|
||||||
Authorization: Basic alice:
|
|
||||||
|
|
||||||
{
|
|
||||||
"ID": 200,
|
|
||||||
"name": "William Shakespeare",
|
|
||||||
"dateOfBirth": "1564-04-26",
|
|
||||||
"dateOfDeath": "1616-04-23"
|
|
||||||
}
|
|
||||||
|
|||||||
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'`))
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -21,3 +21,6 @@ entity Orders_Items {
|
|||||||
entity Products @(cds.persistence.skip:'always') {
|
entity Products @(cds.persistence.skip:'always') {
|
||||||
key ID : String;
|
key ID : String;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Activate extension package
|
||||||
|
using from '@capire/common';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "@capire/orders",
|
"name": "@capire/orders",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@capire/common": "*",
|
||||||
"@sap/cds": "^4.3.0"
|
"@sap/cds": "^4.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,15 +14,14 @@
|
|||||||
"@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",
|
||||||
"@cucumber/cucumber": "^7.0.0",
|
"sqlite3": "^5"
|
||||||
"selenium-webdriver": "^4.0.0-beta.1",
|
|
||||||
"sqlite3": "5.0.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"registry": "node .registry/server.js",
|
"registry": "cd .registry && node server.js",
|
||||||
"bookshop": "cds watch bookshop",
|
"bookshop": "cds watch bookshop",
|
||||||
"fiori": "cds watch fiori",
|
"fiori": "cds watch fiori",
|
||||||
"media": "cds watch media",
|
"media": "cds watch media",
|
||||||
|
|||||||
@@ -3,22 +3,19 @@
|
|||||||
## Run all-in-one
|
## Run all-in-one
|
||||||
|
|
||||||
Open a terminal window and run the bookshop in it:
|
Open a terminal window and run the bookshop in it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run bookshop
|
npm run bookshop
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Run as Separate Services
|
## Run as separate services
|
||||||
|
|
||||||
Open two terminal windows. In the first one start the reviews service stand-alone:
|
|
||||||
|
|
||||||
|
Open two terminal windows, and in the first one start the reviews service stand-alone:
|
||||||
```sh
|
```sh
|
||||||
npm run reviews-service
|
npm run reviews-service
|
||||||
```
|
```
|
||||||
|
|
||||||
In the second one start the bookshop:
|
In the the second one start the bookshop:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run bookshop
|
npm run bookshop
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -57,13 +57,14 @@ Each sub directory essentially is an individual npm package arranged in an [all-
|
|||||||
- [@capire/reviews](reviews)
|
- [@capire/reviews](reviews)
|
||||||
- [@capire/orders](orders)
|
- [@capire/orders](orders)
|
||||||
- [@capire/common](common)
|
- [@capire/common](common)
|
||||||
- [Adds an SAP 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
|
- [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 [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)
|
||||||
- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)
|
- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)
|
||||||
- Serving SAP Fiori apps locally
|
- Serving SAP Fiori apps locally
|
||||||
- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
|
- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
|
||||||
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
# All-in-one Monorepo
|
# All-in-one Monorepo
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ describe('cds.ql → cqn', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!is_v2) expect(
|
expect(
|
||||||
SELECT.from(Foo).where(`x=`, 1, `or y.z is null and (a>`, 2, `or b=`, 3, `)`)
|
SELECT.from(Foo).where(`x=`, 1, `or y.z is null and (a>`, 2, `or b=`, 3, `)`)
|
||||||
).to.eql(CQL`SELECT from Foo where x=1 or y.z is null and (a>2 or b=3)`)
|
).to.eql(CQL`SELECT from Foo where x=1 or y.z is null and (a>2 or b=3)`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
Feature: List Books using Vue.js UI
|
|
||||||
|
|
||||||
Scenario: Launch cds server for bookshop
|
|
||||||
When we run the 'bookshop' server
|
|
||||||
And wait for 1s
|
|
||||||
Then it should listen at 'http://localhost:4004'
|
|
||||||
|
|
||||||
Scenario: Display Books List
|
|
||||||
When we open page '/vue/index.html'
|
|
||||||
And wait for 1s
|
|
||||||
Then it should list these rows in table 'books':
|
|
||||||
| Wuthering Heights | Emily Brontë |
|
|
||||||
| Jane Eyre | Charlotte Brontë |
|
|
||||||
| The Raven | Edgar Allen Poe |
|
|
||||||
| Eleonora | Edgar Allen Poe |
|
|
||||||
| Catweazle | Richard Carpenter |
|
|
||||||
|
|
||||||
Scenario: Select a Book
|
|
||||||
When we click on the 1st row in table 'books'
|
|
||||||
Then it shows '12' in 'stock'
|
|
||||||
|
|
||||||
Scenario: Order One Book
|
|
||||||
When we click on button 'Order:'
|
|
||||||
Then it succeeds with 'ordered 1 item(s)'
|
|
||||||
|
|
||||||
Scenario: Order Four Books
|
|
||||||
When we enter '4' into 'amount'
|
|
||||||
And we click on button 'Order:'
|
|
||||||
Then it succeeds with 'ordered 4 item(s)'
|
|
||||||
|
|
||||||
Scenario: Order Amount Exceeding Stock
|
|
||||||
When we enter '9' into 'amount'
|
|
||||||
And we click on button 'Order:'
|
|
||||||
Then it fails with '9 exceeds stock'
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
const { Given, When, Then, AfterAll } = require('@cucumber/cucumber')
|
|
||||||
const { Builder, By } = require('selenium-webdriver')
|
|
||||||
const browser = (new Builder).forBrowser('safari').build()
|
|
||||||
const cds = require ('@sap/cds/lib')
|
|
||||||
|
|
||||||
process.env.cds_requires_auth_strategy = 'dummy'
|
|
||||||
|
|
||||||
let axios = require('axios').default
|
|
||||||
let {display} = browser
|
|
||||||
|
|
||||||
When('we run the {string} server', project => cds.exec('watch', project))
|
|
||||||
Then('it should listen at {string}', baseURL => {
|
|
||||||
axios = axios.create({ baseURL })
|
|
||||||
display = url => browser.get (baseURL+url)
|
|
||||||
return axios.head()
|
|
||||||
})
|
|
||||||
Then('terminate the server', ()=> process.exit())
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
When(/wait for (\d+)\s*(\w+)/, {timeout:60*1000}, (delay, unit, done) => {
|
|
||||||
const factor = {
|
|
||||||
ms: 1,
|
|
||||||
s: 1000, sec: 1000, second: 1000, seconds: 1000,
|
|
||||||
m: 60*1000, min: 60*1000, minute: 60*1000, minutes: 60*1000,
|
|
||||||
h: 60*60*1000, hr: 60*60*1000, hour: 60*60*1000, hours: 60*60*1000,
|
|
||||||
}[unit]
|
|
||||||
if (!factor) throw `Unknown duration unit: ${unit}`
|
|
||||||
setTimeout (done, delay * factor)
|
|
||||||
})
|
|
||||||
|
|
||||||
AfterAll(()=> setTimeout (process.exit, 111))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
When('we open page {string}', page => display(page))
|
|
||||||
|
|
||||||
Then('it should list these rows in table {string}:', async (id,data) => {
|
|
||||||
let rows = await browser.findElements(By.css(`#${id} tr`)); rows.shift()
|
|
||||||
await Promise.all (data.rawTable.map (async (row,i)=>{
|
|
||||||
const tr = await rows[i].getText()
|
|
||||||
for (let each of row) if (!tr.match(each)) throw `Didn't find '${each}' in web page as expected`
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
When(/we click on the (\d+)(?:st|nd|rd|th) row in table '(\w+)'/, async (row, id) => {
|
|
||||||
let rows = await browser.findElements(By.css(`#${id} tr`))
|
|
||||||
let td = await rows[row].findElement(By.css('td'))
|
|
||||||
return td.click()
|
|
||||||
})
|
|
||||||
|
|
||||||
When('we enter {string} into {string}', async (value,id) => {
|
|
||||||
const field = await browser.findElement(By.css(`input#${id}`))
|
|
||||||
return field.sendKeys('\b\b\b\b\b\b',value)
|
|
||||||
})
|
|
||||||
|
|
||||||
When('we click on button {string}', async (text) => {
|
|
||||||
const button = await browser.findElement(By.css(`input[value='${text}']`))
|
|
||||||
return button.click()
|
|
||||||
})
|
|
||||||
|
|
||||||
Then('it succeeds with {string}', async message => {
|
|
||||||
const element = await browser.wait (browser.findElement(By.css(`span.succeeded`)))
|
|
||||||
return (await element.getText()).includes(message)
|
|
||||||
})
|
|
||||||
|
|
||||||
Then('it fails with {string}', async message => {
|
|
||||||
const element = await browser.wait (browser.findElement(By.css(`span.failed`)))
|
|
||||||
return (await element.getText()).includes(message)
|
|
||||||
})
|
|
||||||
|
|
||||||
Then('it shows {string} in {string}', async (message,id) => {
|
|
||||||
const element = await browser.wait (browser.findElement(By.id(id)))
|
|
||||||
return (await element.getText()).includes(message)
|
|
||||||
})
|
|
||||||
|
|
||||||
Given('we login as {string}, {string}', async (username, password) => {
|
|
||||||
const alert = await browser.switchTo().alert()
|
|
||||||
return alert.authenticateAs(username, password)
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user