Compare commits

..

46 Commits

Author SHA1 Message Date
d045778
175e7b554f no replica context 2021-03-25 13:55:06 +01:00
d045778
803432b8d9 changed modeling of suppliers 2021-03-11 16:58:29 +01:00
Daniel
0ddd70acbc Using event : projection on Reviews 2021-03-11 14:14:59 +01:00
Daniel
3e52a9a102 Adding fix-antlr script 2021-03-08 19:19:29 +01:00
Daniel
b44701ef62 Merge branch 'master' into adding-suppliers 2021-03-08 19:07:56 +01:00
Daniel
647ac72588 Prepare for compiler v2 2021-03-03 10:39:32 +01:00
Iwona Hahn
861094cc83 Merge pull request #206 from SAP-samples/promote-code-tour
promote code tours
2021-03-02 09:15:19 +01:00
Christian Georgi
2368913612 Dummy change 2021-03-02 09:09:51 +01:00
Iwona Hahn
22ece2e586 typo 2021-03-01 20:44:05 +01:00
Iwona Hahn
ae788414f8 promote code tours 2021-03-01 20:42:43 +01:00
Iwona Hahn
ebc7017ded UA review 2021-03-01 20:42:23 +01:00
Christian Georgi
876ff94782 Merge pull request #205 from SAP-samples/db-codetour
Codetour for DB functions
2021-03-01 17:29:31 +01:00
Iwona Hahn
76d1b1865b minor edits 2021-03-01 17:24:53 +01:00
Christian Georgi
85dd8558f4 Some more explanation around file structure 2021-03-01 14:38:00 +01:00
Iwona Hahn
d0d95f3c42 first review, minor edits 2021-02-26 17:14:49 +01:00
Christian Georgi
c46a82d0f8 Codetour for DB functions 2021-02-26 16:40:57 +01:00
Iwona Hahn
7654012292 cosmetics codetour 2021-02-26 09:29:51 +01:00
Christian Georgi
65c8c82f74 Showcase how to integrate DB-specific functions (#201)
Augment `AdminService.Author` by 2 fields `age` and `lifetime` whose
values are computed by SQLite and HANA-specific functions.

Don't do this in bookshop directly, to keep it simple.  Instead, use
the fiori module, which also allows to integrate the fields in the UI.

Co-authored-by: Daniel <daniel.hutzel@sap.com>
2021-02-24 17:26:29 +01:00
Daniel
c23ddc7e54 Merge branch 'master' into adding-suppliers 2021-02-19 16:36:26 +01:00
Daniel
d368eb2ff5 fixed dependency -> never use "latest" 2021-02-19 16:34:15 +01:00
Christian Georgi
3cbb199870 Recommend CodeTours extension 2021-02-19 12:55:35 +01:00
Daniel
66bd2f707c Merge branch 'master' into adding-suppliers 2021-02-19 12:42:08 +01:00
Daniel
b51a08bf4e fixed .registry 2021-02-19 12:40:26 +01:00
Daniel
3320c7e5a2 some corrections and optimizations 2021-02-19 12:15:35 +01:00
Daniel
a35782e775 Merge branch 'master' into adding-suppliers 2021-02-19 12:10:49 +01:00
Iwona Hahn
e5bd8ec5a5 cosmetics 2021-02-17 17:55:27 +01:00
Iwona Hahn
0aa95a0a67 cosmetics 2021-02-17 17:54:47 +01:00
Iwona Hahn
5015eb8c52 cosmetics 2021-02-17 17:53:54 +01:00
Daniel Hutzel
6d3f4c689f Delete settings.json 2021-02-17 16:07:53 +01:00
Daniel Hutzel
f0fead2bc2 Delete launch.json 2021-02-17 16:07:43 +01:00
Daniel Hutzel
f1d780d6d9 Delete tasks.json 2021-02-17 16:07:23 +01:00
Daniel Hutzel
796bf62bde Delete extensions.json 2021-02-17 16:07:02 +01:00
Daniel Hutzel
5f176a0b88 Delete .gitignore 2021-02-17 16:06:51 +01:00
Christian Georgi
a5c8b5101e Make the CatalogService usage more obvious 2021-02-17 16:00:10 +01:00
Iwona Hahn
b31efc8083 minor edits 2021-02-17 14:07:42 +01:00
Christian Georgi
6669b983b1 First code tour with an overview of the sample repo 2021-02-17 14:07:42 +01:00
Daniel
d72ff809b0 Adding suppliers showing integration with S/4 2021-02-17 13:24:17 +01:00
Daniel
b6e5a2fced Removed demo 2021-02-17 12:32:27 +01:00
Daniel
6de09e0940 Make usage of @capire/common optional 2021-02-17 12:32:27 +01:00
Daniel
b6f3914d79 Minor cleanup to npm registry mock 2021-02-17 12:32:27 +01:00
Daniel
28402c58b3 Simplified redirects to imported apps 2021-02-17 12:32:27 +01:00
Daniel
77de0e445e demo -> done 2021-02-17 12:32:27 +01:00
Daniel
a037d92c97 demo for bhagat 2021-02-17 12:32:27 +01:00
Christian Georgi
b5031588ce Remove Node 10 for now 2021-02-09 17:15:24 +01:00
Christian Georgi
85319d9e8d Pin sqlite3 to 5.0.0
5.0.1 is broken at the moment
2021-02-09 17:15:24 +01:00
Iwona Hahn
39872200ae Update README.md 2021-02-01 14:54:55 +01:00
44 changed files with 6197 additions and 2762 deletions

View File

@@ -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
View File

@@ -0,0 +1 @@
*.tgz

View File

@@ -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
View 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
View 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"
}

