Compare commits

...

136 Commits

Author SHA1 Message Date
Johannes Vogel
08a3157f1d use new kinds for audit log 2022-03-18 09:30:42 +01:00
sjvans
c9ecef4e21 Merge branch 'main' into audit-logging 2022-02-08 13:48:35 +01:00
sjvans
46f1be4395 cleanup 2022-02-08 13:47:32 +01:00
sjvans
b932637400 Update manifest.json 2022-02-08 13:45:36 +01:00
sjvans
3c6d49b88e in development, write audit logs to custom sink 2022-02-08 13:41:30 +01:00
Christian Georgi
f4f02f6fe5 Merge pull request #320 from SAP-samples/node16
Node16, dependency updates
2022-02-07 10:38:17 +01:00
Christian Georgi
68c2504fc4 package-lock update 2022-02-07 10:37:00 +01:00
Christian Georgi
51a8762738 Add Node.js 16 to CI 2022-02-07 10:31:08 +01:00
Christian Georgi
ff533754b4 Merge pull request #319 from SAP-samples/data-viewer
Data viewer Misc
2022-02-07 10:30:01 +01:00
Christian Georgi
f6b4d534fc Cosmetics 2022-02-07 10:11:38 +01:00
Christian Georgi
3c5107ba42 Require authentication 2022-02-07 10:02:31 +01:00
Christian Georgi
dbc561d2e3 Show errors in UI, migrate to Vue 3 2022-02-07 09:54:17 +01:00
Christian Georgi
f21488fd71 Merge pull request #318 from seVladimirs/patch-1
adds @sap/cds dependency to package.json
2022-02-07 09:53:17 +01:00
Christian Georgi
0bce8d9ce0 Fix glitch 2022-02-07 09:52:17 +01:00
Vladimirs Semikins
0f03ffd3eb adds @sap/cds dependency to package.json
To avoid further questions (e.g. https://answers.sap.com/questions/13577720/i-push-my-capnodejsto-btp-environment-but-always-c.html) this PR will add missing `@sap/cds` dependency.
2022-02-07 10:38:28 +02:00
Daniel Hutzel
bc03cae550 fix duplicate annotates (#317) 2022-02-04 16:19:54 +01:00
sjvans
8197559065 fix: app crashes if SELECT results in null (#298) 2022-02-03 18:04:10 +01:00
sjvans
6928ae907a initial 2022-02-03 17:57:35 +01:00
Daniel Hutzel
7accf1ae23 Using only bob as reviewer (#315) 2022-02-03 04:35:56 +01:00
Christian Georgi
2ffa1721b3 Merge pull request #314 from SAP-samples/data-browser
Data browser Cosmetics
2022-02-01 19:54:34 +01:00
Christian Georgi
37811381e7 Show errors in UI 2022-02-01 19:52:05 +01:00
Christian Georgi
74f9d5cc1e Sort columns 2022-02-01 19:30:30 +01:00
Christian Georgi
7c0769e059 Stick table headers 2022-02-01 18:42:36 +01:00
Christian Georgi
10dcd6aadd Merge pull request #313 from SAP-samples/browser
Data Browser: UI improvements, cosmetics
2022-01-31 20:24:38 +01:00
Christian Georgi
f8c23f4c54 Better layout; 2022-01-31 20:08:40 +01:00
Christian Georgi
ae6be30aa0 Data browser: cosmetics, reformat 2022-01-31 19:07:00 +01:00
Christian Georgi
e6baa95f74 Data browser: keep data row selection across refresh 2022-01-31 19:03:06 +01:00
Daniel Hutzel
aa28804a51 Basic bookshop w/o reuse deps (#312) 2022-01-29 23:49:30 +01:00
Christian Georgi
ea0773071e Update package-lock 2022-01-27 15:28:04 +01:00
Christian Georgi
1a71a6d28a Simple Data Viewer
- Generic CDS service to fetch data
- Simple Vue.js UI
2022-01-27 15:28:04 +01:00
Christian Georgi
50791bed80 Override tar with newer version 2022-01-17 13:20:10 +01:00
Christian Georgi
d58a71af7f Bump dependencies 2022-01-17 12:13:13 +01:00
Iwona Hahn
c7c979236e Merge pull request #306 from SAP-samples/iwonahahn-update
update year for license in README.md
2022-01-14 15:13:27 +01:00
Iwona Hahn
0675793c87 update year for license in README.md 2022-01-06 12:13:48 +01:00
Daniel
91bca922c4 use common file names for license 2022-01-06 10:24:46 +01:00
Daniel
30403d239a using latest versions 2022-01-06 10:24:46 +01:00
Daniel
589e282288 mocha has to run parallel 2022-01-06 10:24:46 +01:00
Daniel
a8345122ea fixed support for mocha 2022-01-04 13:55:37 +01:00
Daniel
788a101f41 Added .drawio graphics 2021-12-30 05:38:10 +01:00
Daniel
379ddc9fb0 Added npm start script → launching fiori 2021-12-14 10:13:53 +01:00
Daniel
1fde0c928b Fixed submit order -> returned stock was wrong 2021-11-29 15:38:37 +01:00
Christian Georgi
fa854a2411 Add hint on db-ext datasource to code tour
Signed-off-by: Christian Georgi <christian.georgi@sap.com>
2021-11-29 15:33:57 +01:00
Daniel
40b6acee6a Fixing erroneous db.model config 2021-11-29 15:33:57 +01:00
Daniel
a37f1b8e0c Reformatted package.json 2021-11-29 15:33:57 +01:00
Christian Georgi
042ddec41c Adjust code tour 2021-11-29 15:33:57 +01:00
Daniel
8d8aa6c2c9 Fixing copy errors
Signed-off-by: Daniel <daniel.hutzel@sap.com>
2021-11-29 15:33:57 +01:00
Daniel
756ed48ce3 Fiori apps with manage authors
Signed-off-by: Daniel <daniel.hutzel@sap.com>
2021-11-29 15:33:57 +01:00
Daniel
19b680ab3d Moving lifetime + age to where it is used
Signed-off-by: Daniel <daniel.hutzel@sap.com>
2021-11-29 15:33:57 +01:00
Daniel
33380c0792 Restore simple bookshop - no workarounds
Signed-off-by: Daniel <daniel.hutzel@sap.com>
2021-11-29 15:33:57 +01:00
Christian Georgi
0d5b65d93f Merge pull request #291 from gregorwolf/navigation
Demo for Semantic Navigation using the local Fiori Launchpad
2021-11-22 13:08:57 +01:00
Christian Georgi
dcf5ec6068 Merge branch 'main' into navigation 2021-11-18 16:19:57 +01:00
Christian Georgi
fb41dd100b Merge pull request #293 from SAP-samples/bookstore
In Fiori app, use bookstore consistently
2021-11-18 16:12:12 +01:00
Christian Georgi
769aa2b4f5 Adjust samples tour to bookstore 2021-11-18 15:34:59 +01:00
Christian Georgi
f6c5e620c0 In Fiori app, use bookstore consistently
This also fixes validation errors from annotation LSP, which missed some
of the bookstore elements

Signed-off-by: Christian Georgi <christian.georgi@sap.com>
2021-11-18 14:14:09 +01:00
Gregor Wolf
7013cb6296 remove name 2021-11-13 00:48:13 +01:00
Gregor Wolf
6411ce4ea3 remove redundant annotation 2021-11-13 00:48:04 +01:00
Gregor Wolf
cd914a8d87 show ID in LineItem 2021-11-13 00:41:48 +01:00
Gregor Wolf
b35e92b5bc TextArrangement : #TextOnly 2021-11-13 00:41:28 +01:00
Gregor Wolf
8350aff111 correct renameTo 2021-11-13 00:36:22 +01:00
Gregor Wolf
9aa604c5a0 remove @UI.HiddenFilter 2021-11-12 23:56:11 +01:00
Gregor Wolf
f5cb53adf8 add inbounds parameter 2021-11-12 23:48:51 +01:00
Gregor Wolf
9fe16edc55 fix manifest issues 2021-11-12 23:48:26 +01:00
Gregor Wolf
0093883f25 add Author ID column 2021-11-12 23:46:41 +01:00
Gregor Wolf
14024a4837 add semantic object for navigation 2021-11-12 22:31:58 +01:00
Gregor Wolf
2e13b37096 activate automatic load 2021-11-12 22:31:42 +01:00
Gregor Wolf
7407b0aae3 format 2021-11-12 22:04:48 +01:00
Gregor Wolf
152d088ff4 rename author to authorName 2021-11-12 21:49:39 +01:00
Gregor Wolf
19c886d6dc fix implicit redirection issue 2021-11-12 21:41:39 +01:00
Gregor Wolf
575cffe70a migrate to fioriSandboxConfig.json 2021-11-12 21:29:21 +01:00
Gregor Wolf
03869e5896 Fiori UI for Authors 2021-11-12 21:28:49 +01:00
Gregor Wolf
597afeb3d8 format, rename author to authorName 2021-11-12 21:28:23 +01:00
Gregor Wolf
d376e6e063 UI annotations for Authors 2021-11-12 21:27:40 +01:00
Gregor Wolf
2a44cfa9dc add Authors, rename author attribute 2021-11-12 21:25:33 +01:00
Gregor Wolf
7964c9aefd adjust formatting 2021-11-12 20:59:45 +01:00
Christian Georgi
ce752097d0 Merge pull request #290 from dinurp/main
Annotations for Admin.Book service
2021-11-10 08:55:47 +01:00
dinurp
e838f380c5 annotation to make language value list a drop down.
type:#Fixed was not rendering the value list as a drop down.
There was not UI validation for values entered. 
The added annotation does both these: UI validation and renders the a drop down list.
2021-11-10 11:18:41 +04:00
dinurp
e89e012eba Update bookshop/srv/admin-service.cds
Co-authored-by: Christian Georgi <chgeo@users.noreply.github.com>
2021-11-10 11:12:18 +04:00
Christian Georgi
bf0f57efe5 Merge branch 'main' into main 2021-11-09 17:55:55 +01:00
Dinu
40ea057f7f Annotation for value list for languages for locale 2021-11-09 15:46:22 +04:00
Dinu
31d2f4c932 Avoid popup for ID in Fiori preview for Books. 2021-11-09 14:01:53 +04:00
Daniel Hutzel
8cc2db7118 Using managed compositions for Order.Items (#273)
* Using managed compositions for Order.Items
Co-authored-by: sjvans <30337871+sjvans@users.noreply.github.com>
2021-11-08 17:41:33 +01:00
Daniel Hutzel
482b71e60b Remove workaround for integrity check (#173)
* Remove workaround for integrity check
2021-11-08 14:23:41 +01:00
Daniel Hutzel
7536451e29 Enhanced cds.ql tests (#228)
* Enhanced cds.ql tests
2021-11-08 13:49:43 +01:00
Daniel
2703e8b999 Removed authorization workaround 2021-11-08 13:19:12 +01:00
Daniel
ac2e4dd6f9 Updating package-lock to v2 2021-11-08 13:17:31 +01:00
PierreFritsch
c718fa400d import {Request} => import type {Request} 2021-11-08 13:14:02 +01:00
Pierre Fritsch
730da6e435 hello TypeScript: Define type of req object
Make it clearer that `req` is a CDS `Request` object.
2021-11-08 13:14:02 +01:00
Daniel
7d9b4f7a5d Fixes for Node16 2021-11-05 17:01:59 +01:00
Daniel
35d3708592 Fixes for Node16 2021-11-05 15:49:05 +01:00
Daniel
eaaf0d29a5 Updated package-lock.json 2021-11-04 10:37:33 +01:00
Daniel
64fe700d1e ... 2021-11-04 10:37:33 +01:00
Daniel
5c3cec973e Using bookstore as composite app 2021-11-04 10:37:33 +01:00
Daniel
680a6ae68f Introduced bookstore composite app 2021-11-04 10:37:33 +01:00
Daniel
366b0f8f9a Using flat fiori app folder 2021-11-04 10:37:33 +01:00
Daniel
b95df77b9a Fixed access to cds.entities 2021-11-04 10:37:33 +01:00
Christian Georgi
7f6b87171a Merge pull request #284 from SAP-samples/registry
NPM registry: more robust,  work on Windows
2021-10-26 22:21:32 +02:00
Christian Georgi
21e74bbbfb Add test for NPM registry 2021-10-26 22:17:25 +02:00
Christian Georgi
102b15c3cd NPM registry: more robust, work on Windows
- Run `npm rm` in correct dir
- Encode args in `npm conf rm` command for Powershell
- Don't cleanup in exit handler, this might not complete.
  Instead, do it on startup.
- Different cleanup command for Windows and Linux
- Explicitly close server, instead of relying on process.exit()
- Make scope configurable
2021-10-26 19:11:08 +02:00
Dr. David A. Kunz
73db2e96bc Merge pull request #283 from SAP-samples/ql-changes-typo-fix
fix typo in ql changes
2021-10-26 11:57:44 +02:00
Dr. David A. Kunz
7bb58ee2d5 Merge branch 'main' into ql-changes-typo-fix 2021-10-26 11:56:15 +02:00
D065023
27e82d16e0 fix typo 2021-10-26 11:55:33 +02:00
Dr. David A. Kunz
ff9bbe6d8d Merge pull request #282 from SAP-samples/ql-changes
Changes in ql for cds version 5.6.0
2021-10-26 11:32:09 +02:00
Dr. David A. Kunz
5f1b7b8cbf Merge branch 'main' into ql-changes 2021-10-26 11:27:27 +02:00
D065023
0253300557 Changes in ql for cds version 5.6.0 2021-10-26 11:22:43 +02:00
Daniel
139d957495 Added reviewed.count to messsages 2021-10-18 18:04:33 +02:00
Daniel
404427237b Updated reviews samples for messaging GA 2021-10-18 18:04:33 +02:00
Christian Georgi
5f89334403 Remove refs to 'master' branch 2021-10-12 18:46:21 +02:00
Christian Georgi
e612fa97ea Merge pull request #277 from SAP-samples/updated-package.json
Updated package.json
2021-10-08 11:16:10 +02:00
Christian Georgi
da2ea39466 Do not fail on absent cds-swagger-ui-express 2021-10-08 10:23:12 +02:00
Daniel
117000df71 Revert "temporarily using sqlite3 until next release to fix vulnerabilities"
This reverts commit f3ffb69d3a.
2021-10-08 07:50:04 +02:00
Daniel
f3ffb69d3a temporarily using sqlite3 until next release to fix vulnerabilities 2021-10-08 07:41:18 +02:00
Daniel
f908484973 Updated package.json 2021-10-08 06:42:50 +02:00
Christian Georgi
c4529f3cd7 Increase test timeout
At least on slower machines or when file system caches are not filled,
the default 5000 ms are not enough for the tests to finish.
2021-09-29 13:51:49 +02:00
Christian Georgi
0220400484 Avoid mixed spaces/tabs 2021-09-20 15:56:53 +02:00
dependabot[bot]
c1911b6e96 Bump axios from 0.21.1 to 0.21.4
Bumps [axios](https://github.com/axios/axios) from 0.21.1 to 0.21.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.1...v0.21.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-10 15:42:25 +02:00
Daniel
19083d156e Rename amount -> quantity 2021-09-10 15:39:30 +02:00
Christian Georgi
48d547e6cd Make jest really silent 2021-09-10 15:35:36 +02:00
Daniel
bae491a832 one more more more 2021-08-31 20:36:36 +02:00
Daniel
ca41a2141c one more 2021-08-31 19:30:09 +02:00
Daniel Hutzel
08f409af73 more (#267) 2021-08-31 18:05:22 +02:00
Daniel Hutzel
efa60550fb fix cds.ql (#266) 2021-08-31 13:29:05 +02:00
Daniel Hutzel
f599206bf4 Fixed cds.ql in latest release (#232)
* Fixed cds.ql in latest release

* Requires @sap/cds ^5.1.5

* fixed fixes

* More cdr tests

Co-authored-by: Christian Georgi <chgeo@users.noreply.github.com>
2021-08-31 09:59:06 +02:00
Daniel
2f5d159428 using cds.test 2021-08-30 16:08:25 +02:00
Daniel
2be3d50389 More cds.ql cleanup 2021-08-30 11:09:42 +02:00
Daniel
46b58f1b5c skipping ts test 2021-08-27 14:19:17 +02:00
Daniel
e87527cbcd fixed package-lock 2021-08-27 14:19:17 +02:00
Daniel
091844219b Upgrade to jest 27 2021-08-27 14:19:17 +02:00
Daniel
de796e5a89 Fixed '*' in upcomming release 2021-08-27 10:46:53 +02:00
Daniel
f988088412 '*' is still different 2021-08-26 06:50:13 +02:00
Daniel
c4a51ab719 fixed test 2021-08-26 06:50:13 +02:00
Daniel
f58376607a Prep for cleaning up cds.ql 2021-08-26 06:50:13 +02:00
Dr. David A. Kunz
839048d87c Merge pull request #260 from SAP-samples/fixing-test
Cascade delete test: Yes it should have been deleted
2021-08-19 12:57:56 +02:00
D065023
3b3463f889 skipped 2021-08-19 12:55:48 +02:00
D065023
d3396304ec yes it should be deleted 2021-08-19 12:45:03 +02:00
Pierre Fritsch
ae09caf7ad Enable cds watch hello
by moving the `hello` implementation into the subfolder `srv`
2021-08-03 16:01:10 +02:00
Daniel Hutzel
e1052c209b Removing workaround for glitch in drafts (#254)
* Removing workaround for glitch in drafts

* cosmetics
2021-08-02 17:20:46 +02:00
116 changed files with 6970 additions and 23573 deletions

View File

@@ -8,7 +8,7 @@
"mocha": true
},
"parserOptions": {
"ecmaVersion": 2018
"ecmaVersion": 2020
},
"globals": {
"SELECT": true,
@@ -22,6 +22,7 @@
"rules": {
"no-console": "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:
push:
branches: [ master ]
branches: [ main ]
pull_request:
branches: [ master ]
branches: [ main ]
jobs:
build:
@@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
node-version: [12.x, 14.x]
node-version: [16.x, 14.x, 12.x]
steps:
- uses: actions/checkout@v2

View File

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

View File

@@ -68,26 +68,35 @@
},
{
"file": "fiori/package.json",
"description": "#### Configuration\n\nThe `cds` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile.",
"line": 17,
"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.",
"selection": {
"start": {
"line": 41,
"character": 1
},
"end": {
"line": 48,
"character": 1
}
},
"title": "Configuration"
},
{
"file": "fiori/package.json",
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch fiori\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
"line": 28,
"line": 43,
"title": "Run with SQLite"
},
{
"file": "fiori/package.json",
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd fiori; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd fiori; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
"line": 31,
"line": 46,
"title": "Run with SAP HANA"
},
{
"file": "fiori/test/requests.http",
"description": "### Add More Data\n\nOptionally you can add some `Authors` data by clicking on the _Send Request_ link (provided by the [REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension).",
"line": 68,
"line": 72,
"selection": {
"start": {
"line": 67,
@@ -104,6 +113,5 @@
"title": "Wrap-up",
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
}
],
"ref": "master"
]
}

View File

@@ -19,7 +19,7 @@
}
},
{
"file": "hello/world.cds",
"file": "hello/srv/world.cds",
"description": "### Hello World!\n\nThis is a simplistic [Hello World](https://cap.cloud.sap/docs/get-started/hello-world) service using [CDS](https://cap.cloud.sap/docs/cds/) and [cds.services](https://cap.cloud.sap/docs/node.js/api#services-api).",
"line": 2,
"selection": {
@@ -68,7 +68,7 @@
},
{
"file": "orders/db/schema.cds",
"description": "### Compositions and Serving Documents\n\nA standalone orders management service, demonstrating:\n- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with\n- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)\n",
"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",
"line": 1,
"selection": {
"start": {
@@ -84,7 +84,7 @@
},
{
"file": "reviews/db/schema.cds",
"description": "### More Modularity\n\nShows how to implement a modular service to manage product reviews, including:\n- Consuming other services synchronously and asynchronously\n- Serving requests synchronously\n- Emitting events asynchronously\n- Grow as you go, with:\n- Mocking app services\n- Running service meshes\n- Late-cut Micro Services\n- As well as managed data, input validations, and authorization\n",
"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",
"line": 1,
"selection": {
"start": {
@@ -99,8 +99,12 @@
"title": "Reviews"
},
{
"file": "fiori/app/index.cds",
"description": "### Annotations for SAP Fiori Elements\n\nA [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:\n - [@capire/bookshop](bookshop)\n - [@capire/reviews](reviews)\n - [@capire/orders](orders)\n - [@capire/common](common)\n\n[Adds a SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookshop, thereby introducing to:\n - [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files\n - Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)\n - Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)\n - Serving SAP Fiori apps locally\n\n[The Vue.js app](bookshop/app/vue) imported from bookshop is served as well.\n",
"title": "Bookstore",
"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)"
},
{
"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,
"selection": {
"start": {
@@ -117,14 +121,13 @@
{
"file": "package.json",
"description": "### All-in-one Monorepo\n\nEach sample sub directory essentially is a standard npm package, some with standard npm dependencies to other samples. The root folder's [package.json](package.json) has local links to the sub folders, such that an `npm install` populates a local `node_modules` folder acts like a local npm registry to the individual sample packages.\n",
"line": 8,
"selection": {
"start": {
"line": 8,
"character": 1
},
"end": {
"line": 15,
"line": 16,
"character": 1
}
},
@@ -133,4 +136,4 @@
],
"isPrimary": true,
"description": "Overview of CAP Samples for Node.js"
}
}

View File

@@ -13,5 +13,6 @@
"**/cds/lib/req/cls.js",
"**/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
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/master.zip).
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/main.zip).
```sh
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
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.
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.

View File

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

View File

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

View File

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

24
bookshop/db/init.js Normal file
View File

@@ -0,0 +1,24 @@
/**
* 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 +1,2 @@
exports.CatalogService = require('./srv/cat-service')
const { CatalogService } = require('./srv/cat-service')
module.exports = { CatalogService }

View File

@@ -3,7 +3,6 @@
"version": "1.0.0",
"description": "A simple self-contained bookshop service.",
"dependencies": {
"@capire/common": "*",
"@sap/cds": "^5.0.4",
"express": "^4.17.1",
"passport": "0.4.1"

View File

@@ -11,6 +11,6 @@ service CatalogService @(path:'/browse') {
} excluding { createdBy, modifiedBy };
@requires: 'authenticated-user'
action submitOrder ( book: Books:ID, amount: Integer ) returns { stock: Integer };
event OrderedBook : { book: Books:ID; amount: Integer; buyer: String };
action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer };
event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String };
}

View File

@@ -1,18 +1,20 @@
const cds = require('@sap/cds')
const { Books } = cds.entities ('sap.capire.bookshop')
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,amount} = req.data
let {stock} = await SELECT `stock` .from (Books,book)
if (stock >= amount) {
await UPDATE (Books,book) .with (`stock -=`, amount)
await this.emit ('OrderedBook', { book, amount, buyer:req.user.id })
return { stock }
}
else return req.error (409,`${amount} exceeds stock for book #${book}`)
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

View File

@@ -32,6 +32,19 @@ GET {{server}}/admin/Authors?
# &sap-language=de
Authorization: Basic alice:
### ------------------------------------------------------------------------
# Create Author
POST {{server}}/admin/Authors
Content-Type: application/json;IEEE754Compatible=true
Authorization: Basic alice:
{
"ID": 112,
"name": "Shakespeeeeere",
"age": 22
}
### ------------------------------------------------------------------------
# Create book
POST {{server}}/admin/Books
@@ -71,7 +84,7 @@ POST {{server}}/browse/submitOrder
Content-Type: application/json
{{me}}
{ "book":201, "amount":5 }
{ "book":201, "quantity":5 }
### ------------------------------------------------------------------------

2
bookstore/index.cds Normal file
View File

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

34
bookstore/package.json Normal file
View File

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

22
bookstore/server.js Normal file
View File

@@ -0,0 +1,22 @@
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

38
bookstore/srv/mashup.cds Normal file
View File

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

View File

@@ -0,0 +1,10 @@
// -----------------------------------------------------------------------
// 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

@@ -2,7 +2,7 @@
@bookshop = http://localhost:4004
@reviews-service = {{bookshop}}/reviews
# Uncomment this when running a separate reviews service
@reviews-service = http://localhost:4005/reviews
# @reviews-service = http://localhost:4005/reviews

View File

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

View File

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

@@ -0,0 +1,95 @@
<!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>

1
data-viewer/index.cds Normal file
View File

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

13
data-viewer/package.json Normal file
View File

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

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

@@ -0,0 +1,58 @@
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
}

261
etc/bookshop.drawio.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 67 KiB

172
etc/dark.drawio.svg Normal file
View File

@@ -0,0 +1,172 @@
<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>

After

Width:  |  Height:  |  Size: 14 KiB

281
etc/incidents.drawio.svg Normal file
View File

@@ -0,0 +1,281 @@
<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>

After

Width:  |  Height:  |  Size: 24 KiB

4
etc/samples.drawio.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 40 KiB

View File

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

View File

@@ -0,0 +1,47 @@
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

@@ -0,0 +1,7 @@
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

@@ -0,0 +1,11 @@
# 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

@@ -0,0 +1,141 @@
{
"_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

@@ -1,4 +1,4 @@
using { AdminService, sap.capire.bookshop } from '../../db/schema';
using { AdminService } from '@capire/bookstore';
using from '../common'; // to help UI linter get the complete annotations
////////////////////////////////////////////////////////////////////////////
@@ -40,27 +40,6 @@ annotate AdminService.Books with @(
}
);
annotate AdminService.Authors with @(
UI: {
HeaderInfo: {
Description: {Value: lifetime}
},
Facets: [
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Books}', Target: 'books/@UI.LineItem'},
],
FieldGroup#Details: {
Data: [
{Value: placeOfBirth},
{Value: placeOfDeath},
{Value: dateOfBirth},
{Value: dateOfDeath},
{Value: age, Label: '{i18n>Age}'},
]
},
}
);
////////////////////////////////////////////////////////////
@@ -85,11 +64,15 @@ annotate AdminService.Books.texts with @(
// Add Value Help for Locales
annotate AdminService.Books.texts {
locale @ValueList:{entity:'Languages',type:#fixed}
locale @(
ValueList.entity:'Languages', Common.ValueListWithFixedValues, //show as drop down, not a dialog
)
}
// In addition we need to expose Languages and Books.texts through AdminService
// In addition we need to expose Languages through AdminService as a target for ValueList
using { sap } from '@sap/cds/common';
extend service AdminService {
entity Languages as projection on sap.common.Languages;
entity Books.texts as projection on bookshop.Books.texts;
@readonly 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) {
"use strict";
return AppComponent.extend("admin.Component", {
return AppComponent.extend("books.Component", {
metadata: { manifest: "json" }
});
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,14 @@
{
"_version": "1.8.0",
"_version": "1.28.0",
"sap.app": {
"id": "bookshop",
"type": "application",
"title": "Browse Books",
"description": "Sample Application",
"i18n": "i18n/i18n.properties",
"applicationVersion": {
"version": "1.0.0"
},
"dataSources": {
"CatalogService": {
"uri": "/browse/",
@@ -15,14 +18,43 @@
}
}
},
"-sourceTemplate": {
"sourceTemplate": {
"id": "ui5template.basicSAPUI5ApplicationProject",
"-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": {
"dependencies": {
"minUI5Version": "1.81.0",
"libs": {
"sap.fe.templates": {}
}
@@ -68,6 +100,7 @@
"options": {
"settings": {
"entitySet": "Books",
"initialLoad": true,
"navigation": {
"Books": {
"detail": {
@@ -97,7 +130,12 @@
},
"sap.ui": {
"technology": "UI5",
"fullWidth": false
"fullWidth": false,
"deviceTypes":{
"desktop": true,
"tablet": true,
"phone": true
}
},
"sap.fiori": {
"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/bookshop';
using { sap.capire.bookshop as my } from '@capire/bookstore';
using { sap.common } from '@capire/common';
////////////////////////////////////////////////////////////////////////////
@@ -10,39 +10,52 @@ using { sap.common } from '@capire/common';
// Books Lists
//
annotate my.Books with @(
Common.SemanticKey: [title],
UI: {
Identification: [{Value:title}],
SelectionFields: [ ID, author_ID, price, currency_code ],
LineItem: [
{Value: ID},
{Value: title},
{Value: author.name, Label:'{i18n>Author}'},
{Value: genre.name},
{Value: stock},
{Value: price},
{Value: currency.symbol, Label:' '},
]
}
Common.SemanticKey : [ID],
UI : {
Identification : [{Value : title}],
SelectionFields : [
ID,
author_ID,
price,
currency_code
],
LineItem : [
{
Value : ID,
Label : '{i18n>Title}'
},
{
Value : author.ID,
Label : '{i18n>Author}'
},
{Value : genre.name},
{Value : stock},
{Value : price},
{
Value : currency.symbol,
Label : ' '
},
]
}
) {
author @ValueList.entity:'Authors';
ID @Common: {
SemanticObject : 'Books',
Text: title,
TextArrangement : #TextOnly
};
author @ValueList.entity : 'Authors';
};
////////////////////////////////////////////////////////////////////////////
//
// Books Details
//
annotate my.Books with @(
UI: {
HeaderInfo: {
TypeName: '{i18n>Book}',
TypeNamePlural: '{i18n>Books}',
Title: {Value: title},
Description: {Value: author.name}
},
}
);
annotate my.Books with @(UI : {HeaderInfo : {
TypeName : '{i18n>Book}',
TypeNamePlural : '{i18n>Books}',
Title : {Value : title},
Description : {Value : author.name}
}, });
////////////////////////////////////////////////////////////////////////////
@@ -50,13 +63,19 @@ annotate my.Books with @(
// Books Elements
//
annotate my.Books with {
ID @title:'{i18n>ID}' @UI.HiddenFilter;
title @title:'{i18n>Title}';
genre @title:'{i18n>Genre}' @Common: { Text: genre.name, TextArrangement: #TextOnly };
author @title:'{i18n>Author}' @Common: { Text: author.name, TextArrangement: #TextOnly };
price @title:'{i18n>Price}' @Measures.ISOCurrency: currency_code;
stock @title:'{i18n>Stock}';
descr @UI.MultiLineText;
ID @title : '{i18n>ID}';
title @title : '{i18n>Title}';
genre @title : '{i18n>Genre}' @Common : {
Text : genre.name,
TextArrangement : #TextOnly
};
author @title : '{i18n>Author}' @Common : {
Text : author.name,
TextArrangement : #TextOnly
};
price @title : '{i18n>Price}' @Measures.ISOCurrency : currency_code;
stock @title : '{i18n>Stock}';
descr @UI.MultiLineText;
}
////////////////////////////////////////////////////////////////////////////
@@ -64,42 +83,45 @@ annotate my.Books with {
// Genres List
//
annotate my.Genres with @(
Common.SemanticKey: [name],
UI: {
SelectionFields: [ name ],
LineItem:[
{Value: name},
{Value: parent.name, Label: 'Main Genre'},
],
}
Common.SemanticKey : [name],
UI : {
SelectionFields : [name],
LineItem : [
{Value : name},
{
Value : parent.name,
Label : 'Main Genre'
},
],
}
);
////////////////////////////////////////////////////////////////////////////
//
// Genre Details
//
annotate my.Genres with @(
UI: {
Identification: [{Value:name}],
HeaderInfo: {
TypeName: '{i18n>Genre}',
TypeNamePlural: '{i18n>Genres}',
Title: {Value: name},
Description: {Value: ID}
},
Facets: [
{$Type: 'UI.ReferenceFacet', Label: '{i18n>SubGenres}', Target: 'children/@UI.LineItem'},
],
}
);
annotate my.Genres with @(UI : {
Identification : [{Value : name}],
HeaderInfo : {
TypeName : '{i18n>Genre}',
TypeNamePlural : '{i18n>Genres}',
Title : {Value : name},
Description : {Value : ID}
},
Facets : [{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>SubGenres}',
Target : 'children/@UI.LineItem'
}, ],
});
////////////////////////////////////////////////////////////////////////////
//
// Genres Elements
//
annotate my.Genres with {
ID @title: '{i18n>ID}';
name @title: '{i18n>Genre}';
ID @title : '{i18n>ID}';
name @title : '{i18n>Genre}';
}
////////////////////////////////////////////////////////////////////////////
@@ -107,38 +129,42 @@ annotate my.Genres with {
// Authors List
//
annotate my.Authors with @(
Common.SemanticKey: [name],
UI: {
Identification: [{Value:name}],
SelectionFields: [ name ],
LineItem:[
{Value: ID},
{Value: name},
{Value: dateOfBirth},
{Value: dateOfDeath},
{Value: placeOfBirth},
{Value: placeOfDeath},
],
}
);
Common.SemanticKey : [ID],
UI : {
Identification : [{Value : name}],
SelectionFields : [name],
LineItem : [
{Value : ID},
{Value : dateOfBirth},
{Value : dateOfDeath},
{Value : placeOfBirth},
{Value : placeOfDeath},
],
}
) {
ID @Common: {
SemanticObject : 'Authors',
Text: name,
TextArrangement : #TextOnly,
};
};
////////////////////////////////////////////////////////////////////////////
//
// Author Details
//
annotate my.Authors with @(
UI: {
HeaderInfo: {
TypeName: '{i18n>Author}',
TypeNamePlural: '{i18n>Authors}',
Title: {Value: name},
Description: {Value: dateOfBirth}
},
Facets: [
{$Type: 'UI.ReferenceFacet', Target: 'books/@UI.LineItem'},
],
}
);
annotate my.Authors with @(UI : {
HeaderInfo : {
TypeName : '{i18n>Author}',
TypeNamePlural : '{i18n>Authors}',
Title : {Value : name},
Description : {Value : dateOfBirth}
},
Facets : [{
$Type : 'UI.ReferenceFacet',
Target : 'books/@UI.LineItem'
}, ],
});
////////////////////////////////////////////////////////////////////////////
@@ -146,12 +172,12 @@ annotate my.Authors with @(
// Authors Elements
//
annotate my.Authors with {
ID @title:'{i18n>ID}' @UI.HiddenFilter;
name @title:'{i18n>Name}';
dateOfBirth @title:'{i18n>DateOfBirth}';
dateOfDeath @title:'{i18n>DateOfDeath}';
placeOfBirth @title:'{i18n>PlaceOfBirth}';
placeOfDeath @title:'{i18n>PlaceOfDeath}';
ID @title : '{i18n>ID}';
name @title : '{i18n>Name}';
dateOfBirth @title : '{i18n>DateOfBirth}';
dateOfDeath @title : '{i18n>DateOfDeath}';
placeOfBirth @title : '{i18n>PlaceOfBirth}';
placeOfDeath @title : '{i18n>PlaceOfDeath}';
}
////////////////////////////////////////////////////////////////////////////
@@ -159,99 +185,105 @@ annotate my.Authors with {
// Languages List
//
annotate common.Languages with @(
Common.SemanticKey: [code],
Identification: [{Value:code}],
UI: {
SelectionFields: [ name, descr ],
LineItem:[
{Value: code},
{Value: name},
],
}
Common.SemanticKey : [code],
Identification : [{Value : code}],
UI : {
SelectionFields : [
name,
descr
],
LineItem : [
{Value : code},
{Value : name},
],
}
);
////////////////////////////////////////////////////////////////////////////
//
// Language Details
//
annotate common.Languages with @(
UI: {
HeaderInfo: {
TypeName: '{i18n>Language}',
TypeNamePlural: '{i18n>Languages}',
Title: {Value: name},
Description: {Value: descr}
},
Facets: [
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
],
FieldGroup#Details: {
Data: [
{Value: code},
{Value: name},
{Value: descr}
]
},
}
);
annotate common.Languages with @(UI : {
HeaderInfo : {
TypeName : '{i18n>Language}',
TypeNamePlural : '{i18n>Languages}',
Title : {Value : name},
Description : {Value : descr}
},
Facets : [{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Details}',
Target : '@UI.FieldGroup#Details'
}, ],
FieldGroup #Details : {Data : [
{Value : code},
{Value : name},
{Value : descr}
]},
});
////////////////////////////////////////////////////////////////////////////
//
// Currencies List
//
annotate common.Currencies with @(
Common.SemanticKey: [code],
Identification: [{Value:code}],
UI: {
SelectionFields: [ name, descr ],
LineItem:[
{Value: descr},
{Value: symbol},
{Value: code},
],
}
Common.SemanticKey : [code],
Identification : [{Value : code}],
UI : {
SelectionFields : [
name,
descr
],
LineItem : [
{Value : descr},
{Value : symbol},
{Value : code},
],
}
);
////////////////////////////////////////////////////////////////////////////
//
// Currency Details
//
annotate common.Currencies with @(
UI: {
HeaderInfo: {
TypeName: '{i18n>Currency}',
TypeNamePlural: '{i18n>Currencies}',
Title: {Value: descr},
Description: {Value: code}
},
Facets: [
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Extended}', Target: '@UI.FieldGroup#Extended'},
],
FieldGroup#Details: {
Data: [
{Value: name},
{Value: symbol},
{Value: code},
{Value: descr}
]
},
FieldGroup#Extended: {
Data: [
{Value: numcode},
{Value: minor},
{Value: exponent}
]
},
}
);
annotate common.Currencies with @(UI : {
HeaderInfo : {
TypeName : '{i18n>Currency}',
TypeNamePlural : '{i18n>Currencies}',
Title : {Value : descr},
Description : {Value : code}
},
Facets : [
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Details}',
Target : '@UI.FieldGroup#Details'
},
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Extended}',
Target : '@UI.FieldGroup#Extended'
},
],
FieldGroup #Details : {Data : [
{Value : name},
{Value : symbol},
{Value : code},
{Value : descr}
]},
FieldGroup #Extended : {Data : [
{Value : numcode},
{Value : minor},
{Value : exponent}
]},
});
////////////////////////////////////////////////////////////////////////////
//
// Currencies Elements
//
annotate common.Currencies with {
numcode @title:'{i18n>NumCode}';
minor @title:'{i18n>MinorUnit}';
exponent @title:'{i18n>Exponent}';
numcode @title : '{i18n>NumCode}';
minor @title : '{i18n>MinorUnit}';
exponent @title : '{i18n>Exponent}';
}

View File

@@ -10,32 +10,7 @@
<script>
window["sap-ushell-config"] = {
defaultRenderer: "fiori2",
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"
}
}
applications: {}
};
</script>

View File

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

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
using { sap.capire.bookshop } from '@capire/bookshop';
// Forward-declare calculated fields to be filled in database-specific ways
// TODO find a better way to have 'default' fields that still can be overwritten.
extend bookshop.Authors with {
virtual age: Integer;
virtual lifetime: String;
}

View File

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

View File

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

View File

@@ -1,23 +1 @@
const cds = require ('@sap/cds')
module.exports = cds.server
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'))
// Swagger UI - see https://cap.cloud.sap/docs/advanced/openapi
if (process.env.NODE_ENV !== 'production') {
const cds_swagger = require ('cds-swagger-ui-express')
cds.once ('bootstrap', app => app.use (cds_swagger()) )
}
// -----------------------------------------------------------------------
// 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)))
module.exports = require('@capire/bookstore/server.js')

View File

@@ -1,25 +0,0 @@
////////////////////////////////////////////////////////////////////////////
//
// 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
}

28
gdpr/.cdsrc.json Normal file
View File

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

1
gdpr/.env Normal file
View File

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

4
gdpr/.etc/deploy.sh Normal file
View File

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

7
gdpr/.etc/undeploy.sh Normal file
View File

@@ -0,0 +1,7 @@
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

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

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

317
gdpr/app/fiori.cds Normal file
View File

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

56
gdpr/db/data-privacy.cds Normal file
View File

@@ -0,0 +1,56 @@
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

@@ -0,0 +1,3 @@
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

@@ -0,0 +1,3 @@
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

@@ -0,0 +1,4 @@
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

@@ -0,0 +1,3 @@
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

30
gdpr/db/schema.cds Normal file
View File

@@ -0,0 +1,30 @@
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;
};

136
gdpr/db/src/.hdiconfig Normal file
View File

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

31
gdpr/manifest.yml Normal file
View File

@@ -0,0 +1,31 @@
---
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

49
gdpr/package.json Normal file
View File

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

35
gdpr/readme.md Normal file
View File

@@ -0,0 +1,35 @@
# 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

@@ -0,0 +1,20 @@
---
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

43
gdpr/srv/auth.js Normal file
View File

@@ -0,0 +1,43 @@
/*
* 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()
}

10
gdpr/srv/gdpr-service.cds Normal file
View File

@@ -0,0 +1,10 @@
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;
}

24
gdpr/srv/pdm-service.cds Normal file
View File

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

26
gdpr/srv/server.js Normal file
View File

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

14
gdpr/xs-security.json Normal file
View File

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

15
hello/README.md Normal file
View File

@@ -0,0 +1,15 @@
# 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

@@ -3,12 +3,15 @@
"version": "1.0.0",
"scripts": {
"test": "npx jest --silent",
"watch": "cds serve world.cds",
"watch:ts": "cds-ts 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": "^26.0.23",
"@types/node": "^15.12.0",
"@types/jest": "^27.0.2",
"@types/node": "^16.11.6",
"ts-jest": "^27.0.2",
"typescript": "^4.3.5"
},
@@ -25,5 +28,30 @@
}
}
}
},
"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"
}
}
}

7
hello/srv/world.ts Normal file
View File

@@ -0,0 +1,7 @@
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

@@ -2,7 +2,7 @@ process.env.CDS_TYPESCRIPT = 'true';
import * as cds from '@sap/cds';
//@ts-ignore
const {GET} = cds.test.in(__dirname,'..').run('serve', 'world.cds');
const {GET} = cds.test.in(__dirname,'../srv').run('serve', 'world.cds');
describe('Hello world!', () => {
afterAll(() => { delete process.env.CDS_TYPESCRIPT; });

View File

@@ -1,5 +0,0 @@
module.exports = class say {
hello(req: any) {
return `Hello ${req.data.to} from a typescript file!`
}
}

View File

@@ -10,7 +10,7 @@
using { OrdersService } from '../../srv/orders-service';
using { OrdersService } from '../srv/orders-service';
@odata.draft.enabled
@@ -68,16 +68,16 @@ annotate OrdersService.Orders with @(
annotate OrdersService.Orders_Items with @(
annotate OrdersService.Orders.Items with @(
UI: {
LineItem: [
{Value: product_ID, Label:'Product ID'},
{Value: title, Label:'Product Title'},
{Value: price, Label:'Unit Price'},
{Value: amount, Label:'Quantity'},
{Value: quantity, Label:'Quantity'},
],
Identification: [ //Is the main field group
{Value: amount, Label:'Amount'},
{Value: quantity, Label:'Quantity'},
{Value: title, Label:'Product'},
{Value: price, Label:'Unit Price'},
],
@@ -86,7 +86,7 @@ annotate OrdersService.Orders_Items with @(
],
},
) {
amount @(
quantity @(
Common.FieldControl: #Mandatory
);
};

View File

@@ -1,5 +0,0 @@
/*
This model controls what gets served to Fiori frontends...
*/
using from './orders/fiori-service';

View File

@@ -167,4 +167,4 @@
"registrationIds": [],
"archeType": "transactional"
}
}
}

View File

@@ -1,4 +1,4 @@
ID;up__ID;amount;product_ID;title;price
ID;up__ID;quantity;product_ID;title;price
58040e66-1dcd-4ffb-ab10-fdce32028b79;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;201;Wuthering Heights;11.11
64e718c9-ff99-47f1-8ca3-950c850777d4;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;271;Catweazle;15
e9641166-e050-4261-bfee-d1e797e6cb7f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Eleonora;28
1 ID up__ID amount quantity product_ID title price
2 58040e66-1dcd-4ffb-ab10-fdce32028b79 7e2f2640-6866-4dcf-8f4d-3027aa831cad 1 1 201 Wuthering Heights 11.11
3 64e718c9-ff99-47f1-8ca3-950c850777d4 7e2f2640-6866-4dcf-8f4d-3027aa831cad 1 1 271 Catweazle 15
4 e9641166-e050-4261-bfee-d1e797e6cb7f 64e718c9-ff99-47f1-8ca3-950c850777d4 2 2 252 Eleonora 28

View File

@@ -3,21 +3,22 @@ namespace sap.capire.orders;
entity Orders : cuid, managed {
OrderNo : String @title:'Order Number'; //> readable key
Items : Composition of many Orders_Items on Items.up_ = $self;
Items : Composition of many {
key ID : UUID;
product : Association to Products;
quantity : Integer;
title : String; //> intentionally replicated as snapshot from product.title
price : Double; //> materialized calculated field
};
buyer : User;
currency : Currency;
}
entity Orders_Items {
key ID : UUID;
up_ : Association to Orders;
product : Association to Products @assert.integrity:false; // REVISIT: this is a temporary workaround for a glitch in cds-runtime
amount : Integer;
title : String; //> intentionally replicated as snapshot from product.title
price : Double;
}
/** This is a stand-in for arbitrary ordered Products */
entity Products @(cds.persistence.skip:'always') {
key ID : String;
}
// this is to ensure we have filled-in currencies
using from '@capire/common';

View File

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

View File

@@ -3,34 +3,34 @@ class OrdersService extends cds.ApplicationService {
/** register custom handlers */
init(){
const { Orders_Items:OrderItems } = this.entities
const { 'Orders.Items':OrderItems } = this.entities
this.before ('UPDATE', 'Orders', async function(req) {
const { ID, Items } = req.data
if (Items) for (let { product_ID, amount } of Items) {
const { amount:before } = await cds.tx(req).run (
SELECT.one.from (OrderItems, oi => oi.amount) .where ({up__ID:ID, product_ID})
if (Items) for (let { product_ID, quantity } of Items) {
const { quantity:before } = await cds.tx(req).run (
SELECT.one.from (OrderItems, oi => oi.quantity) .where ({up__ID:ID, product_ID})
)
if (amount != before) await this.orderChanged (product_ID, amount-before)
if (quantity != before) await this.orderChanged (product_ID, quantity-before)
}
})
this.before ('DELETE', 'Orders', async function(req) {
const { ID } = req.data
const Items = await cds.tx(req).run (
SELECT.from (OrderItems, oi => { oi.product_ID, oi.amount }) .where ({up__ID:ID})
SELECT.from (OrderItems, oi => { oi.product_ID, oi.quantity }) .where ({up__ID:ID})
)
if (Items) await Promise.all (Items.map(it => this.orderChanged (it.product_ID, -it.amount)))
if (Items) await Promise.all (Items.map(it => this.orderChanged (it.product_ID, -it.quantity)))
})
return super.init()
}
/** order changed -> broadcast event */
orderChanged (product, deltaAmount) {
orderChanged (product, deltaQuantity) {
// Emit events to inform subscribers about changes in orders
console.log ('> emitting:', 'OrderChanged', { product, deltaAmount })
return this.emit ('OrderChanged', { product, deltaAmount })
console.log ('> emitting:', 'OrderChanged', { product, deltaQuantity })
return this.emit ('OrderChanged', { product, deltaQuantity })
}
}

26237
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,39 +5,47 @@
"repository": "https://github.com/sap-samples/cloud-cap-samples.git",
"author": "daniel.hutzel@sap.com",
"dependencies": {
"@capire/bookstore": "./bookstore",
"@capire/bookshop": "./bookshop",
"@capire/common": "./common",
"@capire/data-viewer": "./data-viewer",
"@capire/fiori": "./fiori",
"@capire/gdpr": "./gdpr",
"@capire/hello": "./hello",
"@capire/media": "./media",
"@capire/orders": "./orders",
"@capire/reviews": "./reviews",
"@sap/cds": "^5",
"express": "^4"
"@sap/cds": "^5.5.3"
},
"devDependencies": {
"cds-swagger-ui-express": "^0.2.0",
"chai": "^4.2.0",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"chai-subset": "^1.6.0",
"sqlite3": "^5.0.0"
"sqlite3": "npm:@mendix/sqlite3@^5"
},
"scripts": {
"cleanup": "rm -rf node_modules && rm -rf */node_modules && rm -rf */*/node_modules",
"registry": "node .registry/server.js",
"bookshop": "cds watch bookshop",
"fiori": "cds watch fiori",
"gdpr": "cds watch gdpr",
"hello": "cds watch hello",
"media": "cds watch media",
"mocha": "npx mocha || echo",
"jest": "npx jest",
"test": "npm run jest --silent && npm run test:hello",
"start": "cds watch fiori",
"test": "npm run jest -- --silent",
"test:hello": "cd hello && npm test"
},
"jest": {
"testEnvironment": "node",
"testMatch": ["**/*.test.js"]
"testTimeout": 20000,
"testMatch": [
"**/*.test.js"
]
},
"mocha": {
"recursive": true,
"parallel": true
},
"license": "SAP SAMPLE CODE LICENSE",

View File

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

View File

@@ -1,5 +1,5 @@
subject;rating;title;text
201;5;Intriguing;Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
201;4;Fascinating;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum.
207;2;What is this?;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius.
251;3;It's dark...;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse.
subject;rating;reviewer;title;text
201;5;bob;Intriguing;Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
201;4;bob;Fascinating;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum.
207;2;bob;What is this?;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius.
251;3;bob;It's dark...;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse.
1 subject rating reviewer title text
2 201 5 bob Intriguing Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
3 201 4 bob Fascinating Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum.
4 207 2 bob What is this? Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius.
5 251 3 bob It's dark... Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse.

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