Compare commits

..

1 Commits

Author SHA1 Message Date
Weinstock
a5acea58f2 Play with db constraints 2021-04-14 19:38:38 +02:00
130 changed files with 1010 additions and 16492 deletions

View File

@@ -8,7 +8,7 @@
"mocha": true "mocha": true
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": 2020 "ecmaVersion": 2018
}, },
"globals": { "globals": {
"SELECT": true, "SELECT": true,
@@ -22,7 +22,6 @@
"rules": { "rules": {
"no-console": "off", "no-console": "off",
"require-atomic-updates": "off", "require-atomic-updates": "off",
"require-await":"warn", "require-await":"warn"
"no-unused-vars": ["warn", { "argsIgnorePattern": "_" }]
} }
} }

View File

@@ -5,9 +5,9 @@ name: CI
on: on:
push: push:
branches: [ main ] branches: [ master ]
pull_request: pull_request:
branches: [ main ] branches: [ master ]
jobs: jobs:
build: build:
@@ -16,7 +16,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [16.x, 14.x, 12.x] node-version: [12.x, 14.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -24,5 +24,5 @@ jobs:
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: npm ci - run: npm install
- run: npm test - run: npm test

2
.gitignore vendored
View File

@@ -15,5 +15,3 @@ default-env.json
packages/messageBox packages/messageBox
reviews/msg-box reviews/msg-box
reviews/db/test.db reviews/db/test.db
*.openapi3.json

3
.npmrc
View File

@@ -1,3 +0,0 @@
# Ensure we always use public packages, i.e. avoid using local registries from ~/.npmrc
@sap:registry=https://registry.npmjs.org/
registry=https://registry.npmjs.org/

View File

@@ -1,25 +1,19 @@
const { exec } = require ('child_process') const { exec } = require ('child_process')
const isWin = process.platform === 'win32'
const express = require ('express') const express = require ('express')
const fs = require ('fs') const fs = require ('fs')
const app = express() const app = express()
const { PORT=4444 } = process.env const { PORT=4444 } = process.env
const [,,port=PORT,scope='@capire'] = process.argv const [,,port=PORT] = process.argv
const cwd = __dirname process.chdir(__dirname)
// clean up on start (exit handler might not complete on Windows)
exec(isWin ? 'del *.tgz' : 'rm *.tgz', {cwd})
app.use('/-/:tarball', (req,res,next) => { app.use('/-/:tarball', (req,res,next) => {
console.debug ('GET', req.params) console.debug ('GET', req.params)
try { try {
const { tarball } = req.params const { tarball } = req.params
const [, pkg ] = /^\w+-(\w+)/.exec(tarball) const [, pkg ] = /^capire-(\w+)/.exec(tarball)
fs.lstat(tarball,(err => { fs.lstat(tarball,(err => {
if (err) console.debug (`npm pack ../${pkg}`) if (err) exec(`npm pack ../${pkg}`,next)
if (err) exec(`npm pack ../${pkg}`,{cwd},next)
else next() else next()
})) }))
} catch (e) { } catch (e) {
@@ -31,14 +25,12 @@ app.use('/-/:tarball', (req,res,next) => {
app.use('/-', express.static(__dirname)) app.use('/-', express.static(__dirname))
app.get('/*', (req,res)=>{ app.get('/*', (req,res)=>{
const urlRegex = /^\/(@\w+)\/(\w+)/
const url = decodeURIComponent(req.url) const url = decodeURIComponent(req.url)
console.debug ('GET',url) console.debug ('GET',url)
try { try {
if (!urlRegex.test(url)) return res.sendStatus(404) const [, capire, pkg ] = /^\/(@capire)\/(\w+)/.exec(url)
const [, scpe, pkg ] = urlRegex.exec(url) const package = require (`${capire}/${pkg}/package.json`)
const package = require (`${scpe}/${pkg}/package.json`) const tarball = `capire-${pkg}-${package.version}.tgz`
const tarball = `${scpe.slice(1)}-${pkg}-${package.version}.tgz`
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md // https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
res.json({ res.json({
"name": package.name, "name": package.name,
@@ -50,30 +42,29 @@ app.get('/*', (req,res)=>{
"name": package.name, "name": package.name,
"version": package.version, "version": package.version,
"dist": { "dist": {
"tarball": `${server.url}/-/${tarball}` "tarball": `http://localhost:${port}/-/${tarball}`
}, },
} }
}, },
}) })
} catch (e) { } catch (e) {
if (e.code === 'MODULE_NOT_FOUND') return res.sendStatus(404) console.error(e)
console.error(e); throw e res.sendStatus(404)
} }
}) })
const server = app.listen(port, ()=>{ app.listen(port, ()=>{
const url = server.url = `http://localhost:${server.address().port}` console.log (`npm set @capire:registry=http://localhost:${port}`)
console.log (`npm set ${scope}:registry=${url}`) console.log (`@capire registry listening on http://localhost:${port}`)
exec(`npm set ${scope}:registry=${url}`) exec(`npm set @capire:registry=http://localhost:${port}`)
console.log (`${scope} registry listening on ${url}`)
}) })
const _exit = ()=>{ const _exit = ()=>{
server.close() console.log ('\nnpm conf rm @capire:registry')
exec(`npm conf rm "${scope}:registry"`, ()=> { process.exit() }) exec('npm conf rm @capire:registry')
exec('rm *.tgz')
process.exit()
} }
process.on ('SIGTERM',_exit) process.on ('SIGTERM',_exit)
process.on ('SIGHUP',_exit) process.on ('SIGHUP',_exit)
process.on ('SIGINT',_exit) process.on ('SIGINT',_exit)

View File

@@ -68,35 +68,26 @@
}, },
{ {
"file": "fiori/package.json", "file": "fiori/package.json",
"description": "#### Configuration\n\nThe `cds.requires` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\n\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. `db-ext` is a pseudo datasource, its name doesn't matter.\n\nSee [`cds.resolve`](https://cap.cloud.sap/docs/node.js/cds-compile#cds-resolve) to learn more about how models are found.", "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.",
"selection": { "line": 17,
"start": {
"line": 41,
"character": 1
},
"end": {
"line": 48,
"character": 1
}
},
"title": "Configuration" "title": "Configuration"
}, },
{ {
"file": "fiori/package.json", "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", "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": 43, "line": 28,
"title": "Run with SQLite" "title": "Run with SQLite"
}, },
{ {
"file": "fiori/package.json", "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.", "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": 46, "line": 31,
"title": "Run with SAP HANA" "title": "Run with SAP HANA"
}, },
{ {
"file": "fiori/test/requests.http", "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).", "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": 72, "line": 68,
"selection": { "selection": {
"start": { "start": {
"line": 67, "line": 67,
@@ -113,5 +104,6 @@
"title": "Wrap-up", "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." "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"
} }

View File

@@ -19,7 +19,7 @@
} }
}, },
{ {
"file": "hello/srv/world.cds", "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).", "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, "line": 2,
"selection": { "selection": {
@@ -68,7 +68,7 @@
}, },
{ {
"file": "orders/db/schema.cds", "file": "orders/db/schema.cds",
"description": "### Orders - 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", "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, "line": 1,
"selection": { "selection": {
"start": { "start": {
@@ -84,7 +84,7 @@
}, },
{ {
"file": "reviews/db/schema.cds", "file": "reviews/db/schema.cds",
"description": "### Reviews - 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", "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, "line": 1,
"selection": { "selection": {
"start": { "start": {
@@ -99,12 +99,8 @@
"title": "Reviews" "title": "Reviews"
}, },
{ {
"title": "Bookstore", "file": "fiori/app/index.cds",
"description": "### Bookstore - Reuse and UI\n\n- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/reuse-and-compose) these packages:\n - [@capire/bookshop](bookshop)\n - [@capire/reviews](reviews)\n - [@capire/orders](orders)\n - [@capire/common](common)\n- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well\n- [The Vue.js app](reviews/app/vue) imported from reviews is served as well\n- [The Fiori app](orders/app) imported from orders is served as well\n- [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)" "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",
},
{
"file": "fiori/app/services.cds",
"description": "### Annotations for SAP Fiori Elements\n\n- [Adds an SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookstore, 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",
"line": 1, "line": 1,
"selection": { "selection": {
"start": { "start": {
@@ -121,13 +117,14 @@
{ {
"file": "package.json", "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", "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": { "selection": {
"start": { "start": {
"line": 8, "line": 8,
"character": 1 "character": 1
}, },
"end": { "end": {
"line": 16, "line": 15,
"character": 1 "character": 1
} }
}, },
@@ -136,4 +133,4 @@
], ],
"isPrimary": true, "isPrimary": true,
"description": "Overview of CAP Samples for Node.js" "description": "Overview of CAP Samples for Node.js"
} }

View File

@@ -14,5 +14,4 @@
"**/odata-v4/okra/**" "**/odata-v4/okra/**"
] ]
}, },
"mochaExplorer.parallel": true
} }

View File

@@ -17,7 +17,7 @@ Find here a collection of samples for the [SAP Cloud Application Programming Mod
### Download ### Download
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/main.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 ```sh
git clone https://github.com/sap-samples/cloud-cap-samples samples git clone https://github.com/sap-samples/cloud-cap-samples samples
@@ -83,4 +83,4 @@ In case you've a question, find a bug, or otherwise need support, use our [commu
## License ## License
Copyright (c) 2022 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](LICENSE) 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](LICENSE.txt) file.

2
bookshop/app/index.cds Normal file
View File

@@ -0,0 +1,2 @@
// Incorporate pre-build extensions from...
using from '@capire/common';

View File

@@ -10,7 +10,7 @@ const books = new Vue ({
data: { data: {
list: [], list: [],
book: undefined, book: undefined,
order: { quantity:1, succeeded:'', failed:'' } order: { amount:1, succeeded:'', failed:'' }
}, },
methods: { methods: {
@@ -26,18 +26,18 @@ const books = new Vue ({
const book = books.book = books.list [eve.currentTarget.rowIndex-1] const book = books.book = books.list [eve.currentTarget.rowIndex-1]
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`) const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
Object.assign (book, res.data) Object.assign (book, res.data)
books.order = { quantity:1 } books.order = { amount:1 }
setTimeout (()=> $('form > input').focus(), 111) setTimeout (()=> $('form > input').focus(), 111)
}, },
async submitOrder () { async submitOrder () {
const {book,order} = books, quantity = parseInt (order.quantity) || 1 // REVISIT: Okra should be less strict const {book,order} = books, amount = parseInt (order.amount) || 1 // REVISIT: Okra should be less strict
try { try {
const res = await POST(`/submitOrder`, { quantity, book: book.ID }) const res = await POST(`/submitOrder`, { amount, book: book.ID })
book.stock = res.data.stock book.stock = res.data.stock
books.order = { quantity, succeeded: `Successfully ordered ${quantity} item(s).` } books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` }
} catch (e) { } catch (e) {
books.order = { quantity, failed: e.response.data.error.message } books.order = { amount, failed: e.response.data.error.message }
} }
} }

View File

@@ -34,7 +34,7 @@
<td>{{ book.author }}</td> <td>{{ book.author }}</td>
<td>{{ book.genre.name }}</td> <td>{{ book.genre.name }}</td>
<td class="rating-stars"> <td class="rating-stars">
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} ({{ book.numberOfReviews }}) {{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
</td> </td>
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td> <td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
</tr> </tr>
@@ -48,7 +48,7 @@
&nbsp;&nbsp; {{ book.stock }} in stock &nbsp;&nbsp; {{ book.stock }} in stock
</label> </label>
<form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse"> <form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
<input type="number" v-model="order.quantity" v-bind:class="{ failed: order.failed }" style="width:5em"> <input type="number" v-model="order.amount" v-bind:class="{ failed: order.failed }" style="width:5em">
<input type="submit" value="Order:" class="muted-button"> <input type="submit" value="Order:" class="muted-button">
</form> </form>
<h4> {{ book.title }} </h4> <h4> {{ book.title }} </h4>

View File

@@ -1,5 +1,5 @@
ID;name;dateOfBirth;placeOfBirth;dateOfDeath;placeOfDeath ID;name
101;Emily Brontë;1818-07-30;Thornton, Yorkshire;1848-12-19;Haworth, Yorkshire 101;Emily Brontë
107;Charlotte Brontë;1818-04-21;Thornton, Yorkshire;1855-03-31;Haworth, Yorkshire 107;Charlotte Brontë
150;Edgar Allen Poe;1809-01-19;Boston, Massachusetts;1849-10-07;Baltimore, Maryland 150;Edgar Allen Poe
170;Richard Carpenter;1929-08-14;Kings Lynn, Norfolk;2012-02-26;Hertfordshire, England 170;Richard Carpenter
1 ID name dateOfBirth placeOfBirth dateOfDeath placeOfDeath
2 101 Emily Brontë 1818-07-30 Thornton, Yorkshire 1848-12-19 Haworth, Yorkshire
3 107 Charlotte Brontë 1818-04-21 Thornton, Yorkshire 1855-03-31 Haworth, Yorkshire
4 150 Edgar Allen Poe 1809-01-19 Boston, Massachusetts 1849-10-07 Baltimore, Maryland
5 170 Richard Carpenter 1929-08-14 King’s Lynn, Norfolk 2012-02-26 Hertfordshire, England

View File

@@ -1,6 +1,6 @@
ID;title;descr;author_ID;stock;price;currency_code;genre_ID ID;title;author_ID;genre_ID
201;Wuthering Heights;"Wuthering Heights, Emily Brontë's only novel, was published in 1847 under the pseudonym ""Ellis Bell"". It was written between October 1845 and June 1846. Wuthering Heights and Anne Brontë's Agnes Grey were accepted by publisher Thomas Newby before the success of their sister Charlotte's novel Jane Eyre. After Emily's death, Charlotte edited the manuscript of Wuthering Heights and arranged for the edited version to be published as a posthumous second edition in 1850.";101;12;11.11;GBP;11 201;Wuthering Heights;101;11
207;Jane Eyre;"Jane Eyre /ɛər/ (originally published as Jane Eyre: An Autobiography) is a novel by English writer Charlotte Brontë, published under the pen name ""Currer Bell"", on 16 October 1847, by Smith, Elder & Co. of London. The first American edition was published the following year by Harper & Brothers of New York. Primarily a bildungsroman, Jane Eyre follows the experiences of its eponymous heroine, including her growth to adulthood and her love for Mr. Rochester, the brooding master of Thornfield Hall. The novel revolutionised prose fiction in that the focus on Jane's moral and spiritual development is told through an intimate, first-person narrative, where actions and events are coloured by a psychological intensity. The book contains elements of social criticism, with a strong sense of Christian morality at its core and is considered by many to be ahead of its time because of Jane's individualistic character and how the novel approaches the topics of class, sexuality, religion and feminism.";107;11;12.34;GBP;11 207;Jane Eyre;107;11
251;The Raven;"""The Raven"" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word ""Nevermore"". The poem makes use of folk, mythological, religious, and classical references.";150;333;13.13;USD;16 251;The Raven;150;16
252;Eleonora;"""Eleonora"" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively ""happy"" ending.";150;555;14;USD;16 252;Eleonora;150;16
271;Catweazle;Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.;170;22;150;JPY;13 271;Catweazle;170;13
1 ID title descr author_ID genre_ID stock price currency_code
2 201 Wuthering Heights Wuthering Heights, Emily Brontë's only novel, was published in 1847 under the pseudonym "Ellis Bell". It was written between October 1845 and June 1846. Wuthering Heights and Anne Brontë's Agnes Grey were accepted by publisher Thomas Newby before the success of their sister Charlotte's novel Jane Eyre. After Emily's death, Charlotte edited the manuscript of Wuthering Heights and arranged for the edited version to be published as a posthumous second edition in 1850. 101 11 12 11.11 GBP
3 207 Jane Eyre Jane Eyre /ɛər/ (originally published as Jane Eyre: An Autobiography) is a novel by English writer Charlotte Brontë, published under the pen name "Currer Bell", on 16 October 1847, by Smith, Elder & Co. of London. The first American edition was published the following year by Harper & Brothers of New York. Primarily a bildungsroman, Jane Eyre follows the experiences of its eponymous heroine, including her growth to adulthood and her love for Mr. Rochester, the brooding master of Thornfield Hall. The novel revolutionised prose fiction in that the focus on Jane's moral and spiritual development is told through an intimate, first-person narrative, where actions and events are coloured by a psychological intensity. The book contains elements of social criticism, with a strong sense of Christian morality at its core and is considered by many to be ahead of its time because of Jane's individualistic character and how the novel approaches the topics of class, sexuality, religion and feminism. 107 11 11 12.34 GBP
4 251 The Raven "The Raven" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word "Nevermore". The poem makes use of folk, mythological, religious, and classical references. 150 16 333 13.13 USD
5 252 Eleonora "Eleonora" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively "happy" ending. 150 16 555 14 USD
6 271 Catweazle Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts. 170 13 22 150 JPY

View File

@@ -1,5 +1,5 @@
ID;locale;title;descr ID;locale;title
201;de;Sturmhöhe;Sturmhöhe (Originaltitel: Wuthering Heights) ist der einzige Roman der englischen Schriftstellerin Emily Brontë (18181848). Der 1847 unter dem Pseudonym Ellis Bell veröffentlichte Roman wurde vom viktorianischen Publikum weitgehend abgelehnt, heute gilt er als ein Klassiker der britischen Romanliteratur des 19. Jahrhunderts. 201;de;Sturmhöhe
201;fr;Les Hauts de Hurlevent;Les Hauts de Hurlevent (titre original : Wuthering Heights), parfois orthographié Les Hauts de Hurle-Vent, est l'unique roman d'Emily Brontë, publié pour la première fois en 1847 sous le pseudonyme dEllis Bell. Loin d'être un récit moralisateur, Emily Brontë achève néanmoins le roman dans une atmosphère sereine, suggérant le triomphe de la paix et du Bien sur la vengeance et le Mal. 201;fr;Les Hauts de Hurlevent
207;de;Jane Eyre;Jane Eyre. Eine Autobiographie (Originaltitel: Jane Eyre. An Autobiography), erstmals erschienen im Jahr 1847 unter dem Pseudonym Currer Bell, ist der erste veröffentlichte Roman der britischen Autorin Charlotte Brontë und ein Klassiker der viktorianischen Romanliteratur des 19. Jahrhunderts. Der Roman erzählt in Form einer Ich-Erzählung die Lebensgeschichte von Jane Eyre (ausgesprochen /ˌdʒeɪn ˈɛə/), die nach einer schweren Kindheit eine Stelle als Gouvernante annimmt und sich in ihren Arbeitgeber verliebt, jedoch immer wieder um ihre Freiheit und Selbstbestimmung kämpfen muss. Als klein, dünn, blass, stets schlicht dunkel gekleidet und mit strengem Mittelscheitel beschrieben, gilt die Heldin des Romans Jane Eyre nicht zuletzt aufgrund der Kino- und Fernsehversionen der melodramatischen Romanvorlage als die bekannteste englische Gouvernante der Literaturgeschichte 207;de;Jane Eyre
252;de;Eleonora;“Eleonora” ist eine Erzählung von Edgar Allan Poe. Sie wurde 1841 erstveröffentlicht. In ihr geht es um das Paradox der Treue in der Treulosigkeit. 252;de;Eleonora
1 ID locale title descr
2 201 de Sturmhöhe Sturmhöhe (Originaltitel: Wuthering Heights) ist der einzige Roman der englischen Schriftstellerin Emily Brontë (1818–1848). Der 1847 unter dem Pseudonym Ellis Bell veröffentlichte Roman wurde vom viktorianischen Publikum weitgehend abgelehnt, heute gilt er als ein Klassiker der britischen Romanliteratur des 19. Jahrhunderts.
3 201 fr Les Hauts de Hurlevent Les Hauts de Hurlevent (titre original : Wuthering Heights), parfois orthographié Les Hauts de Hurle-Vent, est l'unique roman d'Emily Brontë, publié pour la première fois en 1847 sous le pseudonyme d’Ellis Bell. Loin d'être un récit moralisateur, Emily Brontë achève néanmoins le roman dans une atmosphère sereine, suggérant le triomphe de la paix et du Bien sur la vengeance et le Mal.
4 207 de Jane Eyre Jane Eyre. Eine Autobiographie (Originaltitel: Jane Eyre. An Autobiography), erstmals erschienen im Jahr 1847 unter dem Pseudonym Currer Bell, ist der erste veröffentlichte Roman der britischen Autorin Charlotte Brontë und ein Klassiker der viktorianischen Romanliteratur des 19. Jahrhunderts. Der Roman erzählt in Form einer Ich-Erzählung die Lebensgeschichte von Jane Eyre (ausgesprochen /ˌdʒeɪn ˈɛə/), die nach einer schweren Kindheit eine Stelle als Gouvernante annimmt und sich in ihren Arbeitgeber verliebt, jedoch immer wieder um ihre Freiheit und Selbstbestimmung kämpfen muss. Als klein, dünn, blass, stets schlicht dunkel gekleidet und mit strengem Mittelscheitel beschrieben, gilt die Heldin des Romans Jane Eyre nicht zuletzt aufgrund der Kino- und Fernsehversionen der melodramatischen Romanvorlage als die bekannteste englische Gouvernante der Literaturgeschichte
5 252 de Eleonora “Eleonora” ist eine Erzählung von Edgar Allan Poe. Sie wurde 1841 erstveröffentlicht. In ihr geht es um das Paradox der Treue in der Treulosigkeit.

View File

@@ -14,3 +14,8 @@ ID;parent_ID;name
22;21;Autobiography 22;21;Autobiography
23;20;Essay 23;20;Essay
24;20;Speech 24;20;Speech
30;;Others
31;30;Other-A
32;30;Other-A
33;30;Other-A
34;30;Other-A
1 ID parent_ID name
14 22 21 Autobiography
15 23 20 Essay
16 24 20 Speech
17 30 Others
18 31 30 Other-A
19 32 30 Other-A
20 33 30 Other-A
21 34 30 Other-A

View File

@@ -1,24 +0,0 @@
/**
* In order to keep basic bookshop sample as simple as possible, we don't add
* reuse dependencies. This db/init.js ensures we still have a minimum set of
* currencies, if not obtained through @capire/common.
*/
module.exports = async (db)=>{
const has_common = db.model.definitions['sap.common.Currencies'].elements.numcode
if (has_common) return
const already_filled = await db.exists('sap.common.Currencies',{code:'EUR'})
if (already_filled) return
await INSERT.into ('sap.common.Currencies') .columns (
'code','symbol','name'
) .rows (
[ 'EUR','€','Euro' ],
[ 'USD','$','US Dollar' ],
[ 'GBP','£','British Pound' ],
[ 'ILS','₪','Shekel' ],
[ 'JPY','¥','Yen' ],
)
}

View File

@@ -1,29 +1,19 @@
using { Currency, managed, sap } from '@sap/cds/common'; using { Currency, managed, sap } from '@sap/cds/common';
namespace sap.capire.bookshop; namespace sap.capire.bookshop;
entity Books : managed { entity Books {
key ID : Integer; key ID : Integer;
title : localized String(111); title : localized String(111);
descr : localized String(1111);
author : Association to Authors; author : Association to Authors;
genre : Association to Genres; genre : Association to Genres;
stock : Integer;
price : Decimal;
currency : Currency;
image : LargeBinary @Core.MediaType : 'image/png';
} }
entity Authors : managed { entity Authors {
key ID : Integer; key ID : Integer;
name : String(111); name : String(111);
dateOfBirth : Date;
dateOfDeath : Date;
placeOfBirth : String;
placeOfDeath : String;
books : Association to many Books on books.author = $self; books : Association to many Books on books.author = $self;
} }
/** Hierarchically organized Code List for Genres */
entity Genres : sap.common.CodeList { entity Genres : sap.common.CodeList {
key ID : Integer; key ID : Integer;
parent : Association to Genres; parent : Association to Genres;

View File

@@ -1,4 +1,3 @@
namespace sap.capire.bookshop; //> important for reflection namespace sap.capire.bookshop; //> important for reflection
using from './db/schema'; using from './db/schema';
using from './srv/cat-service';
using from './srv/admin-service'; using from './srv/admin-service';

View File

@@ -1,2 +0,0 @@
const { CatalogService } = require('./srv/cat-service')
module.exports = { CatalogService }

View File

@@ -5,17 +5,40 @@
"dependencies": { "dependencies": {
"@sap/cds": "^5.0.4", "@sap/cds": "^5.0.4",
"express": "^4.17.1", "express": "^4.17.1",
"passport": "0.4.1" "passport": "0.4.1",
"@sap/hana-client": "^2.7.21"
}, },
"scripts": { "scripts": {
"genres": "cds serve test/genres.cds",
"start": "cds run",
"watch": "cds watch" "watch": "cds watch"
}, },
"cds": { "cds": {
"requires": { "requires": {
"db": { "db": {
"kind": "sql" "kind": "sql",
"xxx-model": "*",
"xxx-credentials": {
"database": "localSqlite.db"
}
},
"[production]": {
"db": {
"kind": "hana",
"model": [
"db",
"srv"
]
}
}
},
"features": {
"assert_integrity": false
},
"hana": {
"deploy-format": "hdbtable"
},
"cdsc": {
"beta": {
"foreignKeyConstraints": true
} }
} }
} }

View File

@@ -1,31 +1,126 @@
# Bookshop Getting Started Sample # Foreign key constraints on DB
This stand-alone sample introduces the essential tasks in the development of CAP-based services as also covered in the [Getting Started guide in capire](https://cap.cloud.sap/docs/get-started/in-a-nutshell). As model we use a simplified version of the bookshop scenario, we work with nodejs runtime.
## Hypothetical Use Cases
1. Build a service that allows to browse _Books_ and _Authors_. ## Preparation
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).
## Running the Sample As we want to see the effects of the database constaints, we first need to switch off the integrity checks done by the node runtime. In the `package.json`:
```json
```sh "cds": {
npm run watch ...,
"features": {
"assert_integrity": false
}
}
``` ```
## Content & Best Practices Then switch on constraint generation. In the `package.json`:
```json
"cds": {
...,
"cdsc": {
"beta": {
"foreignKeyConstraints": true
}
}
}
```
| Links to capire | Sample files / folders | ## SQLite
| --------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| [Project Setup & Layouts](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) | [`./`](./) | In SQLite, FK constraints can only be specified during the creation of a table. They cannot be altered for an existing table. So here we at least don't have issues with existing data.
| [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) | For SQLite, constraint checks need to be enabled by application at runtime for each db connection with the command
| [Single-purposed Services](https://cap.cloud.sap/docs/guides/services#single-purposed-services) | [`./srv/*.cds`](./srv) | ```sql
| [Providing & Consuming Providers](https://cap.cloud.sap/docs/guides/providing-services) | http://localhost:4004 | PRAGMA foreign_keys = ON;
| [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) | We do this via a custom handler in `admin-service.js`.
| Adding Tests | [`./test`](./test) |
| [Sharing for Reuse](https://cap.cloud.sap/docs/guides/reuse-and-compose) | [`./index.cds`](./index.cds) | Now run the requests in `requests.http` (use `http://localhost:4004` as server URL). They behave as expected.
But the error messages are not providing any details. They say "constraint violation", but they don't say which table/constraint is violated:
```
HTTP/1.1 500 Internal Server Error
...
{
"error": {
"code": "SQLITE_CONSTRAINT",
"message": "SQLITE_CONSTRAINT: FOREIGN KEY constraint failed in: \nCOMMIT"
}
}
```
The runtime constraint checks do provide the name of the restricting association (but not the name of the entity where the association lives).
## HANA
Preparation:
* logon to cf account (e.g. trial)
* create HANA service instance: `cf create-service hanatrial hdi-shared bookshop-db`
First make the deplyoment without constraints (switch off constraint generation in `package.json`).
* Run `cds build --production`. No constraints are generated.
* Deploy db model: `cf push -f gen/db`
* Deploy nodejs app: `cf push -f gen/srv --random-route`. Take note of the app URL.
Run the requests in `request.http` (use the URL from the previous step as server URL, prepend with `https://`).
Make sure to introduce some data that violates the constraints. No errors are reported - as expected.
Now switch on constraint generation in `package.json`.
* Run `cds build --production`. The constraints are placed in separate `hdbconstraint` files in `gen\db\src\gen`. They are generated with `VALIDATED ON`.
* Re-deploy db model: `cf push -f gen/db`. It fails, as the existing data violates the constraints.
* Check logs: `cf logs bookshop-db-deployer --recent`.
```
Error: com.sap.hana.di.constraint: Could not create the database object [8250005]
at "src/gen/sap.capire.bookshop.Books_author.hdbconstraint" (1:1)
Error: com.sap.hana.di.constraint: Database error 7: : feature not supported: referential constraint violated by existing value [8201003]
at "src/gen/sap.capire.bookshop.Books_author.hdbconstraint" (1:1)
```
You can see which constraint is violated, but there is no info about the offensive entries.
Now use the requests from `request.http` to fix the entries that have been broken above.
* Re-deploy db model: `cf push -f gen/db`. It should succeed.
Again run the requests from `request.http`. This time all changes that would lead to a
constraint violation fail.
Inserting a book with non-existing author ID or changing a book's author ID to an invalid value yields
the message
```
HTTP/1.1 500 Internal Server Error
...
{
"error": {
"code": "500",
"message": "Internal Server Error"
}
}
```
When trying to delete a genre that is still referenced in a book we get
```
HTTP/1.1 462 status code 462
...
{
"error": {
"code": "462",
"message": "failed on update or delete by foreign key constraint violation: TrexColumnUpdate failed on table '21F87859727E470E94917B690908A09D:SAP_CAPIRE_BOOKSHOP_GENRES' with error: DELETE on 21F87859727E470E94917B690908A09D:SAP_CAPIRE_BOOKSHOP_GENRES(ID) failed, because 1 rows with corresponding foreign keys still exist in 21F87859727E470E94917B690908A09D:SAP_CAPIRE_BOOKSHOP_BOOKS(GENRE_ID), rc=1536"
}
}
```
Note: when using `cdsc` to generate the constraints
```
cdsc Q --beta foreignKeyConstraints -d hana -s hdi -o dbstuff db\schema.cds srv\admin-service.cds
```
there will be constraints for e.g. `sap_common_Currencies_texts`. This cannot be deplyoed, as the corresponding table is removed from `cds` via the tree-shaking mechanism. These constraints need to be removed manually before deployment.
## Todo
* Provide a way to control parameters `VALIDATED` and `ENFORCED` of the constraints (only interesting for HANA, as in SQLite changing constraints after table generation is not possible).
* Provide some tool to identify existing entries that violate constraints. We can generate a list of `SELECT` statements that find these entries, but how do we run it and show the results to the user?
* Can runtimes improve the error messages?

View File

@@ -1,5 +1,6 @@
using { sap.capire.bookshop as my } from '../db/schema'; using { sap.capire.bookshop as my } from '../db/schema';
service AdminService @(requires:'admin') { service AdminService {
entity Books as projection on my.Books; entity Books as projection on my.Books;
entity Authors as projection on my.Authors; entity Authors as projection on my.Authors;
entity Genres as projection on my.Genres;
} }

View File

@@ -1,12 +1,15 @@
// sqlite: FK constraint checks need to be enabled by application at runtime
// for each db connection
const cds = require('@sap/cds') const cds = require('@sap/cds')
module.exports = cds.service.impl (async function(){
module.exports = cds.service.impl (function(){ const db = await cds.connect.to('db')
this.before ('NEW','Authors', genid)
this.before ('NEW','Books', genid) if (db.kind === 'sqlite')
{
db.before('BEGIN', function (req) {
//console.log('--- PRAGMA foreign_keys = ON ---')
return this.dbc.run('PRAGMA foreign_keys = ON;')
})
}
}) })
/** Generate primary keys for target entity in request */
async function genid (req) {
const {ID} = await cds.tx(req).run (SELECT.one.from(req.target).columns('max(ID) as ID'))
req.data.ID = ID - ID % 100 + 100 + 1
}

View File

@@ -1,16 +0,0 @@
using { sap.capire.bookshop as my } from '../db/schema';
service CatalogService @(path:'/browse') {
/** For displaying lists of Books */
@readonly entity ListOfBooks as projection on Books
excluding { descr };
/** For display in details pages */
@readonly entity Books as projection on my.Books { *,
author.name as author
} excluding { createdBy, modifiedBy };
@requires: 'authenticated-user'
action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer };
event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String };
}

View File

@@ -1,28 +0,0 @@
const cds = require('@sap/cds')
class CatalogService extends cds.ApplicationService { init(){
const { Books } = cds.entities ('sap.capire.bookshop')
// Reduce stock of ordered books if available stock suffices
this.on ('submitOrder', async req => {
const {book,quantity} = req.data
if (quantity < 1) return req.reject (400,`quantity has to be 1 or more`)
let b = await SELECT `stock` .from (Books,book)
if (!b) return req.error (404,`Book #${book} doesn't exist`)
let {stock} = b
if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`)
await UPDATE (Books,book) .with ({ stock: stock -= quantity })
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
return { stock }
})
// Add some discount for overstocked books
this.after ('READ','ListOfBooks', each => {
if (each.stock > 111) each.title += ` -- 11% discount!`
})
return super.init()
}}
module.exports = { CatalogService }

View File

@@ -1,4 +0,0 @@
using { sap.capire.bookshop as my } from '../db/schema';
service TestService {
entity Genres as projection on my.Genres;
}

View File

@@ -1,38 +0,0 @@
#################################################
#
# Genres
#
GET http://localhost:4004/test/Genres?
###
GET http://localhost:4004/test/Genres?
&$filter=parent_ID eq null&$select=name
&$expand=children($select=name)
###
POST http://localhost:4004/test/Genres?
Content-Type: application/json
{ "ID":100, "name":"Some Sample Genres...", "children":[
{ "ID":101, "name":"Cat", "children":[
{ "ID":102, "name":"Kitty", "children":[
{ "ID":103, "name":"Kitty Cat", "children":[
{ "ID":104, "name":"Aristocat" } ]},
{ "ID":105, "name":"Kitty Bat" } ]},
{ "ID":106, "name":"Catwoman", "children":[
{ "ID":107, "name":"Catalina" } ]} ]},
{ "ID":108, "name":"Catweazle" }
]}
###
GET http://localhost:4004/test/Genres(100)?
# &$expand=children
# &$expand=children($expand=children($expand=children($expand=children)))
###
DELETE http://localhost:4004/test/Genres(103)
###
DELETE http://localhost:4004/test/Genres(100)
###

View File

@@ -1,95 +1,68 @@
# running locally
@server = http://localhost:4004 @server = http://localhost:4004
@me = Authorization: Basic {{$processEnv USER}}:
# running on cf
#@server = https://bookshop-srv-....hana.ondemand.com
### get all books
GET {{server}}/admin/Books
### get all authors
GET {{server}}/admin/Authors
### get all genres
GET {{server}}/admin/Genres
### ------------------------------------------------------------------------ ### create book with wrong author ID -> should fail
# Get service info POST {{server}}/admin/Books
GET {{server}}/browse Content-Type: application/json
{{me}}
### ------------------------------------------------------------------------
# Get $metadata document
GET {{server}}/browse/$metadata
{{me}}
### ------------------------------------------------------------------------
# Browse Books as any user
GET {{server}}/browse/Books?
# &$select=title,stock
# &$expand=currency
# &sap-language=de
{{me}}
### ------------------------------------------------------------------------
# Fetch Authors as admin
GET {{server}}/admin/Authors?
# &$select=name,dateOfBirth,placeOfBirth
# &$expand=books($select=title;$expand=currency)
# &$filter=ID eq 101
# &sap-language=de
Authorization: Basic alice:
### ------------------------------------------------------------------------
# Create Author
POST {{server}}/admin/Authors
Content-Type: application/json;IEEE754Compatible=true
Authorization: Basic alice:
{ {
"ID": 112, "ID": 701,
"name": "Shakespeeeeere", "title": "XXX",
"age": 22 "author": { "ID": 9999 }
} }
### create book with correct author ID -> should work
### ------------------------------------------------------------------------
# Create book
POST {{server}}/admin/Books POST {{server}}/admin/Books
Content-Type: application/json;IEEE754Compatible=true Content-Type: application/json;IEEE754Compatible=true
Authorization: Basic alice:
{ {
"ID": 2, "ID": 702,
"title": "Poems : Pocket Poets", "title": "Poems : Pocket Poets",
"descr": "The Everyman's Library Pocket Poets hardcover series is popular for its compact size and reasonable price which does not compromise content. Poems: Bronte contains poems that demonstrate a sensibility elemental in its force with an imaginative discipline and flexibility of the highest order. Also included are an Editor's Note and an index of first lines.", "author": { "ID": 101 }
"author": { "ID": 101 },
"genre": { "ID": 12 },
"stock": 5,
"price": "12.05",
"currency": { "code": "USD" }
} }
### delete book
DELETE {{server}}/admin/Books(ID=701)
### ------------------------------------------------------------------------ ### change book to wrong author ID -> should fail
# Put image to books
PUT {{server}}/admin/Books(2)/image
Content-Type: image/png
Authorization: Basic alice:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANwAAADcCAYAAAAbWs+BAAAGwElEQVR4Ae3cwZFbNxBFUY5rkrDTmKAUk5QT03Aa44U22KC7NHptw+DRikVAXf8fzC3u8Hj4R4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgZzAW26USQT+e4HPx+Mz+RRvj0e0kT+SD2cWAQK1gOBqH6sEogKCi3IaRqAWEFztY5VAVEBwUU7DCNQCgqt9rBKICgguymkYgVpAcLWPVQJRAcFFOQ0jUAsIrvaxSiAqILgop2EEagHB1T5WCUQFBBflNIxALSC42scqgaiA4KKchhGoBQRX+1glEBUQXJTTMAK1gOBqH6sEogKCi3IaRqAWeK+Xb1z9iN558fHxcSPS9p2ezx/ROz4e4TtIHt+3j/61hW9f+2+7/+UXbifjewIDAoIbQDWSwE5AcDsZ3xMYEBDcAKqRBHYCgtvJ+J7AgIDgBlCNJLATENxOxvcEBgQEN4BqJIGdgOB2Mr4nMCAguAFUIwnsBAS3k/E9gQEBwQ2gGklgJyC4nYzvCQwICG4A1UgCOwHB7WR8T2BAQHADqEYS2AkIbifjewIDAoIbQDWSwE5AcDsZ3xMYEEjfTzHwiK91B8npd6Q8n8/oGQ/ckRJ9vvQwv3BpUfMIFAKCK3AsEUgLCC4tah6BQkBwBY4lAmkBwaVFzSNQCAiuwLFEIC0guLSoeQQKAcEVOJYIpAUElxY1j0AhILgCxxKBtIDg0qLmESgEBFfgWCKQFhBcWtQ8AoWA4AocSwTSAoJLi5pHoBAQXIFjiUBaQHBpUfMIFAKCK3AsEUgLCC4tah6BQmDgTpPsHSTFs39p6fQ7Q770UsV/Ov19X+2OFL9wxR+rJQJpAcGlRc0jUAgIrsCxRCAtILi0qHkECgHBFTiWCKQFBJcWNY9AISC4AscSgbSA4NKi5hEoBARX4FgikBYQXFrUPAKFgOAKHEsE0gKCS4uaR6AQEFyBY4lAWkBwaVHzCBQCgitwLBFICwguLWoegUJAcAWOJQJpAcGlRc0jUAgIrsCxRCAt8J4eePq89B0ar3ZnyOnve/rfn1+400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810l8JZ/m78+szP/zI47fJo7Q37vgJ7PHwN/07/3TOv/9gu3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhg4P6H9J0maYHXuiMlrXf+vOfA33Turf3C5SxNItAKCK4lsoFATkBwOUuTCLQCgmuJbCCQExBcztIkAq2A4FoiGwjkBASXszSJQCsguJbIBgI5AcHlLE0i0AoIriWygUBOQHA5S5MItAKCa4lsIJATEFzO0iQCrYDgWiIbCOQEBJezNIlAKyC4lsgGAjkBweUsTSLQCgiuJbKBQE5AcDlLkwi0Akff//Dz6U+/I6U1/sUNr3bnytl3kPzi4bXb/cK1RDYQyAkILmdpEoFWQHAtkQ0EcgKCy1maRKAVEFxLZAOBnIDgcpYmEWgFBNcS2UAgJyC4nKVJBFoBwbVENhDICQguZ2kSgVZAcC2RDQRyAoLLWZpEoBUQXEtkA4GcgOByliYRaAUE1xLZQCAnILicpUkEWgHBtUQ2EMgJCC5naRKBVkBwLZENBHIC/4M7TXIv+3PS22d24qvdQfL3C/7N5P5i/MLlLE0i0AoIriWygUBOQHA5S5MItAKCa4lsIJATEFzO0iQCrYDgWiIbCOQEBJezNIlAKyC4lsgGAjkBweUsTSLQCgiuJbKBQE5AcDlLkwi0AoJriWwgkBMQXM7SJAKtgOBaIhsI5AQEl7M0iUArILiWyAYCOQHB5SxNItAKCK4lsoFATkBwOUuTCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDAvyrwDySEJ2VQgUSoAAAAAElFTkSuQmCC PATCH {{server}}/admin/Books(ID=207)
Content-Type:application/json
{"author_ID": 9999}
### change book to correct author ID -> should work
PATCH {{server}}/admin/Books(ID=207)
Content-Type:application/json
{"author_ID": 170}
### ------------------------------------------------------------------------
# Reading image from from the server directly ### delete "leaf" genre that is used in a book -> should not work
GET {{server}}/browse/Books(2)/image DELETE {{server}}/admin/Genres(ID=13)
### delete "leaf" genre that is not used in a book -> should work
DELETE {{server}}/admin/Genres(ID=14)
### ------------------------------------------------------------------------ ### delete "header" genre where leaves are used in a book -> should not work
# Submit Order as authenticated user DELETE {{server}}/admin/Genres(ID=10)
# (send that three times to get out-of-stock message)
POST {{server}}/browse/submitOrder
Content-Type: application/json
{{me}}
{ "book":201, "quantity":5 }
### ------------------------------------------------------------------------ ### delete "header" genre where leaves are not used in a book -> should work and delete all leaves
# Browse Genres DELETE {{server}}/admin/Genres(ID=30)
GET {{server}}/browse/Genres?
# &$filter=parent_ID eq null&$select=name
# &$expand=children($select=name)
{{me}}

View File

@@ -1,2 +0,0 @@
namespace sap.capire.bookshop; //> important for reflection
using from './srv/mashup';

View File

@@ -1,34 +0,0 @@
{
"name": "@capire/bookstore",
"version": "1.0.0",
"dependencies": {
"@capire/bookshop": "*",
"@capire/reviews": "*",
"@capire/orders": "*",
"@capire/common": "*",
"@capire/data-viewer": "*",
"@sap/cds": "^5",
"express": "^4.17.1"
},
"cds": {
"requires": {
"ReviewsService": {
"kind": "odata",
"model": "@capire/reviews"
},
"OrdersService": {
"kind": "odata",
"model": "@capire/orders"
},
"messaging": {
"[development]": { "kind": "file-based-messaging" },
"[hybrid]": { "kind": "enterprise-messaging-shared" },
"[production]": { "kind": "enterprise-messaging" }
},
"db": {
"kind": "sql"
}
},
"log": { "service": true }
}
}

View File

@@ -1,22 +0,0 @@
const cds = require ('@sap/cds')
// Add mashup logic
cds.once('served', require('./srv/mashup'))
// Add routes to UIs from imported packages
cds.once('bootstrap',(app)=>{
app.serve ('/bookshop') .from ('@capire/bookshop','app/vue')
app.serve ('/reviews') .from ('@capire/reviews','app/vue')
app.serve ('/orders') .from('@capire/orders','app/orders')
app.serve ('/data') .from('@capire/data-viewer','app/viewer')
})
// Add Swagger UI
require('./srv/swagger-ui')
// Returning cds.server
module.exports = cds.server
// For didactic reasons in capire
const { ReviewsService, OrdersService } = cds.requires
if (!ReviewsService.credentials && !OrdersService.credentials) cds.requires.messaging = false

View File

@@ -1,38 +0,0 @@
////////////////////////////////////////////////////////////////////////////
//
// Enhancing bookshop with Reviews and Orders provided through
// respective reuse packages and services
//
using { sap.capire.bookshop.Books } from '@capire/bookshop';
//
// Extend Books with access to Reviews and average ratings
//
using { ReviewsService.Reviews } from '@capire/reviews';
extend Books with {
reviews : Composition of many Reviews on reviews.subject = $self.ID;
rating : Decimal;
numberOfReviews : Integer;
}
//
// Extend Orders with Books as Products
//
using { sap.capire.orders.Orders } from '@capire/orders';
extend Orders with {
extend Items with {
book : Association to Books on product.ID = book.ID
}
}
// Add orders fiori app (in case of embedded orders service)
using from '@capire/orders/app/fiori';
// Add data browser
using from '@capire/data-viewer';
// Incorporate pre-build extensions from...
using from '@capire/common';

View File

@@ -1,10 +0,0 @@
// -----------------------------------------------------------------------
// Adding Swagger UI - see https://cap.cloud.sap/docs/advanced/openapi
const cds = require ('@sap/cds')
try {
const cds_swagger = require ('cds-swagger-ui-express')
cds.once ('bootstrap', app => app.use (cds_swagger()) )
} catch (err) {
if (err.code !== 'MODULE_NOT_FOUND') throw err
}

View File

@@ -5,11 +5,9 @@ CAD;de;Kanadischer Dollar;Kanadischer Dollar
AUD;de;Australischer Dollar;Australischer Dollar AUD;de;Australischer Dollar;Australischer Dollar
GBP;de;Pfund;Britische Pfund GBP;de;Pfund;Britische Pfund
ILS;de;Schekel;Israelische Schekel ILS;de;Schekel;Israelische Schekel
JPY;de;Yen;Japanische Yen
EUR;fr;euro;de la Zone euro EUR;fr;euro;de la Zone euro
USD;fr;dollar;dollar des États-Unis USD;fr;dollar;dollar des États-Unis
CAD;fr;dollar canadien;dollar canadien CAD;fr;dollar canadien;dollar canadien
AUD;fr;dollar australien;dollar australien AUD;fr;dollar australien;dollar australien
GBP;fr;livre sterling;pound sterling GBP;fr;livre sterling;pound sterling
ILS;fr;Shekel;shekel israelien ILS;fr;Shekel;shekel israelien
JPY;fr;Yen;Yen japonais
1 code locale name descr
5 AUD de Australischer Dollar Australischer Dollar
6 GBP de Pfund Britische Pfund
7 ILS de Schekel Israelische Schekel
JPY de Yen Japanische Yen
8 EUR fr euro de la Zone euro
9 USD fr dollar dollar des États-Unis
10 CAD fr dollar canadien dollar canadien
11 AUD fr dollar australien dollar australien
12 GBP fr livre sterling pound sterling
13 ILS fr Shekel shekel israelien
JPY fr Yen Yen japonais

View File

@@ -20,7 +20,7 @@ extend sap.common.Currencies with {
* annotate sap.common.Countries with @cds.persistence.skip:false; * annotate sap.common.Countries with @cds.persistence.skip:false;
*/ */
context sap.common.countries { context sap.common_countries {
extend sap.common.Countries { extend sap.common.Countries {
regions : Composition of many Regions on regions._parent = $self.code; regions : Composition of many Regions on regions._parent = $self.code;

View File

@@ -1,119 +0,0 @@
/* global Vue axios */ //> from vue.html
const GET = (url) => axios.get('/-data'+url)
const storageGet = (key, def) => localStorage.getItem('data-viewer:'+key) || def
const storageSet = (key, val) => localStorage.setItem('data-viewer:'+key, val)
const columnKeysFirst = (c1, c2) => {
if (c1.isKey && !c2.isKey) return -1
if (!c1.isKey && c2.isKey) return 1
if (c1.isKey && c2.isKey) return c1.name.localeCompare(c2.name)
return 0 // retain natural order of normal columns
}
const vue = Vue.createApp ({
data() { return {
error: undefined,
dataSource: storageGet('data-source', 'db'),
skip: storageGet('skip', 0),
top: storageGet('top', 20),
entity: storageGet('entity') ? JSON.parse(storageGet('entity')) : undefined,
entities: [],
columns: [],
data: [],
rowDetails: {},
rowKey: storageGet('rowKey')
}},
watch: {
dataSource: (v) => { storageSet('data-source', v); vue.fetchEntities() },
skip: (v) => { storageSet('skip', v); if (vue.entity) vue.fetchData() },
top: (v) => { storageSet('top', v); if (vue.entity) vue.fetchData() },
},
methods: {
async fetchEntities () {
let url = `/Entities`
if (vue.dataSource === 'db') url += `?dataSource=db`
const {data} = await GET(url)
vue.entities = data.value
vue.entities.forEach(entity => entity.columns.sort(columnKeysFirst))
const entity = vue.entity && vue.entities.find(e => e.name === vue.entity.name)
if (entity) { // restore selection from previous fetch
vue.columns = entity.columns
await vue.fetchData(entity)
} else {
vue.entity = undefined
vue.columns = []
vue.data = []
vue.rowDetails = {}
}
},
async inspectEntity (eve) {
const entity = vue.entity = vue.entities [eve.currentTarget.rowIndex-1]
storageSet('entity', JSON.stringify(entity))
vue.columns = vue.entities.find(e => e.name === entity.name).columns
return await this.fetchData()
},
async fetchData () {
let url = `/Data?entity=${vue.entity.name}&$skip=${vue.skip}&$top=${vue.top}`
if (vue.dataSource === 'db') url += `&dataSource=db`
try {
const {data} = await GET(url)
// sort data along column order
const columnIndexes = {}
vue.columns.forEach((col, i) => columnIndexes[col.name] = i)
vue.data = data.value.map(d => d.record
.sort((r1, r2) => columnIndexes[r1.column] - columnIndexes[r2.column])
.map(r => r.data)
)
const row = vue.data.find(data => vue._makeRowKey(data) === vue.rowKey)
if (row) vue._setRowDetails(row)
else vue.rowDetails = {}
vue.error = undefined
} catch (err) {
vue.data = []
vue.rowDetails = {}
if (err.response?.data?.error) {
vue.error = err.response.data.error
} else {
vue.error = { code:err.code, message:err.message }
}
}
},
inspectRow (eve) {
vue.rowDetails = {}
const selectedRow = eve.currentTarget.rowIndex-1
vue.rowKey = vue._makeRowKey(vue.data[selectedRow])
storageSet('rowKey', vue.rowKey)
vue._setRowDetails(vue.data[selectedRow])
},
_setRowDetails(row) {
vue.rowDetails = {}
row.forEach((line, colIndex) => {
vue.rowDetails[vue.columns[colIndex].name] = line
})
},
_makeRowKey(row) {
// to identify a row, build a key string out of all key columns' values
return row
.filter((_, colIndex) => vue.columns[colIndex] && vue.columns[colIndex].isKey)
.reduce(((prev, next) => prev += next), '')
},
isActiveRow(row) {
return vue._makeRowKey(row) === vue.rowKey
}
}
})
.mount('#app')
vue.fetchEntities()

View File

@@ -1,95 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Data Browser</title>
<link rel="stylesheet" href="https://unpkg.com/primitive-ui/dist/css/main.css">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
<script src="app.js" defer></script>
<style>
th { position: sticky; top:0; z-index: 2; background-color: white; }
.noscroll { overflow: hidden; }
.hovering tr:hover td { background: #ebeefc; cursor: pointer; }
.highlight { background: #ebeefc !important; }
.rating-stars { color:teal }
.succeeded { color:teal }
.failed { color:red }
.condensed { max-width: 100px; text-overflow: ellipsis; white-space: nowrap; }
.key { font-weight: bold }
.not-key { font-weight: lighter;}
.with-sidebar { display: flex; flex-wrap: wrap; gap: 1rem; }
.sidebar { flex-basis: 20rem; flex-grow: 1; }
.sidebar-main { height: 100vh; overflow-y: scroll; }
.not-sidebar { flex-basis: 0; flex-grow: 999; min-inline-size: 50%; align-items: stretch;}
.not-sidebar-main { max-height: 40vh; overflow-y: scroll; }
.not-sidebar-sub { max-height: 40vh; overflow-y: scroll; }
.horizontal label { display: inline; }
.horizontal input { width: initial; display: inline; }
.error { color: red; }
</style>
</head>
<body class="noscroll">
<div id='app' class="full-container">
<h1>Data Browser &ndash; {{ entity ? entity.name : '' }}</h1>
<div class="with-sidebar">
<div class="sidebar">
<div class="horizontal" style="padding: 0.75rem 0;">
<label>Datasource:</label>
<input type="radio" id="dataSource-db" value="db" v-model="dataSource">
<label for="dataSource-db">Database</label>
<input type="radio" id="dataSource-srv" value="service" v-model="dataSource">
<label for="dataSource-srv">Service</label>
</div>
<div class="sidebar-main">
<table id='entities' class="hovering">
<thead>
<th>Entity Name</th>
</thead>
<tr v-for="e in entities" :key="e.name" @click="inspectEntity" :class="{'highlight': (entity && e.name === entity.name)}">
<td>{{ e.name }}</td>
</tr>
</table>
</div>
</div>
<div class="not-sidebar">
<div class="horizontal">
<label for="skip">Skip:</label>
<input id="skip" v-model.lazy="skip" title="No. of entries to skip" type="number" min="0">
<label for="top">Top:</label>
<input id="top" v-model.lazy="top" title="No. of entries to read" type="number" min="0">
</div>
<div v-if="data" class="not-sidebar-main">
<table id='data' class="hovering striped-table condensed">
<thead>
<th v-for="col in columns" :title="col.type" :class="[col.isKey ? 'key' : 'not-key']">{{ col.name }} </th>
</thead>
<tr v-for="row in data" @click="inspectRow" :class="{'highlight': isActiveRow(row)}">
<td v-for="d in row" :title="d">{{ d }}</td>
</tr>
</table>
</div>
<div v-if="error" class="not-sidebar-main error">
Error: {{ error.code ? error.code + ' &ndash; ' + error.message : error.message }}
</div>
<p></p>
<div v-if="rowDetails" class="not-sidebar-sub">
<table id='rowDetails'>
<tr v-for="(key, value) in rowDetails" >
<td class="key">{{ value }}</td>
<td>{{ key }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</body>
</html>

View File

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

View File

@@ -1,13 +0,0 @@
{
"name": "@capire/data-viewer",
"version": "0.1.0",
"description": "A generic browser for data",
"dependencies": {
"@sap/cds": "^5.0.4"
},
"files": [
"app",
"srv",
"index.cds"
]
}

View File

@@ -1,29 +0,0 @@
/**
* Exposes data + entity metadata
*/
@requires:'authenticated-user'
service DataService @( path:'-data' ) {
/**
* Metadata like name and columns/elements
*/
entity Entities {
key name : String;
columns: Composition of many {
name : String;
type : String;
isKey: Boolean;
}
}
/**
* The actual data, organized by column name
*/
entity Data {
record : array of {
column : String;
data : String;
}
}
}

View File

@@ -1,58 +0,0 @@
const cds = require('@sap/cds')
const log = cds.log('data')
class DataService extends cds.ApplicationService { init(){
this.on ('READ', 'Entities', req => {
const { dataSource } = req.req.query
const srvPrefixes = cds.db.model.all('service').map(srv => srv.name+'.')
const dataSourceFilter = dataSource === 'db'
? e => e['@cds.persistence.skip'] !== true // for DB, excl. entities w/o persistence
: e => !!srvPrefixes.find(srvName => e.name.startsWith(srvName)) // only entities reachable from a service
return cds.db.model.all('entity')
.filter (e => req.data && req.data.name ? e.name === req.data.name : true) // honor name filter from request, if any
.filter (e => !e.name.startsWith('DRAFT.')) // exclude synthetic stuff
.filter (e => !e.name.startsWith('DataService.')) // exclude this service
.filter (dataSourceFilter)
.sort((e1, e2) => e1.name.localeCompare(e2.name))
.map(e => {
const columns = Object.entries(e.elements)
.filter(([_, el]) => !(el instanceof cds.Association)) // exclude assocs+compositions
.map(([name, el]) => { return { name, type: el.type, isKey:!!el.key }})
return { name: e.name, columns }
})
})
this.on ('READ', 'Data', async req => {
const { entity: entityName, dataSource: dataSourceName } = req.req.query
if (!entityName) return req.reject(400, `Must provide 'entity' query`)
const entity = cds.db.model.definitions[entityName]
if (!entity) return req.reject(404, 'No such entity: ' + entityName)
const query = SELECT.from(entity)
query.SELECT.limit = req.query.SELECT.limit // forward $skip / $top
const dataSource = findDataSource(dataSourceName, entityName)
const res = await dataSource.run(query)
return res.map((line) => {
const record = Object.entries(line).map(([column, data]) => {return {column, data}})
return { record }
})
})
return super.init()
}}
module.exports = { DataService }
function findDataSource(dataSourceName, entityName) {
for (let srv of Object.values(cds.services)) { // all connected services
if (!srv.name) continue // FIXME intermediate/pending in cds.services ?
if (dataSourceName === srv.name || entityName.startsWith(srv.name+'.')) {
log._debug && log.debug(`using ${srv.name} as data source`)
return srv
}
}
return cds.services.db // fallback
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -1,172 +0,0 @@
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="347px" height="279px" viewBox="-0.5 -0.5 347 279" content="&lt;mxfile&gt;&lt;diagram id=&quot;QQJxv4aCTC7ZgE7HHOvM&quot; name=&quot;Page-1&quot;&gt;7Vlbb+I4FP41vCLfL4/T7szuy0oj9WGfczEkasCRMYXur98TEufeqpoBCtUGCezv+Hq+z/bBWdDHzfFPF5XZ3zY1xYKg9LigfywI0QLBdwW81oDUrAbWLk9rCHXAU/6vqUEc0H2eml2D1ZC3tvB5OQQTu92axA+wyDl7GBZb2SIdAGW0NoNhVMBTEhVmUuyfPPVZjSoiO/wvk6+z0DMWurbEUfK8dna/bfpbEIq/VZ/avIlCW02/uyxK7aEH0e8L+uis9XVqc3w0ReXaodt+vGFtx+3M1n+kAqkrvETFvpn6gqEkKnNnqkpERJtyQR+28a76gXwBzT7EDlJr30cCEFv7vMtsGQzQStwVPk3ZvwY3w+zLKpmZY7S2WyhSGpdvjDeuQ38GCJzzcMhyb57KKKmqHUB7gGV+U0AOQ3KVH02QU5XfPRufVB5HJ2NRPNrCulPndLUyIkmqQt7ZZ9OzpFLHCLWWoADSTuHFOG+Ob7oct0TC+jAWBu9eoUhTgcimSrM2KG9UdeiUpht5ZD2RqQaLGm2v25Y7fiHRUDxPNz033avcuvwOuFaJmec6Vpzxq3FNrsg1OzfXzrzkBnbWm2c75UalbI5tRWIqxKXYppwPVza6Htv83GwndrMB4m6e7BWvPhVut76H18+cCMTpuZQIsFIDEbArikCcWwTWpcbdw4qPjFrN7u8iUSZeXY5s+WkrXk7JHnNitum3KhqGXFJEu12eDF08cOnUQVC98b882SPnB/mRq3+cntaxJg0x9ltuhbHavUvMYAeDXtbG96LTqfN7zuUzzg2YM0Xk85fhIOY83vTw0+YwvI5bCOuH5AayQxv16Jtq/SB70pIYtURHLdWTnrR0kkA78Q+pQn0xVYipKuhnqoJIueSKtk8Y4Gtn7lknRH9UMkTjJcOifSQbdqPEsmcVgl5KT/qL6Yncl54ofZ/oM+mJkvdlez49hRueLyModl+CYtcRFLueoPBUUL8X9O72ZVnk/8e978a9o2MvLOsrxL145s5yTOETNAKCRLfPoMEpN7P7khaSRpf7m4rQkEFBrsfgzDXkXZ8BQZL9QwC/4f+bOAXGYUXYvS8cVoRRXOAUmLnt/HKSEp+pKAxUS6Q4QVhqKtv7h7B/aAZKkIghypVk7FfDCqSWIComtSJIgIrpsBfFQNZEcy0YJpxfLqqYuU+9bz2F16i3EqdiTWELUlWoqDUS46tzSpYQm3KOGYJgQ5NflZOADQhLySVCmrJxJ3oJOyFjimuFKVJnUhNkuze4dfHuLTn9/h8=&lt;/diagram&gt;&lt;/mxfile&gt;" style="background-color: rgb(26, 26, 26);">
<defs/>
<g>
<path d="M 192 148 L 242 148 L 262 188 L 242 228 L 192 228 L 172 188 Z" fill="#ffe6cc" stroke="#d79b00" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 188px; margin-left: 173px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
@capire/
<br/>
<b>
bookshop
</b>
</div>
</div>
</div>
</foreignObject>
<text x="217" y="192" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
@capire/...
</text>
</switch>
</g>
<path d="M 192 48 L 242 48 L 262 88 L 242 128 L 192 128 L 172 88 Z" fill="#f8cecc" stroke="#b85450" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 88px; margin-left: 173px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
@capire/
<br/>
<b>
fiori
</b>
</div>
</div>
</div>
</foreignObject>
<text x="217" y="92" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
@capire/...
</text>
</switch>
</g>
<path d="M 276 98 L 326 98 L 346 138 L 326 178 L 276 178 L 256 138 Z" fill="#d5e8d4" stroke="#82b366" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 138px; margin-left: 257px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
@capire/
<br/>
<b>
reviews
</b>
</div>
</div>
</div>
</foreignObject>
<text x="301" y="142" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
@capire/...
</text>
</switch>
</g>
<path d="M 109 198 L 159 198 L 179 238 L 159 278 L 109 278 L 89 238 Z" fill="#f5f5f5" stroke="#666666" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 238px; margin-left: 90px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #333333; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
@capire/
<br/>
<b>
common
</b>
</div>
</div>
</div>
</foreignObject>
<text x="134" y="242" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
@capire/...
</text>
</switch>
</g>
<path d="M 108 98 L 158 98 L 178 138 L 158 178 L 108 178 L 88 138 Z" fill="#dae8fc" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 138px; margin-left: 89px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
@capire/
<br/>
<b>
orders
</b>
</div>
</div>
</div>
</foreignObject>
<text x="133" y="142" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
@capire/...
</text>
</switch>
</g>
<path d="M 168.58 217.17 L 174.72 213.47" fill="none" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 180.5 209.99 L 175.11 218.49 L 174.72 213.47 L 170.47 210.78 Z" fill="#ffffff" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 167.68 117.36 L 174.6 113.24" fill="none" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 180.4 109.79 L 174.97 118.26 L 174.6 113.24 L 170.36 110.52 Z" fill="#ffffff" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 217 148 L 217 136.99" fill="none" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 217 130.24 L 221.5 139.24 L 217 136.99 L 212.5 139.24 Z" fill="#ffffff" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 266.32 117.36 L 259.4 113.24" fill="none" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 253.6 109.79 L 263.64 110.52 L 259.4 113.24 L 259.03 118.26 Z" fill="#ffffff" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 107 1 L 157 1 L 177 41 L 157 81 L 107 81 L 87 41 Z" fill="#dae8fc" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 41px; margin-left: 88px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
@capire/
<br/>
<b>
suppliers
</b>
</div>
</div>
</div>
</foreignObject>
<text x="132" y="45" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
@capire/...
</text>
</switch>
</g>
<path d="M 21 53 L 71 53 L 91 93 L 71 133 L 21 133 L 1 93 Z" fill="#e1d5e7" stroke="#9673a6" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 93px; margin-left: 2px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
<b>
S/4
</b>
</div>
</div>
</div>
</foreignObject>
<text x="46" y="97" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
S/4
</text>
</switch>
</g>
<path d="M 80.55 72.11 L 89.76 66.54" fill="none" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 95.53 63.05 L 90.16 71.56 L 89.76 66.54 L 85.5 63.86 Z" fill="#ffffff" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 81.75 111.49 L 89.27 115.38" fill="none" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 95.26 118.48 L 85.2 118.34 L 89.27 115.38 L 89.33 110.35 Z" fill="#ffffff" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 167.25 60.49 L 173.88 64.16" fill="none" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 179.79 67.42 L 169.74 67.01 L 173.88 64.16 L 174.09 59.13 Z" fill="#ffffff" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
</g>
<switch>
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
Viewer does not support full SVG 1.1
</text>
</a>
</switch>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,281 +0,0 @@
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="569px" height="428px" viewBox="-0.5 -0.5 569 428" content="&lt;mxfile&gt;&lt;diagram id=&quot;QQJxv4aCTC7ZgE7HHOvM&quot; name=&quot;Page-1&quot;&gt;7Vpdd9o4EP01PJZjWf7SY8jHds9pd3tKt30WtjDayBZHFknor18ployNDCXEELbtyUPsQRpLvndmrgZG8Lp4+kPg5eIjzwgb+V72NII3I98HMPLUP21Z15YYBbUhFzQzgzaGKf1OjNHMy1c0I1VnoOScSbrsGlNeliSVHRsWgj92h8056z51iXPzRG9jmKaYEWfYN5rJRW1N/Hhjf09ovrBPBhGqPymwHWwcVwuc8ceWCd6O4LXgXNZXxdM1Yfrl2fdSz7vb8WmzMEFKecgEv57wgNnK7G2qBis0vPdXf12ZNcq13bjgqzIjeq43gpPHBZVkusSp/vRRQa1sC1kwdQfUZcoLmpqh1T2R6cLc/Evz/NljoG7mvJR3uKBMc+E9YQ9E0hTrKVLw++b9qjczwYzmpboW9audPBChx7IrY5dcr6BSC6Jlru7Dxss1Z1w8bwEmvv7TK+WCflcPx8yusZ742QAXm8VNzfaBuW/5Cm70n7Kbt6jWQ552IgEafFVgEF4QKdZqiJnwDtioMEEBLEceNxQDKKxtiza9LIrY0DpvnG+QVxcG/H4iQIcIk1VFS1JVI72OiOkXPhPqKtdXn7CQJRF7CALORJAWHLMkDEKvlzpzylhr5DxJSZoOBRxIxmEXuhC50Fk429CF3uuRCxzkGrgsWn+WqUqYaju+9zEvZAtR5bwZ5aJ8+yRJWVFeqhlXy+WFpgNG5odkA7Q3G5wwsg+IaxjELjlggF7PjtBhhyVDdVnBG6UJmc0PCd4Mk2Q+UPD6YdwFB/lu5IKepDtE5EY9kbsFCSmzKy1Y1F1GccHL7MuCKnZP1Ad3lFkw1J1RSSDqwnRSACqpyoBd3+3nApdrazWL8+y9WR5qYCOZ1Vi7QFMvgq9EakeZl6Oc5cSC0I9tGzsbfIIwLOlD95F94Bl3nzh9zpc2hoMOTQIQ2pRvfdRrNdPaimvLU5Owd7uqt+i4eqZTs82DGBY7DLteVVJ5EBcW/VmMZt5hpXtOoqFKN+pC0Rv8pyrbiQPNdJWmSm/d4VTyvfj8/7V3j9R25Phbam8/ilwmoGjcUwh8bwD1jRwyfOPifs519uuV3yqAtSq7pBC+GPUNPXi+MLYtiRZ0t8WS8TXpR+4LLUi1IET+Bq8fvKQn8k4GHniRApsxnt6rnWdYIWiB2ggxryvE/GOF2NG18KWSCrqKKj6fouoK7xDEx+qpHzgaTk1ZCdqiy0dVsHFOLkxNXcZZCnrADeVTnaWA28DaF8qXephqHZMOO1wdF/nhW0a+H8MuTyA8MvQB2u9owNB3u2xTIh7oDn2m1duldUcv4oh1Xm3mNr9+pfKO3CCvaXyWKAfbwg5GxxZ4tOUqCrdWM2Ccv6wnV/KS7CPJxdQPviTlYNXDJsO3acWByB97CYp0Uw6EHoJ+hxx+hI7jGYjgOAAejGMYJgHyoqTrF5yuuLhtOltAemrL1QOmDM8oo3L9u8K4FeasB0i3i/crVRjb/OiUmPMJSeDUmODIGgO8YL+jAYPd7fVNcHqvcFdGoynd46Rtt37AM8I+8YpK/a2sYhSXkhcaavtDEq/NL6/VuU0VkiqldDi1o4vr0EZ5X+qVFE+5/kXPuFA8Xi3HBRb6X7oSbD0Rz2lmT2vplelGcInNrr09bak5o8uvZnsDZJd3YOv7mtDryS8QuPnFHyC/+G538R/db1Cmr5Q8/lQ8abLWaXliM97APEm2G5nn5YnbyGz/duMGS/wzUaVRxyelSqOsB6aKH6A3ZIrbw3SYYaFJ14wqNSngj6XkrNadH2aNQdW0/FmN/r2Syg0x9qoWN/4YQRSiGIIwgJEPwTaAu085h5+UBsAKJF2sAIpdrJALFQhfDJW63fzusxYWm1/Pwtv/AA==&lt;/diagram&gt;&lt;/mxfile&gt;">
<defs/>
<g>
<rect x="1" y="1" width="195" height="122" fill="#ffffff" stroke="#828282" stroke-width="2" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)rotate(-90 11 13)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe flex-end; width: 107px; height: 1px; padding-top: 13px; margin-left: -96px;">
<div style="box-sizing: border-box; font-size: 0; text-align: right; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #4D4D4D; line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; word-wrap: normal; ">
S/4 HANA
</div>
</div>
</div>
</foreignObject>
<text x="11" y="25" fill="#4D4D4D" font-family="Helvetica" font-size="12px" text-anchor="end" font-weight="bold">
S/4 HANA
</text>
</switch>
</g>
<rect x="42.5" y="40" width="120" height="50" rx="7.5" ry="7.5" fill="#f8cecc" stroke="#b85450" stroke-width="2" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 65px; margin-left: 44px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
Business
<br/>
Partner
</div>
</div>
</div>
</foreignObject>
<text x="103" y="69" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
Business...
</text>
</switch>
</g>
<rect x="221" y="1" width="347" height="349" fill="#ffffff" stroke="#828282" stroke-width="2" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe flex-start; width: 331px; height: 1px; padding-top: 15px; margin-left: 230px;">
<div style="box-sizing: border-box; font-size: 0; text-align: left; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #4D4D4D; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
<b>
Incident Mgmt
</b>
<br/>
Extension App
</div>
</div>
</div>
</foreignObject>
<text x="230" y="27" fill="#4D4D4D" font-family="Helvetica" font-size="12px">
Incident Mgmt...
</text>
</switch>
</g>
<rect x="418" y="73" width="115" height="50" rx="7.5" ry="7.5" fill="#dae8fc" stroke="#6c8ebf" stroke-width="2" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 113px; height: 1px; padding-top: 98px; margin-left: 419px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
Incidents
</div>
</div>
</div>
</foreignObject>
<text x="476" y="102" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
Incidents
</text>
</switch>
</g>
<path d="M 475.5 182 L 475.5 142.97" fill="none" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 481.5 182 L 475.5 170 L 469.5 182" fill="none" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 475.5 124.97 L 480.79 133.97 L 475.5 142.97 L 470.21 133.97 Z" fill="#6c8ebf" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<rect x="255" y="73" width="120" height="50" rx="7.5" ry="7.5" fill="#ffe6cc" stroke="#d79b00" stroke-width="2" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 98px; margin-left: 256px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
Customers
</div>
</div>
</div>
</foreignObject>
<text x="315" y="102" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
Customers
</text>
</switch>
</g>
<rect x="1" y="147" width="196.5" height="202" fill="#ffffff" stroke="#828282" stroke-width="2" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)rotate(-90 11 159)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe flex-end; width: 187px; height: 1px; padding-top: 159px; margin-left: -176px;">
<div style="box-sizing: border-box; font-size: 0; text-align: right; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #4D4D4D; line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; word-wrap: normal; ">
SuccessFactors
</div>
</div>
</div>
</foreignObject>
<text x="11" y="171" fill="#4D4D4D" font-family="Helvetica" font-size="12px" text-anchor="end" font-weight="bold">
SuccessFactors
</text>
</switch>
</g>
<rect x="42.5" y="184" width="120" height="50" rx="7.5" ry="7.5" fill="#f8cecc" stroke="#b85450" stroke-width="2" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 209px; margin-left: 44px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
Workforce
<br/>
Person
</div>
</div>
</div>
</foreignObject>
<text x="103" y="213" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
Workforce...
</text>
</switch>
</g>
<rect x="42.5" y="267" width="120" height="50" rx="7.5" ry="7.5" fill="#f8cecc" stroke="#b85450" stroke-width="2" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 292px; margin-left: 44px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
Employee
<br/>
Timesheet
</div>
</div>
</div>
</foreignObject>
<text x="103" y="296" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
Employee...
</text>
</switch>
</g>
<path d="M 162.5 74.32 L 238.96 86.19" fill="none" stroke="#d79b00" stroke-width="2" stroke-miterlimit="10" stroke-dasharray="6 6" pointer-events="stroke"/>
<path d="M 252.79 88.34 L 237.88 93.11 L 240.03 79.27 Z" fill="none" stroke="#d79b00" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<rect x="418" y="182" width="115" height="50" rx="7.5" ry="7.5" fill="#dae8fc" stroke="#6c8ebf" stroke-width="2" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 113px; height: 1px; padding-top: 207px; margin-left: 419px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
Messages
</div>
</div>
</div>
</foreignObject>
<text x="476" y="211" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
Messages
</text>
</switch>
</g>
<path d="M 418 98 L 394.97 98" fill="none" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 418 92 L 406 98 L 418 104" fill="none" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 376.97 98 L 385.97 92.71 L 394.97 98 L 385.97 103.29 Z" fill="#6c8ebf" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<rect x="255" y="184" width="120" height="50" rx="7.5" ry="7.5" fill="#ffe6cc" stroke="#d79b00" stroke-width="2" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 209px; margin-left: 256px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
Service
<br/>
Worker
</div>
</div>
</div>
</foreignObject>
<text x="315" y="213" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
Service...
</text>
</switch>
</g>
<path d="M 162.5 209 L 238.76 209" fill="none" stroke="#d79b00" stroke-width="2" stroke-miterlimit="10" stroke-dasharray="6 6" pointer-events="stroke"/>
<path d="M 252.76 209 L 238.76 216 L 238.76 202 Z" fill="none" stroke="#d79b00" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 354.83 181.46 L 439.35 123" fill="none" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 358.91 171.95 L 352.99 182.73 L 365.16 180.99" fill="none" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<rect x="255" y="267" width="120" height="50" rx="7.5" ry="7.5" fill="#ffe6cc" stroke="#d79b00" stroke-width="2" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 292px; margin-left: 256px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
Worker
<br/>
Availability
</div>
</div>
</div>
</foreignObject>
<text x="315" y="296" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
Worker...
</text>
</switch>
</g>
<path d="M 162.5 292 L 238.76 292" fill="none" stroke="#d79b00" stroke-width="2" stroke-miterlimit="10" stroke-dasharray="6 6" pointer-events="stroke"/>
<path d="M 252.76 292 L 238.76 299 L 238.76 285 Z" fill="none" stroke="#d79b00" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 37 407 C 37 401.48 41.48 397 47 397 L 92.5 397 C 98.02 397 102.5 392.52 102.5 387 C 102.5 392.52 106.98 397 112.5 397 L 158 397 C 163.52 397 168 401.48 168 407" fill="none" stroke="#b85450" stroke-width="2" stroke-miterlimit="10" transform="translate(0,397)scale(1,-1)translate(0,-397)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 414px; margin-left: 103px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
Backend Services
</div>
</div>
</div>
</foreignObject>
<text x="103" y="426" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
Backend Services
</text>
</switch>
</g>
<path d="M 249.5 407 C 249.5 401.48 253.98 397 259.5 397 L 305 397 C 310.52 397 315 392.52 315 387 C 315 392.52 319.48 397 325 397 L 370.5 397 C 376.02 397 380.5 401.48 380.5 407" fill="none" stroke="#d79b00" stroke-width="2" stroke-miterlimit="10" transform="translate(0,397)scale(1,-1)translate(0,-397)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 414px; margin-left: 315px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
Usage Views
</div>
</div>
</div>
</foreignObject>
<text x="315" y="426" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
Usage Views
</text>
</switch>
</g>
<path d="M 410 407 C 410 401.48 414.48 397 420 397 L 465.5 397 C 471.02 397 475.5 392.52 475.5 387 C 475.5 392.52 479.98 397 485.5 397 L 531 397 C 536.52 397 541 401.48 541 407" fill="none" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" transform="translate(0,397)scale(1,-1)translate(0,-397)" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 414px; margin-left: 476px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
Extension Data
</div>
</div>
</div>
</foreignObject>
<text x="476" y="426" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
Extension Data
</text>
</switch>
</g>
<path d="M 350 80.94 C 350 79.32 354.25 78 359.5 78 C 362.02 78 364.44 78.31 366.22 78.86 C 368 79.41 369 80.16 369 80.94 L 369 90.06 C 369 91.68 364.75 93 359.5 93 C 354.25 93 350 91.68 350 90.06 Z" fill="#dae8fc" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 369 80.94 C 369 82.56 364.75 83.88 359.5 83.88 C 354.25 83.88 350 82.56 350 80.94" fill="none" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
</g>
<switch>
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
Viewer does not support full SVG 1.1
</text>
</a>
</switch>
</svg>

Before

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 40 KiB

2
fiori/.env Normal file
View File

@@ -0,0 +1,2 @@
# cds.requires.messaging.kind = file-based-messaging
PORT = 4004

View File

@@ -1,47 +0,0 @@
using {AdminService} from '@capire/bookshop';
annotate AdminService.Authors with @odata.draft.enabled;
////////////////////////////////////////////////////////////////////////////
//
// Authors Object Page
//
annotate AdminService.Authors with @(UI : {
HeaderInfo : {
TypeName : 'Author',
TypeNamePlural : 'Authors',
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}'
},
]},
});
// Workaround to avoid errors for unknown db-specific calculated fields above
extend sap.capire.bookshop.Authors with {
virtual age : Integer;
virtual lifetime : String;
}
// Workaround for Fiori popup for asking user to enter a new UUID on Create
annotate AdminService.Authors with { ID @Core.Computed; }

View File

@@ -1,7 +0,0 @@
sap.ui.define(["sap/fe/core/AppComponent"], function (AppComponent) {
"use strict";
return AppComponent.extend("authors.Component", {
metadata: { manifest: "json" },
});
});
/* eslint no-undef:0 */

View File

@@ -1,11 +0,0 @@
# This is the resource bundle of itelo
# __ldi.translation.uuid=c3431418-9caf-11e8-98d0-529269fb1459
# JCI app descriptor contains lower case TITLE
appTitle=Bookshop Authors
# JCI app descriptor contains lower case DESCRIPTION
appSubTitle=Bookshop Authors
# JCI app descriptor contains lower case DESCRIPTION
appDescription=Bookshop Authors

View File

@@ -1,141 +0,0 @@
{
"_version": "1.28.0",
"sap.app": {
"id": "authors",
"type": "application",
"title": "Manage Authors",
"description": "Sample Application",
"i18n": "i18n/i18n.properties",
"applicationVersion": {
"version": "1.0.0"
},
"dataSources": {
"AdminService": {
"uri": "/admin/",
"type": "OData",
"settings": {
"odataVersion": "4.0"
}
}
},
"sourceTemplate": {
"id": "ui5template.basicSAPUI5ApplicationProject",
"-id": "ui5template.smartTemplate",
"version": "1.40.12"
},
"crossNavigation": {
"inbounds": {
"intent1": {
"signature": {
"parameters": {
"Books.author.ID":{
"renameTo": "ID"
}
},
"additionalParameters": "ignored"
},
"semanticObject": "Authors",
"action": "display",
"title": "{{appTitle}}",
"info": "{{appInfo}}",
"subTitle": "{{appSubTitle}}",
"icon": "sap-icon://SAP-icons-TNT/user",
"indicatorDataSource": {
"dataSource": "AdminService",
"path": "Authors/$count",
"refresh": 1800
}
}
}
}
},
"sap.ui5": {
"dependencies": {
"minUI5Version": "1.81.0",
"libs": {
"sap.fe.templates": {}
}
},
"models": {
"i18n": {
"type": "sap.ui.model.resource.ResourceModel",
"uri": "i18n/i18n.properties"
},
"": {
"dataSource": "AdminService",
"settings": {
"synchronizationMode": "None",
"operationMode": "Server",
"autoExpandSelect": true,
"earlyRequests": true,
"groupProperties": {
"default": {
"submit": "Auto"
}
}
}
}
},
"routing": {
"routes": [
{
"pattern": ":?query:",
"name": "AuthorsList",
"target": "AuthorsList"
},
{
"pattern": "Authors({key}):?query:",
"name": "AuthorsDetails",
"target": "AuthorsDetails"
}
],
"targets": {
"AuthorsList": {
"type": "Component",
"id": "AuthorsList",
"name": "sap.fe.templates.ListReport",
"options": {
"settings": {
"entitySet": "Authors",
"initialLoad": true,
"navigation": {
"Authors": {
"detail": {
"route": "AuthorsDetails"
}
}
}
}
}
},
"AuthorsDetails": {
"type": "Component",
"id": "AuthorsDetailsList",
"name": "sap.fe.templates.ObjectPage",
"options": {
"settings": {
"entitySet": "Authors"
}
}
}
}
},
"contentDensities": {
"compact": true,
"cozy": true
}
},
"sap.ui": {
"technology": "UI5",
"fullWidth": false,
"deviceTypes":{
"desktop": true,
"tablet": true,
"phone": true
}
},
"sap.fiori": {
"registrationIds": [],
"archeType": "transactional"
}
}

View File

@@ -10,7 +10,32 @@
<script> <script>
window["sap-ushell-config"] = { window["sap-ushell-config"] = {
defaultRenderer: "fiori2", defaultRenderer: "fiori2",
applications: {} applications: {
"browse-books": {
title: "Browse Books",
description: "w/ SAP Fiori Elements",
additionalInformation: "SAPUI5.Component=bookshop",
applicationType : "URL",
url: "/browse/webapp",
navigationMode: "embedded"
},
"manage-books": {
title: "Manage Books",
description: "w/ SAP Fiori Elements",
additionalInformation: "SAPUI5.Component=admin",
applicationType : "URL",
url: "/admin/webapp",
navigationMode: "embedded"
},
"manage-orders": {
title: "Manage Orders",
description: "w/ SAP Fiori Elements",
additionalInformation: "SAPUI5.Component=orders",
applicationType : "URL",
url: "/orders/webapp",
navigationMode: "embedded"
}
}
}; };
</script> </script>

View File

@@ -1,5 +1,4 @@
using { AdminService } from '@capire/bookstore'; using { AdminService } from '../../db/schema';
using from '../common'; // to help UI linter get the complete annotations
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
@@ -40,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}'},
]
},
}
);
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
@@ -50,7 +70,7 @@ annotate AdminService.Books with @(
annotate sap.capire.bookshop.Books with @fiori.draft.enabled; annotate sap.capire.bookshop.Books with @fiori.draft.enabled;
annotate AdminService.Books with @odata.draft.enabled; annotate AdminService.Books with @odata.draft.enabled;
annotate AdminService.Books.texts with @( annotate AdminService.Books_texts with @(
UI: { UI: {
Identification: [{Value:title}], Identification: [{Value:title}],
SelectionFields: [ locale, title ], SelectionFields: [ locale, title ],
@@ -63,16 +83,11 @@ annotate AdminService.Books.texts with @(
); );
// Add Value Help for Locales // Add Value Help for Locales
annotate AdminService.Books.texts { annotate AdminService.Books_texts {
locale @( locale @ValueList:{entity:'Languages',type:#fixed}
ValueList.entity:'Languages', Common.ValueListWithFixedValues, //show as drop down, not a dialog
)
} }
// In addition we need to expose Languages through AdminService as a target for ValueList // In addition we need to expose Languages through AdminService
using { sap } from '@sap/cds/common'; using { sap } from '@sap/cds/common';
extend service AdminService { extend service AdminService {
@readonly entity Languages as projection on sap.common.Languages; entity Languages as projection on sap.common.Languages;
} }
// Workaround for Fiori popup for asking user to enter a new UUID on Create
annotate AdminService.Books with { ID @Core.Computed; }

View File

@@ -1,6 +1,6 @@
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) { sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
"use strict"; "use strict";
return AppComponent.extend("books.Component", { return AppComponent.extend("admin.Component", {
metadata: { manifest: "json" } metadata: { manifest: "json" }
}); });
}); });

View File

@@ -1,7 +1,7 @@
{ {
"_version": "1.8.0", "_version": "1.8.0",
"sap.app": { "sap.app": {
"id": "books", "id": "admin",
"type": "application", "type": "application",
"title": "Manage Books", "title": "Manage Books",
"description": "Sample Application", "description": "Sample Application",
@@ -73,7 +73,6 @@
"options": { "options": {
"settings" : { "settings" : {
"entitySet" : "Books", "entitySet" : "Books",
"initialLoad": true,
"navigation" : { "navigation" : {
"Books" : { "Books" : {
"detail" : { "detail" : {

View File

@@ -1,142 +0,0 @@
{
"services": {
"LaunchPage": {
"adapter": {
"config": {
"catalogs": [],
"groups": [
{
"id": "Bookshop",
"title": "Bookshop",
"isPreset": true,
"isVisible": true,
"isGroupLocked": false,
"tiles": [
{
"id": "BrowseBooks",
"tileType": "sap.ushell.ui.tile.StaticTile",
"properties": {
"title": "Browse Books",
"targetURL": "#Books-display"
}
}
]
},
{
"id": "Administration",
"title": "Administration",
"isPreset": true,
"isVisible": true,
"isGroupLocked": false,
"tiles": [
{
"id": "ManageBooks",
"tileType": "sap.ushell.ui.tile.StaticTile",
"properties": {
"title": "Manage Books",
"targetURL": "#Books-manage"
}
},
{
"id": "ManageAuthors",
"tileType": "sap.ushell.ui.tile.StaticTile",
"properties": {
"title": "Manage Authors",
"targetURL": "#Authors-display"
}
},
{
"id": "ManageOrders",
"tileType": "sap.ushell.ui.tile.StaticTile",
"properties": {
"title": "Manage Orders",
"targetURL": "#Orders-manage"
}
}
]
}
]
}
}
},
"NavTargetResolution": {
"config": {
"enableClientSideTargetResolution": true
}
},
"ClientSideTargetResolution": {
"adapter": {
"config": {
"inbounds": {
"BrowseBooks": {
"semanticObject": "Books",
"action": "display",
"title": "Browse Books",
"signature": {
"parameters": {
"Books.ID": {
"renameTo": "ID"
},
"Authors.books.ID": {
"renameTo": "ID"
}
},
"additionalParameters": "ignored"
},
"resolutionResult": {
"applicationType": "SAPUI5",
"additionalInformation": "SAPUI5.Component=bookshop",
"url": "/browse/webapp"
}
},
"BrowseAuthors": {
"semanticObject": "Authors",
"action": "display",
"title": "Browse Authors",
"signature": {
"parameters": {
"Books.author.ID":{
"renameTo": "ID"
}
},
"additionalParameters": "ignored"
},
"resolutionResult": {
"applicationType": "SAPUI5",
"additionalInformation": "SAPUI5.Component=authors",
"url": "/admin-authors/webapp"
}
},
"ManageBooks": {
"semanticObject": "Books",
"action": "manage",
"title": "Manage Books",
"signature": {
"parameters": {},
"additionalParameters": "allowed"
},
"resolutionResult": {
"applicationType": "SAPUI5",
"additionalInformation": "SAPUI5.Component=books",
"url": "/admin-books/webapp"
}
},
"ManageOrders": {
"semanticObject": "Orders",
"action": "manage",
"signature": {
"parameters": {},
"additionalParameters": "allowed"
},
"resolutionResult": {
"applicationType": "SAPUI5",
"additionalInformation": "SAPUI5.Component=orders",
"url": "/orders/webapp"
}
}
}
}
}
}
}
}

3
fiori/app/bookshop.html Normal file
View File

@@ -0,0 +1,3 @@
<head>
<meta http-equiv="refresh" content="0;url=bookshop/index.html">
</head>

View File

@@ -1,60 +1,50 @@
using CatalogService from '@capire/bookstore'; using CatalogService from '@capire/bookshop';
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Books Object Page // Books Object Page
// //
annotate CatalogService.Books with @(UI : { annotate CatalogService.Books with @(
HeaderInfo : { UI: {
TypeName : 'Book', HeaderInfo: {
TypeNamePlural : 'Books', TypeName: 'Book',
Description : {Value : author} TypeNamePlural: 'Books',
}, Description: {Value: author}
HeaderFacets : [{ },
$Type : 'UI.ReferenceFacet', HeaderFacets: [
Label : '{i18n>Description}', {$Type: 'UI.ReferenceFacet', Label: '{i18n>Description}', Target: '@UI.FieldGroup#Descr'},
Target : '@UI.FieldGroup#Descr' ],
}, ], Facets: [
Facets : [{ {$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Price'},
$Type : 'UI.ReferenceFacet', ],
Label : '{i18n>Details}', FieldGroup#Descr: {
Target : '@UI.FieldGroup#Price' Data: [
}, ], {Value: descr},
FieldGroup #Descr : {Data : [{Value : descr}, ]}, ]
FieldGroup #Price : {Data : [ },
{Value : price}, FieldGroup#Price: {
{ Data: [
Value : currency.symbol, {Value: price},
Label : '{i18n>Currency}' {Value: currency.symbol, Label: '{i18n>Currency}'},
}, ]
]}, },
}); }
);
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Books Object Page // Books Object Page
// //
annotate CatalogService.Books with @(UI : { annotate CatalogService.Books with @(
SelectionFields : [ UI: {
ID, SelectionFields: [ ID, price, currency_code ],
price, LineItem: [
currency_code {Value: title},
], {Value: author, Label:'{i18n>Author}'},
LineItem : [ {Value: genre.name},
{ {Value: price},
Value : ID, {Value: currency.symbol, Label:' '},
Label : '{i18n>Title}' ]
}, },
{ );
Value : author,
Label : '{i18n>Author}'
},
{Value : genre.name},
{Value : price},
{
Value : currency.symbol,
Label : ' '
},
]
}, );

View File

@@ -1,14 +1,11 @@
{ {
"_version": "1.28.0", "_version": "1.8.0",
"sap.app": { "sap.app": {
"id": "bookshop", "id": "bookshop",
"type": "application", "type": "application",
"title": "Browse Books", "title": "Browse Books",
"description": "Sample Application", "description": "Sample Application",
"i18n": "i18n/i18n.properties", "i18n": "i18n/i18n.properties",
"applicationVersion": {
"version": "1.0.0"
},
"dataSources": { "dataSources": {
"CatalogService": { "CatalogService": {
"uri": "/browse/", "uri": "/browse/",
@@ -18,43 +15,14 @@
} }
} }
}, },
"sourceTemplate": { "-sourceTemplate": {
"id": "ui5template.basicSAPUI5ApplicationProject", "id": "ui5template.basicSAPUI5ApplicationProject",
"-id": "ui5template.smartTemplate", "-id": "ui5template.smartTemplate",
"version": "1.40.12" "-version": "1.40.12"
},
"crossNavigation": {
"inbounds": {
"intent1": {
"signature": {
"parameters": {
"Books.ID":{
"renameTo": "ID"
},
"Authors.books.ID": {
"renameTo": "ID"
}
},
"additionalParameters": "ignored"
},
"semanticObject": "Books",
"action": "display",
"title": "{{appTitle}}",
"info": "{{appInfo}}",
"subTitle": "{{appSubTitle}}",
"icon": "sap-icon://course-book",
"indicatorDataSource": {
"dataSource": "CatalogService",
"path": "Books/$count",
"refresh": 1800
}
}
}
} }
}, },
"sap.ui5": { "sap.ui5": {
"dependencies": { "dependencies": {
"minUI5Version": "1.81.0",
"libs": { "libs": {
"sap.fe.templates": {} "sap.fe.templates": {}
} }
@@ -100,7 +68,6 @@
"options": { "options": {
"settings": { "settings": {
"entitySet": "Books", "entitySet": "Books",
"initialLoad": true,
"navigation": { "navigation": {
"Books": { "Books": {
"detail": { "detail": {
@@ -130,12 +97,7 @@
}, },
"sap.ui": { "sap.ui": {
"technology": "UI5", "technology": "UI5",
"fullWidth": false, "fullWidth": false
"deviceTypes":{
"desktop": true,
"tablet": true,
"phone": true
}
}, },
"sap.fiori": { "sap.fiori": {
"registrationIds": [], "registrationIds": [],

View File

@@ -1,8 +1,8 @@
/* /*
Common Annotations shared by all apps Common Annotations shared by all apps
*/ */
using { sap.capire.bookshop as my } from '@capire/bookstore'; using { sap.capire.bookshop as my } from '@capire/bookshop';
using { sap.common } from '@capire/common'; using { sap.common } from '@capire/common';
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@@ -10,52 +10,39 @@ using { sap.common } from '@capire/common';
// Books Lists // Books Lists
// //
annotate my.Books with @( annotate my.Books with @(
Common.SemanticKey : [ID], Common.SemanticKey: [title],
UI : { UI: {
Identification : [{Value : title}], Identification: [{Value:title}],
SelectionFields : [ SelectionFields: [ ID, author_ID, price, currency_code ],
ID, LineItem: [
author_ID, {Value: ID},
price, {Value: title},
currency_code {Value: author.name, Label:'{i18n>Author}'},
], {Value: genre.name},
LineItem : [ {Value: stock},
{ {Value: price},
Value : ID, {Value: currency.symbol, Label:' '},
Label : '{i18n>Title}' ]
}, }
{
Value : author.ID,
Label : '{i18n>Author}'
},
{Value : genre.name},
{Value : stock},
{Value : price},
{
Value : currency.symbol,
Label : ' '
},
]
}
) { ) {
ID @Common: { author @ValueList.entity:'Authors';
SemanticObject : 'Books',
Text: title,
TextArrangement : #TextOnly
};
author @ValueList.entity : 'Authors';
}; };
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Books Details // Books Details
// //
annotate my.Books with @(UI : {HeaderInfo : { annotate my.Books with @(
TypeName : '{i18n>Book}', UI: {
TypeNamePlural : '{i18n>Books}', HeaderInfo: {
Title : {Value : title}, TypeName: '{i18n>Book}',
Description : {Value : author.name} TypeNamePlural: '{i18n>Books}',
}, }); Title: {Value: title},
Description: {Value: author.name}
},
}
);
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@@ -63,19 +50,13 @@ annotate my.Books with @(UI : {HeaderInfo : {
// Books Elements // Books Elements
// //
annotate my.Books with { annotate my.Books with {
ID @title : '{i18n>ID}'; ID @title:'{i18n>ID}' @UI.HiddenFilter;
title @title : '{i18n>Title}'; title @title:'{i18n>Title}';
genre @title : '{i18n>Genre}' @Common : { genre @title:'{i18n>Genre}' @Common: { Text: genre.name, TextArrangement: #TextOnly };
Text : genre.name, author @title:'{i18n>Author}' @Common: { Text: author.name, TextArrangement: #TextOnly };
TextArrangement : #TextOnly price @title:'{i18n>Price}';
}; stock @title:'{i18n>Stock}';
author @title : '{i18n>Author}' @Common : { descr @UI.MultiLineText;
Text : author.name,
TextArrangement : #TextOnly
};
price @title : '{i18n>Price}' @Measures.ISOCurrency : currency_code;
stock @title : '{i18n>Stock}';
descr @UI.MultiLineText;
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@@ -83,45 +64,42 @@ annotate my.Books with {
// Genres List // Genres List
// //
annotate my.Genres with @( annotate my.Genres with @(
Common.SemanticKey : [name], Common.SemanticKey: [name],
UI : { UI: {
SelectionFields : [name], SelectionFields: [ name ],
LineItem : [ LineItem:[
{Value : name}, {Value: name},
{ {Value: parent.name, Label: 'Main Genre'},
Value : parent.name, ],
Label : 'Main Genre' }
},
],
}
); );
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Genre Details // Genre Details
// //
annotate my.Genres with @(UI : { annotate my.Genres with @(
Identification : [{Value : name}], UI: {
HeaderInfo : { Identification: [{Value:name}],
TypeName : '{i18n>Genre}', HeaderInfo: {
TypeNamePlural : '{i18n>Genres}', TypeName: '{i18n>Genre}',
Title : {Value : name}, TypeNamePlural: '{i18n>Genres}',
Description : {Value : ID} Title: {Value: name},
}, Description: {Value: ID}
Facets : [{ },
$Type : 'UI.ReferenceFacet', Facets: [
Label : '{i18n>SubGenres}', {$Type: 'UI.ReferenceFacet', Label: '{i18n>SubGenres}', Target: 'children/@UI.LineItem'},
Target : 'children/@UI.LineItem' ],
}, ], }
}); );
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Genres Elements // Genres Elements
// //
annotate my.Genres with { annotate my.Genres with {
ID @title : '{i18n>ID}'; ID @title: '{i18n>ID}';
name @title : '{i18n>Genre}'; name @title: '{i18n>Genre}';
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@@ -129,42 +107,38 @@ annotate my.Genres with {
// Authors List // Authors List
// //
annotate my.Authors with @( annotate my.Authors with @(
Common.SemanticKey : [ID], Common.SemanticKey: [name],
UI : { UI: {
Identification : [{Value : name}], Identification: [{Value:name}],
SelectionFields : [name], SelectionFields: [ name ],
LineItem : [ LineItem:[
{Value : ID}, {Value: ID},
{Value : dateOfBirth}, {Value: name},
{Value : dateOfDeath}, {Value: dateOfBirth},
{Value : placeOfBirth}, {Value: dateOfDeath},
{Value : placeOfDeath}, {Value: placeOfBirth},
], {Value: placeOfDeath},
} ],
) { }
ID @Common: { );
SemanticObject : 'Authors',
Text: name,
TextArrangement : #TextOnly,
};
};
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Author Details // Author Details
// //
annotate my.Authors with @(UI : { annotate my.Authors with @(
HeaderInfo : { UI: {
TypeName : '{i18n>Author}', HeaderInfo: {
TypeNamePlural : '{i18n>Authors}', TypeName: '{i18n>Author}',
Title : {Value : name}, TypeNamePlural: '{i18n>Authors}',
Description : {Value : dateOfBirth} Title: {Value: name},
}, Description: {Value: dateOfBirth}
Facets : [{ },
$Type : 'UI.ReferenceFacet', Facets: [
Target : 'books/@UI.LineItem' {$Type: 'UI.ReferenceFacet', Target: 'books/@UI.LineItem'},
}, ], ],
}); }
);
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@@ -172,12 +146,12 @@ annotate my.Authors with @(UI : {
// Authors Elements // Authors Elements
// //
annotate my.Authors with { annotate my.Authors with {
ID @title : '{i18n>ID}'; ID @title:'{i18n>ID}' @UI.HiddenFilter;
name @title : '{i18n>Name}'; name @title:'{i18n>Name}';
dateOfBirth @title : '{i18n>DateOfBirth}'; dateOfBirth @title:'{i18n>DateOfBirth}';
dateOfDeath @title : '{i18n>DateOfDeath}'; dateOfDeath @title:'{i18n>DateOfDeath}';
placeOfBirth @title : '{i18n>PlaceOfBirth}'; placeOfBirth @title:'{i18n>PlaceOfBirth}';
placeOfDeath @title : '{i18n>PlaceOfDeath}'; placeOfDeath @title:'{i18n>PlaceOfDeath}';
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@@ -185,105 +159,99 @@ annotate my.Authors with {
// Languages List // Languages List
// //
annotate common.Languages with @( annotate common.Languages with @(
Common.SemanticKey : [code], Common.SemanticKey: [code],
Identification : [{Value : code}], Identification: [{Value:code}],
UI : { UI: {
SelectionFields : [ SelectionFields: [ name, descr ],
name, LineItem:[
descr {Value: code},
], {Value: name},
LineItem : [ ],
{Value : code}, }
{Value : name},
],
}
); );
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Language Details // Language Details
// //
annotate common.Languages with @(UI : { annotate common.Languages with @(
HeaderInfo : { UI: {
TypeName : '{i18n>Language}', HeaderInfo: {
TypeNamePlural : '{i18n>Languages}', TypeName: '{i18n>Language}',
Title : {Value : name}, TypeNamePlural: '{i18n>Languages}',
Description : {Value : descr} Title: {Value: name},
}, Description: {Value: descr}
Facets : [{ },
$Type : 'UI.ReferenceFacet', Facets: [
Label : '{i18n>Details}', {$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
Target : '@UI.FieldGroup#Details' ],
}, ], FieldGroup#Details: {
FieldGroup #Details : {Data : [ Data: [
{Value : code}, {Value: code},
{Value : name}, {Value: name},
{Value : descr} {Value: descr}
]}, ]
}); },
}
);
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Currencies List // Currencies List
// //
annotate common.Currencies with @( annotate common.Currencies with @(
Common.SemanticKey : [code], Common.SemanticKey: [code],
Identification : [{Value : code}], Identification: [{Value:code}],
UI : { UI: {
SelectionFields : [ SelectionFields: [ name, descr ],
name, LineItem:[
descr {Value: descr},
], {Value: symbol},
LineItem : [ {Value: code},
{Value : descr}, ],
{Value : symbol}, }
{Value : code},
],
}
); );
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Currency Details // Currency Details
// //
annotate common.Currencies with @(UI : { annotate common.Currencies with @(
HeaderInfo : { UI: {
TypeName : '{i18n>Currency}', HeaderInfo: {
TypeNamePlural : '{i18n>Currencies}', TypeName: '{i18n>Currency}',
Title : {Value : descr}, TypeNamePlural: '{i18n>Currencies}',
Description : {Value : code} Title: {Value: descr},
}, Description: {Value: code}
Facets : [ },
{ Facets: [
$Type : 'UI.ReferenceFacet', {$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
Label : '{i18n>Details}', {$Type: 'UI.ReferenceFacet', Label: '{i18n>Extended}', Target: '@UI.FieldGroup#Extended'},
Target : '@UI.FieldGroup#Details' ],
}, FieldGroup#Details: {
{ Data: [
$Type : 'UI.ReferenceFacet', {Value: name},
Label : '{i18n>Extended}', {Value: symbol},
Target : '@UI.FieldGroup#Extended' {Value: code},
}, {Value: descr}
], ]
FieldGroup #Details : {Data : [ },
{Value : name}, FieldGroup#Extended: {
{Value : symbol}, Data: [
{Value : code}, {Value: numcode},
{Value : descr} {Value: minor},
]}, {Value: exponent}
FieldGroup #Extended : {Data : [ ]
{Value : numcode}, },
{Value : minor}, }
{Value : exponent} );
]},
});
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Currencies Elements // Currencies Elements
// //
annotate common.Currencies with { annotate common.Currencies with {
numcode @title : '{i18n>NumCode}'; numcode @title:'{i18n>NumCode}';
minor @title : '{i18n>MinorUnit}'; minor @title:'{i18n>MinorUnit}';
exponent @title : '{i18n>Exponent}'; exponent @title:'{i18n>Exponent}';
} }

3
fiori/app/reviews.html Normal file
View File

@@ -0,0 +1,3 @@
<head>
<meta http-equiv="refresh" content="0;url=reviews/index.html">
</head>

View File

@@ -2,8 +2,11 @@
This model controls what gets served to Fiori frontends... This model controls what gets served to Fiori frontends...
*/ */
using from './admin-authors/fiori-service'; using from './admin/fiori-service';
using from './admin-books/fiori-service';
using from './browse/fiori-service'; using from './browse/fiori-service';
using from './common'; using from './common';
using from '@capire/bookstore/srv/mashup';
using from '@capire/common';
// only works in case of embedded orders service
using from '@capire/orders/app/orders/fiori-service';

View File

@@ -2,7 +2,7 @@
// Add Author.age and .lifetime with a DB-specific function // Add Author.age and .lifetime with a DB-specific function
// //
using { AdminService } from '@capire/bookshop'; using { AdminService } from '../schema';
extend projection AdminService.Authors with { extend projection AdminService.Authors with {
YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer, YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer,

8
fiori/db/schema.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;
}

View File

@@ -2,7 +2,7 @@
// Add Author.age and .lifetime with a DB-specific function // Add Author.age and .lifetime with a DB-specific function
// //
using { AdminService } from '@capire/bookshop'; using { AdminService } from '../schema';
extend projection AdminService.Authors with { extend projection AdminService.Authors with {
strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer, strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer,

View File

@@ -2,8 +2,11 @@
"name": "@capire/fiori", "name": "@capire/fiori",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@capire/bookstore": "*", "@capire/bookshop": "*",
"@sap/cds": "^5", "@capire/reviews": "*",
"@capire/orders": "*",
"@capire/common": "*",
"@sap/cds": ">=4",
"express": "^4.17.1", "express": "^4.17.1",
"passport": "^0.4.1" "passport": "^0.4.1"
}, },
@@ -12,6 +15,9 @@
"watch": "cds watch" "watch": "cds watch"
}, },
"cds": { "cds": {
"hana": {
"deploy-format": "hdbtable"
},
"requires": { "requires": {
"auth": { "auth": {
"strategy": "dummy" "strategy": "dummy"
@@ -24,31 +30,15 @@
"kind": "odata", "kind": "odata",
"model": "@capire/orders" "model": "@capire/orders"
}, },
"messaging": {
"[production]": {
"kind": "enterprise-messaging"
},
"[development]": {
"kind": "file-based-messaging"
},
"[hybrid!]": {
"kind": "enterprise-messaging-shared"
}
},
"db": { "db": {
"kind": "sql" "kind": "sql",
},
"db-ext": {
"[development]": { "[development]": {
"model": "db/sqlite" "model": "db/sqlite"
}, },
"[production]": { "[production]": {
"model": "db/hana" "model": "db/hana"
} }
},
"hana": {
"deploy-format": "hdbtable"
} }
} }
} }
} }

View File

@@ -1 +1,18 @@
module.exports = require('@capire/bookstore/server.js') const cds = require ('@sap/cds')
cds.once('bootstrap',(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'))
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)))

25
fiori/srv/mashup.cds Normal file
View File

@@ -0,0 +1,25 @@
////////////////////////////////////////////////////////////////////////////
//
// Mashing up imported models...
//
using { sap.capire.bookshop.Books } from '@capire/bookshop';
//
// Extend Books with access to Reviews and average ratings
//
using { ReviewsService.Reviews } from '@capire/reviews';
extend Books with {
reviews : Composition of many Reviews on reviews.subject = $self.ID;
rating : Decimal;
}
//
// Extend Orders with Books as Products
//
using { sap.capire.orders.Orders_Items } from '@capire/orders';
extend Orders_Items with {
book : Association to Books on product.ID = book.ID
}

View File

@@ -1,6 +1,6 @@
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Mashing up bookshop services with required services... // Mashing up provided and required services...
// //
module.exports = async()=>{ // called by server.js module.exports = async()=>{ // called by server.js
@@ -20,29 +20,30 @@ module.exports = async()=>{ // called by server.js
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => { CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
console.debug ('> delegating request to ReviewsService') console.debug ('> delegating request to ReviewsService')
const [id] = req.params, { columns, limit } = req.query.SELECT const [id] = req.params, { columns, limit } = req.query.SELECT
return ReviewsService.read ('Reviews',columns).limit(limit).where({subject:String(id)}) return ReviewsService.tx(req).read ('Reviews',columns).limit(limit).where({subject:String(id)})
})) }))
// //
// Create an order with the OrdersService when CatalogService signals a new order // Create an order with the OrdersService when CatalogService signals a new order
// //
CatalogService.on ('OrderedBook', async (msg) => { CatalogService.on ('OrderedBook', async (msg) => {
const { book, quantity, buyer } = msg.data const { book, amount, buyer } = msg.data
const { title, price } = await db.tx(msg).read (Books, book, b => { b.title, b.price }) const { title, price } = await db.tx(msg).read (Books, book, b => { b.title, b.price })
return OrdersService.tx(msg).create ('Orders').entries({ return OrdersService.tx(msg).create ('Orders').entries({
OrderNo: 'Order at '+ (new Date).toLocaleString(), OrderNo: 'Order at '+ (new Date).toLocaleString(),
Items: [{ product:{ID:`${book}`}, title, price, quantity }], Items: [{ product:{ID:`${book}`}, title, price, amount }],
buyer, createdBy: buyer buyer, createdBy: buyer
}) })
}) })
// //
// Update Books' average ratings when ReviewsService signals updated reviews // Update Books' average ratings when ReviewsService signals updatd reviews
// //
ReviewsService.on ('reviewed', (msg) => { ReviewsService.on ('reviewed', (msg) => {
console.debug ('> received:', msg.event, msg.data) console.debug ('> received:', msg.event, msg.data)
const { subject, count, rating } = msg.data const { subject, rating } = msg.data
return UPDATE(Books,subject).with({ numberOfReviews:count, rating }) return UPDATE(Books,subject).with({rating})
// ^ Note: the framework will execute this and take care for db.tx
}) })
// //
@@ -50,9 +51,9 @@ module.exports = async()=>{ // called by server.js
// //
OrdersService.on ('OrderChanged', (msg) => { OrdersService.on ('OrderChanged', (msg) => {
console.debug ('> received:', msg.event, msg.data) console.debug ('> received:', msg.event, msg.data)
const { product, deltaQuantity } = msg.data const { product, deltaAmount } = msg.data
return UPDATE (Books) .where ('ID =', product) return UPDATE (Books) .where ('ID =', product)
.and ('stock >=', deltaQuantity) .and ('stock >=', deltaAmount)
.set ('stock -=', deltaQuantity) .set ('stock -=', deltaAmount)
}) })
} }

View File

@@ -2,7 +2,7 @@
@bookshop = http://localhost:4004 @bookshop = http://localhost:4004
@reviews-service = {{bookshop}}/reviews @reviews-service = {{bookshop}}/reviews
# Uncomment this when running a separate reviews service # Uncomment this when running a separate reviews service
# @reviews-service = http://localhost:4005/reviews @reviews-service = http://localhost:4005/reviews
@@ -38,11 +38,7 @@ GET {{bookshop}}/browse/Books(201)?
&$select=ID,title,rating &$select=ID,title,rating
&$expand=reviews &$expand=reviews
###
GET {{bookshop}}/browse/Books?
&$select=title,author&$expand=currency
Accept-Language: de
################################################# #################################################
# #

View File

@@ -1,28 +0,0 @@
{
"build": {
"target": "gen",
"tasks": [{
"for": "hana",
"src": "db",
"options": {
"model": [
"db",
"srv",
"app"
]
}
},
{
"for": "node-cf",
"src": "srv",
"options": {
"model": [
"db",
"srv",
"app"
]
}
}
]
}
}

View File

@@ -1 +0,0 @@
PORT = 4007

View File

@@ -1,4 +0,0 @@
npm run build
cf create-service-push
cf bind-service gdpr-srv gdpr-pdm -c .pdm/pdm-binding-config.json
cf restage gdpr-srv

View File

@@ -1,7 +0,0 @@
cf delete gdpr-srv -f
cf delete gdpr-db-deployer -f
cf delete-service gdpr-pdm -f
cf delete-service gdpr-auditlog -f
cf delete-service gdpr-uaa -f
cf delete-service gdpr-hdi -f
cf delete-service gdpr-logs -f

View File

@@ -1,16 +0,0 @@
{
"fullyQualifiedApplicationName": "capire-gdpr",
"fullyQualifiedModuleName": "gdpr-srv",
"applicationTitle": "Capire GDPR Sample App",
"applicationTitleKey": "Capire GDPR Sample App",
"applicationURL": "https://capire-gdpr-srv.cfapps.eu10.hana.ondemand.com",
"endPoints": [{
"type": "odatav4",
"serviceName": "PDMService",
"serviceURI": "/pdm",
"serviceTitle": "Capire GDPR Sample App PDM Service",
"serviceTitleKey": "Capire GDPR Sample App PDM Service",
"hasGdprV4Annotations": true,
"cacheControl": "no-cache"
}]
}

View File

@@ -1,8 +0,0 @@
{
"xs-security": {
"xsappname": "capire-gdpr",
"authorities": ["$ACCEPT_GRANTED_AUTHORITIES"]
},
"fullyQualifiedApplicationName": "capire-gdpr",
"appConsentServiceEnabled": true
}

View File

@@ -1,317 +0,0 @@
////////////////////////////////////////////////////////////////////////////
//
// Note: this is designed for the GDPRService being co-located with
// orders. It does not work if GDPRService is run as a separate
// process, and is not intended to do so.
//
////////////////////////////////////////////////////////////////////////////
using {GDPRService} from '../srv/gdpr-service';
annotate cds.UUID with @Core.Computed;
/*
* Orders
*/
@odata.draft.enabled
annotate GDPRService.Orders with @(UI : {
SelectionFields : [
createdAt,
createdBy
],
LineItem : [
{
Value : OrderNo,
Label : 'Order number'
},
{
Value : customer.firstName,
Label : 'First Name'
},
{
Value : customer.lastName,
Label : 'Last Name'
}
],
HeaderInfo : {
TypeName : 'Order',
TypeNamePlural : 'Orders',
Title : {
Value : OrderNo,
Label : 'Order number'
}
},
Identification : [
{
Value : createdBy,
Label : 'Created by'
},
{
Value : createdAt,
Label : 'Created at'
}
],
HeaderFacets : [
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Created}',
Target : '@UI.FieldGroup#Created'
},
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Modified}',
Target : '@UI.FieldGroup#Modified'
},
],
Facets : [
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Details}',
Target : '@UI.FieldGroup#Details'
},
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>OrderItems}',
Target : 'Items/@UI.LineItem'
},
],
FieldGroup #Details : {Data : [
{
Value : customer_ID,
Label : 'Customer'
},
{
Value : customer.firstName,
Label : 'First Name'
},
{
Value : customer.lastName,
Label : 'Last Name'
},
{
Value : currency_code,
Label : 'Currency'
}
]},
FieldGroup #Created : {Data : [
{
Value : createdBy,
Label : 'Created by'
},
{
Value : createdAt,
Label : 'Created at'
}
]},
FieldGroup #Modified : {Data : [
{
Value : modifiedBy,
Label : 'Modified by'
},
{
Value : modifiedAt,
Label : 'Modified at'
}
]},
}, ) {
createdAt @UI.HiddenFilter : false;
createdBy @UI.HiddenFilter : false;
customer @ValueList.entity : 'Customers';
};
/*
* TODO: Order Items are not really maintainable in Fiori preview app
*/
annotate GDPRService.Orders.Items with @(UI : {
LineItem : [
{
Value : product_ID,
Label : 'Product ID'
},
{
Value : title,
Label : 'Product Name'
},
{
Value : price,
Label : 'Price'
},
{
Value : quantity,
Label : 'Quantity'
},
],
Identification : [
{
Value : product_ID,
Label : 'Product ID'
},
{
Value : title,
Label : 'Product Name'
},
{
Value : quantity,
Label : 'Quantity'
},
{
Value : price,
Label : 'Price'
},
],
Facets : [{
$Type : 'UI.ReferenceFacet',
Label : 'Order Items',
Target : '@UI.Identification'
}, ],
}, ) {
ID @Core.Computed @UI.Hidden : true;
title @Core.Computed;
price @Core.Computed;
quantity @(Common.FieldControl : #Mandatory);
};
/*
* Customers
*/
@odata.draft.enabled
annotate GDPRService.Customers with @(UI : {
SelectionFields : [
firstName,
lastName
],
LineItem : [
{
Value : firstName,
Label : 'First Name'
},
{
Value : lastName,
Label : 'Last Name'
},
{
Value : dateOfBirth,
Label : 'Date of Birth'
}
],
HeaderInfo : {
TypeName : 'Customer',
TypeNamePlural : 'Customers',
Title : {
Value : lastName,
Label : 'Last Name'
},
Description : {
Value : firstName,
Label : 'First Name'
}
},
Identification : [
{
Value : createdBy,
Label : 'Created by'
},
{
Value : createdAt,
Label : 'Created at'
}
],
HeaderFacets : [
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Created}',
Target : '@UI.FieldGroup#Created'
},
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Modified}',
Target : '@UI.FieldGroup#Modified'
},
],
Facets : [
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Details}',
Target : '@UI.FieldGroup#Details'
},
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Addresses}',
Target : 'addresses/@UI.LineItem'
},
],
FieldGroup #Details : {Data : [
{
Value : dateOfBirth,
Label : 'Date of Birth'
},
{
Value : email,
Label : 'E-Mail'
},
{
Value : creditCardNo,
Label : 'Credit Card Number'
}
]},
FieldGroup #Created : {Data : [
{
Value : createdBy,
Label : 'Created by'
},
{
Value : createdAt,
Label : 'Created at'
}
]},
FieldGroup #Modified : {Data : [
{
Value : modifiedBy,
Label : 'Modified by'
},
{
Value : modifiedAt,
Label : 'Modified at'
}
]},
}, ) {
createdAt @UI.HiddenFilter : false;
createdBy @UI.HiddenFilter : false;
};
annotate GDPRService.CustomerPostalAddresses with @(UI : {
LineItem : [
{
Value : town,
Label : 'Town'
},
{
Value : street,
Label : 'Street'
},
{
Value : country.name,
Label : 'Country'
}
],
Identification : [
{
Value : town,
Label : 'Town'
},
{
Value : street,
Label : 'Street'
},
{
Value : country_code,
Label : 'Country Code'
}
],
Facets : [{
$Type : 'UI.ReferenceFacet',
Label : 'Customer Postal Address',
Target : '@UI.Identification'
}, ],
}, );

View File

@@ -1,56 +0,0 @@
using {sap.capire.orders} from '@capire/orders';
using {sap.capire.gdpr} from './schema';
/*
* annotations for Data Privacy (Personal Data Manager and Audit Logging)
*/
annotate gdpr.Customers with @PersonalData : {
DataSubjectRole : 'Customer',
EntitySemantics : 'DataSubject'
}{
ID @PersonalData.FieldSemantics : 'DataSubjectID';
email @PersonalData.IsPotentiallyPersonal;
firstName @PersonalData.IsPotentiallyPersonal;
lastName @PersonalData.IsPotentiallyPersonal;
creditCardNo @PersonalData.IsPotentiallySensitive;
dateOfBirth @PersonalData.IsPotentiallyPersonal;
}
annotate gdpr.CustomerPostalAddresses with @PersonalData : {
DataSubjectRole : 'Customer',
EntitySemantics : 'DataSubjectDetails'
}{
customer @PersonalData.FieldSemantics : 'DataSubjectID';
street @PersonalData.IsPotentiallyPersonal;
town @PersonalData.IsPotentiallyPersonal;
country @PersonalData.IsPotentiallyPersonal;
}
/*
* TODO: Personal Data Manager doesn't know EntitySemantics: 'Other' and FieldSemantics: 'ContractRelatedID'
* see: https://help.sap.com/viewer/620a3ea6aaf64610accdd05cca9e3de2/Cloud/en-US/5a55fae1eb7c496c92c56071186d76b3.html
*/
annotate orders.Orders with @PersonalData : {
DataSubjectRole : 'Customer',
EntitySemantics : 'LegalGround'
}{
ID @PersonalData.FieldSemantics : 'LegalGroundID';
customer @PersonalData.FieldSemantics : 'DataSubjectID';
}
/*
* additional annotations for Audit Logging
*/
annotate gdpr.Customers with @AuditLog.Operation : {
Read : true,
Insert : true,
Update : true,
Delete : true
};
annotate gdpr.CustomerPostalAddresses with @AuditLog.Operation : {
Read : true,
Insert : true,
Update : true,
Delete : true
};

View File

@@ -1,3 +0,0 @@
ID;modifiedAt;createdAt;createdBy;modifiedBy;customer_ID;street;town;country_code
1e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-04-04;2019-01-31;admin@business.com;admin@business.com;8e2f2640-6866-4dcf-8f4d-3027aa831cad;Hauptstrasse 11;Berlin;DE
24e718c9-ff99-47f1-8ca3-950c850777d4;2019-04-04;2019-01-30;admin@business.com;admin@business.com;74e718c9-ff99-47f1-8ca3-950c850777d4;Main Street 22;London;GB
1 ID modifiedAt createdAt createdBy modifiedBy customer_ID street town country_code
2 1e2f2640-6866-4dcf-8f4d-3027aa831cad 2019-04-04 2019-01-31 admin@business.com admin@business.com 8e2f2640-6866-4dcf-8f4d-3027aa831cad Hauptstrasse 11 Berlin DE
3 24e718c9-ff99-47f1-8ca3-950c850777d4 2019-04-04 2019-01-30 admin@business.com admin@business.com 74e718c9-ff99-47f1-8ca3-950c850777d4 Main Street 22 London GB

View File

@@ -1,3 +0,0 @@
ID;modifiedAt;createdAt;createdBy;modifiedBy;email;firstName;lastName;creditCardNo;dateOfBirth
8e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-04-04;2019-01-31;admin@business.com;admin@business.com;john.doe@test.com;John;Doe;9977-6655-4433-2211;1970-01-01
74e718c9-ff99-47f1-8ca3-950c850777d4;2019-04-04;2019-01-30;admin@business.com;admin@business.com;jane.doe@sap.com;Jane;Doe;2211-3344-5566-7788;1980-11-11
1 ID modifiedAt createdAt createdBy modifiedBy email firstName lastName creditCardNo dateOfBirth
2 8e2f2640-6866-4dcf-8f4d-3027aa831cad 2019-04-04 2019-01-31 admin@business.com admin@business.com john.doe@test.com John Doe 9977-6655-4433-2211 1970-01-01
3 74e718c9-ff99-47f1-8ca3-950c850777d4 2019-04-04 2019-01-30 admin@business.com admin@business.com jane.doe@sap.com Jane Doe 2211-3344-5566-7788 1980-11-11

View File

@@ -1,4 +0,0 @@
ID;up__ID;quantity;product_ID;title;price
4bd2c9df-c19f-47b8-a921-3cde0d863b52;29f15ef6-4a13-47d4-aef4-329a403b49eb;1;201;Wuthering Heights;11.11
6c42a40d-5f7c-4c2f-816b-a73c7c28d722;29f15ef6-4a13-47d4-aef4-329a403b49eb;1;271;Catweazle;15
748555fc-2cb0-42b5-a361-dd19a50bd682;31c2bd15-5146-4418-b574-866a08911de7;2;252;Eleonora;28
1 ID up__ID quantity product_ID title price
2 4bd2c9df-c19f-47b8-a921-3cde0d863b52 29f15ef6-4a13-47d4-aef4-329a403b49eb 1 201 Wuthering Heights 11.11
3 6c42a40d-5f7c-4c2f-816b-a73c7c28d722 29f15ef6-4a13-47d4-aef4-329a403b49eb 1 271 Catweazle 15
4 748555fc-2cb0-42b5-a361-dd19a50bd682 31c2bd15-5146-4418-b574-866a08911de7 2 252 Eleonora 28

View File

@@ -1,3 +0,0 @@
ID;createdAt;createdBy;customer_ID;OrderNo;currency_code
29f15ef6-4a13-47d4-aef4-329a403b49eb;2019-01-31;john.doe@test.com;8e2f2640-6866-4dcf-8f4d-3027aa831cad;1;EUR
31c2bd15-5146-4418-b574-866a08911de7;2019-01-30;jane.doe@test.com;74e718c9-ff99-47f1-8ca3-950c850777d4;2;EUR
1 ID createdAt createdBy customer_ID OrderNo currency_code
2 29f15ef6-4a13-47d4-aef4-329a403b49eb 2019-01-31 john.doe@test.com 8e2f2640-6866-4dcf-8f4d-3027aa831cad 1 EUR
3 31c2bd15-5146-4418-b574-866a08911de7 2019-01-30 jane.doe@test.com 74e718c9-ff99-47f1-8ca3-950c850777d4 2 EUR

View File

@@ -1,30 +0,0 @@
using {
Country,
managed,
cuid
} from '@sap/cds/common';
using {sap.capire.orders} from '@capire/orders';
namespace sap.capire.gdpr;
extend orders.Orders with {
customer : Association to Customers;
}
entity Customers : cuid, managed {
email : String;
firstName : String;
lastName : String;
creditCardNo : String;
dateOfBirth : Date;
addresses : Composition of many CustomerPostalAddresses
on addresses.customer = $self;
}
entity CustomerPostalAddresses : cuid, managed {
customer : Association to Customers;
street : String(128);
town : String(128);
@assert.integrity : false
country : Country;
};

View File

@@ -1,136 +0,0 @@
{
"file_suffixes": {
"csv": {
"plugin_name": "com.sap.hana.di.tabledata.source"
},
"hdbafllangprocedure": {
"plugin_name": "com.sap.hana.di.afllangprocedure"
},
"hdbanalyticprivilege": {
"plugin_name": "com.sap.hana.di.analyticprivilege"
},
"hdbcalculationview": {
"plugin_name": "com.sap.hana.di.calculationview"
},
"hdbcollection": {
"plugin_name": "com.sap.hana.di.collection"
},
"hdbconstraint": {
"plugin_name": "com.sap.hana.di.constraint"
},
"hdbdropcreatetable": {
"plugin_name": "com.sap.hana.di.dropcreatetable"
},
"hdbflowgraph": {
"plugin_name": "com.sap.hana.di.flowgraph"
},
"hdbfunction": {
"plugin_name": "com.sap.hana.di.function"
},
"hdbgraphworkspace": {
"plugin_name": "com.sap.hana.di.graphworkspace"
},
"hdbhadoopmrjob": {
"plugin_name": "com.sap.hana.di.virtualfunctionpackage.hadoop"
},
"hdbindex": {
"plugin_name": "com.sap.hana.di.index"
},
"hdblibrary": {
"plugin_name": "com.sap.hana.di.library"
},
"hdbmigrationtable": {
"plugin_name": "com.sap.hana.di.table.migration"
},
"hdbprocedure": {
"plugin_name": "com.sap.hana.di.procedure"
},
"hdbprojectionview": {
"plugin_name": "com.sap.hana.di.projectionview"
},
"hdbprojectionviewconfig": {
"plugin_name": "com.sap.hana.di.projectionview.config"
},
"hdbreptask": {
"plugin_name": "com.sap.hana.di.reptask"
},
"hdbresultcache": {
"plugin_name": "com.sap.hana.di.resultcache"
},
"hdbrole": {
"plugin_name": "com.sap.hana.di.role"
},
"hdbroleconfig": {
"plugin_name": "com.sap.hana.di.role.config"
},
"hdbsearchruleset": {
"plugin_name": "com.sap.hana.di.searchruleset"
},
"hdbsequence": {
"plugin_name": "com.sap.hana.di.sequence"
},
"hdbstatistics": {
"plugin_name": "com.sap.hana.di.statistics"
},
"hdbstructuredprivilege": {
"plugin_name": "com.sap.hana.di.structuredprivilege"
},
"hdbsynonym": {
"plugin_name": "com.sap.hana.di.synonym"
},
"hdbsynonymconfig": {
"plugin_name": "com.sap.hana.di.synonym.config"
},
"hdbsystemversioning": {
"plugin_name": "com.sap.hana.di.systemversioning"
},
"hdbtable": {
"plugin_name": "com.sap.hana.di.table"
},
"hdbtabledata": {
"plugin_name": "com.sap.hana.di.tabledata"
},
"hdbtabletype": {
"plugin_name": "com.sap.hana.di.tabletype"
},
"hdbtrigger": {
"plugin_name": "com.sap.hana.di.trigger"
},
"hdbview": {
"plugin_name": "com.sap.hana.di.view"
},
"hdbvirtualfunction": {
"plugin_name": "com.sap.hana.di.virtualfunction"
},
"hdbvirtualfunctionconfig": {
"plugin_name": "com.sap.hana.di.virtualfunction.config"
},
"hdbvirtualpackagehadoop": {
"plugin_name": "com.sap.hana.di.virtualpackage.hadoop"
},
"hdbvirtualpackagesparksql": {
"plugin_name": "com.sap.hana.di.virtualpackage.sparksql"
},
"hdbvirtualprocedure": {
"plugin_name": "com.sap.hana.di.virtualprocedure"
},
"hdbvirtualprocedureconfig": {
"plugin_name": "com.sap.hana.di.virtualprocedure.config"
},
"hdbvirtualtable": {
"plugin_name": "com.sap.hana.di.virtualtable"
},
"hdbvirtualtableconfig": {
"plugin_name": "com.sap.hana.di.virtualtable.config"
},
"properties": {
"plugin_name": "com.sap.hana.di.tabledata.properties"
},
"tags": {
"plugin_name": "com.sap.hana.di.tabledata.properties"
},
"txt": {
"plugin_name": "com.sap.hana.di.copyonly"
}
}
}

View File

@@ -1,31 +0,0 @@
---
applications:
# -----------------------------------------------------------------------------------
# HANA Database Content Deployer App
# -----------------------------------------------------------------------------------
- name: gdpr-db-deployer
path: gen/db
no-route: true
health-check-type: process
memory: 256M
buildpack: nodejs_buildpack
services:
- gdpr-logs
- gdpr-hdi
# -----------------------------------------------------------------------------------
# Backend Service
# -----------------------------------------------------------------------------------
- name: gdpr-srv
path: gen/srv
memory: 256M
buildpack: nodejs_buildpack
routes:
- route: capire-gdpr-srv.cfapps.eu10.hana.ondemand.com
services:
- gdpr-logs
- gdpr-hdi
- gdpr-uaa
- gdpr-auditlog
# binding with parameters not yet supported -> binding done manually in .etc/deploy.sh
#- name: gdpr-pdm
# parameters: ./pdm-binding-config.json

View File

@@ -1,49 +0,0 @@
{
"name": "@capire/gdpr",
"version": "0.0.1",
"dependencies": {
"@capire/orders": "../orders",
"@sap/audit-logging": "^5.1.0",
"@sap/cds": "^5.9",
"express": "^4.17.1",
"hdb": "^0.19.0"
},
"scripts": {
"build": "rm -rf gen && cds build --production",
"deploy": "sh .etc/deploy.sh",
"undeploy": "sh .etc/undeploy.sh",
"start": "cds run"
},
"cds": {
"requires": {
"auth": {
"__comment__": "workaround to avoid approuter et al. setup",
"impl": "srv/auth.js"
},
"audit-log": {
"[development]": {
"kind": "audit-log-to-console"
},
"[production]": {
"kind": "audit-log-service"
}
},
"db": {
"kind": "sql"
},
"uaa": {
"kind": "xsuaa"
}
},
"features": {
"audit_personal_data": true,
"fiori_preview": true,
"[production]": {
"kibana_formatter": true
}
},
"hana": {
"deploy-format": "hdbtable"
}
}
}

View File

@@ -1,35 +0,0 @@
# how-to
## required services and subscriptions
services:
- Audit Log Service
- SAP HANA Cloud
- SAP HANA Schemas & HDI Containers
- Application Logging Service
- Personal Data Manager
- Authorization and Trust Management Service
subscriptions:
- Audit Log Viewer Service
- Personal Data Manager
## deploy
after adding the necessary entitlements, do:
- `cf l` to log into the respective account
- `cd gdpr` (if still in root of `cloud-cap-samples`)
- `npm run deploy`, which executes build and deployment via `.etc/deploy.sh`
## authorization
create roles for Audit Log Viewer Service and Personal Data Manager, and assign the roles to the respective users
# open issues
- deploy via mta, which can bind with parameters, and get rid of scripts in `.etc`
- use approuter to remove hacky custom auth impl (`srv/auth.js`)
- clarify annotation `EntitySemantics`, which differs between audit logging (`Other`) and personal data manager (`LegalGround`)
- annotations for order items Fiori preview app
+ `Products` has `@cds.persistence.skip:'always'`
- how to reuse intial data from `common`?

View File

@@ -1,20 +0,0 @@
---
create-services:
- name: gdpr-logs # > for kibana
broker: application-logs
plan: standard
- name: gdpr-hdi # > hana
broker: hana
plan: hdi-shared
- name: gdpr-auditlog # > audit log sink
broker: auditlog
plan: standard
# gdpr-pdm needs to exist before creating gdpr-uaa for authorization grant
- name: gdpr-pdm # > personal data manager
broker: personal-data-manager-service
plan: standard
parameters: ./.pdm/pdm-instance-config.json
- name: gdpr-uaa # > uaa for authentication
broker: xsuaa
plan: application
parameters: xs-security.json

View File

@@ -1,43 +0,0 @@
/*
* workaround to avoid approuter et al. setup
*/
const jwt = require('jsonwebtoken')
const tenant = process.env.VCAP_SERVICES
? JSON.parse(process.env.VCAP_SERVICES).xsuaa[0].credentials.tenantid
: 'anonymous'
module.exports = (req, res, next) => {
/*
* decode JWT coming from Personal Data Manager
*
* DO NOT USE FOR PRODUCTION!
* - no token validation
* - no xsappname check
*/
const bearer = req.headers.authorization && req.headers.authorization.split('Bearer ')[1]
if (bearer) {
const { client_id: id, zid: tenant, scope: roles } = jwt.decode(bearer)
req.user = {
id,
tenant,
is: role => roles.some(r => r.endsWith(`.${role}`))
}
return next()
}
// mock user that has every role EXCEPT PersonalDataManagerUser
const basic = req.headers.authorization && req.headers.authorization.split('Basic ')[1]
if (basic) {
const [id] = Buffer.from(basic, 'base64').toString('utf-8').split(':')
req.user = {
id,
tenant,
is: role => role !== 'PersonalDataManagerUser'
}
return next()
}
// no bearer & no basic -> 401
res.set('WWW-Authenticate', 'Basic realm="Users"').status(401).end()
}

View File

@@ -1,10 +0,0 @@
using {
sap.capire.orders,
sap.capire.gdpr
} from '../db/schema';
@requires : 'admin' // > authorization check
service GDPRService {
entity Customers as projection on gdpr.Customers;
entity Orders as projection on orders.Orders;
}

View File

@@ -1,24 +0,0 @@
using {
sap.capire.gdpr as gdpr,
sap.capire.orders as orders
} from '../db/data-privacy';
@requires : 'PersonalDataManagerUser' // > authorization check
service PDMService {
entity Customers as projection on gdpr.Customers;
entity CustomerPostalAddresses as projection on gdpr.CustomerPostalAddresses;
entity Orders as projection on orders.Orders;
/*
* additional annotations for Personal Data Manager's Search Fields
*/
annotate Customers with @(Communication.Contact : {
n : {
surname : lastName,
given : firstName
},
bday : dateOfBirth
});
};

View File

@@ -1,26 +0,0 @@
const cds = require('@sap/cds')
/*
* in development, write audit logs to custom sink (i.e., to console in this example)
*/
cds.on('served', async () => {
if (process.env.NODE_ENV === 'production') return
const auditLogService = await cds.connect.to('audit-log')
// use prepend to get called before the generic implementation
auditLogService.prepend(function() {
const LOG = cds.log('my custom audit logging impl')
// triggered when reading sensitive personal data
this.on('dataAccessLog', function(req) {
const { accesses } = req.data
for (const access of accesses) LOG.info(access)
})
// triggered when modifying personal data
this.on('dataModificationLog', function(req) {
const { modifications } = req.data
for (const modification of modifications) LOG.info(modification)
})
})
})
module.exports = cds.server

View File

@@ -1,14 +0,0 @@
{
"xsappname": "capire-gdpr",
"tenant-mode": "shared",
"scopes": [{
"name": "$XSAPPNAME.PersonalDataManagerUser",
"description": "Authority for Personal Data Manager",
"grant-as-authority-to-apps": [
"$XSSERVICENAME(gdpr-pdm)"
]
}, {
"name": "$XSAPPNAME.admin",
"description": "Administrator"
}]
}

View File

@@ -1,15 +0,0 @@
# Hello World Getting Started Sample
## Next Steps
- To run the JavaScript implementation, open a new terminal and run `cds watch`.
- To run the TypeScript implementation, open a new terminal and run `cds-ts watch`.
Then call the service at: http://localhost:4004/say/hello(to='world')
## Learn More
Learn more about:
- [Hello World!](https://cap.cloud.sap/docs/get-started/hello-world)
- [Using TypeScript](https://cap.cloud.sap/docs/get-started/using-typescript)

View File

@@ -2,56 +2,6 @@
"name": "@capire/hello-world", "name": "@capire/hello-world",
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"test": "npx jest --silent", "watch": "cds serve world.cds"
"start": "cds serve srv/world.cds",
"start:ts": "cds-ts serve srv/world.cds"
},
"dependencies": {
"@sap/cds": "^5.0.4"
},
"devDependencies": {
"@types/jest": "^27.0.2",
"@types/node": "^16.11.6",
"ts-jest": "^27.0.2",
"typescript": "^4.3.5"
},
"jest": {
"testEnvironment": "node",
"preset": "ts-jest",
"globals": {
"ts-jest": {
"diagnostics": {
"_comment": "see https://githubmemory.com/repo/kulshekhar/ts-jest/issues/2722",
"ignoreCodes": [
151001
]
}
}
}
},
"eslintConfig": {
"extends": "eslint:recommended",
"env": {
"es2020": true,
"node": true,
"jest": true,
"mocha": true
},
"globals": {
"SELECT": true,
"INSERT": true,
"UPDATE": true,
"DELETE": true,
"CREATE": true,
"DROP": true,
"CDL": true,
"CQL": true,
"CXL": true,
"cds": true
},
"rules": {
"no-console": "off",
"require-atomic-updates": "off"
}
} }
} }

View File

@@ -1,7 +0,0 @@
import type { Request } from "@sap/cds/apis/services"
module.exports = class say {
hello(req: Request) {
return `Hello ${req.data.to} from a TypeScript file!`
}
}

View File

@@ -1,15 +0,0 @@
process.env.CDS_TYPESCRIPT = 'true';
import * as cds from '@sap/cds';
//@ts-ignore
const {GET} = cds.test.in(__dirname,'../srv').run('serve', 'world.cds');
describe('Hello world!', () => {
afterAll(() => { delete process.env.CDS_TYPESCRIPT; });
it('should say hello with class impl from a typescript file', async () => {
const {data} = await GET`/say/hello(to='world')`
expect(data.value).toMatch(/Hello world.*typescript.*/i)
})
})

Some files were not shown because too many files have changed in this diff Show More