Compare commits
60 Commits
containmen
...
suppliers-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
175e7b554f | ||
|
|
803432b8d9 | ||
|
|
0ddd70acbc | ||
|
|
3e52a9a102 | ||
|
|
b44701ef62 | ||
|
|
647ac72588 | ||
|
|
861094cc83 | ||
|
|
2368913612 | ||
|
|
22ece2e586 | ||
|
|
ae788414f8 | ||
|
|
ebc7017ded | ||
|
|
876ff94782 | ||
|
|
76d1b1865b | ||
|
|
85dd8558f4 | ||
|
|
d0d95f3c42 | ||
|
|
c46a82d0f8 | ||
|
|
7654012292 | ||
|
|
65c8c82f74 | ||
|
|
c23ddc7e54 | ||
|
|
d368eb2ff5 | ||
|
|
3cbb199870 | ||
|
|
66bd2f707c | ||
|
|
b51a08bf4e | ||
|
|
3320c7e5a2 | ||
|
|
a35782e775 | ||
|
|
e5bd8ec5a5 | ||
|
|
0aa95a0a67 | ||
|
|
5015eb8c52 | ||
|
|
6d3f4c689f | ||
|
|
f0fead2bc2 | ||
|
|
f1d780d6d9 | ||
|
|
796bf62bde | ||
|
|
5f176a0b88 | ||
|
|
a5c8b5101e | ||
|
|
b31efc8083 | ||
|
|
6669b983b1 | ||
|
|
d72ff809b0 | ||
|
|
b6e5a2fced | ||
|
|
6de09e0940 | ||
|
|
b6f3914d79 | ||
|
|
28402c58b3 | ||
|
|
77de0e445e | ||
|
|
a037d92c97 | ||
|
|
b5031588ce | ||
|
|
85319d9e8d | ||
|
|
39872200ae | ||
|
|
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"
|
||||
}
|
||||
}
|
||||
|
||||
2
.github/workflows/node.js.yml
vendored
2
.github/workflows/node.js.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x]
|
||||
node-version: [12.x, 14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
1
.registry/.gitignore
vendored
Normal file
1
.registry/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.tgz
|
||||
@@ -5,9 +5,9 @@ const app = express()
|
||||
|
||||
const { PORT=4444 } = process.env
|
||||
const [,,port=PORT] = process.argv
|
||||
process.chdir(__dirname)
|
||||
|
||||
app.use('/-/:tarball', (req,res,next) => {
|
||||
const url = decodeURIComponent(req.url)
|
||||
console.debug ('GET', req.params)
|
||||
try {
|
||||
const { tarball } = req.params
|
||||
|
||||
109
.tours/db-native.tour
Normal file
109
.tours/db-native.tour
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
136
.tours/samples.tour
Normal file
136
.tours/samples.tour
Normal file
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"$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,14 +4,15 @@
|
||||
|
||||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": [
|
||||
"SAPSE.vscode-cds",
|
||||
"sapse.vscode-cds",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"mechatroner.rainbow-csv",
|
||||
"humao.rest-client",
|
||||
"alexcvzz.vscode-sqlite",
|
||||
"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.
|
||||
"unwantedRecommendations": [
|
||||
|
||||
34
README.md
34
README.md
@@ -3,18 +3,22 @@
|
||||
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
|
||||
|
||||
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've [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,37 +43,45 @@ 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
|
||||
```
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
Check out the documentation at [https://cap.cloud.sap](https://cap.cloud.sap). <br>
|
||||
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).
|
||||
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.
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<td class="rating-stars">
|
||||
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
|
||||
</td>
|
||||
<td>{{ book.currency.symbol }} {{ book.price }}</td>
|
||||
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -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": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ DateOfBirth = Date of Birth
|
||||
DateOfDeath = Date of Death
|
||||
PlaceOfBirth = Place of Birth
|
||||
PlaceOfDeath = Place of Death
|
||||
Age = Age
|
||||
Authors = Authors
|
||||
Order = Order
|
||||
Orders = Orders
|
||||
|
||||
@@ -6,6 +6,7 @@ Authors = Autoren
|
||||
Author = Autor
|
||||
AuthorID = ID des Autors
|
||||
AuthorName = Name des Autors
|
||||
Age = Alter
|
||||
Name = Name
|
||||
Stock = Bestand
|
||||
Order = Bestellung
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using AdminService from '@capire/bookshop';
|
||||
using { AdminService } from '../../db';
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
@@ -39,6 +39,27 @@ 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>
|
||||
<meta http-equiv="refresh" content="0;url=vue/bookshop/index.html">
|
||||
<meta http-equiv="refresh" content="0;url=bookshop/index.html">
|
||||
</head>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0;url=vue/reviews/index.html">
|
||||
<meta http-equiv="refresh" content="0;url=reviews/index.html">
|
||||
</head>
|
||||
|
||||
10
fiori/db/hana/index.cds
Normal file
10
fiori/db/hana/index.cds
Normal file
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
8
fiori/db/index.cds
Normal file
8
fiori/db/index.cds
Normal file
@@ -0,0 +1,8 @@
|
||||
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;
|
||||
}
|
||||
10
fiori/db/sqlite/index.cds
Normal file
10
fiori/db/sqlite/index.cds
Normal file
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
@@ -3,28 +3,47 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@capire/bookshop": "*",
|
||||
"@capire/reviews": "*",
|
||||
"@capire/orders": "*",
|
||||
"@capire/common": "*",
|
||||
"@capire/orders": "*",
|
||||
"@capire/reviews": "*",
|
||||
"@capire/suppliers": "*",
|
||||
"@sap/cds": "^4",
|
||||
"express": "^4.17.1",
|
||||
"passport": "0.4.1"
|
||||
"passport": "^0.4.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cds run --in-memory?",
|
||||
"watch": "cds watch"
|
||||
},
|
||||
"cds": {
|
||||
"hana": {
|
||||
"deploy-format": "hdbtable"
|
||||
},
|
||||
"requires": {
|
||||
"API_BUSINESS_PARTNER": {
|
||||
"kind": "odata",
|
||||
"model": "@capire/suppliers"
|
||||
},
|
||||
"auth": {
|
||||
"strategy": "dummy"
|
||||
},
|
||||
"ReviewsService": {
|
||||
"kind": "odata", "model": "@capire/reviews"
|
||||
"kind": "odata",
|
||||
"model": "@capire/reviews"
|
||||
},
|
||||
"OrdersService": {
|
||||
"kind": "odata", "model": "@capire/orders"
|
||||
"kind": "odata",
|
||||
"model": "@capire/orders"
|
||||
},
|
||||
"db": {
|
||||
"kind": "sql"
|
||||
"kind": "sql",
|
||||
"[development]": {
|
||||
"model": "db/sqlite"
|
||||
},
|
||||
"[production]": {
|
||||
"model": "db/hana"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
const express = require ('express')
|
||||
const cds = require ('@sap/cds')
|
||||
|
||||
cds.once('bootstrap',(app)=>{
|
||||
const {dirname} = require ('path')
|
||||
// serving the orders app imported from @capire/orders
|
||||
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))
|
||||
app.use ('/orders/webapp', _from('@capire/orders/app/orders/webapp/manifest.json'))
|
||||
app.use ('/bookshop', _from('@capire/bookshop/app/vue/index.html'))
|
||||
app.use ('/reviews', _from('@capire/reviews/app/vue/index.html'))
|
||||
})
|
||||
|
||||
cds.once('served', require('./srv/mashup'))
|
||||
cds.once('served', require('@capire/suppliers/srv/mashup'))
|
||||
|
||||
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)))
|
||||
|
||||
@@ -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,36 @@ 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)
|
||||
|
||||
### 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"
|
||||
}
|
||||
|
||||
@@ -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,23 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"name": "@capire/orders",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@capire/common": "*",
|
||||
"@sap/cds": "^4.3.0"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -11,16 +11,18 @@
|
||||
"@capire/hello": "./hello",
|
||||
"@capire/media": "./media",
|
||||
"@capire/orders": "./orders",
|
||||
"@capire/reviews": "./reviews"
|
||||
"@capire/reviews": "./reviews",
|
||||
"@capire/suppliers": "./suppliers"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-subset": "^1.6.0",
|
||||
"sqlite3": "^5"
|
||||
"sqlite3": "5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"registry": "cd .registry && node server.js",
|
||||
"fix-antlr": "sed -i -e 's/INVALID_ALT_NUMBER = require.*/INVALID_ALT_NUMBER = 0/' node_modules/antlr4/tree/Trees.js node_modules/antlr4/RuleContext.js",
|
||||
"registry": "node .registry/server.js",
|
||||
"bookshop": "cds watch bookshop",
|
||||
"fiori": "cds watch fiori",
|
||||
"media": "cds watch media",
|
||||
|
||||
@@ -37,7 +37,7 @@ const reviews = new Vue ({
|
||||
reviews.message = {}
|
||||
},
|
||||
|
||||
async newReview () {
|
||||
newReview () {
|
||||
reviews.review = {}
|
||||
reviews.message = {}
|
||||
setTimeout (()=> $('form > input').focus(), 111)
|
||||
|
||||
@@ -3,19 +3,22 @@
|
||||
## Run all-in-one
|
||||
|
||||
Open a terminal window and run the bookshop in it:
|
||||
|
||||
```sh
|
||||
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
|
||||
npm run reviews-service
|
||||
```
|
||||
|
||||
In the the second one start the bookshop:
|
||||
In the second one start the bookshop:
|
||||
|
||||
```sh
|
||||
npm run bookshop
|
||||
```
|
||||
|
||||
@@ -8,9 +8,9 @@ service ReviewsService {
|
||||
action unlike (review: type of Reviews:ID);
|
||||
|
||||
// Async API
|
||||
event reviewed : {
|
||||
subject: type of Reviews:subject;
|
||||
rating: Decimal(2,1)
|
||||
event reviewed : projection on Reviews {
|
||||
subject,
|
||||
rating
|
||||
}
|
||||
|
||||
// Input validation
|
||||
|
||||
@@ -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
|
||||
|
||||
33
samples.md
33
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,39 +32,46 @@ 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/suppliers](suppliers)
|
||||
|
||||
- Shows how to integrate remote services, in this case the BusinessPartner service from SAP S/4HANA.
|
||||
- Extending [@capire/bookshop](bookshop) with suppliers from SAP S/4HANA
|
||||
- Providing that as a pre-built integration & extension package
|
||||
- Used in [@capire/fiori](fiori)
|
||||
|
||||
|
||||
## [@capire/fiori](fiori)
|
||||
|
||||
- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:
|
||||
- [@capire/bookshop](bookshop)
|
||||
- [@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...
|
||||
- [@capire/orders](orders)
|
||||
- [@capire/reviews](reviews)
|
||||
- [@capire/suppliers](suppliers)
|
||||
- [Adds an 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
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
# All-in-one Monorepo
|
||||
|
||||
1
suppliers/index.cds
Normal file
1
suppliers/index.cds
Normal file
@@ -0,0 +1 @@
|
||||
using from './srv/mashup';
|
||||
24
suppliers/package.json
Normal file
24
suppliers/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@capire/suppliers",
|
||||
"version": "1.0.0",
|
||||
"description": "Shows integration with SAP S/4HANA, in turn provided as a reusable extension package to bookshop.",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@capire/common": "*",
|
||||
"@sap/cds": "^4",
|
||||
"express": "^4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cds run --in-memory?",
|
||||
"watch": "cds watch",
|
||||
"mocked-s4": "cds mock API_BUSINESS_PARTNER"
|
||||
},
|
||||
"cds": {
|
||||
"requires": {
|
||||
"API_BUSINESS_PARTNER": {
|
||||
"kind": "odata",
|
||||
"model": "srv/external/API_BUSINESS_PARTNER"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
suppliers/server.js
Normal file
3
suppliers/server.js
Normal file
@@ -0,0 +1,3 @@
|
||||
const cds = require ('@sap/cds')
|
||||
cds.once('served', require('./srv/mashup'))
|
||||
module.exports = cds.server
|
||||
2425
suppliers/srv/external/API_BUSINESS_PARTNER.csn
vendored
Normal file
2425
suppliers/srv/external/API_BUSINESS_PARTNER.csn
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3261
suppliers/srv/external/API_BUSINESS_PARTNER.edmx
vendored
Normal file
3261
suppliers/srv/external/API_BUSINESS_PARTNER.edmx
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
suppliers/srv/external/data/API_BUSINESS_PARTNER-A_BusinessPartner.csv
vendored
Normal file
7
suppliers/srv/external/data/API_BUSINESS_PARTNER-A_BusinessPartner.csv
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
BusinessPartner;BusinessPartnerFullName
|
||||
ACME;A Company Making Everything
|
||||
B4U;Books for You
|
||||
S&C;Shakespeare & Co.
|
||||
WSL;Waterstones
|
||||
TLD;Thalia
|
||||
PNG;Penguin Books
|
||||
|
5
suppliers/srv/external/data/API_BUSINESS_PARTNER-Suppliers.csv
vendored
Normal file
5
suppliers/srv/external/data/API_BUSINESS_PARTNER-Suppliers.csv
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
ID;name
|
||||
ACME;A Company Making Everything
|
||||
B4U;Books for You
|
||||
S&C;Shakespeare & Co.
|
||||
WSL;Waterstones
|
||||
|
25
suppliers/srv/mashup.cds
Normal file
25
suppliers/srv/mashup.cds
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Optionally add projections to external entities, to capture what
|
||||
you actually want to use from there.
|
||||
*/
|
||||
|
||||
using {API_BUSINESS_PARTNER as S4} from './external/API_BUSINESS_PARTNER.csn';
|
||||
|
||||
@cds.autoexpose // or expose explicitly in Catalog and AdminService
|
||||
@cds.persistence: {table,skip:false}
|
||||
entity sap.capire.bookshop.Suppliers as projection on S4.A_BusinessPartner {
|
||||
key BusinessPartner as ID, BusinessPartnerFullName as name
|
||||
}
|
||||
|
||||
using { sap.capire.bookshop.Books, CatalogService } from '@capire/bookshop';
|
||||
extend Books with {
|
||||
supplier: Association to sap.capire.bookshop.Suppliers;
|
||||
}
|
||||
|
||||
extend service AdminService with { // why is AdminService visible?
|
||||
entity Suppliers as projection on sap.capire.bookshop.Suppliers;
|
||||
}
|
||||
|
||||
extend projection CatalogService.ListOfBooks with {
|
||||
supplier
|
||||
}
|
||||
57
suppliers/srv/mashup.js
Normal file
57
suppliers/srv/mashup.js
Normal file
@@ -0,0 +1,57 @@
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Mashing up provided and required services...
|
||||
//
|
||||
module.exports = async()=>{ // called by server.js
|
||||
|
||||
if (!cds.services.AdminService) return //> mocking S4 service only
|
||||
|
||||
// Connect to services we want to mashup below...
|
||||
const S4bupa = await cds.connect.to('API_BUSINESS_PARTNER') //> external S4 service
|
||||
const admin = await cds.connect.to('AdminService') //> local domain service
|
||||
const db = await cds.connect.to('db') //> our primary database
|
||||
|
||||
// Reflect CDS definition of the Suppliers entity
|
||||
const { Suppliers } = S4bupa.entities
|
||||
|
||||
admin.prepend (()=>{ //> to ensure our .on handlers below go before the default ones
|
||||
|
||||
// Delegate Value Help reads for Suppliers to S4 backend
|
||||
admin.on ('READ', 'Suppliers', req => {
|
||||
console.log ('>> delegating to S4 service...')
|
||||
return S4bupa.run(req.query)
|
||||
})
|
||||
|
||||
// Replicate Supplier data when edited Books have suppliers
|
||||
admin.on (['CREATE','UPDATE'], 'Books', ({data:{supplier}}, next) => {
|
||||
// Using Promise.all(...) to parallelize local write, i.e. next(), and replication
|
||||
if (supplier) return Promise.all ([ next(), async()=>{
|
||||
let replicated = await db.exists (Suppliers, supplier)
|
||||
if (!replicated) await replicate (supplier, 'initial')
|
||||
}])
|
||||
else return next() //> don't forget to pass down the interceptor stack
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
// Subscribe to changes in the S4 origin of Suppliers data
|
||||
S4bupa.on ('BusinessPartners/Changed', async msg => { //> would be great if we had batch events from S/4
|
||||
let replicas = await SELECT('ID').from (Suppliers) .where ('ID in', msg.businessPartners)
|
||||
return replicate (replicas.map(each => each.ID))
|
||||
})
|
||||
|
||||
/**
|
||||
* Helper function to replicate Suppliers data.
|
||||
* @param {string|string[]} IDs a single ID or an array of IDs
|
||||
* @param {truthy|falsy} _initial indicates whether an insert or an update is required
|
||||
*/
|
||||
async function replicate (IDs,_initial) {
|
||||
if (!Array.isArray(IDs)) IDs = [ IDs ]
|
||||
let suppliers = await S4bupa.read (Suppliers).where('ID in',IDs)
|
||||
if (_initial) return db.insert (suppliers) .into (Suppliers) //> using bulk insert
|
||||
else return Promise.all(suppliers.map ( //> parallelizing updates
|
||||
each => db.update (Suppliers,each.ID) .with (each)
|
||||
))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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: [
|
||||
@@ -346,7 +365,7 @@ describe('cds.ql → cqn', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(
|
||||
if (!is_v2) expect(
|
||||
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,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