View File

@@ -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": [

View File

@@ -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). &rarr; See [**Overview** of contained samples](samples.md)
![](https://github.com/SAP-samples/cloud-cap-samples/workflows/CI/badge.svg)
[![REUSE status](https://api.reuse.software/badge/github.com/SAP-samples/cloud-cap-samples)](https://api.reuse.software/info/github.com/SAP-samples/cloud-cap-samples)
<!--[![REUSE status](https://api.reuse.software/badge/github.com/SAP-samples/cloud-cap-samples)](https://api.reuse.software/info/github.com/SAP-samples/cloud-cap-samples)-->
### Preliminaries
@@ -18,7 +18,7 @@ Find here a collection of samples for the [SAP Cloud Application Programming Mod
### Download
If you have [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, 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
@@ -72,10 +72,14 @@ 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

View 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>

View File

@@ -3,6 +3,6 @@
"description": "Provides a pre-built extension package for std @sap/cds/common",
"version": "1.0.0",
"dependencies": {
"@sap/cds": "latest"
"@sap/cds": "*"
}
}

View File

@@ -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

View File

@@ -6,6 +6,7 @@ Authors = Autoren
Author = Autor
AuthorID = ID des Autors
AuthorName = Name des Autors
Age = Alter
Name = Name
Stock = Bestand
Order = Bestellung

View File

@@ -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}'},
]
},
}
);
////////////////////////////////////////////////////////////

View File

@@ -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>

View File

@@ -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
View 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
View 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
View 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
}

View File

@@ -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"
}
}
}
}
}
}

View File

@@ -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)))

View File

@@ -63,3 +63,15 @@ 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"
}

View File

@@ -1 +0,0 @@
@sap:registry=http://nexus.wdf.sap.corp:8081/nexus/repository/build.milestones.npm/

View File

@@ -1,15 +0,0 @@
{
// 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": [
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +0,0 @@
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')
}

View File

@@ -1,200 +0,0 @@
/** ------------------------------------------
* 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]+
//
// ------------------------------------

View File

@@ -1,15 +0,0 @@
{
"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
}

View File

@@ -1,105 +0,0 @@
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'`))
});
});
});

View File

@@ -21,6 +21,3 @@ entity Orders_Items {
entity Products @(cds.persistence.skip:'always') {
key ID : String;
}
// Activate extension package
using from '@capire/common';

View File

@@ -2,7 +2,6 @@
"name": "@capire/orders",
"version": "1.0.0",
"dependencies": {
"@capire/common": "*",
"@sap/cds": "^4.3.0"
}
}

View File

@@ -11,17 +11,18 @@
"@capire/hello": "./hello",
"@capire/media": "./media",
"@capire/orders": "./orders",
"@capire/reviews": "./reviews"
"@capire/reviews": "./reviews",
"@capire/suppliers": "./suppliers"
},
"devDependencies": {
"pegjs": "^0.10.0",
"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",

View File

@@ -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
```

View File

@@ -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

View File

@@ -49,22 +49,29 @@ Each sub directory essentially is an individual npm package arranged in an [all-
- Late-cut Micro Services
- 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 SAP 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 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
View File

@@ -0,0 +1 @@
using from './srv/mashup';

24
suppliers/package.json Normal file
View 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
View File

@@ -0,0 +1,3 @@
const cds = require ('@sap/cds')
cds.once('served', require('./srv/mashup'))
module.exports = cds.server

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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
1 BusinessPartner BusinessPartnerFullName
2 ACME A Company Making Everything
3 B4U Books for You
4 S&C Shakespeare & Co.
5 WSL Waterstones
6 TLD Thalia
7 PNG Penguin Books

View File

@@ -0,0 +1,5 @@
ID;name
ACME;A Company Making Everything
B4U;Books for You
S&C;Shakespeare & Co.
WSL;Waterstones
1 ID name
2 ACME A Company Making Everything
3 B4U Books for You
4 S&C Shakespeare & Co.
5 WSL Waterstones

25
suppliers/srv/mashup.cds Normal file
View 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
View 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)
))
}
}

View File

@@ -365,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)`)