Compare commits
256 Commits
suppliers-
...
sandbox_vm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f1af693c9 | ||
|
|
d2ab511d6d | ||
|
|
6c27228d62 | ||
|
|
44880c7745 | ||
|
|
9c2a7598f2 | ||
|
|
9617e576f0 | ||
|
|
c3c9dae80d | ||
|
|
2b6d4c625e | ||
|
|
7d46db42ec | ||
|
|
df57a9d8d0 | ||
|
|
2d92b851f1 | ||
|
|
e157fc8a92 | ||
|
|
3b69a80975 | ||
|
|
b80ba7445d | ||
|
|
b733643f2a | ||
|
|
bf317bc2c9 | ||
|
|
bbebff4066 | ||
|
|
eb75394044 | ||
|
|
b8f65b687f | ||
|
|
c49c9691bd | ||
|
|
7e4bc0985e | ||
|
|
912271338d | ||
|
|
bbf1194a09 | ||
|
|
529c431518 | ||
|
|
3f85676edd | ||
|
|
959c07cee3 | ||
|
|
8cc09fde38 | ||
|
|
bb55c432c9 | ||
|
|
5fc86d45ad | ||
|
|
e86d0dc3a2 | ||
|
|
a661fb2d45 | ||
|
|
bfe0c831d6 | ||
|
|
cad615a662 | ||
|
|
7101c58920 | ||
|
|
07dc1e88b3 | ||
|
|
6f8d74dc9a | ||
|
|
ec57c5ea48 | ||
|
|
c040a47279 | ||
|
|
30bfd70c49 | ||
|
|
6fb9581cf1 | ||
|
|
e87d6cdfc5 | ||
|
|
29ea2bc2da | ||
|
|
12574271ac | ||
|
|
17b5cc1ad2 | ||
|
|
26ca7f54ad | ||
|
|
573e78253d | ||
|
|
a7511ed677 | ||
|
|
f1289b436b | ||
|
|
984ea2133b | ||
|
|
179d301acb | ||
|
|
bbf20b1ca3 | ||
|
|
f4f02f6fe5 | ||
|
|
68c2504fc4 | ||
|
|
51a8762738 | ||
|
|
ff533754b4 | ||
|
|
f6b4d534fc | ||
|
|
3c5107ba42 | ||
|
|
dbc561d2e3 | ||
|
|
f21488fd71 | ||
|
|
0bce8d9ce0 | ||
|
|
0f03ffd3eb | ||
|
|
bc03cae550 | ||
|
|
8197559065 | ||
|
|
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 | ||
|
|
5f160c0927 | ||
|
|
0c4658a3ef | ||
|
|
33cd70065a | ||
|
|
450577d5a7 | ||
|
|
5a23ba0f76 | ||
|
|
064ce32b8a | ||
|
|
2c064d0200 | ||
|
|
51ee6d167c | ||
|
|
049345a39c | ||
|
|
a7e0cf61a3 | ||
|
|
1b0462f52e | ||
|
|
2bae86079e | ||
|
|
dcab44ab68 | ||
|
|
9f0629eef1 | ||
|
|
f6c938457f | ||
|
|
2975038696 | ||
|
|
5e829ae7bb | ||
|
|
d265a385f8 | ||
|
|
80f469b5b6 | ||
|
|
8e8ae949df | ||
|
|
3d24dc491d | ||
|
|
f593265687 | ||
|
|
6d488b042c | ||
|
|
d5ef630743 | ||
|
|
b47c9d75df |
@@ -8,7 +8,7 @@
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"globals": {
|
||||
"SELECT": true,
|
||||
@@ -22,6 +22,7 @@
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"require-atomic-updates": "off",
|
||||
"require-await":"warn"
|
||||
"require-await":"warn",
|
||||
"no-unused-vars": ["warn", { "argsIgnorePattern": "_" }]
|
||||
}
|
||||
}
|
||||
|
||||
8
.github/dependabot.yml
vendored
Normal file
8
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
versioning-strategy: increase-if-necessary
|
||||
schedule:
|
||||
interval: daily
|
||||
8
.github/workflows/node.js.yml
vendored
8
.github/workflows/node.js.yml
vendored
@@ -5,9 +5,9 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x, 14.x]
|
||||
node-version: [16.x, 14.x, 12.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -24,5 +24,5 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,3 +15,5 @@ default-env.json
|
||||
packages/messageBox
|
||||
reviews/msg-box
|
||||
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,26 @@
|
||||
const { exec } = require ('child_process')
|
||||
const isWin = process.platform === 'win32'
|
||||
const express = require ('express')
|
||||
const fs = require ('fs')
|
||||
const app = express()
|
||||
|
||||
const { PORT=4444 } = process.env
|
||||
const [,,port=PORT] = process.argv
|
||||
process.chdir(__dirname)
|
||||
const [,,port=PORT,scope='@capire'] = process.argv
|
||||
const cwd = __dirname
|
||||
|
||||
// clean up on start (exit handler might not complete on Windows)
|
||||
exec(isWin ? 'del *.tgz' : 'rm *.tgz', {cwd})
|
||||
|
||||
|
||||
app.use('/-/:tarball', (req,res,next) => {
|
||||
console.debug ('GET', req.params)
|
||||
try {
|
||||
const { tarball } = req.params
|
||||
const [, pkg ] = /^capire-(\w+)/.exec(tarball)
|
||||
const pkgFull = tarball.substring(0, tarball.lastIndexOf('-'))
|
||||
const [, pkg ] = /^\w+-(.+)/.exec(pkgFull)
|
||||
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()
|
||||
}))
|
||||
} catch (e) {
|
||||
@@ -25,12 +32,14 @@ app.use('/-/:tarball', (req,res,next) => {
|
||||
app.use('/-', express.static(__dirname))
|
||||
|
||||
app.get('/*', (req,res)=>{
|
||||
const urlRegex = /^\/(@[\w-]+)\/(.+)/
|
||||
const url = decodeURIComponent(req.url)
|
||||
console.debug ('GET',url)
|
||||
try {
|
||||
const [, capire, pkg ] = /^\/(@capire)\/(\w+)/.exec(url)
|
||||
const package = require (`${capire}/${pkg}/package.json`)
|
||||
const tarball = `capire-${pkg}-${package.version}.tgz`
|
||||
if (!urlRegex.test(url)) return res.sendStatus(404)
|
||||
const [, scpe, pkg ] = urlRegex.exec(url)
|
||||
const package = require (`${scpe}/${pkg}/package.json`)
|
||||
const tarball = `${scpe.slice(1)}-${pkg}-${package.version}.tgz`
|
||||
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
|
||||
res.json({
|
||||
"name": package.name,
|
||||
@@ -42,29 +51,30 @@ app.get('/*', (req,res)=>{
|
||||
"name": package.name,
|
||||
"version": package.version,
|
||||
"dist": {
|
||||
"tarball": `http://localhost:${port}/-/${tarball}`
|
||||
"tarball": `${server.url}/-/${tarball}`
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
res.sendStatus(404)
|
||||
if (e.code === 'MODULE_NOT_FOUND') return res.sendStatus(404)
|
||||
console.error(e); throw e
|
||||
}
|
||||
})
|
||||
|
||||
app.listen(port, ()=>{
|
||||
console.log (`npm set @capire:registry=http://localhost:${port}`)
|
||||
console.log (`@capire registry listening on http://localhost:${port}`)
|
||||
exec(`npm set @capire:registry=http://localhost:${port}`)
|
||||
const server = app.listen(port, ()=>{
|
||||
const url = server.url = `http://localhost:${server.address().port}`
|
||||
console.log (`npm set ${scope}:registry=${url}`)
|
||||
exec(`npm set ${scope}:registry=${url}`)
|
||||
console.log (`${scope} registry listening on ${url}`)
|
||||
})
|
||||
|
||||
|
||||
const _exit = ()=>{
|
||||
console.log ('\nnpm conf rm @capire:registry')
|
||||
exec('npm conf rm @capire:registry')
|
||||
exec('rm *.tgz')
|
||||
process.exit()
|
||||
server.close()
|
||||
exec(`npm conf rm "${scope}:registry"`, ()=> { process.exit() })
|
||||
}
|
||||
|
||||
process.on ('SIGTERM',_exit)
|
||||
process.on ('SIGHUP',_exit)
|
||||
process.on ('SIGINT',_exit)
|
||||
|
||||
@@ -68,26 +68,35 @@
|
||||
},
|
||||
{
|
||||
"file": "fiori/package.json",
|
||||
"description": "#### Configuration\n\nThe `cds` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile.",
|
||||
"line": 17,
|
||||
"description": "#### Configuration\n\nThe `cds.requires` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\n\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile. `db-ext` is a pseudo datasource, its name doesn't matter.\n\nSee [`cds.resolve`](https://cap.cloud.sap/docs/node.js/cds-compile#cds-resolve) to learn more about how models are found.",
|
||||
"selection": {
|
||||
"start": {
|
||||
"line": 41,
|
||||
"character": 1
|
||||
},
|
||||
"end": {
|
||||
"line": 48,
|
||||
"character": 1
|
||||
}
|
||||
},
|
||||
"title": "Configuration"
|
||||
},
|
||||
{
|
||||
"file": "fiori/package.json",
|
||||
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch fiori\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
|
||||
"line": 28,
|
||||
"line": 43,
|
||||
"title": "Run with SQLite"
|
||||
},
|
||||
{
|
||||
"file": "fiori/package.json",
|
||||
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd fiori; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd fiori; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
|
||||
"line": 31,
|
||||
"line": 46,
|
||||
"title": "Run with SAP HANA"
|
||||
},
|
||||
{
|
||||
"file": "fiori/test/requests.http",
|
||||
"description": "### Add More Data\n\nOptionally you can add some `Authors` data by clicking on the _Send Request_ link (provided by the [REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension).",
|
||||
"line": 68,
|
||||
"line": 72,
|
||||
"selection": {
|
||||
"start": {
|
||||
"line": 67,
|
||||
@@ -104,6 +113,5 @@
|
||||
"title": "Wrap-up",
|
||||
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
|
||||
}
|
||||
],
|
||||
"ref": "master"
|
||||
]
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "hello/world.cds",
|
||||
"file": "hello/srv/world.cds",
|
||||
"description": "### Hello World!\n\nThis is a simplistic [Hello World](https://cap.cloud.sap/docs/get-started/hello-world) service using [CDS](https://cap.cloud.sap/docs/cds/) and [cds.services](https://cap.cloud.sap/docs/node.js/api#services-api).",
|
||||
"line": 2,
|
||||
"selection": {
|
||||
@@ -68,7 +68,7 @@
|
||||
},
|
||||
{
|
||||
"file": "orders/db/schema.cds",
|
||||
"description": "### Compositions and Serving Documents\n\nA standalone orders management service, demonstrating:\n- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with\n- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)\n",
|
||||
"description": "### Orders - Compositions and Serving Documents\n\nA standalone orders management service, demonstrating:\n- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with\n- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)\n",
|
||||
"line": 1,
|
||||
"selection": {
|
||||
"start": {
|
||||
@@ -84,7 +84,7 @@
|
||||
},
|
||||
{
|
||||
"file": "reviews/db/schema.cds",
|
||||
"description": "### More Modularity\n\nShows how to implement a modular service to manage product reviews, including:\n- Consuming other services synchronously and asynchronously\n- Serving requests synchronously\n- Emitting events asynchronously\n- Grow as you go, with:\n- Mocking app services\n- Running service meshes\n- Late-cut Micro Services\n- As well as managed data, input validations, and authorization\n",
|
||||
"description": "### Reviews - More Modularity\n\nShows how to implement a modular service to manage product reviews, including:\n- Consuming other services synchronously and asynchronously\n- Serving requests synchronously\n- Emitting events asynchronously\n- Grow as you go, with:\n- Mocking app services\n- Running service meshes\n- Late-cut Micro Services\n- As well as managed data, input validations, and authorization\n",
|
||||
"line": 1,
|
||||
"selection": {
|
||||
"start": {
|
||||
@@ -99,8 +99,12 @@
|
||||
"title": "Reviews"
|
||||
},
|
||||
{
|
||||
"file": "fiori/app/index.cds",
|
||||
"description": "### Annotations for SAP Fiori Elements\n\nA [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:\n - [@capire/bookshop](bookshop)\n - [@capire/reviews](reviews)\n - [@capire/orders](orders)\n - [@capire/common](common)\n\n[Adds a SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookshop, thereby introducing to:\n - [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files\n - Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)\n - Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)\n - Serving SAP Fiori apps locally\n\n[The Vue.js app](bookshop/app/vue) imported from bookshop is served as well.\n",
|
||||
"title": "Bookstore",
|
||||
"description": "### Bookstore - Reuse and UI\n\n- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/reuse-and-compose) these packages:\n - [@capire/bookshop](bookshop)\n - [@capire/reviews](reviews)\n - [@capire/orders](orders)\n - [@capire/common](common)\n- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well\n- [The Vue.js app](reviews/app/vue) imported from reviews is served as well\n- [The Fiori app](orders/app) imported from orders is served as well\n- [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)"
|
||||
},
|
||||
{
|
||||
"file": "fiori/app/services.cds",
|
||||
"description": "### Annotations for SAP Fiori Elements\n\n- [Adds an SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookstore, thereby introducing to:\n- [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files\n- Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)\n- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)\n- Serving SAP Fiori apps locally\n",
|
||||
"line": 1,
|
||||
"selection": {
|
||||
"start": {
|
||||
@@ -117,14 +121,13 @@
|
||||
{
|
||||
"file": "package.json",
|
||||
"description": "### All-in-one Monorepo\n\nEach sample sub directory essentially is a standard npm package, some with standard npm dependencies to other samples. The root folder's [package.json](package.json) has local links to the sub folders, such that an `npm install` populates a local `node_modules` folder acts like a local npm registry to the individual sample packages.\n",
|
||||
"line": 8,
|
||||
"selection": {
|
||||
"start": {
|
||||
"line": 8,
|
||||
"character": 1
|
||||
},
|
||||
"end": {
|
||||
"line": 15,
|
||||
"line": 16,
|
||||
"character": 1
|
||||
}
|
||||
},
|
||||
@@ -133,4 +136,4 @@
|
||||
],
|
||||
"isPrimary": true,
|
||||
"description": "Overview of CAP Samples for Node.js"
|
||||
}
|
||||
}
|
||||
37
.vscode/launch.json
vendored
37
.vscode/launch.json
vendored
@@ -4,28 +4,31 @@
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach by Process ID",
|
||||
"processId": "${command:PickProcess}",
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "pwa-node"
|
||||
},
|
||||
{
|
||||
"name": "bookshop",
|
||||
"command": "cds watch bookshop",
|
||||
"request": "launch",
|
||||
"command": "npx cds watch bookshop",
|
||||
"type": "node-terminal",
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
"request": "launch",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**",
|
||||
"**/cds/lib/lazy.js",
|
||||
"**/cds/lib/req/cls.js",
|
||||
"**/odata-v4/okra/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Fiori app",
|
||||
"command": "cds watch fiori",
|
||||
"request": "launch",
|
||||
"name": "Fiori App",
|
||||
"command": "npx cds watch fiori",
|
||||
"type": "node-terminal",
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
"request": "launch",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**",
|
||||
"**/cds/lib/lazy.js",
|
||||
"**/cds/lib/req/cls.js",
|
||||
"**/odata-v4/okra/**"
|
||||
]
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
@@ -33,7 +36,7 @@
|
||||
"type": "pickString",
|
||||
"id": "sample",
|
||||
"description": "Which sample do you want to start?",
|
||||
"options": ["bookshop", "fiori", "reviews", "reviews/test/bookshop"],
|
||||
"options": [ "bookshop", "fiori", "reviews", "reviews" ],
|
||||
"default": "bookshop"
|
||||
}
|
||||
]
|
||||
|
||||
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
@@ -1,6 +1,18 @@
|
||||
{
|
||||
"files.exclude": {
|
||||
".reuse/**": true,
|
||||
"**/.gitignore": true,
|
||||
"**/.vscode": true
|
||||
}
|
||||
"**/.vscode": true,
|
||||
"LICENSES/**": true
|
||||
},
|
||||
"debug.javascript.terminalOptions": {
|
||||
"skipFiles": [
|
||||
"<node_internals>/**",
|
||||
"**/node_modules/**",
|
||||
"**/cds/lib/lazy.js",
|
||||
"**/cds/lib/req/cls.js",
|
||||
"**/odata-v4/okra/**"
|
||||
]
|
||||
},
|
||||
"mochaExplorer.parallel": true
|
||||
}
|
||||
|
||||
343
LICENSE
343
LICENSE
@@ -1,208 +1,201 @@
|
||||
Apache License
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
Version 2.0, January 2004
|
||||
|
||||
http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION,
|
||||
AND DISTRIBUTION
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution
|
||||
as defined by Sections 1 through 9 of this document.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, "control" means (i) the power, direct
|
||||
or indirect, to cause the direction or management of such entity, whether
|
||||
by contract or otherwise, or (ii) ownership of fifty percent (50%) or more
|
||||
of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions
|
||||
granted by this License.
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation
|
||||
or translation of a Source form, including but not limited to compiled object
|
||||
code, generated documentation, and conversions to other media types.
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form,
|
||||
made available under the License, as indicated by a copyright notice that
|
||||
is included in or attached to the work (an example is provided in the Appendix
|
||||
below).
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form,
|
||||
that is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative
|
||||
Works shall not include works that remain separable from, or merely link (or
|
||||
bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative
|
||||
Works thereof, that is intentionally submitted to Licensor for inclusion in
|
||||
the Work by the copyright owner or by an individual or Legal Entity authorized
|
||||
to submit on behalf of the copyright owner. For the purposes of this definition,
|
||||
"submitted" means any form of electronic, verbal, or written communication
|
||||
sent to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor
|
||||
for the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as "Not a Contribution."
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently incorporated
|
||||
within the Work.
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this
|
||||
License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
|
||||
no-charge, royalty-free, irrevocable copyright license to reproduce, prepare
|
||||
Derivative Works of, publicly display, publicly perform, sublicense, and distribute
|
||||
the Work and such Derivative Works in Source or Object form.
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License,
|
||||
each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
|
||||
no-charge, royalty-free, irrevocable (except as stated in this section) patent
|
||||
license to make, have made, use, offer to sell, sell, import, and otherwise
|
||||
transfer the Work, where such license applies only to those patent claims
|
||||
licensable by such Contributor that are necessarily infringed by their Contribution(s)
|
||||
alone or by combination of their Contribution(s) with the Work to which such
|
||||
Contribution(s) was submitted. If You institute patent litigation against
|
||||
any entity (including a cross-claim or counterclaim in a lawsuit) alleging
|
||||
that the Work or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses granted to You
|
||||
under this License for that Work shall terminate as of the date such litigation
|
||||
is filed.
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or
|
||||
Derivative Works thereof in any medium, with or without modifications, and
|
||||
in Source or Object form, provided that You meet the following conditions:
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative Works a copy
|
||||
of this License; and
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices stating that
|
||||
You changed the files; and
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source
|
||||
form of the Work, excluding those notices that do not pertain to any part
|
||||
of the Derivative Works; and
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its distribution,
|
||||
then any Derivative Works that You distribute must include a readable copy
|
||||
of the attribution notices contained within such NOTICE file, excluding those
|
||||
notices that do not pertain to any part of the Derivative Works, in at least
|
||||
one of the following places: within a NOTICE text file distributed as part
|
||||
of the Derivative Works; within the Source form or documentation, if provided
|
||||
along with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works
|
||||
that You distribute, alongside or as an addendum to the NOTICE text from the
|
||||
Work, provided that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction,
|
||||
or distribution of Your modifications, or for any such Derivative Works as
|
||||
a whole, provided Your use, reproduction, and distribution of the Work otherwise
|
||||
complies with the conditions stated in this License.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any
|
||||
Contribution intentionally submitted for inclusion in the Work by You to the
|
||||
Licensor shall be under the terms and conditions of this License, without
|
||||
any additional terms or conditions. Notwithstanding the above, nothing herein
|
||||
shall supersede or modify the terms of any separate license agreement you
|
||||
may have executed with Licensor regarding such Contributions.
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names,
|
||||
trademarks, service marks, or product names of the Licensor, except as required
|
||||
for reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to
|
||||
in writing, Licensor provides the Work (and each Contributor provides its
|
||||
Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied, including, without limitation, any warranties
|
||||
or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness
|
||||
of using or redistributing the Work and assume any risks associated with Your
|
||||
exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether
|
||||
in tort (including negligence), contract, or otherwise, unless required by
|
||||
applicable law (such as deliberate and grossly negligent acts) or agreed to
|
||||
in writing, shall any Contributor be liable to You for damages, including
|
||||
any direct, indirect, special, incidental, or consequential damages of any
|
||||
character arising as a result of this License or out of the use or inability
|
||||
to use the Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all other commercial
|
||||
damages or losses), even if such Contributor has been advised of the possibility
|
||||
of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work
|
||||
or Derivative Works thereof, You may choose to offer, and charge a fee for,
|
||||
acceptance of support, warranty, indemnity, or other liability obligations
|
||||
and/or rights consistent with this License. However, in accepting such obligations,
|
||||
You may act only on Your own behalf and on Your sole responsibility, not on
|
||||
behalf of any other Contributor, and only if You agree to indemnify, defend,
|
||||
and hold each Contributor harmless for any liability incurred by, or claims
|
||||
asserted against, such Contributor by reason of your accepting any such warranty
|
||||
or additional liability. END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets "[]" replaced with your own identifying
|
||||
information. (Don't include the brackets!) The text should be enclosed in
|
||||
the appropriate comment syntax for the file format. We also recommend that
|
||||
a file or class name and description of purpose be included on the same "printed
|
||||
page" as the copyright notice for easier identification within third-party
|
||||
archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
||||
See the License for the specific language governing permissions and
|
||||
|
||||
limitations under the License.
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
Find here a collection of samples for the [SAP Cloud Application Programming Model](https://cap.cloud.sap) organized in a simplistic [monorepo setup](samples.md#all-in-one-monorepo). → See [**Overview** of contained samples](samples.md)
|
||||
|
||||

|
||||
<!--[](https://api.reuse.software/info/github.com/SAP-samples/cloud-cap-samples)-->
|
||||
|
||||
[](https://api.reuse.software/info/github.com/SAP-samples/cloud-cap-samples)
|
||||
|
||||
### Preliminaries
|
||||
|
||||
@@ -18,7 +17,7 @@ Find here a collection of samples for the [SAP Cloud Application Programming Mod
|
||||
|
||||
### Download
|
||||
|
||||
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/master.zip).
|
||||
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/main.zip).
|
||||
|
||||
```sh
|
||||
git clone https://github.com/sap-samples/cloud-cap-samples samples
|
||||
@@ -84,4 +83,4 @@ In case you've a question, find a bug, or otherwise need support, use our [commu
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2021 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSES/Apache-2.0.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';
|
||||
@@ -3,14 +3,15 @@ const $ = sel => document.querySelector(sel)
|
||||
const GET = (url) => axios.get('/browse'+url)
|
||||
const POST = (cmd,data) => axios.post('/browse'+cmd,data)
|
||||
|
||||
const books = new Vue ({
|
||||
const books = Vue.createApp ({
|
||||
|
||||
el:'#app',
|
||||
|
||||
data: {
|
||||
data() {
|
||||
return {
|
||||
list: [],
|
||||
book: undefined,
|
||||
order: { amount:1, succeeded:'', failed:'' }
|
||||
order: { quantity:1, succeeded:'', failed:'' },
|
||||
user: {}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -26,23 +27,35 @@ const books = new Vue ({
|
||||
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
|
||||
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
|
||||
Object.assign (book, res.data)
|
||||
books.order = { amount:1 }
|
||||
books.order = { quantity:1 }
|
||||
setTimeout (()=> $('form > input').focus(), 111)
|
||||
},
|
||||
|
||||
async submitOrder () {
|
||||
const {book,order} = books, amount = parseInt (order.amount) || 1 // REVISIT: Okra should be less strict
|
||||
const {book,order} = books, quantity = parseInt (order.quantity) || 1 // REVISIT: Okra should be less strict
|
||||
try {
|
||||
const res = await POST(`/submitOrder`, { amount, book: book.ID })
|
||||
const res = await POST(`/submitOrder`, { quantity, book: book.ID })
|
||||
book.stock = res.data.stock
|
||||
books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` }
|
||||
books.order = { quantity, succeeded: `Successfully ordered ${quantity} item(s).` }
|
||||
} catch (e) {
|
||||
books.order = { amount, failed: e.response.data.error.message }
|
||||
books.order = { quantity, failed: e.response.data.error ? e.response.data.error.message : e.response.data }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async fetchUserInfo() {
|
||||
try {
|
||||
const { data } = await axios.get('/user/me')
|
||||
books.user = data
|
||||
} catch (err) { books.user = { id: err.message } }
|
||||
}
|
||||
}
|
||||
})
|
||||
}).mount("#app")
|
||||
|
||||
// initially fill list of books
|
||||
books.fetch()
|
||||
|
||||
books.fetchUserInfo()
|
||||
document.addEventListener('keydown', (event) => {
|
||||
// hide user info on request
|
||||
if (event.key === 'u') books.user = undefined
|
||||
})
|
||||
|
||||
@@ -5,19 +5,26 @@
|
||||
<title> Capire Books </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"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
||||
<style>
|
||||
.hovering tr:hover td { color:cyan; background: #123; cursor: pointer; }
|
||||
.rating-stars { color:teal }
|
||||
.succeeded { color:teal }
|
||||
.failed { color:red }
|
||||
.user {text-align: end; color: grey;}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="small-container", style="margin-top: 70px;">
|
||||
<div id='app'>
|
||||
|
||||
<h1> {{ document.title }} </h1>
|
||||
<div v-if="user" class="user">
|
||||
<div>User: {{ user.id || 'anonymous' }}</div>
|
||||
<div>Locale: {{ user.locale }}</div>
|
||||
<div v-if="user.tenant">Tenant: {{ user.tenant }}</div>
|
||||
</div>
|
||||
|
||||
<h1> Capire Books </h1>
|
||||
|
||||
<input type="text" placeholder="Search..." @input="search">
|
||||
|
||||
@@ -34,7 +41,7 @@
|
||||
<td>{{ book.author }}</td>
|
||||
<td>{{ book.genre.name }}</td>
|
||||
<td class="rating-stars">
|
||||
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
|
||||
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} ({{ book.numberOfReviews }})
|
||||
</td>
|
||||
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
||||
</tr>
|
||||
@@ -48,7 +55,7 @@
|
||||
{{ book.stock }} in stock
|
||||
</label>
|
||||
<form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
|
||||
<input type="number" v-model="order.amount" v-bind:class="{ failed: order.failed }" style="width:5em">
|
||||
<input type="number" v-model="order.quantity" v-bind:class="{ failed: order.failed }" style="width:5em">
|
||||
<input type="submit" value="Order:" class="muted-button">
|
||||
</form>
|
||||
<h4> {{ book.title }} </h4>
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
|
2
bookshop/db/handlers/sandbox.cds
Normal file
2
bookshop/db/handlers/sandbox.cds
Normal file
@@ -0,0 +1,2 @@
|
||||
using from '..\..\schema';
|
||||
|
||||
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,31 +1,56 @@
|
||||
using { Currency, managed, sap } from '@sap/cds/common';
|
||||
using {
|
||||
Currency,
|
||||
managed,
|
||||
sap,
|
||||
extensible
|
||||
} from '@sap/cds/common';
|
||||
|
||||
namespace sap.capire.bookshop;
|
||||
|
||||
entity Books : managed {
|
||||
key ID : Integer;
|
||||
title : localized String(111);
|
||||
descr : localized String(1111);
|
||||
author : Association to Authors;
|
||||
genre : Association to Genres;
|
||||
stock : Integer;
|
||||
price : Decimal;
|
||||
currency : Currency;
|
||||
image : LargeBinary @Core.MediaType : 'image/png';
|
||||
@Extensibility.Any.Enabled : true
|
||||
entity Books : managed, extensible {
|
||||
key ID : Integer;
|
||||
title : localized String(111);
|
||||
descr : localized String(1111);
|
||||
author : Association to Authors;
|
||||
genre : Association to Genres;
|
||||
stock : Integer;
|
||||
price : Decimal;
|
||||
currency : Currency;
|
||||
image : LargeBinary @Core.MediaType : 'image/png';
|
||||
authorName : String;
|
||||
}
|
||||
|
||||
entity Authors : managed {
|
||||
key ID : Integer;
|
||||
name : String(111);
|
||||
dateOfBirth : Date;
|
||||
dateOfDeath : Date;
|
||||
placeOfBirth : String;
|
||||
placeOfDeath : String;
|
||||
books : Association to many Books on books.author = $self;
|
||||
|
||||
entity Authors : managed, extensible {
|
||||
key ID : Integer;
|
||||
name : String(111);
|
||||
dateOfBirth : Date;
|
||||
dateOfDeath : Date;
|
||||
placeOfBirth : String;
|
||||
placeOfDeath : String;
|
||||
|
||||
books : Association to many Books
|
||||
on books.author = $self;
|
||||
}
|
||||
|
||||
/** Hierarchically organized Code List for Genres */
|
||||
extend Authors with {
|
||||
virtual age : Integer;
|
||||
virtual exampleBook: String;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hierarchically organized Code List for Genres
|
||||
*/
|
||||
entity Genres : sap.common.CodeList {
|
||||
key ID : Integer;
|
||||
parent : Association to Genres;
|
||||
children : Composition of many Genres on children.parent = $self;
|
||||
key ID : Integer;
|
||||
parent : Association to Genres;
|
||||
children : Composition of many Genres
|
||||
on children.parent = $self;
|
||||
}
|
||||
|
||||
entity Publishers: managed {
|
||||
key ID: Integer;
|
||||
name: String(111);
|
||||
|
||||
}
|
||||
17
bookshop/handlers/AdminService.Authors.CREATE.js
Normal file
17
bookshop/handlers/AdminService.Authors.CREATE.js
Normal file
@@ -0,0 +1,17 @@
|
||||
async function run() {
|
||||
//debugger
|
||||
//while (true) {}
|
||||
//process.exit()
|
||||
//1.substring()
|
||||
// let res = await specialselect
|
||||
let res = await SELECT.one`title`.from(`Books`).where(`ID=201`)
|
||||
let { title } = res
|
||||
let Author = req.data
|
||||
//await srv.read('Books')
|
||||
|
||||
Author.modifiedBy = "Custom Event handler changed this!"
|
||||
Author.placeOfDeath = " --- Somewhere over " + title + " --- create in Sandbox"
|
||||
//await this.emit("createdAuthor", { Author })
|
||||
return Author
|
||||
}
|
||||
run()
|
||||
41
bookshop/handlers/AdminService.Authors.READ.js
Normal file
41
bookshop/handlers/AdminService.Authors.READ.js
Normal file
@@ -0,0 +1,41 @@
|
||||
function getYear(v) {
|
||||
return parseInt(v.substr(0, 4))
|
||||
}
|
||||
function getMonth(v) {
|
||||
return parseInt(v.substr(5, 2))
|
||||
}
|
||||
function getDay(v) {
|
||||
return parseInt(v.substr(8, 2))
|
||||
}
|
||||
|
||||
function getAge(from, to) {
|
||||
if (from === undefined || from == null) return 0
|
||||
if (to === undefined || to == null) to = new Date().toISOString()
|
||||
let year = getYear(to) - getYear(from) - 1
|
||||
if (
|
||||
getMonth(to) > getMonth(from) ||
|
||||
(getMonth(to) === getMonth(from) && getDay(to) >= getDay(from))
|
||||
) {
|
||||
year++
|
||||
}
|
||||
return year
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const result_ = Array.isArray(result) ? result : [result]
|
||||
for (const row of result_) {
|
||||
row.age = getAge(row.dateOfBirth, row.dateOfDeath)
|
||||
let res = await SELECT.one`title`.from(`Books`).where({ author_ID: row.ID })
|
||||
if (!res) {
|
||||
res = {}
|
||||
}
|
||||
let { title } = res
|
||||
if (!title) {
|
||||
title = "no Books yet"
|
||||
}
|
||||
row.exampleBook = title
|
||||
//let pub = await SELECT.one`name`.from(`sap_capire_bookshop_Publishers`)
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
9
bookshop/handlers/AdminService.Books.CREATE.js
Normal file
9
bookshop/handlers/AdminService.Books.CREATE.js
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
async function run() {
|
||||
const {stock, price, author_ID} = req.data
|
||||
if (stock<0) return req.reject('409', 'Stock must not be negative')
|
||||
if (price<0) return req.reject('409', 'Price must not be negative')
|
||||
let {name} = await SELECT.one`name`.from(`Authors`).where({ID: author_ID})
|
||||
req.data.authorName=name
|
||||
}
|
||||
output = run()
|
||||
6
bookshop/handlers/AdminService.Books.READ.js
Normal file
6
bookshop/handlers/AdminService.Books.READ.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const result_ = Array.isArray(result) ? result : [result];
|
||||
for (const row of result_) {
|
||||
if (row.stock > 50) {
|
||||
row.title += " ---Order now for a 10% discount!";
|
||||
}
|
||||
}
|
||||
9
bookshop/handlers/AdminService.renameAuthor.ON.js
Normal file
9
bookshop/handlers/AdminService.renameAuthor.ON.js
Normal file
@@ -0,0 +1,9 @@
|
||||
async function run() {
|
||||
const {author, newName} = req.data
|
||||
let a = await SELECT `name`.from(`Authors`).where({ID: author})
|
||||
if(!a) return req.error (404, `Can't rename a non-existing author`)
|
||||
await UPDATE (`Authors`,author).with ({ name: newName })
|
||||
//await this.emit ('renamedAuthor', { author, newName })
|
||||
output.msg = 'Success'
|
||||
}
|
||||
run()
|
||||
5
bookshop/handlers/createdAuthor.ON.js
Normal file
5
bookshop/handlers/createdAuthor.ON.js
Normal file
@@ -0,0 +1,5 @@
|
||||
async function run() {
|
||||
let {Author} = req.data
|
||||
Author.placeOfBirth += ' --- modified in custom event'
|
||||
}
|
||||
run()
|
||||
@@ -1 +1,2 @@
|
||||
exports.CatalogService = require('./srv/cat-service')
|
||||
const { CatalogService } = require('./srv/cat-service')
|
||||
module.exports = { CatalogService }
|
||||
|
||||
112
bookshop/notebook.md
Normal file
112
bookshop/notebook.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Base assumption
|
||||
|
||||
Event handlers will always use **publicly available application API's**(services)
|
||||
|
||||
- already done in Sandbox API by overwriting **SELECT**, **UPDATE**, **READ** and **CREATE**
|
||||
|
||||
## Inbound data for validations
|
||||
|
||||
- req.target plus expand on related data
|
||||
- lazy loading on expand
|
||||
- event facade could have an explicit publishing of specific services or documents (e.g. remote services)
|
||||
- CQN Protocol adapter for subsequent reads --> req.data plus application service calls
|
||||
- what is the CDS subset to put in?
|
||||
- req.data + target-rec (proxy, unloaded)
|
||||
- ORM type lazy loading (dereferenced)
|
||||
- application developer could actually provide custom proxies for specific functions
|
||||
- performance impact of multiple accesses to object graph and multiple DB roundtrips
|
||||
- can static code checking or developer annotations influence what is loaded into a graph?
|
||||
- alternative: Stripped-down SELECT limited to req.target and ID
|
||||
- application service only
|
||||
- access rights of user respected
|
||||
- What about to-many relationships? For compositions essential, for associations to be questioned
|
||||
- Application Service Reads
|
||||
- outbound data for changes
|
||||
- call remote services
|
||||
- register new remote services dynamically
|
||||
- CAP provides an API on remote services - connect doesn't need to be done by extension developer
|
||||
- alternative: declarative remote services plumbing with CDS service facade
|
||||
- model looks like static internal services, remote calls done transparently behind the scenes
|
||||
|
||||
-Emit Events
|
||||
|
||||
- choreography of extension points
|
||||
- deep inserts vs. fine grained operations
|
||||
- input validation may be suited for fine grained operations
|
||||
- today not in scope for performance reasons
|
||||
- two different use case: Insert new page to book vs. update order-header with items-constraints in place
|
||||
- reject request, return errors and warnings - suitable for UI, too
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
Annotations available:
|
||||
|
||||
Entity level
|
||||
@expression.constraint : [{if: 'expression evaluates to bool'}, on: ['INSERT, UPDATE, DELETE'], error: 'Transaction Rollback and error message', warning: 'Transaction proceeds and warning message']
|
||||
@expression.computed : [{expression: 'ability to access request payload and modify it', on: ['INSERT, UPDATE']}]
|
||||
@event : [{if: 'expression evaluates to bool', on: ['INSERT, UPDATE, DELETE, READ'], when: 'before or after, default before', emit: 'Event Name', to: 'Messaging target, optional'}]
|
||||
@expresion.code :[{file: 'file name', on:['insert', 'update'], when: 'before or after, default before'},
|
||||
{source: 'each => { if (each.stock > 111) {each.title += `-- 11% discount!`; each.price= each.price*0.9}', on:['insert', 'update'], when: 'before or after'}]
|
||||
Atribute Level
|
||||
@assert.constraint : {if: 'stock>=0 OR stock <1000', error: 'i18n/error102'};
|
||||
@event : {if: 'expression evaluates to bool', on: ['INSERT, UPDATE, DELETE, READ'], when: 'before or after', emit: 'Event Name', to: 'Messaging target, optional' }
|
||||
|
||||
Functions available:
|
||||
EXISTS(association target)
|
||||
COUNT,AVG,MIN,MAX,SUM: Composition items, arrays etc
|
||||
OLD: before image
|
||||
EACH: loop over composition items
|
||||
|
||||
Events covered:
|
||||
CRUD --> Longhand and Shorthand supported?
|
||||
Upsert as one event?
|
||||
Before and after:
|
||||
Before can change change request payload and stop transaction
|
||||
After should trigger only asynchronous messages
|
||||
Specific Events for status changes? I think expression based event emitter suffices
|
||||
*/
|
||||
|
||||
//Entity level annotations
|
||||
@expression.constraint : [{if: 'stock>100 AND price>15)', on: ['INSERT', 'UPDATE'], error: 'No Book over price 15 should have more than 100 stock' }, // error, rollback transactions
|
||||
{if: 'stock>90 AND price>15)', on: ['I', 'U'], warning: 'No Book over price 15 should have more than 100 stock' }] //warning, proceed with transaction but report warning back to UI
|
||||
@expression.computed : {expression: 'if(stock>100) then price=price*0.9', on: ['INSERT']} //ability to modify the payload of the request, but nothing beyond it
|
||||
@expresion.code :[{file: 'sap.capire.bookshop-Books-beforeInsert', on:['insert', 'update'], when: 'before'}, //naming can be arbitrary?
|
||||
{source: 'each => { if (each.stock > 111) {each.title += `-- 11% discount!`; each.price= each.price*0.9}', on:['insert', 'update'], when: 'before'}] //alternative
|
||||
@event : { if:'price>200', emit: 'Expensive Book', to: 'RulesEngine'}
|
||||
entity Books : managed {
|
||||
key ID : Integer;
|
||||
title : localized String(111); @event : {if: 'old.title="Hello"', emit: 'Hello changed' } //old refers to before Image. No "to" clause means message is emitted to any subscriber interested
|
||||
descr : localized String(1111);
|
||||
author : Association to Authors @assert.constraint: 'exists(author)'; //function calls need to evaluate to bool
|
||||
genre : Association to Genres;
|
||||
stock : Integer @assert.constraint : {if: 'stock>=0 OR stock <1000', error: 'Stock not within permitted parameters'}; //when operand is used, no auto-insert
|
||||
price : Decimal(9,2) @assert.constraint : '>0'; //insert operand on left side by default
|
||||
currency : Currency;
|
||||
image : LargeBinary @Core.MediaType : 'image/png';
|
||||
stockWorth: Decimal(9,2) @expression.computed : 'stock*price'; //persisted on write. Overhead in runtime, but performance benefit on read. Payload ignored?
|
||||
// stockWorth2 = stock*price; -- long term goal from compiler team, not persisted on write, but calculated on read
|
||||
stockWorth3 : Decimal @expression.computed: 'if (stock*price>1000) then stockWorth3=stock.price else stockworth3=1000'; //which altenative?
|
||||
stockWorth4 : Decimal @expression.computed: {if: '(stock*price>1000)', then: 'stockWorth3=stock.price', else: 'stockworth3=1000'};
|
||||
}
|
||||
//@assert.expression: 'dateOfBirth<dateOfDeath'
|
||||
entity Authors : managed {
|
||||
key ID : Integer;
|
||||
name : String(111);
|
||||
dateOfBirth : Date ;
|
||||
dateOfDeath : Date @expression.constraint: '>dateOfBirth';
|
||||
placeOfBirth : String;
|
||||
placeOfDeath : String;
|
||||
books : Association to many Books on books.author = $self;
|
||||
}
|
||||
|
||||
/** Hierarchically organized Code List for Genres */
|
||||
entity Genres : sap.common.CodeList {
|
||||
key ID : Integer;
|
||||
parent : Association to Genres;
|
||||
children : Composition of many Genres on children.parent = $self;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -2,11 +2,18 @@
|
||||
"name": "@capire/bookshop",
|
||||
"version": "1.0.0",
|
||||
"description": "A simple self-contained bookshop service.",
|
||||
"files": [
|
||||
"app",
|
||||
"srv",
|
||||
"db",
|
||||
"index.cds",
|
||||
"index.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"@capire/common": "*",
|
||||
"@sap/cds": "^4",
|
||||
"@sap/cds": ">=5.9",
|
||||
"express": "^4.17.1",
|
||||
"passport": "0.4.1"
|
||||
"passport": ">=0.4.1",
|
||||
"vm2": ">=3.9.9"
|
||||
},
|
||||
"scripts": {
|
||||
"genres": "cds serve test/genres.cds",
|
||||
@@ -15,9 +22,13 @@
|
||||
},
|
||||
"cds": {
|
||||
"requires": {
|
||||
"code-extensibility" : true,
|
||||
"db": {
|
||||
"kind": "sql"
|
||||
"kind": "sqlite",
|
||||
"credentials": {
|
||||
"database": "sqlite.db"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
bookshop/sqlite.db
Normal file
BIN
bookshop/sqlite.db
Normal file
Binary file not shown.
@@ -1,5 +1,30 @@
|
||||
using { sap.capire.bookshop as my } from '../db/schema';
|
||||
service AdminService @(requires:'admin') {
|
||||
entity Books as projection on my.Books;
|
||||
entity Authors as projection on my.Authors;
|
||||
using {sap.capire.bookshop as my} from '../db/schema';
|
||||
|
||||
service AdminService // @(requires : 'admin')
|
||||
{
|
||||
entity Books as projection on my.Books actions {
|
||||
action increaseStock(count : Integer);
|
||||
function stock() returns Integer;
|
||||
};
|
||||
|
||||
@Extensibility : {
|
||||
Fields.Enabled : true,
|
||||
Fields.Quota: 100,
|
||||
Relations.Enabled : false,
|
||||
Annotations.Enabled : true,
|
||||
Logic.Enabled : true,
|
||||
Logic.constraints: true,
|
||||
Logic.calculations: true,
|
||||
Logic.Handler : [create, update, delete, read]
|
||||
}
|
||||
entity Authors as projection on my.Authors;
|
||||
|
||||
action renameAuthor(author : Authors:ID, newName : String) returns {
|
||||
msg : String
|
||||
};
|
||||
function getStock(book: Books:ID) returns Integer;
|
||||
event newBook : {
|
||||
book : Books:ID;
|
||||
name : Books:title
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,135 @@
|
||||
const cds = require('@sap/cds')
|
||||
const cds = require("@sap/cds")
|
||||
//const cds_sandbox = require("sap/cds/sandbox")
|
||||
const { VM, VMScript } = require("vm2")
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const { nextTick } = require("process")
|
||||
|
||||
module.exports = cds.service.impl (function(){
|
||||
this.before ('NEW','Authors', genid)
|
||||
this.before ('NEW','Books', genid)
|
||||
})
|
||||
class AdminService extends cds.ApplicationService {
|
||||
init() {
|
||||
this.after("READ", async (result, req) => {
|
||||
if (!(result === undefined || result == null)) {
|
||||
const code = getCode(req.target.name, "READ")
|
||||
if (code) {
|
||||
await executeCode.call(this, code, req, result)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/** Generate primary keys for target entity in request */
|
||||
async function genid (req) {
|
||||
const {ID} = await cds.tx(req).run (SELECT.one.from(req.target).columns('max(ID) as ID'))
|
||||
req.data.ID = ID - ID % 100 + 100 + 1
|
||||
this.before("CREATE", async (req) => {
|
||||
const code = getCode(req.target.name, "CREATE")
|
||||
if (code) {
|
||||
await executeCode.call(this, code, req)
|
||||
}
|
||||
})
|
||||
|
||||
this.before("UPDATE", async (req) => {
|
||||
const code = getCode(req.target.name, "CREATE")
|
||||
if (code) {
|
||||
await executeCode.call(this, code, req)
|
||||
}
|
||||
})
|
||||
|
||||
this.on("*", async (req, next) => {
|
||||
if (!(req.target === undefined || req.target == null)) return next()
|
||||
//ToDo: check whether action or event is part of an extension
|
||||
// DO NOT OVERWRITE EXISTING Action Implementations!
|
||||
// evaluate: Can we augment action implementation with super.next?
|
||||
if (req.constructor.name === "EventMessage") {
|
||||
const code = getCode(req.event, "ON")
|
||||
if (code) {
|
||||
await executeCode.call(this, code, req)
|
||||
}
|
||||
} else if (req.constructor.name === "ODataRequest") {
|
||||
var output = {}
|
||||
const code = getCode(this.name + "." + req.event, "ON")
|
||||
if (code) {
|
||||
await executeCode.call(this, code, req, {}, output)
|
||||
return output
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
//ToDo: Prefix for Service not in event emitter
|
||||
this.before("CREATE", "Authors", async (req) => {
|
||||
let Author = req.data
|
||||
await this.emit("createdAuthor", { Author })
|
||||
|
||||
})
|
||||
|
||||
return super.init()
|
||||
}
|
||||
}
|
||||
|
||||
var counter = 1;
|
||||
|
||||
function newLabel() {return "VM2 - req: " + counter++}
|
||||
|
||||
//should only work in local exection (cds watch)
|
||||
// alternative: Upon Bootstrapping, merge files into CSN
|
||||
function getCodeFromFile(name, operation) {
|
||||
const filename = name + "." + operation + ".js"
|
||||
const file = path.join(__dirname, "..", "handlers", filename)
|
||||
try {
|
||||
const code = fs.readFileSync(file, "utf8")
|
||||
return code
|
||||
} catch (error) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
//after push this should be the only thing that works
|
||||
function getCodeFromAnnotation(name, operation) {
|
||||
return ""
|
||||
}
|
||||
|
||||
function getCode(name, operation) {
|
||||
let code=getCodeFromAnnotation(name, operation)
|
||||
if (code==="") {code=getCodeFromFile(name, operation)}
|
||||
return code
|
||||
}
|
||||
|
||||
function scanCode(code) {
|
||||
//ESLINT
|
||||
}
|
||||
|
||||
async function executeCode(code, req, result, output) {
|
||||
const srv = this
|
||||
const label=newLabel()
|
||||
console.time(label)
|
||||
const vm = new VM({
|
||||
console: "inherit",
|
||||
timeout: 500,
|
||||
allowAsync: true,
|
||||
sandbox: { req, //todo: isolate req.data, req.reject, req.error, req.message
|
||||
result, //important for READ
|
||||
output, //used for Action Implementation
|
||||
SELECT : (class extends require('@sap/cds/lib/ql/SELECT') {then(r,e) {return srv.run(this).then(r,e)}})._api(),
|
||||
INSERT : (class extends require('@sap/cds/lib/ql/INSERT') {then(r,e) {return srv.run(this).then(r,e)}})._api(),
|
||||
UPDATE : (class extends require('@sap/cds/lib/ql/UPDATE') {then(r,e) {return srv.run(this).then(r,e)}})._api(),
|
||||
CREATE : (class extends require('@sap/cds/lib/ql/CREATE') {then(r,e) {return srv.run(this).then(r,e)}})._api(),
|
||||
//srv: this,
|
||||
JSON },
|
||||
})
|
||||
|
||||
try {
|
||||
await vm.run(code)
|
||||
return output
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
req.reject("409", "Error in VM")
|
||||
}
|
||||
finally {
|
||||
console.timeEnd(label)
|
||||
}
|
||||
// console.log(req.data)
|
||||
}
|
||||
/** Generate primary keys for target entity in request */
|
||||
async function genid(req) {
|
||||
const { ID } = await cds
|
||||
.tx(req)
|
||||
.run(SELECT.one.from(req.target).columns("max(ID) as ID"))
|
||||
req.data.ID = ID - (ID % 100) + 100 + 1
|
||||
}
|
||||
|
||||
module.exports = { AdminService }
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
using { sap.capire.bookshop as my } from '../db/schema';
|
||||
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
|
||||
} excluding { createdBy, modifiedBy };
|
||||
|
||||
@readonly entity ListOfBooks as SELECT from Books
|
||||
excluding { descr };
|
||||
@readonly entity Publishers as projection on my.Publishers;
|
||||
|
||||
@requires: 'authenticated-user'
|
||||
action submitOrder ( book: Books:ID, amount: Integer ) returns { stock: Integer };
|
||||
event OrderedBook : { book: Books:ID; amount: Integer; buyer: String };
|
||||
action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer };
|
||||
event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String };
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
const cds = require('@sap/cds')
|
||||
const { Books } = cds.entities ('sap.capire.bookshop')
|
||||
|
||||
class CatalogService extends cds.ApplicationService { init(){
|
||||
|
||||
const { Books } = cds.entities ('sap.capire.bookshop')
|
||||
|
||||
// Reduce stock of ordered books if available stock suffices
|
||||
this.on ('submitOrder', async req => {
|
||||
const {book,amount} = req.data, tx = cds.tx(req)
|
||||
let {stock} = await tx.read('stock').from(Books,book)
|
||||
if (stock >= amount) {
|
||||
await tx.update (Books,book).with ({ stock: stock -= amount })
|
||||
await this.emit ('OrderedBook', { book, amount, buyer:req.user.id })
|
||||
return { stock }
|
||||
}
|
||||
else return req.error (409,`${amount} exceeds stock for book #${book}`)
|
||||
const {book,quantity} = req.data
|
||||
if (quantity < 1) return req.reject (400,`quantity has to be 1 or more`)
|
||||
let b = await SELECT `stock` .from (Books,book)
|
||||
if (!b) return req.error (404,`Book #${book} doesn't exist`)
|
||||
let {stock} = b
|
||||
if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`)
|
||||
await UPDATE (Books,book) .with ({ stock: stock -= quantity })
|
||||
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
|
||||
return { stock }
|
||||
})
|
||||
|
||||
// Add some discount for overstocked books
|
||||
this.after ('READ','Books', each => {
|
||||
if (each.stock > 111) {
|
||||
each.title += ` -- 11% discount!`
|
||||
}
|
||||
this.after ('READ','ListOfBooks', each => {
|
||||
if (each.stock > 111) each.title += ` -- 11% discount!`
|
||||
})
|
||||
|
||||
return super.init()
|
||||
|
||||
11
bookshop/srv/code-extensions.cds
Normal file
11
bookshop/srv/code-extensions.cds
Normal file
@@ -0,0 +1,11 @@
|
||||
//this file is machine-created during cds.build
|
||||
namespace sap.capire.bookshop; //> important for reflection
|
||||
using from '../db/schema';
|
||||
using from '../srv/cat-service';
|
||||
using from '../srv/admin-service';
|
||||
|
||||
annotate AdminService.Authors with @extension.logic: [{when: 'CREATE', code: 'async function run() {\r\n \/\/debugger\r\n \/\/while (true) {}\r\n \/\/process.exit()\r\n \/\/1.substring()\r\n \/\/ let res = await specialselect\r\n let res = await SELECT.one`title`.from(`Books`).where(`ID=201`)\r\n let { title } = res\r\n let Author = req.data\r\n Author.modifiedBy = \"Custom Event handler changed this!\"\r\n Author.placeOfDeath = \" --- Somewhere over \" + title + \" --- create in Sandbox\"\r\n \/\/await this.emit(\"createdAuthor\", { Author })\r\n return Author\r\n}\r\nrun()\r\n'},
|
||||
{when: 'READ', code: 'function getYear(v) {\r\n return parseInt(v.substr(0, 4))\r\n}\r\nfunction getMonth(v) {\r\n return parseInt(v.substr(5, 2))\r\n}\r\nfunction getDay(v) {\r\n return parseInt(v.substr(8, 2))\r\n}\r\n\r\nfunction getAge(from, to) {\r\n if (from === undefined || from == null) return 0\r\n if (to === undefined || to == null) to = new Date().toISOString()\r\n let year = getYear(to) - getYear(from) - 1\r\n if (\r\n getMonth(to) > getMonth(from) ||\r\n (getMonth(to) === getMonth(from) && getDay(to) >= getDay(from))\r\n ) {\r\n year++\r\n }\r\n return year\r\n}\r\n\r\nconst result_ = Array.isArray(result) ? result : [result]\r\nfor (const row of result_) {\r\n row.modifiedBy += \" --- read in sandbox\"\r\n row.age = getAge(row.dateOfBirth, row.dateOfDeath)\r\n}'}
|
||||
];
|
||||
annotate AdminService.Books with @extension.logic;
|
||||
annotate CatalogService.ListOfBooks with @extension.logic;
|
||||
16
bookshop/srv/user-service.cds
Normal file
16
bookshop/srv/user-service.cds
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Exposes user information
|
||||
*/
|
||||
@requires: 'authenticated-user'
|
||||
service UserService {
|
||||
|
||||
/**
|
||||
* The current user
|
||||
*/
|
||||
@odata.singleton entity me {
|
||||
id : String; // user id
|
||||
locale : String;
|
||||
tenant : String;
|
||||
}
|
||||
|
||||
}
|
||||
4
bookshop/srv/user-service.js
Normal file
4
bookshop/srv/user-service.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const cds = require('@sap/cds')
|
||||
module.exports = cds.service.impl((srv) => {
|
||||
srv.on('READ', 'me', ({ tenant, user, locale }) => ({ id: user.id, locale, tenant }))
|
||||
})
|
||||
@@ -1,10 +1,46 @@
|
||||
@server = http://localhost:4004
|
||||
@me = Authorization: Basic {{$processEnv USER}}:
|
||||
@id = 2000
|
||||
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# Fetch Authors
|
||||
GET {{server}}/admin/Authors
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# Fetch one Author
|
||||
GET {{server}}/admin/Authors({{id}})
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# Create Author
|
||||
POST {{server}}/admin/Authors
|
||||
Content-Type: application/json;IEEE754Compatible=true
|
||||
|
||||
{
|
||||
"ID": {{id}},
|
||||
"name": "Nick",
|
||||
"placeOfBirth": "Somewhere",
|
||||
"placeOfDeath": "over the Rainbox",
|
||||
"dateOfBirth" : "1975-05-27"
|
||||
}
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# rename author via unbound action
|
||||
POST {{server}}/admin/renameAuthor
|
||||
Content-Type: application/json
|
||||
{{me}}
|
||||
|
||||
{ "author":{{id}}, "newName":"Super Nick" }
|
||||
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# Get service info
|
||||
GET {{server}}/browse
|
||||
GET {{server}}/admin
|
||||
{{me}}
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# Get $metadata document
|
||||
GET {{server}}/admin/$metadata
|
||||
{{me}}
|
||||
|
||||
|
||||
@@ -16,21 +52,19 @@ GET {{server}}/browse/$metadata
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# Browse Books as any user
|
||||
GET {{server}}/browse/Books?
|
||||
GET {{server}}/browse/ListOfBooks?
|
||||
# &$select=title,stock
|
||||
# &$expand=currency
|
||||
&$expand=genre
|
||||
# &sap-language=de
|
||||
{{me}}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# Fetch Authors as admin
|
||||
GET {{server}}/admin/Authors?
|
||||
# &$select=name,dateOfBirth,placeOfBirth
|
||||
# &$expand=books($select=title;$expand=currency)
|
||||
# &$filter=ID eq 101
|
||||
# &sap-language=de
|
||||
Authorization: Basic alice:
|
||||
# Fetch Books as admin
|
||||
GET {{server}}/admin/Books
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# Create book
|
||||
@@ -39,12 +73,12 @@ Content-Type: application/json;IEEE754Compatible=true
|
||||
Authorization: Basic alice:
|
||||
|
||||
{
|
||||
"ID": 2,
|
||||
"title": "Poems : Pocket Poets",
|
||||
"ID": 16,
|
||||
"title": "Deh4",
|
||||
"descr": "The Everyman's Library Pocket Poets hardcover series is popular for its compact size and reasonable price which does not compromise content. Poems: Bronte contains poems that demonstrate a sensibility elemental in its force with an imaginative discipline and flexibility of the highest order. Also included are an Editor's Note and an index of first lines.",
|
||||
"author": { "ID": 101 },
|
||||
"genre": { "ID": 12 },
|
||||
"stock": 5,
|
||||
"stock": -100,
|
||||
"price": "12.05",
|
||||
"currency": { "code": "USD" }
|
||||
}
|
||||
@@ -71,7 +105,7 @@ POST {{server}}/browse/submitOrder
|
||||
Content-Type: application/json
|
||||
{{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
|
||||
|
||||
@@ -20,30 +20,29 @@ module.exports = async()=>{ // called by server.js
|
||||
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
||||
console.debug ('> delegating request to ReviewsService')
|
||||
const [id] = req.params, { columns, limit } = req.query.SELECT
|
||||
return ReviewsService.tx(req).read ('Reviews',columns).limit(limit).where({subject:String(id)})
|
||||
return ReviewsService.read ('Reviews',columns).limit(limit).where({subject:String(id)})
|
||||
}))
|
||||
|
||||
//
|
||||
// Create an order with the OrdersService when CatalogService signals a new order
|
||||
//
|
||||
CatalogService.on ('OrderedBook', async (msg) => {
|
||||
const { book, amount, buyer } = msg.data
|
||||
const { book, quantity, buyer } = msg.data
|
||||
const { title, price } = await db.tx(msg).read (Books, book, b => { b.title, b.price })
|
||||
return OrdersService.tx(msg).create ('Orders').entries({
|
||||
OrderNo: 'Order at '+ (new Date).toLocaleString(),
|
||||
Items: [{ product:{ID:`${book}`}, title, price, amount }],
|
||||
Items: [{ product:{ID:`${book}`}, title, price, quantity }],
|
||||
buyer, createdBy: buyer
|
||||
})
|
||||
})
|
||||
|
||||
//
|
||||
// Update Books' average ratings when ReviewsService signals updatd reviews
|
||||
// Update Books' average ratings when ReviewsService signals updated reviews
|
||||
//
|
||||
ReviewsService.on ('reviewed', (msg) => {
|
||||
console.debug ('> received:', msg.event, msg.data)
|
||||
const { subject, rating } = msg.data
|
||||
return UPDATE(Books,subject).with({rating})
|
||||
// ^ Note: the framework will execute this and take care for db.tx
|
||||
const { subject, count, rating } = msg.data
|
||||
return UPDATE(Books,subject).with({ numberOfReviews:count, rating })
|
||||
})
|
||||
|
||||
//
|
||||
@@ -51,9 +50,9 @@ module.exports = async()=>{ // called by server.js
|
||||
//
|
||||
OrdersService.on ('OrderChanged', (msg) => {
|
||||
console.debug ('> received:', msg.event, msg.data)
|
||||
const { product, deltaAmount } = msg.data
|
||||
const { product, deltaQuantity } = msg.data
|
||||
return UPDATE (Books) .where ('ID =', product)
|
||||
.and ('stock >=', deltaAmount)
|
||||
.set ('stock -=', deltaAmount)
|
||||
.and ('stock >=', deltaQuantity)
|
||||
.set ('stock -=', deltaQuantity)
|
||||
})
|
||||
}
|
||||
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
|
||||
@reviews-service = {{bookshop}}/reviews
|
||||
# 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
|
||||
&$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
|
||||
GBP;de;Pfund;Britische Pfund
|
||||
ILS;de;Schekel;Israelische Schekel
|
||||
JPY;de;Yen;Japanische Yen
|
||||
EUR;fr;euro;de la Zone euro
|
||||
USD;fr;dollar;dollar des États-Unis
|
||||
CAD;fr;dollar canadien;dollar canadien
|
||||
AUD;fr;dollar australien;dollar australien
|
||||
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;
|
||||
*/
|
||||
|
||||
context sap.common_countries {
|
||||
context sap.common.countries {
|
||||
|
||||
extend sap.common.Countries {
|
||||
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
|
||||
145
fiori/app/admin-authors/webapp/manifest.json
Normal file
145
fiori/app/admin-authors/webapp/manifest.json
Normal file
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"_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": {
|
||||
"flexEnabled": true,
|
||||
"config": {
|
||||
"experimentalCAPScenario": true
|
||||
},
|
||||
"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';
|
||||
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 AdminService.Books with @odata.draft.enabled;
|
||||
|
||||
annotate AdminService.Books_texts with @(
|
||||
annotate AdminService.Books.texts with @(
|
||||
UI: {
|
||||
Identification: [{Value:title}],
|
||||
SelectionFields: [ locale, title ],
|
||||
@@ -83,11 +63,16 @@ annotate AdminService.Books_texts with @(
|
||||
);
|
||||
|
||||
// Add Value Help for Locales
|
||||
annotate AdminService.Books_texts {
|
||||
locale @ValueList:{entity:'Languages',type:#fixed}
|
||||
annotate AdminService.Books.texts {
|
||||
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';
|
||||
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) {
|
||||
"use strict";
|
||||
return AppComponent.extend("admin.Component", {
|
||||
return AppComponent.extend("books.Component", {
|
||||
metadata: { manifest: "json" }
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"_version": "1.8.0",
|
||||
"sap.app": {
|
||||
"id": "admin",
|
||||
"id": "books",
|
||||
"type": "application",
|
||||
"title": "Manage Books",
|
||||
"description": "Sample Application",
|
||||
"i18n": "i18n/i18n.properties",
|
||||
"dataSources": {
|
||||
"AdminService": {
|
||||
"uri": "/admin/",
|
||||
"uri": "admin/",
|
||||
"type": "OData",
|
||||
"settings": {
|
||||
"odataVersion": "4.0"
|
||||
@@ -22,6 +22,10 @@
|
||||
}
|
||||
},
|
||||
"sap.ui5": {
|
||||
"flexEnabled": true,
|
||||
"config": {
|
||||
"experimentalCAPScenario": true
|
||||
},
|
||||
"dependencies": {
|
||||
"libs": {
|
||||
"sap.fe.templates": {}
|
||||
@@ -73,6 +77,7 @@
|
||||
"options": {
|
||||
"settings" : {
|
||||
"entitySet" : "Books",
|
||||
"initialLoad": true,
|
||||
"navigation" : {
|
||||
"Books" : {
|
||||
"detail" : {
|
||||
@@ -1,55 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Bookshop</title>
|
||||
|
||||
<script>
|
||||
window["sap-ushell-config"] = {
|
||||
defaultRenderer: "fiori2",
|
||||
applications: {
|
||||
"browse-books": {
|
||||
title: "Browse Books",
|
||||
description: "w/ SAP Fiori Elements",
|
||||
additionalInformation: "SAPUI5.Component=bookshop",
|
||||
applicationType : "URL",
|
||||
url: "/browse/webapp",
|
||||
navigationMode: "embedded"
|
||||
},
|
||||
"manage-books": {
|
||||
title: "Manage Books",
|
||||
description: "w/ SAP Fiori Elements",
|
||||
additionalInformation: "SAPUI5.Component=admin",
|
||||
applicationType : "URL",
|
||||
url: "/admin/webapp",
|
||||
navigationMode: "embedded"
|
||||
},
|
||||
"manage-orders": {
|
||||
title: "Manage Orders",
|
||||
description: "w/ SAP Fiori Elements",
|
||||
additionalInformation: "SAPUI5.Component=orders",
|
||||
applicationType : "URL",
|
||||
url: "/orders/webapp",
|
||||
navigationMode: "embedded"
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script id="sap-ushell-bootstrap" src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
|
||||
<script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
|
||||
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
|
||||
data-sap-ui-compatVersion="edge"
|
||||
data-sap-ui-theme="sap_fiori_3"
|
||||
data-sap-ui-frameOptions="allow"
|
||||
></script>
|
||||
<script>
|
||||
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"))
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body class="sapUiBody" id="content"></body>
|
||||
</html>
|
||||
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
|
||||
//
|
||||
annotate CatalogService.Books with @(
|
||||
UI: {
|
||||
HeaderInfo: {
|
||||
TypeName: 'Book',
|
||||
TypeNamePlural: 'Books',
|
||||
Description: {Value: author}
|
||||
},
|
||||
HeaderFacets: [
|
||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Description}', Target: '@UI.FieldGroup#Descr'},
|
||||
],
|
||||
Facets: [
|
||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Price'},
|
||||
],
|
||||
FieldGroup#Descr: {
|
||||
Data: [
|
||||
{Value: descr},
|
||||
]
|
||||
},
|
||||
FieldGroup#Price: {
|
||||
Data: [
|
||||
{Value: price},
|
||||
{Value: currency.symbol, Label: '{i18n>Currency}'},
|
||||
]
|
||||
},
|
||||
}
|
||||
);
|
||||
annotate CatalogService.Books with @(UI : {
|
||||
HeaderInfo : {
|
||||
TypeName : 'Book',
|
||||
TypeNamePlural : 'Books',
|
||||
Description : {Value : author}
|
||||
},
|
||||
HeaderFacets : [{
|
||||
$Type : 'UI.ReferenceFacet',
|
||||
Label : '{i18n>Description}',
|
||||
Target : '@UI.FieldGroup#Descr'
|
||||
}, ],
|
||||
Facets : [{
|
||||
$Type : 'UI.ReferenceFacet',
|
||||
Label : '{i18n>Details}',
|
||||
Target : '@UI.FieldGroup#Price'
|
||||
}, ],
|
||||
FieldGroup #Descr : {Data : [{Value : descr}, ]},
|
||||
FieldGroup #Price : {Data : [
|
||||
{Value : price},
|
||||
{
|
||||
Value : currency.symbol,
|
||||
Label : '{i18n>Currency}'
|
||||
},
|
||||
]},
|
||||
});
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Books Object Page
|
||||
//
|
||||
annotate CatalogService.Books with @(
|
||||
UI: {
|
||||
SelectionFields: [ ID, price, currency_code ],
|
||||
LineItem: [
|
||||
{Value: title},
|
||||
{Value: author, Label:'{i18n>Author}'},
|
||||
{Value: genre.name},
|
||||
{Value: price},
|
||||
{Value: currency.symbol, Label:' '},
|
||||
]
|
||||
},
|
||||
);
|
||||
annotate CatalogService.Books with @(UI : {
|
||||
SelectionFields : [
|
||||
ID,
|
||||
price,
|
||||
currency_code
|
||||
],
|
||||
LineItem : [
|
||||
{
|
||||
Value : ID,
|
||||
Label : '{i18n>Title}'
|
||||
},
|
||||
{
|
||||
Value : author,
|
||||
Label : '{i18n>Author}'
|
||||
},
|
||||
{Value : genre.name},
|
||||
{Value : price},
|
||||
{
|
||||
Value : currency.symbol,
|
||||
Label : ' '
|
||||
},
|
||||
]
|
||||
}, );
|
||||
|
||||
@@ -1,28 +1,64 @@
|
||||
{
|
||||
"_version": "1.8.0",
|
||||
"_version": "1.28.0",
|
||||
"sap.app": {
|
||||
"id": "bookshop",
|
||||
"type": "application",
|
||||
"title": "Browse Books",
|
||||
"description": "Sample Application",
|
||||
"i18n": "i18n/i18n.properties",
|
||||
"applicationVersion": {
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"dataSources": {
|
||||
"CatalogService": {
|
||||
"uri": "/browse/",
|
||||
"uri": "browse/",
|
||||
"type": "OData",
|
||||
"settings": {
|
||||
"odataVersion": "4.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"-sourceTemplate": {
|
||||
"sourceTemplate": {
|
||||
"id": "ui5template.basicSAPUI5ApplicationProject",
|
||||
"-id": "ui5template.smartTemplate",
|
||||
"-version": "1.40.12"
|
||||
"version": "1.40.12"
|
||||
},
|
||||
"crossNavigation": {
|
||||
"inbounds": {
|
||||
"intent1": {
|
||||
"signature": {
|
||||
"parameters": {
|
||||
"Books.ID":{
|
||||
"renameTo": "ID"
|
||||
},
|
||||
"Authors.books.ID": {
|
||||
"renameTo": "ID"
|
||||
}
|
||||
},
|
||||
"additionalParameters": "ignored"
|
||||
},
|
||||
"semanticObject": "Books",
|
||||
"action": "display",
|
||||
"title": "{{appTitle}}",
|
||||
"info": "{{appInfo}}",
|
||||
"subTitle": "{{appSubTitle}}",
|
||||
"icon": "sap-icon://course-book",
|
||||
"indicatorDataSource": {
|
||||
"dataSource": "CatalogService",
|
||||
"path": "Books/$count",
|
||||
"refresh": 1800
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sap.ui5": {
|
||||
"flexEnabled": true,
|
||||
"config": {
|
||||
"experimentalCAPScenario": true
|
||||
},
|
||||
"dependencies": {
|
||||
"minUI5Version": "1.81.0",
|
||||
"libs": {
|
||||
"sap.fe.templates": {}
|
||||
}
|
||||
@@ -68,6 +104,7 @@
|
||||
"options": {
|
||||
"settings": {
|
||||
"entitySet": "Books",
|
||||
"initialLoad": true,
|
||||
"navigation": {
|
||||
"Books": {
|
||||
"detail": {
|
||||
@@ -97,7 +134,12 @@
|
||||
},
|
||||
"sap.ui": {
|
||||
"technology": "UI5",
|
||||
"fullWidth": false
|
||||
"fullWidth": false,
|
||||
"deviceTypes":{
|
||||
"desktop": true,
|
||||
"tablet": true,
|
||||
"phone": true
|
||||
}
|
||||
},
|
||||
"sap.fiori": {
|
||||
"registrationIds": [],
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
Common Annotations shared by all apps
|
||||
Common Annotations shared by all apps
|
||||
*/
|
||||
|
||||
using { sap.capire.bookshop as my } from '@capire/bookshop';
|
||||
using { sap.capire.bookshop as my } from '@capire/bookstore';
|
||||
using { sap.common } from '@capire/common';
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -10,39 +10,52 @@ using { sap.common } from '@capire/common';
|
||||
// Books Lists
|
||||
//
|
||||
annotate my.Books with @(
|
||||
Common.SemanticKey: [title],
|
||||
UI: {
|
||||
Identification: [{Value:title}],
|
||||
SelectionFields: [ ID, author_ID, price, currency_code ],
|
||||
LineItem: [
|
||||
{Value: ID},
|
||||
{Value: title},
|
||||
{Value: author.name, Label:'{i18n>Author}'},
|
||||
{Value: genre.name},
|
||||
{Value: stock},
|
||||
{Value: price},
|
||||
{Value: currency.symbol, Label:' '},
|
||||
]
|
||||
}
|
||||
Common.SemanticKey : [ID],
|
||||
UI : {
|
||||
Identification : [{Value : title}],
|
||||
SelectionFields : [
|
||||
ID,
|
||||
author_ID,
|
||||
price,
|
||||
currency_code
|
||||
],
|
||||
LineItem : [
|
||||
{
|
||||
Value : ID,
|
||||
Label : '{i18n>Title}'
|
||||
},
|
||||
{
|
||||
Value : author.ID,
|
||||
Label : '{i18n>Author}'
|
||||
},
|
||||
{Value : genre.name},
|
||||
{Value : stock},
|
||||
{Value : price},
|
||||
{
|
||||
Value : currency.symbol,
|
||||
Label : ' '
|
||||
},
|
||||
]
|
||||
}
|
||||
) {
|
||||
author @ValueList.entity:'Authors';
|
||||
ID @Common: {
|
||||
SemanticObject : 'Books',
|
||||
Text: title,
|
||||
TextArrangement : #TextOnly
|
||||
};
|
||||
author @ValueList.entity : 'Authors';
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Books Details
|
||||
//
|
||||
annotate my.Books with @(
|
||||
UI: {
|
||||
HeaderInfo: {
|
||||
TypeName: '{i18n>Book}',
|
||||
TypeNamePlural: '{i18n>Books}',
|
||||
Title: {Value: title},
|
||||
Description: {Value: author.name}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
annotate my.Books with @(UI : {HeaderInfo : {
|
||||
TypeName : '{i18n>Book}',
|
||||
TypeNamePlural : '{i18n>Books}',
|
||||
Title : {Value : title},
|
||||
Description : {Value : author.name}
|
||||
}, });
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -50,13 +63,19 @@ annotate my.Books with @(
|
||||
// Books Elements
|
||||
//
|
||||
annotate my.Books with {
|
||||
ID @title:'{i18n>ID}' @UI.HiddenFilter;
|
||||
title @title:'{i18n>Title}';
|
||||
genre @title:'{i18n>Genre}' @Common: { Text: genre.name, TextArrangement: #TextOnly };
|
||||
author @title:'{i18n>Author}' @Common: { Text: author.name, TextArrangement: #TextOnly };
|
||||
price @title:'{i18n>Price}';
|
||||
stock @title:'{i18n>Stock}';
|
||||
descr @UI.MultiLineText;
|
||||
ID @title : '{i18n>ID}';
|
||||
title @title : '{i18n>Title}';
|
||||
genre @title : '{i18n>Genre}' @Common : {
|
||||
Text : genre.name,
|
||||
TextArrangement : #TextOnly
|
||||
};
|
||||
author @title : '{i18n>Author}' @Common : {
|
||||
Text : author.name,
|
||||
TextArrangement : #TextOnly
|
||||
};
|
||||
price @title : '{i18n>Price}' @Measures.ISOCurrency : currency_code;
|
||||
stock @title : '{i18n>Stock}';
|
||||
descr @UI.MultiLineText;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -64,42 +83,45 @@ annotate my.Books with {
|
||||
// Genres List
|
||||
//
|
||||
annotate my.Genres with @(
|
||||
Common.SemanticKey: [name],
|
||||
UI: {
|
||||
SelectionFields: [ name ],
|
||||
LineItem:[
|
||||
{Value: name},
|
||||
{Value: parent.name, Label: 'Main Genre'},
|
||||
],
|
||||
}
|
||||
Common.SemanticKey : [name],
|
||||
UI : {
|
||||
SelectionFields : [name],
|
||||
LineItem : [
|
||||
{Value : name},
|
||||
{
|
||||
Value : parent.name,
|
||||
Label : 'Main Genre'
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Genre Details
|
||||
//
|
||||
annotate my.Genres with @(
|
||||
UI: {
|
||||
Identification: [{Value:name}],
|
||||
HeaderInfo: {
|
||||
TypeName: '{i18n>Genre}',
|
||||
TypeNamePlural: '{i18n>Genres}',
|
||||
Title: {Value: name},
|
||||
Description: {Value: ID}
|
||||
},
|
||||
Facets: [
|
||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>SubGenres}', Target: 'children/@UI.LineItem'},
|
||||
],
|
||||
}
|
||||
);
|
||||
annotate my.Genres with @(UI : {
|
||||
Identification : [{Value : name}],
|
||||
HeaderInfo : {
|
||||
TypeName : '{i18n>Genre}',
|
||||
TypeNamePlural : '{i18n>Genres}',
|
||||
Title : {Value : name},
|
||||
Description : {Value : ID}
|
||||
},
|
||||
Facets : [{
|
||||
$Type : 'UI.ReferenceFacet',
|
||||
Label : '{i18n>SubGenres}',
|
||||
Target : 'children/@UI.LineItem'
|
||||
}, ],
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Genres Elements
|
||||
//
|
||||
annotate my.Genres with {
|
||||
ID @title: '{i18n>ID}';
|
||||
name @title: '{i18n>Genre}';
|
||||
ID @title : '{i18n>ID}';
|
||||
name @title : '{i18n>Genre}';
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -107,38 +129,42 @@ annotate my.Genres with {
|
||||
// Authors List
|
||||
//
|
||||
annotate my.Authors with @(
|
||||
Common.SemanticKey: [name],
|
||||
UI: {
|
||||
Identification: [{Value:name}],
|
||||
SelectionFields: [ name ],
|
||||
LineItem:[
|
||||
{Value: ID},
|
||||
{Value: name},
|
||||
{Value: dateOfBirth},
|
||||
{Value: dateOfDeath},
|
||||
{Value: placeOfBirth},
|
||||
{Value: placeOfDeath},
|
||||
],
|
||||
}
|
||||
);
|
||||
Common.SemanticKey : [ID],
|
||||
UI : {
|
||||
Identification : [{Value : name}],
|
||||
SelectionFields : [name],
|
||||
LineItem : [
|
||||
{Value : ID},
|
||||
{Value : dateOfBirth},
|
||||
{Value : dateOfDeath},
|
||||
{Value : placeOfBirth},
|
||||
{Value : placeOfDeath},
|
||||
],
|
||||
}
|
||||
) {
|
||||
ID @Common: {
|
||||
SemanticObject : 'Authors',
|
||||
Text: name,
|
||||
TextArrangement : #TextOnly,
|
||||
};
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Author Details
|
||||
//
|
||||
annotate my.Authors with @(
|
||||
UI: {
|
||||
HeaderInfo: {
|
||||
TypeName: '{i18n>Author}',
|
||||
TypeNamePlural: '{i18n>Authors}',
|
||||
Title: {Value: name},
|
||||
Description: {Value: dateOfBirth}
|
||||
},
|
||||
Facets: [
|
||||
{$Type: 'UI.ReferenceFacet', Target: 'books/@UI.LineItem'},
|
||||
],
|
||||
}
|
||||
);
|
||||
annotate my.Authors with @(UI : {
|
||||
HeaderInfo : {
|
||||
TypeName : '{i18n>Author}',
|
||||
TypeNamePlural : '{i18n>Authors}',
|
||||
Title : {Value : name},
|
||||
Description : {Value : dateOfBirth}
|
||||
},
|
||||
Facets : [{
|
||||
$Type : 'UI.ReferenceFacet',
|
||||
Target : 'books/@UI.LineItem'
|
||||
}, ],
|
||||
});
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -146,12 +172,12 @@ annotate my.Authors with @(
|
||||
// Authors Elements
|
||||
//
|
||||
annotate my.Authors with {
|
||||
ID @title:'{i18n>ID}' @UI.HiddenFilter;
|
||||
name @title:'{i18n>Name}';
|
||||
dateOfBirth @title:'{i18n>DateOfBirth}';
|
||||
dateOfDeath @title:'{i18n>DateOfDeath}';
|
||||
placeOfBirth @title:'{i18n>PlaceOfBirth}';
|
||||
placeOfDeath @title:'{i18n>PlaceOfDeath}';
|
||||
ID @title : '{i18n>ID}';
|
||||
name @title : '{i18n>Name}';
|
||||
dateOfBirth @title : '{i18n>DateOfBirth}';
|
||||
dateOfDeath @title : '{i18n>DateOfDeath}';
|
||||
placeOfBirth @title : '{i18n>PlaceOfBirth}';
|
||||
placeOfDeath @title : '{i18n>PlaceOfDeath}';
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -159,99 +185,105 @@ annotate my.Authors with {
|
||||
// Languages List
|
||||
//
|
||||
annotate common.Languages with @(
|
||||
Common.SemanticKey: [code],
|
||||
Identification: [{Value:code}],
|
||||
UI: {
|
||||
SelectionFields: [ name, descr ],
|
||||
LineItem:[
|
||||
{Value: code},
|
||||
{Value: name},
|
||||
],
|
||||
}
|
||||
Common.SemanticKey : [code],
|
||||
Identification : [{Value : code}],
|
||||
UI : {
|
||||
SelectionFields : [
|
||||
name,
|
||||
descr
|
||||
],
|
||||
LineItem : [
|
||||
{Value : code},
|
||||
{Value : name},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Language Details
|
||||
//
|
||||
annotate common.Languages with @(
|
||||
UI: {
|
||||
HeaderInfo: {
|
||||
TypeName: '{i18n>Language}',
|
||||
TypeNamePlural: '{i18n>Languages}',
|
||||
Title: {Value: name},
|
||||
Description: {Value: descr}
|
||||
},
|
||||
Facets: [
|
||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
||||
],
|
||||
FieldGroup#Details: {
|
||||
Data: [
|
||||
{Value: code},
|
||||
{Value: name},
|
||||
{Value: descr}
|
||||
]
|
||||
},
|
||||
}
|
||||
);
|
||||
annotate common.Languages with @(UI : {
|
||||
HeaderInfo : {
|
||||
TypeName : '{i18n>Language}',
|
||||
TypeNamePlural : '{i18n>Languages}',
|
||||
Title : {Value : name},
|
||||
Description : {Value : descr}
|
||||
},
|
||||
Facets : [{
|
||||
$Type : 'UI.ReferenceFacet',
|
||||
Label : '{i18n>Details}',
|
||||
Target : '@UI.FieldGroup#Details'
|
||||
}, ],
|
||||
FieldGroup #Details : {Data : [
|
||||
{Value : code},
|
||||
{Value : name},
|
||||
{Value : descr}
|
||||
]},
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Currencies List
|
||||
//
|
||||
annotate common.Currencies with @(
|
||||
Common.SemanticKey: [code],
|
||||
Identification: [{Value:code}],
|
||||
UI: {
|
||||
SelectionFields: [ name, descr ],
|
||||
LineItem:[
|
||||
{Value: descr},
|
||||
{Value: symbol},
|
||||
{Value: code},
|
||||
],
|
||||
}
|
||||
Common.SemanticKey : [code],
|
||||
Identification : [{Value : code}],
|
||||
UI : {
|
||||
SelectionFields : [
|
||||
name,
|
||||
descr
|
||||
],
|
||||
LineItem : [
|
||||
{Value : descr},
|
||||
{Value : symbol},
|
||||
{Value : code},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Currency Details
|
||||
//
|
||||
annotate common.Currencies with @(
|
||||
UI: {
|
||||
HeaderInfo: {
|
||||
TypeName: '{i18n>Currency}',
|
||||
TypeNamePlural: '{i18n>Currencies}',
|
||||
Title: {Value: descr},
|
||||
Description: {Value: code}
|
||||
},
|
||||
Facets: [
|
||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Extended}', Target: '@UI.FieldGroup#Extended'},
|
||||
],
|
||||
FieldGroup#Details: {
|
||||
Data: [
|
||||
{Value: name},
|
||||
{Value: symbol},
|
||||
{Value: code},
|
||||
{Value: descr}
|
||||
]
|
||||
},
|
||||
FieldGroup#Extended: {
|
||||
Data: [
|
||||
{Value: numcode},
|
||||
{Value: minor},
|
||||
{Value: exponent}
|
||||
]
|
||||
},
|
||||
}
|
||||
);
|
||||
annotate common.Currencies with @(UI : {
|
||||
HeaderInfo : {
|
||||
TypeName : '{i18n>Currency}',
|
||||
TypeNamePlural : '{i18n>Currencies}',
|
||||
Title : {Value : descr},
|
||||
Description : {Value : code}
|
||||
},
|
||||
Facets : [
|
||||
{
|
||||
$Type : 'UI.ReferenceFacet',
|
||||
Label : '{i18n>Details}',
|
||||
Target : '@UI.FieldGroup#Details'
|
||||
},
|
||||
{
|
||||
$Type : 'UI.ReferenceFacet',
|
||||
Label : '{i18n>Extended}',
|
||||
Target : '@UI.FieldGroup#Extended'
|
||||
},
|
||||
],
|
||||
FieldGroup #Details : {Data : [
|
||||
{Value : name},
|
||||
{Value : symbol},
|
||||
{Value : code},
|
||||
{Value : descr}
|
||||
]},
|
||||
FieldGroup #Extended : {Data : [
|
||||
{Value : numcode},
|
||||
{Value : minor},
|
||||
{Value : exponent}
|
||||
]},
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Currencies Elements
|
||||
//
|
||||
annotate common.Currencies with {
|
||||
numcode @title:'{i18n>NumCode}';
|
||||
minor @title:'{i18n>MinorUnit}';
|
||||
exponent @title:'{i18n>Exponent}';
|
||||
numcode @title : '{i18n>NumCode}';
|
||||
minor @title : '{i18n>MinorUnit}';
|
||||
exponent @title : '{i18n>Exponent}';
|
||||
}
|
||||
|
||||
48
fiori/app/fiori-apps.html
Normal file
48
fiori/app/fiori-apps.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Bookshop</title>
|
||||
|
||||
<script>
|
||||
window["sap-ushell-config"] = {
|
||||
defaultRenderer: "fiori2",
|
||||
applications: {},
|
||||
bootstrapPlugins: {
|
||||
RuntimeAuthoringPlugin: {
|
||||
component: "sap.ushell.plugins.rta",
|
||||
config: {
|
||||
validateAppVersion: false,
|
||||
},
|
||||
},
|
||||
PersonalizePlugin: {
|
||||
component: "sap.ushell.plugins.rta-personalize",
|
||||
config: {
|
||||
validateAppVersion: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script id="sap-ushell-bootstrap" src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
|
||||
<script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
|
||||
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
|
||||
data-sap-ui-compatVersion="edge"
|
||||
data-sap-ui-theme="sap_horizon"
|
||||
data-sap-ui-frameOptions="allow"
|
||||
></script>
|
||||
<script>
|
||||
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"));
|
||||
sap.ui
|
||||
.getCore()
|
||||
.getConfiguration()
|
||||
.setFlexibilityServices([{ connector: "SessionStorageConnector" }]);
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body class="sapUiBody" id="content"></body>
|
||||
</html>
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
This model controls what gets served to Fiori frontends...
|
||||
*/
|
||||
|
||||
using from './admin/fiori-service';
|
||||
using from './browse/fiori-service';
|
||||
using from './common';
|
||||
|
||||
using from '@capire/common';
|
||||
|
||||
// only works in case of embedded orders service
|
||||
using from '@capire/orders/app/orders/fiori-service';
|
||||
@@ -1,3 +0,0 @@
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0;url=reviews/index.html">
|
||||
</head>
|
||||
9
fiori/app/services.cds
Normal file
9
fiori/app/services.cds
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
This model controls what gets served to Fiori frontends...
|
||||
*/
|
||||
|
||||
using from './admin-authors/fiori-service';
|
||||
using from './admin-books/fiori-service';
|
||||
using from './browse/fiori-service';
|
||||
using from './common';
|
||||
using from '@capire/bookstore/srv/mashup';
|
||||
@@ -2,7 +2,7 @@
|
||||
// Add Author.age and .lifetime with a DB-specific function
|
||||
//
|
||||
|
||||
using { AdminService } from '..';
|
||||
using { AdminService } from '@capire/bookshop';
|
||||
|
||||
extend projection AdminService.Authors with {
|
||||
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
|
||||
//
|
||||
|
||||
using { AdminService } from '..';
|
||||
using { AdminService } from '@capire/bookshop';
|
||||
|
||||
extend projection AdminService.Authors with {
|
||||
strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer,
|
||||
|
||||
@@ -2,25 +2,19 @@
|
||||
"name": "@capire/fiori",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@capire/bookshop": "*",
|
||||
"@capire/reviews": "*",
|
||||
"@capire/orders": "*",
|
||||
"@capire/common": "*",
|
||||
"@sap/cds": "^4",
|
||||
"@capire/bookstore": "*",
|
||||
"@sap/cds": ">=5",
|
||||
"express": "^4.17.1",
|
||||
"passport": "^0.4.1"
|
||||
"passport": ">=0.4.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cds run --in-memory?",
|
||||
"watch": "cds watch"
|
||||
},
|
||||
"cds": {
|
||||
"hana": {
|
||||
"deploy-format": "hdbtable"
|
||||
},
|
||||
"requires": {
|
||||
"auth": {
|
||||
"strategy": "dummy"
|
||||
"kind": "dummy-auth"
|
||||
},
|
||||
"ReviewsService": {
|
||||
"kind": "odata",
|
||||
@@ -30,15 +24,34 @@
|
||||
"kind": "odata",
|
||||
"model": "@capire/orders"
|
||||
},
|
||||
"messaging": {
|
||||
"[production]": {
|
||||
"kind": "enterprise-messaging"
|
||||
},
|
||||
"[development]": {
|
||||
"kind": "file-based-messaging"
|
||||
},
|
||||
"[hybrid!]": {
|
||||
"kind": "enterprise-messaging-shared"
|
||||
}
|
||||
},
|
||||
"db": {
|
||||
"kind": "sql",
|
||||
"kind": "sqlite",
|
||||
"credentials": {
|
||||
"database": "sqlite.db"
|
||||
}
|
||||
},
|
||||
"db-ext": {
|
||||
"[development]": {
|
||||
"model": "db/sqlite"
|
||||
},
|
||||
"[production]": {
|
||||
"model": "db/hana"
|
||||
}
|
||||
},
|
||||
"hana": {
|
||||
"deploy-format": "hdbtable"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1 @@
|
||||
const cds = require ('@sap/cds')
|
||||
|
||||
cds.once('bootstrap',(app)=>{
|
||||
app.use ('/orders/webapp', _from('@capire/orders/app/orders/webapp/manifest.json'))
|
||||
app.use ('/bookshop', _from('@capire/bookshop/app/vue/index.html'))
|
||||
app.use ('/reviews', _from('@capire/reviews/app/vue/index.html'))
|
||||
})
|
||||
|
||||
cds.once('served', require('./srv/mashup'))
|
||||
|
||||
module.exports = cds.server
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Helper for serving static content from npm-installed packages
|
||||
const {static} = require('express')
|
||||
const {dirname} = require('path')
|
||||
const _from = target => static (dirname (require.resolve(target)))
|
||||
module.exports = require('@capire/bookstore/server.js')
|
||||
|
||||
@@ -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
|
||||
}
|
||||
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",
|
||||
"version": "1.0.0",
|
||||
"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": "*",
|
||||
"@types/node": "*",
|
||||
"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
|
||||
@@ -68,16 +68,16 @@ annotate OrdersService.Orders with @(
|
||||
|
||||
|
||||
|
||||
annotate OrdersService.Orders_Items with @(
|
||||
annotate OrdersService.Orders.Items with @(
|
||||
UI: {
|
||||
LineItem: [
|
||||
{Value: product_ID, Label:'Product ID'},
|
||||
{Value: title, Label:'Product Title'},
|
||||
{Value: price, Label:'Unit Price'},
|
||||
{Value: amount, Label:'Quantity'},
|
||||
{Value: quantity, Label:'Quantity'},
|
||||
],
|
||||
Identification: [ //Is the main field group
|
||||
{Value: amount, Label:'Amount'},
|
||||
{Value: quantity, Label:'Quantity'},
|
||||
{Value: title, Label:'Product'},
|
||||
{Value: price, Label:'Unit Price'},
|
||||
],
|
||||
@@ -86,7 +86,7 @@ annotate OrdersService.Orders_Items with @(
|
||||
],
|
||||
},
|
||||
) {
|
||||
amount @(
|
||||
quantity @(
|
||||
Common.FieldControl: #Mandatory
|
||||
);
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
/*
|
||||
This model controls what gets served to Fiori frontends...
|
||||
*/
|
||||
|
||||
using from './orders/fiori-service';
|
||||
@@ -25,10 +25,10 @@
|
||||
|
||||
<script id="sap-ushell-bootstrap" src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
|
||||
<script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
|
||||
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
|
||||
data-sap-ui-compatVersion="edge"
|
||||
data-sap-ui-theme="sap_fiori_3"
|
||||
data-sap-ui-frameOptions="allow"
|
||||
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
|
||||
data-sap-ui-compatVersion="edge"
|
||||
data-sap-ui-theme="sap_horizon"
|
||||
data-sap-ui-frameOptions="allow"
|
||||
></script>
|
||||
<script>
|
||||
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"))
|
||||
@@ -1,4 +1,4 @@
|
||||
ID;up__ID;amount;product_ID;title;price
|
||||
ID;up__ID;quantity;product_ID;title;price
|
||||
58040e66-1dcd-4ffb-ab10-fdce32028b79;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;201;Wuthering Heights;11.11
|
||||
64e718c9-ff99-47f1-8ca3-950c850777d4;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;271;Catweazle;15
|
||||
e9641166-e050-4261-bfee-d1e797e6cb7f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Eleonora;28
|
||||
|
@@ -3,21 +3,22 @@ namespace sap.capire.orders;
|
||||
|
||||
entity Orders : cuid, managed {
|
||||
OrderNo : String @title:'Order Number'; //> readable key
|
||||
Items : Composition of many Orders_Items on Items.up_ = $self;
|
||||
Items : Composition of many {
|
||||
key ID : UUID;
|
||||
product : Association to Products;
|
||||
quantity : Integer;
|
||||
title : String; //> intentionally replicated as snapshot from product.title
|
||||
price : Double; //> materialized calculated field
|
||||
};
|
||||
buyer : User;
|
||||
currency : Currency;
|
||||
}
|
||||
|
||||
entity Orders_Items {
|
||||
key ID : UUID;
|
||||
up_ : Association to Orders;
|
||||
product : Association to Products @assert.integrity:false; // REVISIT: this is a temporary workaround for a glitch in cds-runtime
|
||||
amount : Integer;
|
||||
title : String;
|
||||
price : Double;
|
||||
}
|
||||
|
||||
/** This is a stand-in for arbitrary ordered Products */
|
||||
entity Products @(cds.persistence.skip:'always') {
|
||||
key ID : String;
|
||||
}
|
||||
|
||||
|
||||
// this is to ensure we have filled-in currencies
|
||||
using from '@capire/common';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "@capire/orders",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@sap/cds": "^4.3.0"
|
||||
"@capire/common": "*",
|
||||
"@sap/cds": ">=5"
|
||||
}
|
||||
}
|
||||
@@ -3,34 +3,34 @@ class OrdersService extends cds.ApplicationService {
|
||||
|
||||
/** register custom handlers */
|
||||
init(){
|
||||
const { Orders_Items:OrderItems } = this.entities
|
||||
const { 'Orders.Items':OrderItems } = this.entities
|
||||
|
||||
this.before ('UPDATE', 'Orders', async function(req) {
|
||||
const { ID, Items } = req.data
|
||||
if (Items) for (let { product_ID, amount } of Items) {
|
||||
const { amount:before } = await cds.tx(req).run (
|
||||
SELECT.one.from (OrderItems, oi => oi.amount) .where ({up__ID:ID, product_ID})
|
||||
if (Items) for (let { product_ID, quantity } of Items) {
|
||||
const { quantity:before } = await cds.tx(req).run (
|
||||
SELECT.one.from (OrderItems, oi => oi.quantity) .where ({up__ID:ID, product_ID})
|
||||
)
|
||||
if (amount != before) await this.orderChanged (product_ID, amount-before)
|
||||
if (quantity != before) await this.orderChanged (product_ID, quantity-before)
|
||||
}
|
||||
})
|
||||
|
||||
this.before ('DELETE', 'Orders', async function(req) {
|
||||
const { ID } = req.data
|
||||
const Items = await cds.tx(req).run (
|
||||
SELECT.from (OrderItems, oi => { oi.product_ID, oi.amount }) .where ({up__ID:ID})
|
||||
SELECT.from (OrderItems, oi => { oi.product_ID, oi.quantity }) .where ({up__ID:ID})
|
||||
)
|
||||
if (Items) await Promise.all (Items.map(it => this.orderChanged (it.product_ID, -it.amount)))
|
||||
if (Items) await Promise.all (Items.map(it => this.orderChanged (it.product_ID, -it.quantity)))
|
||||
})
|
||||
|
||||
return super.init()
|
||||
}
|
||||
|
||||
/** order changed -> broadcast event */
|
||||
orderChanged (product, deltaAmount) {
|
||||
orderChanged (product, deltaQuantity) {
|
||||
// Emit events to inform subscribers about changes in orders
|
||||
console.log ('> emitting:', 'OrderChanged', { product, deltaAmount })
|
||||
return this.emit ('OrderChanged', { product, deltaAmount })
|
||||
console.log ('> emitting:', 'OrderChanged', { product, deltaQuantity })
|
||||
return this.emit ('OrderChanged', { product, deltaQuantity })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
2540
package-lock.json
generated
Normal file
2540
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@@ -5,34 +5,47 @@
|
||||
"repository": "https://github.com/sap-samples/cloud-cap-samples.git",
|
||||
"author": "daniel.hutzel@sap.com",
|
||||
"dependencies": {
|
||||
"@capire/bookstore": "./bookstore",
|
||||
"@capire/bookshop": "./bookshop",
|
||||
"@capire/common": "./common",
|
||||
"@capire/data-viewer": "./data-viewer",
|
||||
"@capire/fiori": "./fiori",
|
||||
"@capire/hello": "./hello",
|
||||
"@capire/media": "./media",
|
||||
"@capire/orders": "./orders",
|
||||
"@capire/reviews": "./reviews"
|
||||
"@capire/reviews": "./reviews",
|
||||
"@sap/cds": ">=5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.2.0",
|
||||
"chai": "^4.3.4",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-subset": "^1.6.0",
|
||||
"sqlite3": "5.0.0"
|
||||
"semver": "^7",
|
||||
"sqlite3": "^5"
|
||||
},
|
||||
"scripts": {
|
||||
"cleanup": "rm -rf node_modules && rm -rf */node_modules && rm -rf */*/node_modules",
|
||||
"registry": "node .registry/server.js",
|
||||
"bookshop": "cds watch bookshop",
|
||||
"fiori": "cds watch fiori",
|
||||
"hello": "cds watch hello",
|
||||
"media": "cds watch media",
|
||||
"mocha": "npx mocha || echo",
|
||||
"jest": "npx jest",
|
||||
"test": "npm run jest --silent"
|
||||
},
|
||||
"mocha": {
|
||||
"parallel": true
|
||||
"start": "cds watch fiori",
|
||||
"test": "npm run jest -- --silent",
|
||||
"test:hello": "cd hello && npm test"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node"
|
||||
"testEnvironment": "node",
|
||||
"testTimeout": 20000,
|
||||
"testMatch": [
|
||||
"**/*.test.js"
|
||||
]
|
||||
},
|
||||
"mocha": {
|
||||
"recursive": true,
|
||||
"parallel": true
|
||||
},
|
||||
"license": "SAP SAMPLE CODE LICENSE",
|
||||
"private": true
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
cds.requires.messaging.kind = file-based-messaging
|
||||
# cds.requires.messaging.kind = file-based-messaging
|
||||
PORT = 4005
|
||||
@@ -4,21 +4,21 @@ const GET = (url) => axios.get('/reviews'+url)
|
||||
const PUT = (cmd,data) => axios.patch('/reviews'+cmd,data)
|
||||
const POST = (cmd,data) => axios.post('/reviews'+cmd,data)
|
||||
|
||||
const reviews = new Vue ({
|
||||
const reviews = Vue.createApp ({
|
||||
|
||||
el:'#app',
|
||||
|
||||
data: {
|
||||
list: [],
|
||||
review: undefined,
|
||||
message: {},
|
||||
Ratings: Object.entries({
|
||||
data() {
|
||||
return {
|
||||
list: [],
|
||||
review: undefined,
|
||||
message: {},
|
||||
Ratings: Object.entries({
|
||||
5 : '★★★★★',
|
||||
4 : '★★★★',
|
||||
3 : '★★★',
|
||||
2 : '★★',
|
||||
1 : '★',
|
||||
}).reverse()
|
||||
}).reverse()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -66,7 +66,7 @@ const reviews = new Vue ({
|
||||
datetime: (d) => d && new Date(d).toLocaleString(),
|
||||
},
|
||||
|
||||
})
|
||||
}).mount("#app")
|
||||
|
||||
// initially fill list of my reviews
|
||||
reviews.fetch()
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<title> Capire Reviews </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"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
||||
<style>
|
||||
.hovering tr:hover td { color:cyan; background: #123; cursor: pointer; }
|
||||
.rating-stars { color:teal }
|
||||
@@ -18,7 +18,7 @@
|
||||
<body class="small-container", style="margin-top: 70px;">
|
||||
<div id='app'>
|
||||
|
||||
<h1> {{ document.title }} </h1>
|
||||
<h1> Capire Reviews </h1>
|
||||
|
||||
<input type="text" placeholder="Search..." @input="search">
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user