Compare commits
195 Commits
temporal
...
audit-log-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08a3157f1d | ||
|
|
c9ecef4e21 | ||
|
|
46f1be4395 | ||
|
|
b932637400 | ||
|
|
3c6d49b88e | ||
|
|
f4f02f6fe5 | ||
|
|
68c2504fc4 | ||
|
|
51a8762738 | ||
|
|
ff533754b4 | ||
|
|
f6b4d534fc | ||
|
|
3c5107ba42 | ||
|
|
dbc561d2e3 | ||
|
|
f21488fd71 | ||
|
|
0bce8d9ce0 | ||
|
|
0f03ffd3eb | ||
|
|
bc03cae550 | ||
|
|
8197559065 | ||
|
|
6928ae907a | ||
|
|
7accf1ae23 | ||
|
|
2ffa1721b3 | ||
|
|
37811381e7 | ||
|
|
74f9d5cc1e | ||
|
|
7c0769e059 | ||
|
|
10dcd6aadd | ||
|
|
f8c23f4c54 | ||
|
|
ae6be30aa0 | ||
|
|
e6baa95f74 | ||
|
|
aa28804a51 | ||
|
|
ea0773071e | ||
|
|
1a71a6d28a | ||
|
|
50791bed80 | ||
|
|
d58a71af7f | ||
|
|
c7c979236e | ||
|
|
0675793c87 | ||
|
|
91bca922c4 | ||
|
|
30403d239a | ||
|
|
589e282288 | ||
|
|
a8345122ea | ||
|
|
788a101f41 | ||
|
|
379ddc9fb0 | ||
|
|
1fde0c928b | ||
|
|
fa854a2411 | ||
|
|
40b6acee6a | ||
|
|
a37f1b8e0c | ||
|
|
042ddec41c | ||
|
|
8d8aa6c2c9 | ||
|
|
756ed48ce3 | ||
|
|
19b680ab3d | ||
|
|
33380c0792 | ||
|
|
0d5b65d93f | ||
|
|
dcf5ec6068 | ||
|
|
fb41dd100b | ||
|
|
769aa2b4f5 | ||
|
|
f6c5e620c0 | ||
|
|
7013cb6296 | ||
|
|
6411ce4ea3 | ||
|
|
cd914a8d87 | ||
|
|
b35e92b5bc | ||
|
|
8350aff111 | ||
|
|
9aa604c5a0 | ||
|
|
f5cb53adf8 | ||
|
|
9fe16edc55 | ||
|
|
0093883f25 | ||
|
|
14024a4837 | ||
|
|
2e13b37096 | ||
|
|
7407b0aae3 | ||
|
|
152d088ff4 | ||
|
|
19c886d6dc | ||
|
|
575cffe70a | ||
|
|
03869e5896 | ||
|
|
597afeb3d8 | ||
|
|
d376e6e063 | ||
|
|
2a44cfa9dc | ||
|
|
7964c9aefd | ||
|
|
ce752097d0 | ||
|
|
e838f380c5 | ||
|
|
e89e012eba | ||
|
|
bf0f57efe5 | ||
|
|
40ea057f7f | ||
|
|
31d2f4c932 | ||
|
|
8cc2db7118 | ||
|
|
482b71e60b | ||
|
|
7536451e29 | ||
|
|
2703e8b999 | ||
|
|
ac2e4dd6f9 | ||
|
|
c718fa400d | ||
|
|
730da6e435 | ||
|
|
7d9b4f7a5d | ||
|
|
35d3708592 | ||
|
|
eaaf0d29a5 | ||
|
|
64fe700d1e | ||
|
|
5c3cec973e | ||
|
|
680a6ae68f | ||
|
|
366b0f8f9a | ||
|
|
b95df77b9a | ||
|
|
7f6b87171a | ||
|
|
21e74bbbfb | ||
|
|
102b15c3cd | ||
|
|
73db2e96bc | ||
|
|
7bb58ee2d5 | ||
|
|
27e82d16e0 | ||
|
|
ff9bbe6d8d | ||
|
|
5f1b7b8cbf | ||
|
|
0253300557 | ||
|
|
139d957495 | ||
|
|
404427237b | ||
|
|
5f89334403 | ||
|
|
e612fa97ea | ||
|
|
da2ea39466 | ||
|
|
117000df71 | ||
|
|
f3ffb69d3a | ||
|
|
f908484973 | ||
|
|
c4529f3cd7 | ||
|
|
0220400484 | ||
|
|
c1911b6e96 | ||
|
|
19083d156e | ||
|
|
48d547e6cd | ||
|
|
bae491a832 | ||
|
|
ca41a2141c | ||
|
|
08f409af73 | ||
|
|
efa60550fb | ||
|
|
f599206bf4 | ||
|
|
2f5d159428 | ||
|
|
2be3d50389 | ||
|
|
46b58f1b5c | ||
|
|
e87527cbcd | ||
|
|
091844219b | ||
|
|
de796e5a89 | ||
|
|
f988088412 | ||
|
|
c4a51ab719 | ||
|
|
f58376607a | ||
|
|
839048d87c | ||
|
|
3b3463f889 | ||
|
|
d3396304ec | ||
|
|
ae09caf7ad | ||
|
|
e1052c209b | ||
|
|
cd3aad59e1 | ||
|
|
bcbf65f95e | ||
|
|
1189374860 | ||
|
|
9f2eb8fa83 | ||
|
|
7b18e15e5b | ||
|
|
00bfc1507e | ||
|
|
47a58e7393 | ||
|
|
c04b972d27 | ||
|
|
85b9b9d3a3 | ||
|
|
3655eef971 | ||
|
|
87dd356204 | ||
|
|
a89ec34f7c | ||
|
|
444dcacb14 | ||
|
|
2011a1ca7a | ||
|
|
7e0c36ede5 | ||
|
|
e8d08d039e | ||
|
|
5e3258913e | ||
|
|
44e14926e4 | ||
|
|
592e945c28 | ||
|
|
5d5c787365 | ||
|
|
bb79def1d9 | ||
|
|
bd4a38bf54 | ||
|
|
abf18d74db | ||
|
|
f144354229 | ||
|
|
97670bbc84 | ||
|
|
82d6f54337 | ||
|
|
a4baee6e3b | ||
|
|
95b01e7d7a | ||
|
|
c2437fc419 | ||
|
|
9381459458 | ||
|
|
28a7c16895 | ||
|
|
c1141e6c87 | ||
|
|
6e686c6c8b | ||
|
|
1ccddb2155 | ||
|
|
7c8e52d5e9 | ||
|
|
0dc1dc198d | ||
|
|
20bfb5c5f7 | ||
|
|
be87f5617a | ||
|
|
3117df1282 | ||
|
|
be685437aa | ||
|
|
ff3801be71 | ||
|
|
702c3245bd | ||
|
|
14002300c5 | ||
|
|
ad417ec061 | ||
|
|
a6ec296129 | ||
|
|
1b10bbbfe4 | ||
|
|
aaa1b2d6c7 | ||
|
|
1cb169e886 | ||
|
|
9b9bb4c114 | ||
|
|
2536f36596 | ||
|
|
5e829ae7bb | ||
|
|
d265a385f8 | ||
|
|
80f469b5b6 | ||
|
|
8e8ae949df | ||
|
|
3d24dc491d | ||
|
|
f593265687 | ||
|
|
6d488b042c | ||
|
|
d5ef630743 | ||
|
|
b47c9d75df |
@@ -8,7 +8,7 @@
|
|||||||
"mocha": true
|
"mocha": true
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2018
|
"ecmaVersion": 2020
|
||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
"SELECT": true,
|
"SELECT": true,
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"require-atomic-updates": "off",
|
"require-atomic-updates": "off",
|
||||||
"require-await":"warn"
|
"require-await":"warn",
|
||||||
|
"no-unused-vars": ["warn", { "argsIgnorePattern": "_" }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
.github/workflows/node.js.yml
vendored
8
.github/workflows/node.js.yml
vendored
@@ -5,9 +5,9 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [ main ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x, 14.x]
|
node-version: [16.x, 14.x, 12.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -24,5 +24,5 @@ jobs:
|
|||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: npm install
|
- run: npm ci
|
||||||
- run: npm test
|
- run: npm test
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,3 +15,5 @@ default-env.json
|
|||||||
packages/messageBox
|
packages/messageBox
|
||||||
reviews/msg-box
|
reviews/msg-box
|
||||||
reviews/db/test.db
|
reviews/db/test.db
|
||||||
|
|
||||||
|
*.openapi3.json
|
||||||
|
|||||||
3
.npmrc
Normal file
3
.npmrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Ensure we always use public packages, i.e. avoid using local registries from ~/.npmrc
|
||||||
|
@sap:registry=https://registry.npmjs.org/
|
||||||
|
registry=https://registry.npmjs.org/
|
||||||
@@ -1,19 +1,25 @@
|
|||||||
const { exec } = require ('child_process')
|
const { exec } = require ('child_process')
|
||||||
|
const isWin = process.platform === 'win32'
|
||||||
const express = require ('express')
|
const express = require ('express')
|
||||||
const fs = require ('fs')
|
const fs = require ('fs')
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
const { PORT=4444 } = process.env
|
const { PORT=4444 } = process.env
|
||||||
const [,,port=PORT] = process.argv
|
const [,,port=PORT,scope='@capire'] = process.argv
|
||||||
process.chdir(__dirname)
|
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) => {
|
app.use('/-/:tarball', (req,res,next) => {
|
||||||
console.debug ('GET', req.params)
|
console.debug ('GET', req.params)
|
||||||
try {
|
try {
|
||||||
const { tarball } = req.params
|
const { tarball } = req.params
|
||||||
const [, pkg ] = /^capire-(\w+)/.exec(tarball)
|
const [, pkg ] = /^\w+-(\w+)/.exec(tarball)
|
||||||
fs.lstat(tarball,(err => {
|
fs.lstat(tarball,(err => {
|
||||||
if (err) exec(`npm pack ../${pkg}`,next)
|
if (err) console.debug (`npm pack ../${pkg}`)
|
||||||
|
if (err) exec(`npm pack ../${pkg}`,{cwd},next)
|
||||||
else next()
|
else next()
|
||||||
}))
|
}))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -25,12 +31,14 @@ app.use('/-/:tarball', (req,res,next) => {
|
|||||||
app.use('/-', express.static(__dirname))
|
app.use('/-', express.static(__dirname))
|
||||||
|
|
||||||
app.get('/*', (req,res)=>{
|
app.get('/*', (req,res)=>{
|
||||||
|
const urlRegex = /^\/(@\w+)\/(\w+)/
|
||||||
const url = decodeURIComponent(req.url)
|
const url = decodeURIComponent(req.url)
|
||||||
console.debug ('GET',url)
|
console.debug ('GET',url)
|
||||||
try {
|
try {
|
||||||
const [, capire, pkg ] = /^\/(@capire)\/(\w+)/.exec(url)
|
if (!urlRegex.test(url)) return res.sendStatus(404)
|
||||||
const package = require (`${capire}/${pkg}/package.json`)
|
const [, scpe, pkg ] = urlRegex.exec(url)
|
||||||
const tarball = `capire-${pkg}-${package.version}.tgz`
|
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
|
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
|
||||||
res.json({
|
res.json({
|
||||||
"name": package.name,
|
"name": package.name,
|
||||||
@@ -42,29 +50,30 @@ app.get('/*', (req,res)=>{
|
|||||||
"name": package.name,
|
"name": package.name,
|
||||||
"version": package.version,
|
"version": package.version,
|
||||||
"dist": {
|
"dist": {
|
||||||
"tarball": `http://localhost:${port}/-/${tarball}`
|
"tarball": `${server.url}/-/${tarball}`
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
if (e.code === 'MODULE_NOT_FOUND') return res.sendStatus(404)
|
||||||
res.sendStatus(404)
|
console.error(e); throw e
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.listen(port, ()=>{
|
const server = app.listen(port, ()=>{
|
||||||
console.log (`npm set @capire:registry=http://localhost:${port}`)
|
const url = server.url = `http://localhost:${server.address().port}`
|
||||||
console.log (`@capire registry listening on http://localhost:${port}`)
|
console.log (`npm set ${scope}:registry=${url}`)
|
||||||
exec(`npm set @capire:registry=http://localhost:${port}`)
|
exec(`npm set ${scope}:registry=${url}`)
|
||||||
|
console.log (`${scope} registry listening on ${url}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const _exit = ()=>{
|
const _exit = ()=>{
|
||||||
console.log ('\nnpm conf rm @capire:registry')
|
server.close()
|
||||||
exec('npm conf rm @capire:registry')
|
exec(`npm conf rm "${scope}:registry"`, ()=> { process.exit() })
|
||||||
exec('rm *.tgz')
|
|
||||||
process.exit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on ('SIGTERM',_exit)
|
process.on ('SIGTERM',_exit)
|
||||||
process.on ('SIGHUP',_exit)
|
process.on ('SIGHUP',_exit)
|
||||||
process.on ('SIGINT',_exit)
|
process.on ('SIGINT',_exit)
|
||||||
|
|||||||
@@ -68,26 +68,35 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/package.json",
|
"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.",
|
"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.",
|
||||||
"line": 17,
|
"selection": {
|
||||||
|
"start": {
|
||||||
|
"line": 41,
|
||||||
|
"character": 1
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 48,
|
||||||
|
"character": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
"title": "Configuration"
|
"title": "Configuration"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/package.json",
|
"file": "fiori/package.json",
|
||||||
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch fiori\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
|
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch fiori\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
|
||||||
"line": 28,
|
"line": 43,
|
||||||
"title": "Run with SQLite"
|
"title": "Run with SQLite"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/package.json",
|
"file": "fiori/package.json",
|
||||||
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd fiori; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd fiori; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
|
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd fiori; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd fiori; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
|
||||||
"line": 31,
|
"line": 46,
|
||||||
"title": "Run with SAP HANA"
|
"title": "Run with SAP HANA"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/test/requests.http",
|
"file": "fiori/test/requests.http",
|
||||||
"description": "### Add More Data\n\nOptionally you can add some `Authors` data by clicking on the _Send Request_ link (provided by the [REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension).",
|
"description": "### Add More Data\n\nOptionally you can add some `Authors` data by clicking on the _Send Request_ link (provided by the [REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension).",
|
||||||
"line": 68,
|
"line": 72,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
"line": 67,
|
"line": 67,
|
||||||
@@ -104,6 +113,5 @@
|
|||||||
"title": "Wrap-up",
|
"title": "Wrap-up",
|
||||||
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
|
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"ref": "master"
|
|
||||||
}
|
}
|
||||||
@@ -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).",
|
"description": "### Hello World!\n\nThis is a simplistic [Hello World](https://cap.cloud.sap/docs/get-started/hello-world) service using [CDS](https://cap.cloud.sap/docs/cds/) and [cds.services](https://cap.cloud.sap/docs/node.js/api#services-api).",
|
||||||
"line": 2,
|
"line": 2,
|
||||||
"selection": {
|
"selection": {
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "orders/db/schema.cds",
|
"file": "orders/db/schema.cds",
|
||||||
"description": "### 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,
|
"line": 1,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "reviews/db/schema.cds",
|
"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,
|
"line": 1,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
@@ -99,8 +99,12 @@
|
|||||||
"title": "Reviews"
|
"title": "Reviews"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/app/index.cds",
|
"title": "Bookstore",
|
||||||
"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",
|
"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,
|
"line": 1,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
@@ -117,14 +121,13 @@
|
|||||||
{
|
{
|
||||||
"file": "package.json",
|
"file": "package.json",
|
||||||
"description": "### All-in-one Monorepo\n\nEach sample sub directory essentially is a standard npm package, some with standard npm dependencies to other samples. The root folder's [package.json](package.json) has local links to the sub folders, such that an `npm install` populates a local `node_modules` folder acts like a local npm registry to the individual sample packages.\n",
|
"description": "### All-in-one Monorepo\n\nEach sample sub directory essentially is a standard npm package, some with standard npm dependencies to other samples. The root folder's [package.json](package.json) has local links to the sub folders, such that an `npm install` populates a local `node_modules` folder acts like a local npm registry to the individual sample packages.\n",
|
||||||
"line": 8,
|
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
"line": 8,
|
"line": 8,
|
||||||
"character": 1
|
"character": 1
|
||||||
},
|
},
|
||||||
"end": {
|
"end": {
|
||||||
"line": 15,
|
"line": 16,
|
||||||
"character": 1
|
"character": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -14,4 +14,5 @@
|
|||||||
"**/odata-v4/okra/**"
|
"**/odata-v4/okra/**"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"mochaExplorer.parallel": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Find here a collection of samples for the [SAP Cloud Application Programming Mod
|
|||||||
|
|
||||||
### Download
|
### Download
|
||||||
|
|
||||||
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/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
|
```sh
|
||||||
git clone https://github.com/sap-samples/cloud-cap-samples samples
|
git clone https://github.com/sap-samples/cloud-cap-samples samples
|
||||||
@@ -83,4 +83,4 @@ In case you've a question, find a bug, or otherwise need support, use our [commu
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 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.
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
// Incorporate pre-build extensions from...
|
|
||||||
using from '@capire/common';
|
|
||||||
@@ -10,7 +10,7 @@ const books = new Vue ({
|
|||||||
data: {
|
data: {
|
||||||
list: [],
|
list: [],
|
||||||
book: undefined,
|
book: undefined,
|
||||||
order: { amount:1, succeeded:'', failed:'' }
|
order: { quantity:1, succeeded:'', failed:'' }
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -26,18 +26,18 @@ const books = new Vue ({
|
|||||||
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
|
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
|
||||||
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
|
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
|
||||||
Object.assign (book, res.data)
|
Object.assign (book, res.data)
|
||||||
books.order = { amount:1 }
|
books.order = { quantity:1 }
|
||||||
setTimeout (()=> $('form > input').focus(), 111)
|
setTimeout (()=> $('form > input').focus(), 111)
|
||||||
},
|
},
|
||||||
|
|
||||||
async submitOrder () {
|
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 {
|
try {
|
||||||
const res = await POST(`/submitOrder`, { amount, book: book.ID })
|
const res = await POST(`/submitOrder`, { quantity, book: book.ID })
|
||||||
book.stock = res.data.stock
|
book.stock = res.data.stock
|
||||||
books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` }
|
books.order = { quantity, succeeded: `Successfully ordered ${quantity} item(s).` }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
books.order = { amount, failed: e.response.data.error.message }
|
books.order = { quantity, failed: e.response.data.error.message }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<td>{{ book.author }}</td>
|
<td>{{ book.author }}</td>
|
||||||
<td>{{ book.genre.name }}</td>
|
<td>{{ book.genre.name }}</td>
|
||||||
<td class="rating-stars">
|
<td class="rating-stars">
|
||||||
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
|
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} ({{ book.numberOfReviews }})
|
||||||
</td>
|
</td>
|
||||||
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
{{ book.stock }} in stock
|
{{ book.stock }} in stock
|
||||||
</label>
|
</label>
|
||||||
<form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
|
<form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
|
||||||
<input type="number" v-model="order.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">
|
<input type="submit" value="Order:" class="muted-button">
|
||||||
</form>
|
</form>
|
||||||
<h4> {{ book.title }} </h4>
|
<h4> {{ book.title }} </h4>
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ ID;title;descr;author_ID;stock;price;currency_code;genre_ID
|
|||||||
207;Jane Eyre;"Jane Eyre /ɛər/ (originally published as Jane Eyre: An Autobiography) is a novel by English writer Charlotte Brontë, published under the pen name ""Currer Bell"", on 16 October 1847, by Smith, Elder & Co. of London. The first American edition was published the following year by Harper & Brothers of New York. Primarily a bildungsroman, Jane Eyre follows the experiences of its eponymous heroine, including her growth to adulthood and her love for Mr. Rochester, the brooding master of Thornfield Hall. The novel revolutionised prose fiction in that the focus on Jane's moral and spiritual development is told through an intimate, first-person narrative, where actions and events are coloured by a psychological intensity. The book contains elements of social criticism, with a strong sense of Christian morality at its core and is considered by many to be ahead of its time because of Jane's individualistic character and how the novel approaches the topics of class, sexuality, religion and feminism.";107;11;12.34;GBP;11
|
207;Jane Eyre;"Jane Eyre /ɛər/ (originally published as Jane Eyre: An Autobiography) is a novel by English writer Charlotte Brontë, published under the pen name ""Currer Bell"", on 16 October 1847, by Smith, Elder & Co. of London. The first American edition was published the following year by Harper & Brothers of New York. Primarily a bildungsroman, Jane Eyre follows the experiences of its eponymous heroine, including her growth to adulthood and her love for Mr. Rochester, the brooding master of Thornfield Hall. The novel revolutionised prose fiction in that the focus on Jane's moral and spiritual development is told through an intimate, first-person narrative, where actions and events are coloured by a psychological intensity. The book contains elements of social criticism, with a strong sense of Christian morality at its core and is considered by many to be ahead of its time because of Jane's individualistic character and how the novel approaches the topics of class, sexuality, religion and feminism.";107;11;12.34;GBP;11
|
||||||
251;The Raven;"""The Raven"" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word ""Nevermore"". The poem makes use of folk, mythological, religious, and classical references.";150;333;13.13;USD;16
|
251;The Raven;"""The Raven"" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word ""Nevermore"". The poem makes use of folk, mythological, religious, and classical references.";150;333;13.13;USD;16
|
||||||
252;Eleonora;"""Eleonora"" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively ""happy"" ending.";150;555;14;USD;16
|
252;Eleonora;"""Eleonora"" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively ""happy"" ending.";150;555;14;USD;16
|
||||||
271;Catweazle;Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.;170;22;15;EUR;13
|
271;Catweazle;Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.;170;22;150;JPY;13
|
||||||
|
24
bookshop/db/init.js
Normal file
24
bookshop/db/init.js
Normal 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' ],
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
exports.CatalogService = require('./srv/cat-service')
|
const { CatalogService } = require('./srv/cat-service')
|
||||||
|
module.exports = { CatalogService }
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "A simple self-contained bookshop service.",
|
"description": "A simple self-contained bookshop service.",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/common": "*",
|
|
||||||
"@sap/cds": "^5.0.4",
|
"@sap/cds": "^5.0.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"passport": "0.4.1"
|
"passport": "0.4.1"
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
using { sap.capire.bookshop as my } from '../db/schema';
|
using { sap.capire.bookshop as my } from '../db/schema';
|
||||||
service CatalogService @(path:'/browse') {
|
service CatalogService @(path:'/browse') {
|
||||||
|
|
||||||
@readonly entity Books as SELECT from my.Books { *,
|
/** For displaying lists of Books */
|
||||||
|
@readonly entity ListOfBooks as projection on Books
|
||||||
|
excluding { descr };
|
||||||
|
|
||||||
|
/** For display in details pages */
|
||||||
|
@readonly entity Books as projection on my.Books { *,
|
||||||
author.name as author
|
author.name as author
|
||||||
} excluding { createdBy, modifiedBy };
|
} excluding { createdBy, modifiedBy };
|
||||||
|
|
||||||
@readonly entity ListOfBooks as SELECT from Books
|
|
||||||
excluding { descr };
|
|
||||||
|
|
||||||
@requires: 'authenticated-user'
|
@requires: 'authenticated-user'
|
||||||
action submitOrder ( book: Books:ID, amount: Integer ) returns { stock: Integer };
|
action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer };
|
||||||
event OrderedBook : { book: Books:ID; amount: Integer; buyer: String };
|
event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
const cds = require('@sap/cds')
|
const cds = require('@sap/cds')
|
||||||
const { Books } = cds.entities ('sap.capire.bookshop')
|
|
||||||
|
|
||||||
class CatalogService extends cds.ApplicationService { init(){
|
class CatalogService extends cds.ApplicationService { init(){
|
||||||
|
|
||||||
|
const { Books } = cds.entities ('sap.capire.bookshop')
|
||||||
|
|
||||||
// Reduce stock of ordered books if available stock suffices
|
// Reduce stock of ordered books if available stock suffices
|
||||||
this.on ('submitOrder', async req => {
|
this.on ('submitOrder', async req => {
|
||||||
const {book,amount} = req.data
|
const {book,quantity} = req.data
|
||||||
let {stock} = await SELECT `stock` .from (Books,book)
|
if (quantity < 1) return req.reject (400,`quantity has to be 1 or more`)
|
||||||
if (stock >= amount) {
|
let b = await SELECT `stock` .from (Books,book)
|
||||||
await UPDATE (Books,book) .with (`stock -=`, amount)
|
if (!b) return req.error (404,`Book #${book} doesn't exist`)
|
||||||
await this.emit ('OrderedBook', { book, amount, buyer:req.user.id })
|
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 }
|
return { stock }
|
||||||
}
|
|
||||||
else return req.error (409,`${amount} exceeds stock for book #${book}`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add some discount for overstocked books
|
// Add some discount for overstocked books
|
||||||
|
|||||||
@@ -32,6 +32,19 @@ GET {{server}}/admin/Authors?
|
|||||||
# &sap-language=de
|
# &sap-language=de
|
||||||
Authorization: Basic alice:
|
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
|
# Create book
|
||||||
POST {{server}}/admin/Books
|
POST {{server}}/admin/Books
|
||||||
@@ -71,7 +84,7 @@ POST {{server}}/browse/submitOrder
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
{{me}}
|
{{me}}
|
||||||
|
|
||||||
{ "book":201, "amount":5 }
|
{ "book":201, "quantity":5 }
|
||||||
|
|
||||||
|
|
||||||
### ------------------------------------------------------------------------
|
### ------------------------------------------------------------------------
|
||||||
|
|||||||
2
bookstore/index.cds
Normal file
2
bookstore/index.cds
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
namespace sap.capire.bookshop; //> important for reflection
|
||||||
|
using from './srv/mashup';
|
||||||
34
bookstore/package.json
Normal file
34
bookstore/package.json
Normal 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
22
bookstore/server.js
Normal 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
38
bookstore/srv/mashup.cds
Normal 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';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Mashing up provided and required services...
|
// Mashing up bookshop services with required services...
|
||||||
//
|
//
|
||||||
module.exports = async()=>{ // called by server.js
|
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) => {
|
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
||||||
console.debug ('> delegating request to ReviewsService')
|
console.debug ('> delegating request to ReviewsService')
|
||||||
const [id] = req.params, { columns, limit } = req.query.SELECT
|
const [id] = req.params, { columns, limit } = req.query.SELECT
|
||||||
return ReviewsService.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
|
// Create an order with the OrdersService when CatalogService signals a new order
|
||||||
//
|
//
|
||||||
CatalogService.on ('OrderedBook', async (msg) => {
|
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 })
|
const { title, price } = await db.tx(msg).read (Books, book, b => { b.title, b.price })
|
||||||
return OrdersService.tx(msg).create ('Orders').entries({
|
return OrdersService.tx(msg).create ('Orders').entries({
|
||||||
OrderNo: 'Order at '+ (new Date).toLocaleString(),
|
OrderNo: 'Order at '+ (new Date).toLocaleString(),
|
||||||
Items: [{ product:{ID:`${book}`}, title, price, amount }],
|
Items: [{ product:{ID:`${book}`}, title, price, quantity }],
|
||||||
buyer, createdBy: buyer
|
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) => {
|
ReviewsService.on ('reviewed', (msg) => {
|
||||||
console.debug ('> received:', msg.event, msg.data)
|
console.debug ('> received:', msg.event, msg.data)
|
||||||
const { subject, rating } = msg.data
|
const { subject, count, rating } = msg.data
|
||||||
return UPDATE(Books,subject).with({rating})
|
return UPDATE(Books,subject).with({ numberOfReviews:count, rating })
|
||||||
// ^ Note: the framework will execute this and take care for db.tx
|
|
||||||
})
|
})
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -51,9 +50,9 @@ module.exports = async()=>{ // called by server.js
|
|||||||
//
|
//
|
||||||
OrdersService.on ('OrderChanged', (msg) => {
|
OrdersService.on ('OrderChanged', (msg) => {
|
||||||
console.debug ('> received:', msg.event, msg.data)
|
console.debug ('> received:', msg.event, msg.data)
|
||||||
const { product, deltaAmount } = msg.data
|
const { product, deltaQuantity } = msg.data
|
||||||
return UPDATE (Books) .where ('ID =', product)
|
return UPDATE (Books) .where ('ID =', product)
|
||||||
.and ('stock >=', deltaAmount)
|
.and ('stock >=', deltaQuantity)
|
||||||
.set ('stock -=', deltaAmount)
|
.set ('stock -=', deltaQuantity)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
10
bookstore/srv/swagger-ui.js
Normal file
10
bookstore/srv/swagger-ui.js
Normal 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
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
@bookshop = http://localhost:4004
|
@bookshop = http://localhost:4004
|
||||||
@reviews-service = {{bookshop}}/reviews
|
@reviews-service = {{bookshop}}/reviews
|
||||||
# Uncomment this when running a separate reviews service
|
# Uncomment this when running a separate reviews service
|
||||||
@reviews-service = http://localhost:4005/reviews
|
# @reviews-service = http://localhost:4005/reviews
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +38,11 @@ GET {{bookshop}}/browse/Books(201)?
|
|||||||
&$select=ID,title,rating
|
&$select=ID,title,rating
|
||||||
&$expand=reviews
|
&$expand=reviews
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
GET {{bookshop}}/browse/Books?
|
||||||
|
&$select=title,author&$expand=currency
|
||||||
|
Accept-Language: de
|
||||||
|
|
||||||
#################################################
|
#################################################
|
||||||
#
|
#
|
||||||
@@ -5,9 +5,11 @@ CAD;de;Kanadischer Dollar;Kanadischer Dollar
|
|||||||
AUD;de;Australischer Dollar;Australischer Dollar
|
AUD;de;Australischer Dollar;Australischer Dollar
|
||||||
GBP;de;Pfund;Britische Pfund
|
GBP;de;Pfund;Britische Pfund
|
||||||
ILS;de;Schekel;Israelische Schekel
|
ILS;de;Schekel;Israelische Schekel
|
||||||
|
JPY;de;Yen;Japanische Yen
|
||||||
EUR;fr;euro;de la Zone euro
|
EUR;fr;euro;de la Zone euro
|
||||||
USD;fr;dollar;dollar des États-Unis
|
USD;fr;dollar;dollar des États-Unis
|
||||||
CAD;fr;dollar canadien;dollar canadien
|
CAD;fr;dollar canadien;dollar canadien
|
||||||
AUD;fr;dollar australien;dollar australien
|
AUD;fr;dollar australien;dollar australien
|
||||||
GBP;fr;livre sterling;pound sterling
|
GBP;fr;livre sterling;pound sterling
|
||||||
ILS;fr;Shekel;shekel israelien
|
ILS;fr;Shekel;shekel israelien
|
||||||
|
JPY;fr;Yen;Yen japonais
|
||||||
|
@@ -20,7 +20,7 @@ extend sap.common.Currencies with {
|
|||||||
* annotate sap.common.Countries with @cds.persistence.skip:false;
|
* annotate sap.common.Countries with @cds.persistence.skip:false;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
context sap.common_countries {
|
context sap.common.countries {
|
||||||
|
|
||||||
extend sap.common.Countries {
|
extend sap.common.Countries {
|
||||||
regions : Composition of many Regions on regions._parent = $self.code;
|
regions : Composition of many Regions on regions._parent = $self.code;
|
||||||
|
|||||||
119
data-viewer/app/viewer/app.js
Normal file
119
data-viewer/app/viewer/app.js
Normal 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()
|
||||||
95
data-viewer/app/viewer/index.html
Normal file
95
data-viewer/app/viewer/index.html
Normal 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 – {{ 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 + ' – ' + 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
1
data-viewer/index.cds
Normal file
@@ -0,0 +1 @@
|
|||||||
|
using from './srv/data-service';
|
||||||
13
data-viewer/package.json
Normal file
13
data-viewer/package.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
29
data-viewer/srv/data-service.cds
Normal file
29
data-viewer/srv/data-service.cds
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
58
data-viewer/srv/data-service.js
Normal file
58
data-viewer/srv/data-service.js
Normal 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
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
172
etc/dark.drawio.svg
Normal 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="<mxfile><diagram id="QQJxv4aCTC7ZgE7HHOvM" name="Page-1">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=</diagram></mxfile>" 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
281
etc/incidents.drawio.svg
Normal 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="<mxfile><diagram id="QQJxv4aCTC7ZgE7HHOvM" name="Page-1">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==</diagram></mxfile>">
|
||||||
|
<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
4
etc/samples.drawio.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 40 KiB |
@@ -1,2 +0,0 @@
|
|||||||
# cds.requires.messaging.kind = file-based-messaging
|
|
||||||
PORT = 4004
|
|
||||||
47
fiori/app/admin-authors/fiori-service.cds
Normal file
47
fiori/app/admin-authors/fiori-service.cds
Normal 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; }
|
||||||
7
fiori/app/admin-authors/webapp/Component.js
Normal file
7
fiori/app/admin-authors/webapp/Component.js
Normal 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 */
|
||||||
11
fiori/app/admin-authors/webapp/i18n/i18n.properties
Normal file
11
fiori/app/admin-authors/webapp/i18n/i18n.properties
Normal 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
|
||||||
141
fiori/app/admin-authors/webapp/manifest.json
Normal file
141
fiori/app/admin-authors/webapp/manifest.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using { AdminService } from '../../db/schema';
|
using { AdminService } from '@capire/bookstore';
|
||||||
|
using from '../common'; // to help UI linter get the complete annotations
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
@@ -39,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}'},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
@@ -70,7 +50,7 @@ annotate AdminService.Authors with @(
|
|||||||
annotate sap.capire.bookshop.Books with @fiori.draft.enabled;
|
annotate sap.capire.bookshop.Books with @fiori.draft.enabled;
|
||||||
annotate AdminService.Books with @odata.draft.enabled;
|
annotate AdminService.Books with @odata.draft.enabled;
|
||||||
|
|
||||||
annotate AdminService.Books_texts with @(
|
annotate AdminService.Books.texts with @(
|
||||||
UI: {
|
UI: {
|
||||||
Identification: [{Value:title}],
|
Identification: [{Value:title}],
|
||||||
SelectionFields: [ locale, title ],
|
SelectionFields: [ locale, title ],
|
||||||
@@ -83,11 +63,16 @@ annotate AdminService.Books_texts with @(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Add Value Help for Locales
|
// Add Value Help for Locales
|
||||||
annotate AdminService.Books_texts {
|
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 through AdminService
|
// In addition we need to expose Languages through AdminService as a target for ValueList
|
||||||
using { sap } from '@sap/cds/common';
|
using { sap } from '@sap/cds/common';
|
||||||
extend service AdminService {
|
extend service AdminService {
|
||||||
entity Languages as projection on sap.common.Languages;
|
@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; }
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
||||||
"use strict";
|
"use strict";
|
||||||
return AppComponent.extend("admin.Component", {
|
return AppComponent.extend("books.Component", {
|
||||||
metadata: { manifest: "json" }
|
metadata: { manifest: "json" }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_version": "1.8.0",
|
"_version": "1.8.0",
|
||||||
"sap.app": {
|
"sap.app": {
|
||||||
"id": "admin",
|
"id": "books",
|
||||||
"type": "application",
|
"type": "application",
|
||||||
"title": "Manage Books",
|
"title": "Manage Books",
|
||||||
"description": "Sample Application",
|
"description": "Sample Application",
|
||||||
@@ -73,6 +73,7 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"settings" : {
|
"settings" : {
|
||||||
"entitySet" : "Books",
|
"entitySet" : "Books",
|
||||||
|
"initialLoad": true,
|
||||||
"navigation" : {
|
"navigation" : {
|
||||||
"Books" : {
|
"Books" : {
|
||||||
"detail" : {
|
"detail" : {
|
||||||
142
fiori/app/appconfig/fioriSandboxConfig.json
Normal file
142
fiori/app/appconfig/fioriSandboxConfig.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<head>
|
|
||||||
<meta http-equiv="refresh" content="0;url=bookshop/index.html">
|
|
||||||
</head>
|
|
||||||
@@ -1,50 +1,60 @@
|
|||||||
using CatalogService from '@capire/bookshop';
|
using CatalogService from '@capire/bookstore';
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Books Object Page
|
// Books Object Page
|
||||||
//
|
//
|
||||||
annotate CatalogService.Books with @(
|
annotate CatalogService.Books with @(UI : {
|
||||||
UI: {
|
HeaderInfo : {
|
||||||
HeaderInfo: {
|
TypeName : 'Book',
|
||||||
TypeName: 'Book',
|
TypeNamePlural : 'Books',
|
||||||
TypeNamePlural: 'Books',
|
Description : {Value : author}
|
||||||
Description: {Value: author}
|
|
||||||
},
|
},
|
||||||
HeaderFacets: [
|
HeaderFacets : [{
|
||||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Description}', Target: '@UI.FieldGroup#Descr'},
|
$Type : 'UI.ReferenceFacet',
|
||||||
],
|
Label : '{i18n>Description}',
|
||||||
Facets: [
|
Target : '@UI.FieldGroup#Descr'
|
||||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Price'},
|
}, ],
|
||||||
],
|
Facets : [{
|
||||||
FieldGroup#Descr: {
|
$Type : 'UI.ReferenceFacet',
|
||||||
Data: [
|
Label : '{i18n>Details}',
|
||||||
{Value: descr},
|
Target : '@UI.FieldGroup#Price'
|
||||||
]
|
}, ],
|
||||||
|
FieldGroup #Descr : {Data : [{Value : descr}, ]},
|
||||||
|
FieldGroup #Price : {Data : [
|
||||||
|
{Value : price},
|
||||||
|
{
|
||||||
|
Value : currency.symbol,
|
||||||
|
Label : '{i18n>Currency}'
|
||||||
},
|
},
|
||||||
FieldGroup#Price: {
|
]},
|
||||||
Data: [
|
});
|
||||||
{Value: price},
|
|
||||||
{Value: currency.symbol, Label: '{i18n>Currency}'},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Books Object Page
|
// Books Object Page
|
||||||
//
|
//
|
||||||
annotate CatalogService.Books with @(
|
annotate CatalogService.Books with @(UI : {
|
||||||
UI: {
|
SelectionFields : [
|
||||||
SelectionFields: [ ID, price, currency_code ],
|
ID,
|
||||||
LineItem: [
|
price,
|
||||||
{Value: title},
|
currency_code
|
||||||
{Value: author, Label:'{i18n>Author}'},
|
],
|
||||||
{Value: genre.name},
|
LineItem : [
|
||||||
{Value: price},
|
{
|
||||||
{Value: currency.symbol, Label:' '},
|
Value : ID,
|
||||||
]
|
Label : '{i18n>Title}'
|
||||||
},
|
},
|
||||||
);
|
{
|
||||||
|
Value : author,
|
||||||
|
Label : '{i18n>Author}'
|
||||||
|
},
|
||||||
|
{Value : genre.name},
|
||||||
|
{Value : price},
|
||||||
|
{
|
||||||
|
Value : currency.symbol,
|
||||||
|
Label : ' '
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}, );
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
{
|
{
|
||||||
"_version": "1.8.0",
|
"_version": "1.28.0",
|
||||||
"sap.app": {
|
"sap.app": {
|
||||||
"id": "bookshop",
|
"id": "bookshop",
|
||||||
"type": "application",
|
"type": "application",
|
||||||
"title": "Browse Books",
|
"title": "Browse Books",
|
||||||
"description": "Sample Application",
|
"description": "Sample Application",
|
||||||
"i18n": "i18n/i18n.properties",
|
"i18n": "i18n/i18n.properties",
|
||||||
|
"applicationVersion": {
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
"dataSources": {
|
"dataSources": {
|
||||||
"CatalogService": {
|
"CatalogService": {
|
||||||
"uri": "/browse/",
|
"uri": "/browse/",
|
||||||
@@ -15,14 +18,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"-sourceTemplate": {
|
"sourceTemplate": {
|
||||||
"id": "ui5template.basicSAPUI5ApplicationProject",
|
"id": "ui5template.basicSAPUI5ApplicationProject",
|
||||||
"-id": "ui5template.smartTemplate",
|
"-id": "ui5template.smartTemplate",
|
||||||
"-version": "1.40.12"
|
"version": "1.40.12"
|
||||||
|
},
|
||||||
|
"crossNavigation": {
|
||||||
|
"inbounds": {
|
||||||
|
"intent1": {
|
||||||
|
"signature": {
|
||||||
|
"parameters": {
|
||||||
|
"Books.ID":{
|
||||||
|
"renameTo": "ID"
|
||||||
|
},
|
||||||
|
"Authors.books.ID": {
|
||||||
|
"renameTo": "ID"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalParameters": "ignored"
|
||||||
|
},
|
||||||
|
"semanticObject": "Books",
|
||||||
|
"action": "display",
|
||||||
|
"title": "{{appTitle}}",
|
||||||
|
"info": "{{appInfo}}",
|
||||||
|
"subTitle": "{{appSubTitle}}",
|
||||||
|
"icon": "sap-icon://course-book",
|
||||||
|
"indicatorDataSource": {
|
||||||
|
"dataSource": "CatalogService",
|
||||||
|
"path": "Books/$count",
|
||||||
|
"refresh": 1800
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sap.ui5": {
|
"sap.ui5": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"minUI5Version": "1.81.0",
|
||||||
"libs": {
|
"libs": {
|
||||||
"sap.fe.templates": {}
|
"sap.fe.templates": {}
|
||||||
}
|
}
|
||||||
@@ -68,6 +100,7 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"settings": {
|
"settings": {
|
||||||
"entitySet": "Books",
|
"entitySet": "Books",
|
||||||
|
"initialLoad": true,
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"Books": {
|
"Books": {
|
||||||
"detail": {
|
"detail": {
|
||||||
@@ -97,7 +130,12 @@
|
|||||||
},
|
},
|
||||||
"sap.ui": {
|
"sap.ui": {
|
||||||
"technology": "UI5",
|
"technology": "UI5",
|
||||||
"fullWidth": false
|
"fullWidth": false,
|
||||||
|
"deviceTypes":{
|
||||||
|
"desktop": true,
|
||||||
|
"tablet": true,
|
||||||
|
"phone": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sap.fiori": {
|
"sap.fiori": {
|
||||||
"registrationIds": [],
|
"registrationIds": [],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
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';
|
using { sap.common } from '@capire/common';
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -10,39 +10,52 @@ using { sap.common } from '@capire/common';
|
|||||||
// Books Lists
|
// Books Lists
|
||||||
//
|
//
|
||||||
annotate my.Books with @(
|
annotate my.Books with @(
|
||||||
Common.SemanticKey: [title],
|
Common.SemanticKey : [ID],
|
||||||
UI: {
|
UI : {
|
||||||
Identification: [{Value:title}],
|
Identification : [{Value : title}],
|
||||||
SelectionFields: [ ID, author_ID, price, currency_code ],
|
SelectionFields : [
|
||||||
LineItem: [
|
ID,
|
||||||
{Value: ID},
|
author_ID,
|
||||||
{Value: title},
|
price,
|
||||||
{Value: author.name, Label:'{i18n>Author}'},
|
currency_code
|
||||||
{Value: genre.name},
|
],
|
||||||
{Value: stock},
|
LineItem : [
|
||||||
{Value: price},
|
{
|
||||||
{Value: currency.symbol, Label:' '},
|
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
|
// Books Details
|
||||||
//
|
//
|
||||||
annotate my.Books with @(
|
annotate my.Books with @(UI : {HeaderInfo : {
|
||||||
UI: {
|
TypeName : '{i18n>Book}',
|
||||||
HeaderInfo: {
|
TypeNamePlural : '{i18n>Books}',
|
||||||
TypeName: '{i18n>Book}',
|
Title : {Value : title},
|
||||||
TypeNamePlural: '{i18n>Books}',
|
Description : {Value : author.name}
|
||||||
Title: {Value: title},
|
}, });
|
||||||
Description: {Value: author.name}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -50,12 +63,18 @@ annotate my.Books with @(
|
|||||||
// Books Elements
|
// Books Elements
|
||||||
//
|
//
|
||||||
annotate my.Books with {
|
annotate my.Books with {
|
||||||
ID @title:'{i18n>ID}' @UI.HiddenFilter;
|
ID @title : '{i18n>ID}';
|
||||||
title @title:'{i18n>Title}';
|
title @title : '{i18n>Title}';
|
||||||
genre @title:'{i18n>Genre}' @Common: { Text: genre.name, TextArrangement: #TextOnly };
|
genre @title : '{i18n>Genre}' @Common : {
|
||||||
author @title:'{i18n>Author}' @Common: { Text: author.name, TextArrangement: #TextOnly };
|
Text : genre.name,
|
||||||
price @title:'{i18n>Price}';
|
TextArrangement : #TextOnly
|
||||||
stock @title:'{i18n>Stock}';
|
};
|
||||||
|
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;
|
descr @UI.MultiLineText;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,12 +83,15 @@ annotate my.Books with {
|
|||||||
// Genres List
|
// Genres List
|
||||||
//
|
//
|
||||||
annotate my.Genres with @(
|
annotate my.Genres with @(
|
||||||
Common.SemanticKey: [name],
|
Common.SemanticKey : [name],
|
||||||
UI: {
|
UI : {
|
||||||
SelectionFields: [ name ],
|
SelectionFields : [name],
|
||||||
LineItem:[
|
LineItem : [
|
||||||
{Value: name},
|
{Value : name},
|
||||||
{Value: parent.name, Label: 'Main Genre'},
|
{
|
||||||
|
Value : parent.name,
|
||||||
|
Label : 'Main Genre'
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -78,28 +100,28 @@ annotate my.Genres with @(
|
|||||||
//
|
//
|
||||||
// Genre Details
|
// Genre Details
|
||||||
//
|
//
|
||||||
annotate my.Genres with @(
|
annotate my.Genres with @(UI : {
|
||||||
UI: {
|
Identification : [{Value : name}],
|
||||||
Identification: [{Value:name}],
|
HeaderInfo : {
|
||||||
HeaderInfo: {
|
TypeName : '{i18n>Genre}',
|
||||||
TypeName: '{i18n>Genre}',
|
TypeNamePlural : '{i18n>Genres}',
|
||||||
TypeNamePlural: '{i18n>Genres}',
|
Title : {Value : name},
|
||||||
Title: {Value: name},
|
Description : {Value : ID}
|
||||||
Description: {Value: ID}
|
|
||||||
},
|
},
|
||||||
Facets: [
|
Facets : [{
|
||||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>SubGenres}', Target: 'children/@UI.LineItem'},
|
$Type : 'UI.ReferenceFacet',
|
||||||
],
|
Label : '{i18n>SubGenres}',
|
||||||
}
|
Target : 'children/@UI.LineItem'
|
||||||
);
|
}, ],
|
||||||
|
});
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Genres Elements
|
// Genres Elements
|
||||||
//
|
//
|
||||||
annotate my.Genres with {
|
annotate my.Genres with {
|
||||||
ID @title: '{i18n>ID}';
|
ID @title : '{i18n>ID}';
|
||||||
name @title: '{i18n>Genre}';
|
name @title : '{i18n>Genre}';
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -107,38 +129,42 @@ annotate my.Genres with {
|
|||||||
// Authors List
|
// Authors List
|
||||||
//
|
//
|
||||||
annotate my.Authors with @(
|
annotate my.Authors with @(
|
||||||
Common.SemanticKey: [name],
|
Common.SemanticKey : [ID],
|
||||||
UI: {
|
UI : {
|
||||||
Identification: [{Value:name}],
|
Identification : [{Value : name}],
|
||||||
SelectionFields: [ name ],
|
SelectionFields : [name],
|
||||||
LineItem:[
|
LineItem : [
|
||||||
{Value: ID},
|
{Value : ID},
|
||||||
{Value: name},
|
{Value : dateOfBirth},
|
||||||
{Value: dateOfBirth},
|
{Value : dateOfDeath},
|
||||||
{Value: dateOfDeath},
|
{Value : placeOfBirth},
|
||||||
{Value: placeOfBirth},
|
{Value : placeOfDeath},
|
||||||
{Value: placeOfDeath},
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
);
|
) {
|
||||||
|
ID @Common: {
|
||||||
|
SemanticObject : 'Authors',
|
||||||
|
Text: name,
|
||||||
|
TextArrangement : #TextOnly,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Author Details
|
// Author Details
|
||||||
//
|
//
|
||||||
annotate my.Authors with @(
|
annotate my.Authors with @(UI : {
|
||||||
UI: {
|
HeaderInfo : {
|
||||||
HeaderInfo: {
|
TypeName : '{i18n>Author}',
|
||||||
TypeName: '{i18n>Author}',
|
TypeNamePlural : '{i18n>Authors}',
|
||||||
TypeNamePlural: '{i18n>Authors}',
|
Title : {Value : name},
|
||||||
Title: {Value: name},
|
Description : {Value : dateOfBirth}
|
||||||
Description: {Value: dateOfBirth}
|
|
||||||
},
|
},
|
||||||
Facets: [
|
Facets : [{
|
||||||
{$Type: 'UI.ReferenceFacet', Target: 'books/@UI.LineItem'},
|
$Type : 'UI.ReferenceFacet',
|
||||||
],
|
Target : 'books/@UI.LineItem'
|
||||||
}
|
}, ],
|
||||||
);
|
});
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -146,12 +172,12 @@ annotate my.Authors with @(
|
|||||||
// Authors Elements
|
// Authors Elements
|
||||||
//
|
//
|
||||||
annotate my.Authors with {
|
annotate my.Authors with {
|
||||||
ID @title:'{i18n>ID}' @UI.HiddenFilter;
|
ID @title : '{i18n>ID}';
|
||||||
name @title:'{i18n>Name}';
|
name @title : '{i18n>Name}';
|
||||||
dateOfBirth @title:'{i18n>DateOfBirth}';
|
dateOfBirth @title : '{i18n>DateOfBirth}';
|
||||||
dateOfDeath @title:'{i18n>DateOfDeath}';
|
dateOfDeath @title : '{i18n>DateOfDeath}';
|
||||||
placeOfBirth @title:'{i18n>PlaceOfBirth}';
|
placeOfBirth @title : '{i18n>PlaceOfBirth}';
|
||||||
placeOfDeath @title:'{i18n>PlaceOfDeath}';
|
placeOfDeath @title : '{i18n>PlaceOfDeath}';
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -159,13 +185,16 @@ annotate my.Authors with {
|
|||||||
// Languages List
|
// Languages List
|
||||||
//
|
//
|
||||||
annotate common.Languages with @(
|
annotate common.Languages with @(
|
||||||
Common.SemanticKey: [code],
|
Common.SemanticKey : [code],
|
||||||
Identification: [{Value:code}],
|
Identification : [{Value : code}],
|
||||||
UI: {
|
UI : {
|
||||||
SelectionFields: [ name, descr ],
|
SelectionFields : [
|
||||||
LineItem:[
|
name,
|
||||||
{Value: code},
|
descr
|
||||||
{Value: name},
|
],
|
||||||
|
LineItem : [
|
||||||
|
{Value : code},
|
||||||
|
{Value : name},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -174,40 +203,41 @@ annotate common.Languages with @(
|
|||||||
//
|
//
|
||||||
// Language Details
|
// Language Details
|
||||||
//
|
//
|
||||||
annotate common.Languages with @(
|
annotate common.Languages with @(UI : {
|
||||||
UI: {
|
HeaderInfo : {
|
||||||
HeaderInfo: {
|
TypeName : '{i18n>Language}',
|
||||||
TypeName: '{i18n>Language}',
|
TypeNamePlural : '{i18n>Languages}',
|
||||||
TypeNamePlural: '{i18n>Languages}',
|
Title : {Value : name},
|
||||||
Title: {Value: name},
|
Description : {Value : descr}
|
||||||
Description: {Value: descr}
|
|
||||||
},
|
},
|
||||||
Facets: [
|
Facets : [{
|
||||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
$Type : 'UI.ReferenceFacet',
|
||||||
],
|
Label : '{i18n>Details}',
|
||||||
FieldGroup#Details: {
|
Target : '@UI.FieldGroup#Details'
|
||||||
Data: [
|
}, ],
|
||||||
{Value: code},
|
FieldGroup #Details : {Data : [
|
||||||
{Value: name},
|
{Value : code},
|
||||||
{Value: descr}
|
{Value : name},
|
||||||
]
|
{Value : descr}
|
||||||
},
|
]},
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Currencies List
|
// Currencies List
|
||||||
//
|
//
|
||||||
annotate common.Currencies with @(
|
annotate common.Currencies with @(
|
||||||
Common.SemanticKey: [code],
|
Common.SemanticKey : [code],
|
||||||
Identification: [{Value:code}],
|
Identification : [{Value : code}],
|
||||||
UI: {
|
UI : {
|
||||||
SelectionFields: [ name, descr ],
|
SelectionFields : [
|
||||||
LineItem:[
|
name,
|
||||||
{Value: descr},
|
descr
|
||||||
{Value: symbol},
|
],
|
||||||
{Value: code},
|
LineItem : [
|
||||||
|
{Value : descr},
|
||||||
|
{Value : symbol},
|
||||||
|
{Value : code},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -216,42 +246,44 @@ annotate common.Currencies with @(
|
|||||||
//
|
//
|
||||||
// Currency Details
|
// Currency Details
|
||||||
//
|
//
|
||||||
annotate common.Currencies with @(
|
annotate common.Currencies with @(UI : {
|
||||||
UI: {
|
HeaderInfo : {
|
||||||
HeaderInfo: {
|
TypeName : '{i18n>Currency}',
|
||||||
TypeName: '{i18n>Currency}',
|
TypeNamePlural : '{i18n>Currencies}',
|
||||||
TypeNamePlural: '{i18n>Currencies}',
|
Title : {Value : descr},
|
||||||
Title: {Value: descr},
|
Description : {Value : code}
|
||||||
Description: {Value: code}
|
},
|
||||||
|
Facets : [
|
||||||
|
{
|
||||||
|
$Type : 'UI.ReferenceFacet',
|
||||||
|
Label : '{i18n>Details}',
|
||||||
|
Target : '@UI.FieldGroup#Details'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$Type : 'UI.ReferenceFacet',
|
||||||
|
Label : '{i18n>Extended}',
|
||||||
|
Target : '@UI.FieldGroup#Extended'
|
||||||
},
|
},
|
||||||
Facets: [
|
|
||||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
|
||||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Extended}', Target: '@UI.FieldGroup#Extended'},
|
|
||||||
],
|
],
|
||||||
FieldGroup#Details: {
|
FieldGroup #Details : {Data : [
|
||||||
Data: [
|
{Value : name},
|
||||||
{Value: name},
|
{Value : symbol},
|
||||||
{Value: symbol},
|
{Value : code},
|
||||||
{Value: code},
|
{Value : descr}
|
||||||
{Value: descr}
|
]},
|
||||||
]
|
FieldGroup #Extended : {Data : [
|
||||||
},
|
{Value : numcode},
|
||||||
FieldGroup#Extended: {
|
{Value : minor},
|
||||||
Data: [
|
{Value : exponent}
|
||||||
{Value: numcode},
|
]},
|
||||||
{Value: minor},
|
});
|
||||||
{Value: exponent}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Currencies Elements
|
// Currencies Elements
|
||||||
//
|
//
|
||||||
annotate common.Currencies with {
|
annotate common.Currencies with {
|
||||||
numcode @title:'{i18n>NumCode}';
|
numcode @title : '{i18n>NumCode}';
|
||||||
minor @title:'{i18n>MinorUnit}';
|
minor @title : '{i18n>MinorUnit}';
|
||||||
exponent @title:'{i18n>Exponent}';
|
exponent @title : '{i18n>Exponent}';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,32 +10,7 @@
|
|||||||
<script>
|
<script>
|
||||||
window["sap-ushell-config"] = {
|
window["sap-ushell-config"] = {
|
||||||
defaultRenderer: "fiori2",
|
defaultRenderer: "fiori2",
|
||||||
applications: {
|
applications: {}
|
||||||
"browse-books": {
|
|
||||||
title: "Browse Books",
|
|
||||||
description: "w/ SAP Fiori Elements",
|
|
||||||
additionalInformation: "SAPUI5.Component=bookshop",
|
|
||||||
applicationType : "URL",
|
|
||||||
url: "/browse/webapp",
|
|
||||||
navigationMode: "embedded"
|
|
||||||
},
|
|
||||||
"manage-books": {
|
|
||||||
title: "Manage Books",
|
|
||||||
description: "w/ SAP Fiori Elements",
|
|
||||||
additionalInformation: "SAPUI5.Component=admin",
|
|
||||||
applicationType : "URL",
|
|
||||||
url: "/admin/webapp",
|
|
||||||
navigationMode: "embedded"
|
|
||||||
},
|
|
||||||
"manage-orders": {
|
|
||||||
title: "Manage Orders",
|
|
||||||
description: "w/ SAP Fiori Elements",
|
|
||||||
additionalInformation: "SAPUI5.Component=orders",
|
|
||||||
applicationType : "URL",
|
|
||||||
url: "/orders/webapp",
|
|
||||||
navigationMode: "embedded"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<head>
|
|
||||||
<meta http-equiv="refresh" content="0;url=reviews/index.html">
|
|
||||||
</head>
|
|
||||||
@@ -2,11 +2,8 @@
|
|||||||
This model controls what gets served to Fiori frontends...
|
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 './browse/fiori-service';
|
||||||
using from './common';
|
using from './common';
|
||||||
|
using from '@capire/bookstore/srv/mashup';
|
||||||
using from '@capire/common';
|
|
||||||
|
|
||||||
// only works in case of embedded orders service
|
|
||||||
using from '@capire/orders/app/orders/fiori-service';
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Add Author.age and .lifetime with a DB-specific function
|
// Add Author.age and .lifetime with a DB-specific function
|
||||||
//
|
//
|
||||||
|
|
||||||
using { AdminService } from '../schema';
|
using { AdminService } from '@capire/bookshop';
|
||||||
|
|
||||||
extend projection AdminService.Authors with {
|
extend projection AdminService.Authors with {
|
||||||
YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer,
|
YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// Add Author.age and .lifetime with a DB-specific function
|
// Add Author.age and .lifetime with a DB-specific function
|
||||||
//
|
//
|
||||||
|
|
||||||
using { AdminService } from '../schema';
|
using { AdminService } from '@capire/bookshop';
|
||||||
|
|
||||||
extend projection AdminService.Authors with {
|
extend projection AdminService.Authors with {
|
||||||
strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer,
|
strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer,
|
||||||
|
|||||||
@@ -2,11 +2,8 @@
|
|||||||
"name": "@capire/fiori",
|
"name": "@capire/fiori",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/bookshop": "*",
|
"@capire/bookstore": "*",
|
||||||
"@capire/reviews": "*",
|
"@sap/cds": "^5",
|
||||||
"@capire/orders": "*",
|
|
||||||
"@capire/common": "*",
|
|
||||||
"@sap/cds": ">=4",
|
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"passport": "^0.4.1"
|
"passport": "^0.4.1"
|
||||||
},
|
},
|
||||||
@@ -15,9 +12,6 @@
|
|||||||
"watch": "cds watch"
|
"watch": "cds watch"
|
||||||
},
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
"hana": {
|
|
||||||
"deploy-format": "hdbtable"
|
|
||||||
},
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"strategy": "dummy"
|
"strategy": "dummy"
|
||||||
@@ -30,14 +24,30 @@
|
|||||||
"kind": "odata",
|
"kind": "odata",
|
||||||
"model": "@capire/orders"
|
"model": "@capire/orders"
|
||||||
},
|
},
|
||||||
|
"messaging": {
|
||||||
|
"[production]": {
|
||||||
|
"kind": "enterprise-messaging"
|
||||||
|
},
|
||||||
|
"[development]": {
|
||||||
|
"kind": "file-based-messaging"
|
||||||
|
},
|
||||||
|
"[hybrid!]": {
|
||||||
|
"kind": "enterprise-messaging-shared"
|
||||||
|
}
|
||||||
|
},
|
||||||
"db": {
|
"db": {
|
||||||
"kind": "sql",
|
"kind": "sql"
|
||||||
|
},
|
||||||
|
"db-ext": {
|
||||||
"[development]": {
|
"[development]": {
|
||||||
"model": "db/sqlite"
|
"model": "db/sqlite"
|
||||||
},
|
},
|
||||||
"[production]": {
|
"[production]": {
|
||||||
"model": "db/hana"
|
"model": "db/hana"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"hana": {
|
||||||
|
"deploy-format": "hdbtable"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1 @@
|
|||||||
const cds = require ('@sap/cds')
|
module.exports = require('@capire/bookstore/server.js')
|
||||||
|
|
||||||
cds.once('bootstrap',(app)=>{
|
|
||||||
app.use ('/orders/webapp', _from('@capire/orders/app/orders/webapp/manifest.json'))
|
|
||||||
app.use ('/bookshop', _from('@capire/bookshop/app/vue/index.html'))
|
|
||||||
app.use ('/reviews', _from('@capire/reviews/app/vue/index.html'))
|
|
||||||
})
|
|
||||||
|
|
||||||
cds.once('served', require('./srv/mashup'))
|
|
||||||
|
|
||||||
module.exports = cds.server
|
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Helper for serving static content from npm-installed packages
|
|
||||||
const {static} = require('express')
|
|
||||||
const {dirname} = require('path')
|
|
||||||
const _from = target => static (dirname (require.resolve(target)))
|
|
||||||
|
|||||||
@@ -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
28
gdpr/.cdsrc.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
4
gdpr/.etc/deploy.sh
Normal file
4
gdpr/.etc/deploy.sh
Normal 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
7
gdpr/.etc/undeploy.sh
Normal 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
|
||||||
16
gdpr/.pdm/pdm-binding-config.json
Normal file
16
gdpr/.pdm/pdm-binding-config.json
Normal 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"
|
||||||
|
}]
|
||||||
|
}
|
||||||
8
gdpr/.pdm/pdm-instance-config.json
Normal file
8
gdpr/.pdm/pdm-instance-config.json
Normal 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
317
gdpr/app/fiori.cds
Normal 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
56
gdpr/db/data-privacy.cds
Normal 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
|
||||||
|
};
|
||||||
3
gdpr/db/data/sap.capire.gdpr-CustomerPostalAddresses.csv
Normal file
3
gdpr/db/data/sap.capire.gdpr-CustomerPostalAddresses.csv
Normal 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
|
||||||
|
3
gdpr/db/data/sap.capire.gdpr-Customers.csv
Normal file
3
gdpr/db/data/sap.capire.gdpr-Customers.csv
Normal 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
|
||||||
|
4
gdpr/db/data/sap.capire.orders-Orders.Items.csv
Normal file
4
gdpr/db/data/sap.capire.orders-Orders.Items.csv
Normal 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
|
||||||
|
3
gdpr/db/data/sap.capire.orders-Orders.csv
Normal file
3
gdpr/db/data/sap.capire.orders-Orders.csv
Normal 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
|
||||||
|
30
gdpr/db/schema.cds
Normal file
30
gdpr/db/schema.cds
Normal 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
136
gdpr/db/src/.hdiconfig
Normal 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
31
gdpr/manifest.yml
Normal 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
49
gdpr/package.json
Normal 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
35
gdpr/readme.md
Normal 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`?
|
||||||
20
gdpr/services-manifest.yml
Normal file
20
gdpr/services-manifest.yml
Normal 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
43
gdpr/srv/auth.js
Normal 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
10
gdpr/srv/gdpr-service.cds
Normal 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
24
gdpr/srv/pdm-service.cds
Normal 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
26
gdpr/srv/server.js
Normal 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
14
gdpr/xs-security.json
Normal 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
15
hello/README.md
Normal 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)
|
||||||
@@ -2,6 +2,56 @@
|
|||||||
"name": "@capire/hello-world",
|
"name": "@capire/hello-world",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "cds serve world.cds"
|
"test": "npx jest --silent",
|
||||||
|
"start": "cds serve srv/world.cds",
|
||||||
|
"start:ts": "cds-ts serve srv/world.cds"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sap/cds": "^5.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/jest": "^27.0.2",
|
||||||
|
"@types/node": "^16.11.6",
|
||||||
|
"ts-jest": "^27.0.2",
|
||||||
|
"typescript": "^4.3.5"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"preset": "ts-jest",
|
||||||
|
"globals": {
|
||||||
|
"ts-jest": {
|
||||||
|
"diagnostics": {
|
||||||
|
"_comment": "see https://githubmemory.com/repo/kulshekhar/ts-jest/issues/2722",
|
||||||
|
"ignoreCodes": [
|
||||||
|
151001
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "eslint:recommended",
|
||||||
|
"env": {
|
||||||
|
"es2020": true,
|
||||||
|
"node": true,
|
||||||
|
"jest": true,
|
||||||
|
"mocha": true
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"SELECT": true,
|
||||||
|
"INSERT": true,
|
||||||
|
"UPDATE": true,
|
||||||
|
"DELETE": true,
|
||||||
|
"CREATE": true,
|
||||||
|
"DROP": true,
|
||||||
|
"CDL": true,
|
||||||
|
"CQL": true,
|
||||||
|
"CXL": true,
|
||||||
|
"cds": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-console": "off",
|
||||||
|
"require-atomic-updates": "off"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
7
hello/srv/world.ts
Normal file
7
hello/srv/world.ts
Normal 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!`
|
||||||
|
}
|
||||||
|
}
|
||||||
15
hello/test/hello-world-ts.test.ts
Normal file
15
hello/test/hello-world-ts.test.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
process.env.CDS_TYPESCRIPT = 'true';
|
||||||
|
import * as cds from '@sap/cds';
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
const {GET} = cds.test.in(__dirname,'../srv').run('serve', 'world.cds');
|
||||||
|
|
||||||
|
describe('Hello world!', () => {
|
||||||
|
afterAll(() => { delete process.env.CDS_TYPESCRIPT; });
|
||||||
|
|
||||||
|
it('should say hello with class impl from a typescript file', async () => {
|
||||||
|
const {data} = await GET`/say/hello(to='world')`
|
||||||
|
expect(data.value).toMatch(/Hello world.*typescript.*/i)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
using { OrdersService } from '../../srv/orders-service';
|
using { OrdersService } from '../srv/orders-service';
|
||||||
|
|
||||||
|
|
||||||
@odata.draft.enabled
|
@odata.draft.enabled
|
||||||
@@ -68,16 +68,16 @@ annotate OrdersService.Orders with @(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
annotate OrdersService.Orders_Items with @(
|
annotate OrdersService.Orders.Items with @(
|
||||||
UI: {
|
UI: {
|
||||||
LineItem: [
|
LineItem: [
|
||||||
{Value: product_ID, Label:'Product ID'},
|
{Value: product_ID, Label:'Product ID'},
|
||||||
{Value: title, Label:'Product Title'},
|
{Value: title, Label:'Product Title'},
|
||||||
{Value: price, Label:'Unit Price'},
|
{Value: price, Label:'Unit Price'},
|
||||||
{Value: amount, Label:'Quantity'},
|
{Value: quantity, Label:'Quantity'},
|
||||||
],
|
],
|
||||||
Identification: [ //Is the main field group
|
Identification: [ //Is the main field group
|
||||||
{Value: amount, Label:'Amount'},
|
{Value: quantity, Label:'Quantity'},
|
||||||
{Value: title, Label:'Product'},
|
{Value: title, Label:'Product'},
|
||||||
{Value: price, Label:'Unit Price'},
|
{Value: price, Label:'Unit Price'},
|
||||||
],
|
],
|
||||||
@@ -86,7 +86,7 @@ annotate OrdersService.Orders_Items with @(
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
amount @(
|
quantity @(
|
||||||
Common.FieldControl: #Mandatory
|
Common.FieldControl: #Mandatory
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
/*
|
|
||||||
This model controls what gets served to Fiori frontends...
|
|
||||||
*/
|
|
||||||
|
|
||||||
using from './orders/fiori-service';
|
|
||||||
@@ -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
|
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
|
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
|
e9641166-e050-4261-bfee-d1e797e6cb7f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Eleonora;28
|
||||||
|
@@ -3,21 +3,22 @@ namespace sap.capire.orders;
|
|||||||
|
|
||||||
entity Orders : cuid, managed {
|
entity Orders : cuid, managed {
|
||||||
OrderNo : String @title:'Order Number'; //> readable key
|
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;
|
buyer : User;
|
||||||
currency : Currency;
|
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 */
|
/** This is a stand-in for arbitrary ordered Products */
|
||||||
entity Products @(cds.persistence.skip:'always') {
|
entity Products @(cds.persistence.skip:'always') {
|
||||||
key ID : String;
|
key ID : String;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// this is to ensure we have filled-in currencies
|
||||||
|
using from '@capire/common';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "@capire/orders",
|
"name": "@capire/orders",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sap/cds": ">=4.3.0"
|
"@capire/common": "*",
|
||||||
|
"@sap/cds": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,34 +3,34 @@ class OrdersService extends cds.ApplicationService {
|
|||||||
|
|
||||||
/** register custom handlers */
|
/** register custom handlers */
|
||||||
init(){
|
init(){
|
||||||
const { Orders_Items:OrderItems } = this.entities
|
const { 'Orders.Items':OrderItems } = this.entities
|
||||||
|
|
||||||
this.before ('UPDATE', 'Orders', async function(req) {
|
this.before ('UPDATE', 'Orders', async function(req) {
|
||||||
const { ID, Items } = req.data
|
const { ID, Items } = req.data
|
||||||
if (Items) for (let { product_ID, amount } of Items) {
|
if (Items) for (let { product_ID, quantity } of Items) {
|
||||||
const { amount:before } = await cds.tx(req).run (
|
const { quantity:before } = await cds.tx(req).run (
|
||||||
SELECT.one.from (OrderItems, oi => oi.amount) .where ({up__ID:ID, product_ID})
|
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) {
|
this.before ('DELETE', 'Orders', async function(req) {
|
||||||
const { ID } = req.data
|
const { ID } = req.data
|
||||||
const Items = await cds.tx(req).run (
|
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()
|
return super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** order changed -> broadcast event */
|
/** order changed -> broadcast event */
|
||||||
orderChanged (product, deltaAmount) {
|
orderChanged (product, deltaQuantity) {
|
||||||
// Emit events to inform subscribers about changes in orders
|
// Emit events to inform subscribers about changes in orders
|
||||||
console.log ('> emitting:', 'OrderChanged', { product, deltaAmount })
|
console.log ('> emitting:', 'OrderChanged', { product, deltaQuantity })
|
||||||
return this.emit ('OrderChanged', { product, deltaAmount })
|
return this.emit ('OrderChanged', { product, deltaQuantity })
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
12683
package-lock.json
generated
Normal file
12683
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user