Compare commits
4 Commits
audit-log-
...
dreiklang
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c49454f44 | ||
|
|
d570ad20b2 | ||
|
|
d891a1b905 | ||
|
|
9f604b9d13 |
@@ -8,7 +8,7 @@
|
|||||||
"mocha": true
|
"mocha": true
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2020
|
"ecmaVersion": 2018
|
||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
"SELECT": true,
|
"SELECT": true,
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"require-atomic-updates": "off",
|
"require-atomic-updates": "off",
|
||||||
"require-await":"warn",
|
"require-await":"warn"
|
||||||
"no-unused-vars": ["warn", { "argsIgnorePattern": "_" }]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
.github/workflows/node.js.yml
vendored
8
.github/workflows/node.js.yml
vendored
@@ -5,9 +5,9 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ master ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [ master ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [16.x, 14.x, 12.x]
|
node-version: [12.x, 14.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -24,5 +24,5 @@ jobs:
|
|||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: npm ci
|
- run: npm install
|
||||||
- run: npm test
|
- run: npm test
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,5 +15,3 @@ default-env.json
|
|||||||
packages/messageBox
|
packages/messageBox
|
||||||
reviews/msg-box
|
reviews/msg-box
|
||||||
reviews/db/test.db
|
reviews/db/test.db
|
||||||
|
|
||||||
*.openapi3.json
|
|
||||||
|
|||||||
3
.npmrc
3
.npmrc
@@ -1,3 +0,0 @@
|
|||||||
# Ensure we always use public packages, i.e. avoid using local registries from ~/.npmrc
|
|
||||||
@sap:registry=https://registry.npmjs.org/
|
|
||||||
registry=https://registry.npmjs.org/
|
|
||||||
@@ -1,24 +1,18 @@
|
|||||||
const { exec } = require ('child_process')
|
const { exec } = require ('child_process')
|
||||||
const isWin = process.platform === 'win32'
|
|
||||||
const express = require ('express')
|
const express = require ('express')
|
||||||
const fs = require ('fs')
|
const fs = require ('fs')
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
const { PORT=4444 } = process.env
|
const { PORT=4444 } = process.env
|
||||||
const [,,port=PORT,scope='@capire'] = process.argv
|
const [,,port=PORT] = process.argv
|
||||||
const cwd = __dirname
|
const cwd = __dirname
|
||||||
|
|
||||||
// clean up on start (exit handler might not complete on Windows)
|
|
||||||
exec(isWin ? 'del *.tgz' : 'rm *.tgz', {cwd})
|
|
||||||
|
|
||||||
|
|
||||||
app.use('/-/:tarball', (req,res,next) => {
|
app.use('/-/:tarball', (req,res,next) => {
|
||||||
console.debug ('GET', req.params)
|
console.debug ('GET', req.params)
|
||||||
try {
|
try {
|
||||||
const { tarball } = req.params
|
const { tarball } = req.params
|
||||||
const [, pkg ] = /^\w+-(\w+)/.exec(tarball)
|
const [, pkg ] = /^capire-(\w+)/.exec(tarball)
|
||||||
fs.lstat(tarball,(err => {
|
fs.lstat(tarball,(err => {
|
||||||
if (err) console.debug (`npm pack ../${pkg}`)
|
|
||||||
if (err) exec(`npm pack ../${pkg}`,{cwd},next)
|
if (err) exec(`npm pack ../${pkg}`,{cwd},next)
|
||||||
else next()
|
else next()
|
||||||
}))
|
}))
|
||||||
@@ -31,14 +25,12 @@ app.use('/-/:tarball', (req,res,next) => {
|
|||||||
app.use('/-', express.static(__dirname))
|
app.use('/-', express.static(__dirname))
|
||||||
|
|
||||||
app.get('/*', (req,res)=>{
|
app.get('/*', (req,res)=>{
|
||||||
const urlRegex = /^\/(@\w+)\/(\w+)/
|
|
||||||
const url = decodeURIComponent(req.url)
|
const url = decodeURIComponent(req.url)
|
||||||
console.debug ('GET',url)
|
console.debug ('GET',url)
|
||||||
try {
|
try {
|
||||||
if (!urlRegex.test(url)) return res.sendStatus(404)
|
const [, capire, pkg ] = /^\/(@capire)\/(\w+)/.exec(url)
|
||||||
const [, scpe, pkg ] = urlRegex.exec(url)
|
const package = require (`${capire}/${pkg}/package.json`)
|
||||||
const package = require (`${scpe}/${pkg}/package.json`)
|
const tarball = `capire-${pkg}-${package.version}.tgz`
|
||||||
const tarball = `${scpe.slice(1)}-${pkg}-${package.version}.tgz`
|
|
||||||
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
|
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
|
||||||
res.json({
|
res.json({
|
||||||
"name": package.name,
|
"name": package.name,
|
||||||
@@ -50,30 +42,29 @@ app.get('/*', (req,res)=>{
|
|||||||
"name": package.name,
|
"name": package.name,
|
||||||
"version": package.version,
|
"version": package.version,
|
||||||
"dist": {
|
"dist": {
|
||||||
"tarball": `${server.url}/-/${tarball}`
|
"tarball": `http://localhost:${port}/-/${tarball}`
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code === 'MODULE_NOT_FOUND') return res.sendStatus(404)
|
console.error(e)
|
||||||
console.error(e); throw e
|
res.sendStatus(404)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const server = app.listen(port, ()=>{
|
app.listen(port, ()=>{
|
||||||
const url = server.url = `http://localhost:${server.address().port}`
|
console.log (`npm set @capire:registry=http://localhost:${port}`)
|
||||||
console.log (`npm set ${scope}:registry=${url}`)
|
console.log (`@capire registry listening on http://localhost:${port}`)
|
||||||
exec(`npm set ${scope}:registry=${url}`)
|
exec(`npm set @capire:registry=http://localhost:${port}`)
|
||||||
console.log (`${scope} registry listening on ${url}`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const _exit = ()=>{
|
const _exit = ()=>{
|
||||||
server.close()
|
console.log ('\nnpm conf rm @capire:registry')
|
||||||
exec(`npm conf rm "${scope}:registry"`, ()=> { process.exit() })
|
exec('npm conf rm @capire:registry')
|
||||||
|
exec('rm *.tgz')
|
||||||
|
process.exit()
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on ('SIGTERM',_exit)
|
process.on ('SIGTERM',_exit)
|
||||||
process.on ('SIGHUP',_exit)
|
process.on ('SIGHUP',_exit)
|
||||||
process.on ('SIGINT',_exit)
|
process.on ('SIGINT',_exit)
|
||||||
|
|||||||
@@ -68,35 +68,26 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/package.json",
|
"file": "fiori/package.json",
|
||||||
"description": "#### Configuration\n\nThe `cds.requires` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\n\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile. `db-ext` is a pseudo datasource, its name doesn't matter.\n\nSee [`cds.resolve`](https://cap.cloud.sap/docs/node.js/cds-compile#cds-resolve) to learn more about how models are found.",
|
"description": "#### Configuration\n\nThe `cds` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile.",
|
||||||
"selection": {
|
"line": 17,
|
||||||
"start": {
|
|
||||||
"line": 41,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 48,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Configuration"
|
"title": "Configuration"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/package.json",
|
"file": "fiori/package.json",
|
||||||
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch fiori\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
|
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch fiori\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
|
||||||
"line": 43,
|
"line": 28,
|
||||||
"title": "Run with SQLite"
|
"title": "Run with SQLite"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/package.json",
|
"file": "fiori/package.json",
|
||||||
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd fiori; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd fiori; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
|
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd fiori; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd fiori; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
|
||||||
"line": 46,
|
"line": 31,
|
||||||
"title": "Run with SAP HANA"
|
"title": "Run with SAP HANA"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/test/requests.http",
|
"file": "fiori/test/requests.http",
|
||||||
"description": "### Add More Data\n\nOptionally you can add some `Authors` data by clicking on the _Send Request_ link (provided by the [REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension).",
|
"description": "### Add More Data\n\nOptionally you can add some `Authors` data by clicking on the _Send Request_ link (provided by the [REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension).",
|
||||||
"line": 72,
|
"line": 68,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
"line": 67,
|
"line": 67,
|
||||||
@@ -113,5 +104,6 @@
|
|||||||
"title": "Wrap-up",
|
"title": "Wrap-up",
|
||||||
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
|
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"ref": "master"
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "hello/srv/world.cds",
|
"file": "hello/world.cds",
|
||||||
"description": "### Hello World!\n\nThis is a simplistic [Hello World](https://cap.cloud.sap/docs/get-started/hello-world) service using [CDS](https://cap.cloud.sap/docs/cds/) and [cds.services](https://cap.cloud.sap/docs/node.js/api#services-api).",
|
"description": "### Hello World!\n\nThis is a simplistic [Hello World](https://cap.cloud.sap/docs/get-started/hello-world) service using [CDS](https://cap.cloud.sap/docs/cds/) and [cds.services](https://cap.cloud.sap/docs/node.js/api#services-api).",
|
||||||
"line": 2,
|
"line": 2,
|
||||||
"selection": {
|
"selection": {
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "orders/db/schema.cds",
|
"file": "orders/db/schema.cds",
|
||||||
"description": "### Orders - Compositions and Serving Documents\n\nA standalone orders management service, demonstrating:\n- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with\n- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)\n",
|
"description": "### Compositions and Serving Documents\n\nA standalone orders management service, demonstrating:\n- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with\n- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)\n",
|
||||||
"line": 1,
|
"line": 1,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "reviews/db/schema.cds",
|
"file": "reviews/db/schema.cds",
|
||||||
"description": "### Reviews - More Modularity\n\nShows how to implement a modular service to manage product reviews, including:\n- Consuming other services synchronously and asynchronously\n- Serving requests synchronously\n- Emitting events asynchronously\n- Grow as you go, with:\n- Mocking app services\n- Running service meshes\n- Late-cut Micro Services\n- As well as managed data, input validations, and authorization\n",
|
"description": "### More Modularity\n\nShows how to implement a modular service to manage product reviews, including:\n- Consuming other services synchronously and asynchronously\n- Serving requests synchronously\n- Emitting events asynchronously\n- Grow as you go, with:\n- Mocking app services\n- Running service meshes\n- Late-cut Micro Services\n- As well as managed data, input validations, and authorization\n",
|
||||||
"line": 1,
|
"line": 1,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
@@ -99,12 +99,8 @@
|
|||||||
"title": "Reviews"
|
"title": "Reviews"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Bookstore",
|
"file": "fiori/app/index.cds",
|
||||||
"description": "### Bookstore - Reuse and UI\n\n- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/reuse-and-compose) these packages:\n - [@capire/bookshop](bookshop)\n - [@capire/reviews](reviews)\n - [@capire/orders](orders)\n - [@capire/common](common)\n- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well\n- [The Vue.js app](reviews/app/vue) imported from reviews is served as well\n- [The Fiori app](orders/app) imported from orders is served as well\n- [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)"
|
"description": "### Annotations for SAP Fiori Elements\n\nA [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:\n - [@capire/bookshop](bookshop)\n - [@capire/reviews](reviews)\n - [@capire/orders](orders)\n - [@capire/common](common)\n\n[Adds a SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookshop, thereby introducing to:\n - [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files\n - Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)\n - Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)\n - Serving SAP Fiori apps locally\n\n[The Vue.js app](bookshop/app/vue) imported from bookshop is served as well.\n",
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/app/services.cds",
|
|
||||||
"description": "### Annotations for SAP Fiori Elements\n\n- [Adds an SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookstore, thereby introducing to:\n- [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files\n- Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)\n- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)\n- Serving SAP Fiori apps locally\n",
|
|
||||||
"line": 1,
|
"line": 1,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
@@ -121,13 +117,14 @@
|
|||||||
{
|
{
|
||||||
"file": "package.json",
|
"file": "package.json",
|
||||||
"description": "### All-in-one Monorepo\n\nEach sample sub directory essentially is a standard npm package, some with standard npm dependencies to other samples. The root folder's [package.json](package.json) has local links to the sub folders, such that an `npm install` populates a local `node_modules` folder acts like a local npm registry to the individual sample packages.\n",
|
"description": "### All-in-one Monorepo\n\nEach sample sub directory essentially is a standard npm package, some with standard npm dependencies to other samples. The root folder's [package.json](package.json) has local links to the sub folders, such that an `npm install` populates a local `node_modules` folder acts like a local npm registry to the individual sample packages.\n",
|
||||||
|
"line": 8,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
"line": 8,
|
"line": 8,
|
||||||
"character": 1
|
"character": 1
|
||||||
},
|
},
|
||||||
"end": {
|
"end": {
|
||||||
"line": 16,
|
"line": 15,
|
||||||
"character": 1
|
"character": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -13,6 +13,5 @@
|
|||||||
"**/cds/lib/req/cls.js",
|
"**/cds/lib/req/cls.js",
|
||||||
"**/odata-v4/okra/**"
|
"**/odata-v4/okra/**"
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"mochaExplorer.parallel": true
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Find here a collection of samples for the [SAP Cloud Application Programming Mod
|
|||||||
|
|
||||||
### Download
|
### Download
|
||||||
|
|
||||||
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/main.zip).
|
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/master.zip).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/sap-samples/cloud-cap-samples samples
|
git clone https://github.com/sap-samples/cloud-cap-samples samples
|
||||||
@@ -83,4 +83,4 @@ In case you've a question, find a bug, or otherwise need support, use our [commu
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2022 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSE) file.
|
Copyright (c) 2021 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSE.txt) file.
|
||||||
|
|||||||
2
bookshop/app/services.cds
Normal file
2
bookshop/app/services.cds
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Incorporate pre-build extensions from...
|
||||||
|
using from '@capire/common';
|
||||||
@@ -10,7 +10,7 @@ const books = new Vue ({
|
|||||||
data: {
|
data: {
|
||||||
list: [],
|
list: [],
|
||||||
book: undefined,
|
book: undefined,
|
||||||
order: { quantity:1, succeeded:'', failed:'' }
|
order: { amount:1, succeeded:'', failed:'' }
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -26,18 +26,18 @@ const books = new Vue ({
|
|||||||
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
|
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
|
||||||
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
|
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
|
||||||
Object.assign (book, res.data)
|
Object.assign (book, res.data)
|
||||||
books.order = { quantity:1 }
|
books.order = { amount:1 }
|
||||||
setTimeout (()=> $('form > input').focus(), 111)
|
setTimeout (()=> $('form > input').focus(), 111)
|
||||||
},
|
},
|
||||||
|
|
||||||
async submitOrder () {
|
async submitOrder () {
|
||||||
const {book,order} = books, quantity = parseInt (order.quantity) || 1 // REVISIT: Okra should be less strict
|
const {book,order} = books, amount = parseInt (order.amount) || 1 // REVISIT: Okra should be less strict
|
||||||
try {
|
try {
|
||||||
const res = await POST(`/submitOrder`, { quantity, book: book.ID })
|
const res = await POST(`/submitOrder`, { amount, book: book.ID })
|
||||||
book.stock = res.data.stock
|
book.stock = res.data.stock
|
||||||
books.order = { quantity, succeeded: `Successfully ordered ${quantity} item(s).` }
|
books.order = { amount, succeeded: `Successfully ordered ${amount} item(s).` }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
books.order = { quantity, failed: e.response.data.error.message }
|
books.order = { amount, failed: e.response.data.error.message }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<td>{{ book.author }}</td>
|
<td>{{ book.author }}</td>
|
||||||
<td>{{ book.genre.name }}</td>
|
<td>{{ book.genre.name }}</td>
|
||||||
<td class="rating-stars">
|
<td class="rating-stars">
|
||||||
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} ({{ book.numberOfReviews }})
|
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
{{ book.stock }} in stock
|
{{ book.stock }} in stock
|
||||||
</label>
|
</label>
|
||||||
<form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
|
<form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
|
||||||
<input type="number" v-model="order.quantity" v-bind:class="{ failed: order.failed }" style="width:5em">
|
<input type="number" v-model="order.amount" v-bind:class="{ failed: order.failed }" style="width:5em">
|
||||||
<input type="submit" value="Order:" class="muted-button">
|
<input type="submit" value="Order:" class="muted-button">
|
||||||
</form>
|
</form>
|
||||||
<h4> {{ book.title }} </h4>
|
<h4> {{ book.title }} </h4>
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
/**
|
|
||||||
* In order to keep basic bookshop sample as simple as possible, we don't add
|
|
||||||
* reuse dependencies. This db/init.js ensures we still have a minimum set of
|
|
||||||
* currencies, if not obtained through @capire/common.
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = async (db)=>{
|
|
||||||
|
|
||||||
const has_common = db.model.definitions['sap.common.Currencies'].elements.numcode
|
|
||||||
if (has_common) return
|
|
||||||
|
|
||||||
const already_filled = await db.exists('sap.common.Currencies',{code:'EUR'})
|
|
||||||
if (already_filled) return
|
|
||||||
|
|
||||||
await INSERT.into ('sap.common.Currencies') .columns (
|
|
||||||
'code','symbol','name'
|
|
||||||
) .rows (
|
|
||||||
[ 'EUR','€','Euro' ],
|
|
||||||
[ 'USD','$','US Dollar' ],
|
|
||||||
[ 'GBP','£','British Pound' ],
|
|
||||||
[ 'ILS','₪','Shekel' ],
|
|
||||||
[ 'JPY','¥','Yen' ],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
const { CatalogService } = require('./srv/cat-service')
|
exports.CatalogService = require('./srv/cat-service')
|
||||||
module.exports = { CatalogService }
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "A simple self-contained bookshop service.",
|
"description": "A simple self-contained bookshop service.",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@capire/common": "*",
|
||||||
"@sap/cds": "^5.0.4",
|
"@sap/cds": "^5.0.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"passport": "0.4.1"
|
"passport": "0.4.1"
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ service CatalogService @(path:'/browse') {
|
|||||||
} excluding { createdBy, modifiedBy };
|
} excluding { createdBy, modifiedBy };
|
||||||
|
|
||||||
@requires: 'authenticated-user'
|
@requires: 'authenticated-user'
|
||||||
action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer };
|
action submitOrder ( book: Books:ID, amount: Integer ) returns { stock: Integer };
|
||||||
event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String };
|
event OrderedBook : { book: Books:ID; amount: Integer; buyer: String };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
const cds = require('@sap/cds')
|
const cds = require('@sap/cds')
|
||||||
|
const { Books } = cds.entities ('sap.capire.bookshop')
|
||||||
|
|
||||||
class CatalogService extends cds.ApplicationService { init(){
|
class CatalogService extends cds.ApplicationService { init(){
|
||||||
|
|
||||||
const { Books } = cds.entities ('sap.capire.bookshop')
|
|
||||||
|
|
||||||
// Reduce stock of ordered books if available stock suffices
|
// Reduce stock of ordered books if available stock suffices
|
||||||
this.on ('submitOrder', async req => {
|
this.on ('submitOrder', async req => {
|
||||||
const {book,quantity} = req.data
|
const {book,amount} = req.data
|
||||||
if (quantity < 1) return req.reject (400,`quantity has to be 1 or more`)
|
let {stock} = await SELECT `stock` .from (Books,book)
|
||||||
let b = await SELECT `stock` .from (Books,book)
|
if (stock >= amount) {
|
||||||
if (!b) return req.error (404,`Book #${book} doesn't exist`)
|
await UPDATE (Books,book) .with (`stock -=`, amount)
|
||||||
let {stock} = b
|
await this.emit ('OrderedBook', { book, amount, buyer:req.user.id })
|
||||||
if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`)
|
return { stock }
|
||||||
await UPDATE (Books,book) .with ({ stock: stock -= quantity })
|
}
|
||||||
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
|
else return req.error (409,`${amount} exceeds stock for book #${book}`)
|
||||||
return { stock }
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add some discount for overstocked books
|
// Add some discount for overstocked books
|
||||||
|
|||||||
@@ -32,19 +32,6 @@ GET {{server}}/admin/Authors?
|
|||||||
# &sap-language=de
|
# &sap-language=de
|
||||||
Authorization: Basic alice:
|
Authorization: Basic alice:
|
||||||
|
|
||||||
### ------------------------------------------------------------------------
|
|
||||||
# Create Author
|
|
||||||
POST {{server}}/admin/Authors
|
|
||||||
Content-Type: application/json;IEEE754Compatible=true
|
|
||||||
Authorization: Basic alice:
|
|
||||||
|
|
||||||
{
|
|
||||||
"ID": 112,
|
|
||||||
"name": "Shakespeeeeere",
|
|
||||||
"age": 22
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
### ------------------------------------------------------------------------
|
### ------------------------------------------------------------------------
|
||||||
# Create book
|
# Create book
|
||||||
POST {{server}}/admin/Books
|
POST {{server}}/admin/Books
|
||||||
@@ -84,7 +71,7 @@ POST {{server}}/browse/submitOrder
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
{{me}}
|
{{me}}
|
||||||
|
|
||||||
{ "book":201, "quantity":5 }
|
{ "book":201, "amount":5 }
|
||||||
|
|
||||||
|
|
||||||
### ------------------------------------------------------------------------
|
### ------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
namespace sap.capire.bookshop; //> important for reflection
|
|
||||||
using from './srv/mashup';
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@capire/bookstore",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@capire/bookshop": "*",
|
|
||||||
"@capire/reviews": "*",
|
|
||||||
"@capire/orders": "*",
|
|
||||||
"@capire/common": "*",
|
|
||||||
"@capire/data-viewer": "*",
|
|
||||||
"@sap/cds": "^5",
|
|
||||||
"express": "^4.17.1"
|
|
||||||
},
|
|
||||||
"cds": {
|
|
||||||
"requires": {
|
|
||||||
"ReviewsService": {
|
|
||||||
"kind": "odata",
|
|
||||||
"model": "@capire/reviews"
|
|
||||||
},
|
|
||||||
"OrdersService": {
|
|
||||||
"kind": "odata",
|
|
||||||
"model": "@capire/orders"
|
|
||||||
},
|
|
||||||
"messaging": {
|
|
||||||
"[development]": { "kind": "file-based-messaging" },
|
|
||||||
"[hybrid]": { "kind": "enterprise-messaging-shared" },
|
|
||||||
"[production]": { "kind": "enterprise-messaging" }
|
|
||||||
},
|
|
||||||
"db": {
|
|
||||||
"kind": "sql"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"log": { "service": true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
const cds = require ('@sap/cds')
|
|
||||||
|
|
||||||
// Add mashup logic
|
|
||||||
cds.once('served', require('./srv/mashup'))
|
|
||||||
|
|
||||||
// Add routes to UIs from imported packages
|
|
||||||
cds.once('bootstrap',(app)=>{
|
|
||||||
app.serve ('/bookshop') .from ('@capire/bookshop','app/vue')
|
|
||||||
app.serve ('/reviews') .from ('@capire/reviews','app/vue')
|
|
||||||
app.serve ('/orders') .from('@capire/orders','app/orders')
|
|
||||||
app.serve ('/data') .from('@capire/data-viewer','app/viewer')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add Swagger UI
|
|
||||||
require('./srv/swagger-ui')
|
|
||||||
|
|
||||||
// Returning cds.server
|
|
||||||
module.exports = cds.server
|
|
||||||
|
|
||||||
// For didactic reasons in capire
|
|
||||||
const { ReviewsService, OrdersService } = cds.requires
|
|
||||||
if (!ReviewsService.credentials && !OrdersService.credentials) cds.requires.messaging = false
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Adding Swagger UI - see https://cap.cloud.sap/docs/advanced/openapi
|
|
||||||
const cds = require ('@sap/cds')
|
|
||||||
try {
|
|
||||||
const cds_swagger = require ('cds-swagger-ui-express')
|
|
||||||
cds.once ('bootstrap', app => app.use (cds_swagger()) )
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'MODULE_NOT_FOUND') throw err
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,7 @@ extend sap.common.Currencies with {
|
|||||||
* annotate sap.common.Countries with @cds.persistence.skip:false;
|
* annotate sap.common.Countries with @cds.persistence.skip:false;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
context sap.common.countries {
|
context sap.common_countries {
|
||||||
|
|
||||||
extend sap.common.Countries {
|
extend sap.common.Countries {
|
||||||
regions : Composition of many Regions on regions._parent = $self.code;
|
regions : Composition of many Regions on regions._parent = $self.code;
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
/* global Vue axios */ //> from vue.html
|
|
||||||
const GET = (url) => axios.get('/-data'+url)
|
|
||||||
const storageGet = (key, def) => localStorage.getItem('data-viewer:'+key) || def
|
|
||||||
const storageSet = (key, val) => localStorage.setItem('data-viewer:'+key, val)
|
|
||||||
const columnKeysFirst = (c1, c2) => {
|
|
||||||
if (c1.isKey && !c2.isKey) return -1
|
|
||||||
if (!c1.isKey && c2.isKey) return 1
|
|
||||||
if (c1.isKey && c2.isKey) return c1.name.localeCompare(c2.name)
|
|
||||||
return 0 // retain natural order of normal columns
|
|
||||||
}
|
|
||||||
|
|
||||||
const vue = Vue.createApp ({
|
|
||||||
|
|
||||||
data() { return {
|
|
||||||
error: undefined,
|
|
||||||
dataSource: storageGet('data-source', 'db'),
|
|
||||||
skip: storageGet('skip', 0),
|
|
||||||
top: storageGet('top', 20),
|
|
||||||
entity: storageGet('entity') ? JSON.parse(storageGet('entity')) : undefined,
|
|
||||||
entities: [],
|
|
||||||
columns: [],
|
|
||||||
data: [],
|
|
||||||
rowDetails: {},
|
|
||||||
rowKey: storageGet('rowKey')
|
|
||||||
}},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
dataSource: (v) => { storageSet('data-source', v); vue.fetchEntities() },
|
|
||||||
skip: (v) => { storageSet('skip', v); if (vue.entity) vue.fetchData() },
|
|
||||||
top: (v) => { storageSet('top', v); if (vue.entity) vue.fetchData() },
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
|
|
||||||
async fetchEntities () {
|
|
||||||
let url = `/Entities`
|
|
||||||
if (vue.dataSource === 'db') url += `?dataSource=db`
|
|
||||||
const {data} = await GET(url)
|
|
||||||
vue.entities = data.value
|
|
||||||
vue.entities.forEach(entity => entity.columns.sort(columnKeysFirst))
|
|
||||||
const entity = vue.entity && vue.entities.find(e => e.name === vue.entity.name)
|
|
||||||
if (entity) { // restore selection from previous fetch
|
|
||||||
vue.columns = entity.columns
|
|
||||||
await vue.fetchData(entity)
|
|
||||||
} else {
|
|
||||||
vue.entity = undefined
|
|
||||||
vue.columns = []
|
|
||||||
vue.data = []
|
|
||||||
vue.rowDetails = {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async inspectEntity (eve) {
|
|
||||||
const entity = vue.entity = vue.entities [eve.currentTarget.rowIndex-1]
|
|
||||||
storageSet('entity', JSON.stringify(entity))
|
|
||||||
vue.columns = vue.entities.find(e => e.name === entity.name).columns
|
|
||||||
return await this.fetchData()
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchData () {
|
|
||||||
let url = `/Data?entity=${vue.entity.name}&$skip=${vue.skip}&$top=${vue.top}`
|
|
||||||
if (vue.dataSource === 'db') url += `&dataSource=db`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const {data} = await GET(url)
|
|
||||||
// sort data along column order
|
|
||||||
const columnIndexes = {}
|
|
||||||
vue.columns.forEach((col, i) => columnIndexes[col.name] = i)
|
|
||||||
vue.data = data.value.map(d => d.record
|
|
||||||
.sort((r1, r2) => columnIndexes[r1.column] - columnIndexes[r2.column])
|
|
||||||
.map(r => r.data)
|
|
||||||
)
|
|
||||||
const row = vue.data.find(data => vue._makeRowKey(data) === vue.rowKey)
|
|
||||||
if (row) vue._setRowDetails(row)
|
|
||||||
else vue.rowDetails = {}
|
|
||||||
vue.error = undefined
|
|
||||||
} catch (err) {
|
|
||||||
vue.data = []
|
|
||||||
vue.rowDetails = {}
|
|
||||||
if (err.response?.data?.error) {
|
|
||||||
vue.error = err.response.data.error
|
|
||||||
} else {
|
|
||||||
vue.error = { code:err.code, message:err.message }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
inspectRow (eve) {
|
|
||||||
vue.rowDetails = {}
|
|
||||||
const selectedRow = eve.currentTarget.rowIndex-1
|
|
||||||
vue.rowKey = vue._makeRowKey(vue.data[selectedRow])
|
|
||||||
storageSet('rowKey', vue.rowKey)
|
|
||||||
vue._setRowDetails(vue.data[selectedRow])
|
|
||||||
},
|
|
||||||
|
|
||||||
_setRowDetails(row) {
|
|
||||||
vue.rowDetails = {}
|
|
||||||
row.forEach((line, colIndex) => {
|
|
||||||
vue.rowDetails[vue.columns[colIndex].name] = line
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
_makeRowKey(row) {
|
|
||||||
// to identify a row, build a key string out of all key columns' values
|
|
||||||
return row
|
|
||||||
.filter((_, colIndex) => vue.columns[colIndex] && vue.columns[colIndex].isKey)
|
|
||||||
.reduce(((prev, next) => prev += next), '')
|
|
||||||
},
|
|
||||||
|
|
||||||
isActiveRow(row) {
|
|
||||||
return vue._makeRowKey(row) === vue.rowKey
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.mount('#app')
|
|
||||||
|
|
||||||
vue.fetchEntities()
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>Data Browser</title>
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/primitive-ui/dist/css/main.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
|
||||||
<script src="app.js" defer></script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
th { position: sticky; top:0; z-index: 2; background-color: white; }
|
|
||||||
.noscroll { overflow: hidden; }
|
|
||||||
.hovering tr:hover td { background: #ebeefc; cursor: pointer; }
|
|
||||||
.highlight { background: #ebeefc !important; }
|
|
||||||
.rating-stars { color:teal }
|
|
||||||
.succeeded { color:teal }
|
|
||||||
.failed { color:red }
|
|
||||||
.condensed { max-width: 100px; text-overflow: ellipsis; white-space: nowrap; }
|
|
||||||
.key { font-weight: bold }
|
|
||||||
.not-key { font-weight: lighter;}
|
|
||||||
.with-sidebar { display: flex; flex-wrap: wrap; gap: 1rem; }
|
|
||||||
.sidebar { flex-basis: 20rem; flex-grow: 1; }
|
|
||||||
.sidebar-main { height: 100vh; overflow-y: scroll; }
|
|
||||||
.not-sidebar { flex-basis: 0; flex-grow: 999; min-inline-size: 50%; align-items: stretch;}
|
|
||||||
.not-sidebar-main { max-height: 40vh; overflow-y: scroll; }
|
|
||||||
.not-sidebar-sub { max-height: 40vh; overflow-y: scroll; }
|
|
||||||
.horizontal label { display: inline; }
|
|
||||||
.horizontal input { width: initial; display: inline; }
|
|
||||||
.error { color: red; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="noscroll">
|
|
||||||
<div id='app' class="full-container">
|
|
||||||
|
|
||||||
<h1>Data Browser – {{ 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 +0,0 @@
|
|||||||
using from './srv/data-service';
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@capire/data-viewer",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "A generic browser for data",
|
|
||||||
"dependencies": {
|
|
||||||
"@sap/cds": "^5.0.4"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"app",
|
|
||||||
"srv",
|
|
||||||
"index.cds"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
/**
|
|
||||||
* Exposes data + entity metadata
|
|
||||||
*/
|
|
||||||
@requires:'authenticated-user'
|
|
||||||
service DataService @( path:'-data' ) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Metadata like name and columns/elements
|
|
||||||
*/
|
|
||||||
entity Entities {
|
|
||||||
key name : String;
|
|
||||||
columns: Composition of many {
|
|
||||||
name : String;
|
|
||||||
type : String;
|
|
||||||
isKey: Boolean;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The actual data, organized by column name
|
|
||||||
*/
|
|
||||||
entity Data {
|
|
||||||
record : array of {
|
|
||||||
column : String;
|
|
||||||
data : String;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
const cds = require('@sap/cds')
|
|
||||||
const log = cds.log('data')
|
|
||||||
|
|
||||||
class DataService extends cds.ApplicationService { init(){
|
|
||||||
|
|
||||||
this.on ('READ', 'Entities', req => {
|
|
||||||
const { dataSource } = req.req.query
|
|
||||||
const srvPrefixes = cds.db.model.all('service').map(srv => srv.name+'.')
|
|
||||||
const dataSourceFilter = dataSource === 'db'
|
|
||||||
? e => e['@cds.persistence.skip'] !== true // for DB, excl. entities w/o persistence
|
|
||||||
: e => !!srvPrefixes.find(srvName => e.name.startsWith(srvName)) // only entities reachable from a service
|
|
||||||
|
|
||||||
return cds.db.model.all('entity')
|
|
||||||
.filter (e => req.data && req.data.name ? e.name === req.data.name : true) // honor name filter from request, if any
|
|
||||||
.filter (e => !e.name.startsWith('DRAFT.')) // exclude synthetic stuff
|
|
||||||
.filter (e => !e.name.startsWith('DataService.')) // exclude this service
|
|
||||||
.filter (dataSourceFilter)
|
|
||||||
.sort((e1, e2) => e1.name.localeCompare(e2.name))
|
|
||||||
.map(e => {
|
|
||||||
const columns = Object.entries(e.elements)
|
|
||||||
.filter(([_, el]) => !(el instanceof cds.Association)) // exclude assocs+compositions
|
|
||||||
.map(([name, el]) => { return { name, type: el.type, isKey:!!el.key }})
|
|
||||||
return { name: e.name, columns }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.on ('READ', 'Data', async req => {
|
|
||||||
const { entity: entityName, dataSource: dataSourceName } = req.req.query
|
|
||||||
if (!entityName) return req.reject(400, `Must provide 'entity' query`)
|
|
||||||
const entity = cds.db.model.definitions[entityName]
|
|
||||||
if (!entity) return req.reject(404, 'No such entity: ' + entityName)
|
|
||||||
|
|
||||||
const query = SELECT.from(entity)
|
|
||||||
query.SELECT.limit = req.query.SELECT.limit // forward $skip / $top
|
|
||||||
|
|
||||||
const dataSource = findDataSource(dataSourceName, entityName)
|
|
||||||
const res = await dataSource.run(query)
|
|
||||||
return res.map((line) => {
|
|
||||||
const record = Object.entries(line).map(([column, data]) => {return {column, data}})
|
|
||||||
return { record }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return super.init()
|
|
||||||
}}
|
|
||||||
|
|
||||||
module.exports = { DataService }
|
|
||||||
|
|
||||||
function findDataSource(dataSourceName, entityName) {
|
|
||||||
for (let srv of Object.values(cds.services)) { // all connected services
|
|
||||||
if (!srv.name) continue // FIXME intermediate/pending in cds.services ?
|
|
||||||
if (dataSourceName === srv.name || entityName.startsWith(srv.name+'.')) {
|
|
||||||
log._debug && log.debug(`using ${srv.name} as data source`)
|
|
||||||
return srv
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cds.services.db // fallback
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 67 KiB |
@@ -1,172 +0,0 @@
|
|||||||
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="347px" height="279px" viewBox="-0.5 -0.5 347 279" content="<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>
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,281 +0,0 @@
|
|||||||
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="569px" height="428px" viewBox="-0.5 -0.5 569 428" content="<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>
|
|
||||||
|
Before Width: | Height: | Size: 24 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 40 KiB |
2
fiori/.env
Normal file
2
fiori/.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# cds.requires.messaging.kind = file-based-messaging
|
||||||
|
PORT = 4004
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using {AdminService} from '@capire/bookshop';
|
|
||||||
|
|
||||||
annotate AdminService.Authors with @odata.draft.enabled;
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Authors Object Page
|
|
||||||
//
|
|
||||||
annotate AdminService.Authors with @(UI : {
|
|
||||||
HeaderInfo : {
|
|
||||||
TypeName : 'Author',
|
|
||||||
TypeNamePlural : 'Authors',
|
|
||||||
Description : {Value : lifetime}
|
|
||||||
},
|
|
||||||
Facets : [
|
|
||||||
{
|
|
||||||
$Type : 'UI.ReferenceFacet',
|
|
||||||
Label : '{i18n>Details}',
|
|
||||||
Target : '@UI.FieldGroup#Details'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$Type : 'UI.ReferenceFacet',
|
|
||||||
Label : '{i18n>Books}',
|
|
||||||
Target : 'books/@UI.LineItem'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
FieldGroup #Details : {Data : [
|
|
||||||
{Value : placeOfBirth},
|
|
||||||
{Value : placeOfDeath},
|
|
||||||
{Value : dateOfBirth},
|
|
||||||
{Value : dateOfDeath},
|
|
||||||
{
|
|
||||||
Value : age,
|
|
||||||
Label : '{i18n>Age}'
|
|
||||||
},
|
|
||||||
]},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Workaround to avoid errors for unknown db-specific calculated fields above
|
|
||||||
extend sap.capire.bookshop.Authors with {
|
|
||||||
virtual age : Integer;
|
|
||||||
virtual lifetime : String;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Workaround for Fiori popup for asking user to enter a new UUID on Create
|
|
||||||
annotate AdminService.Authors with { ID @Core.Computed; }
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
sap.ui.define(["sap/fe/core/AppComponent"], function (AppComponent) {
|
|
||||||
"use strict";
|
|
||||||
return AppComponent.extend("authors.Component", {
|
|
||||||
metadata: { manifest: "json" },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
/* eslint no-undef:0 */
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# This is the resource bundle of itelo
|
|
||||||
# __ldi.translation.uuid=c3431418-9caf-11e8-98d0-529269fb1459
|
|
||||||
|
|
||||||
# JCI app descriptor contains lower case TITLE
|
|
||||||
appTitle=Bookshop Authors
|
|
||||||
|
|
||||||
# JCI app descriptor contains lower case DESCRIPTION
|
|
||||||
appSubTitle=Bookshop Authors
|
|
||||||
|
|
||||||
# JCI app descriptor contains lower case DESCRIPTION
|
|
||||||
appDescription=Bookshop Authors
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
{
|
|
||||||
"_version": "1.28.0",
|
|
||||||
"sap.app": {
|
|
||||||
"id": "authors",
|
|
||||||
"type": "application",
|
|
||||||
"title": "Manage Authors",
|
|
||||||
"description": "Sample Application",
|
|
||||||
"i18n": "i18n/i18n.properties",
|
|
||||||
"applicationVersion": {
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"dataSources": {
|
|
||||||
"AdminService": {
|
|
||||||
"uri": "/admin/",
|
|
||||||
"type": "OData",
|
|
||||||
"settings": {
|
|
||||||
"odataVersion": "4.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sourceTemplate": {
|
|
||||||
"id": "ui5template.basicSAPUI5ApplicationProject",
|
|
||||||
"-id": "ui5template.smartTemplate",
|
|
||||||
"version": "1.40.12"
|
|
||||||
},
|
|
||||||
"crossNavigation": {
|
|
||||||
"inbounds": {
|
|
||||||
"intent1": {
|
|
||||||
"signature": {
|
|
||||||
"parameters": {
|
|
||||||
"Books.author.ID":{
|
|
||||||
"renameTo": "ID"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalParameters": "ignored"
|
|
||||||
},
|
|
||||||
"semanticObject": "Authors",
|
|
||||||
"action": "display",
|
|
||||||
"title": "{{appTitle}}",
|
|
||||||
"info": "{{appInfo}}",
|
|
||||||
"subTitle": "{{appSubTitle}}",
|
|
||||||
"icon": "sap-icon://SAP-icons-TNT/user",
|
|
||||||
"indicatorDataSource": {
|
|
||||||
"dataSource": "AdminService",
|
|
||||||
"path": "Authors/$count",
|
|
||||||
"refresh": 1800
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sap.ui5": {
|
|
||||||
"dependencies": {
|
|
||||||
"minUI5Version": "1.81.0",
|
|
||||||
"libs": {
|
|
||||||
"sap.fe.templates": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"models": {
|
|
||||||
"i18n": {
|
|
||||||
"type": "sap.ui.model.resource.ResourceModel",
|
|
||||||
"uri": "i18n/i18n.properties"
|
|
||||||
},
|
|
||||||
"": {
|
|
||||||
"dataSource": "AdminService",
|
|
||||||
"settings": {
|
|
||||||
"synchronizationMode": "None",
|
|
||||||
"operationMode": "Server",
|
|
||||||
"autoExpandSelect": true,
|
|
||||||
"earlyRequests": true,
|
|
||||||
"groupProperties": {
|
|
||||||
"default": {
|
|
||||||
"submit": "Auto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"routing": {
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": ":?query:",
|
|
||||||
"name": "AuthorsList",
|
|
||||||
"target": "AuthorsList"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pattern": "Authors({key}):?query:",
|
|
||||||
"name": "AuthorsDetails",
|
|
||||||
"target": "AuthorsDetails"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"targets": {
|
|
||||||
"AuthorsList": {
|
|
||||||
"type": "Component",
|
|
||||||
"id": "AuthorsList",
|
|
||||||
"name": "sap.fe.templates.ListReport",
|
|
||||||
"options": {
|
|
||||||
"settings": {
|
|
||||||
"entitySet": "Authors",
|
|
||||||
"initialLoad": true,
|
|
||||||
"navigation": {
|
|
||||||
"Authors": {
|
|
||||||
"detail": {
|
|
||||||
"route": "AuthorsDetails"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AuthorsDetails": {
|
|
||||||
"type": "Component",
|
|
||||||
"id": "AuthorsDetailsList",
|
|
||||||
"name": "sap.fe.templates.ObjectPage",
|
|
||||||
"options": {
|
|
||||||
"settings": {
|
|
||||||
"entitySet": "Authors"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"contentDensities": {
|
|
||||||
"compact": true,
|
|
||||||
"cozy": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sap.ui": {
|
|
||||||
"technology": "UI5",
|
|
||||||
"fullWidth": false,
|
|
||||||
"deviceTypes":{
|
|
||||||
"desktop": true,
|
|
||||||
"tablet": true,
|
|
||||||
"phone": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sap.fiori": {
|
|
||||||
"registrationIds": [],
|
|
||||||
"archeType": "transactional"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,32 @@
|
|||||||
<script>
|
<script>
|
||||||
window["sap-ushell-config"] = {
|
window["sap-ushell-config"] = {
|
||||||
defaultRenderer: "fiori2",
|
defaultRenderer: "fiori2",
|
||||||
applications: {}
|
applications: {
|
||||||
|
"browse-books": {
|
||||||
|
title: "Browse Books",
|
||||||
|
description: "w/ SAP Fiori Elements",
|
||||||
|
additionalInformation: "SAPUI5.Component=bookshop",
|
||||||
|
applicationType : "URL",
|
||||||
|
url: "/browse/webapp",
|
||||||
|
navigationMode: "embedded"
|
||||||
|
},
|
||||||
|
"manage-books": {
|
||||||
|
title: "Manage Books",
|
||||||
|
description: "w/ SAP Fiori Elements",
|
||||||
|
additionalInformation: "SAPUI5.Component=admin",
|
||||||
|
applicationType : "URL",
|
||||||
|
url: "/admin/webapp",
|
||||||
|
navigationMode: "embedded"
|
||||||
|
},
|
||||||
|
"manage-orders": {
|
||||||
|
title: "Manage Orders",
|
||||||
|
description: "w/ SAP Fiori Elements",
|
||||||
|
additionalInformation: "SAPUI5.Component=orders",
|
||||||
|
applicationType : "URL",
|
||||||
|
url: "/orders/webapp",
|
||||||
|
navigationMode: "embedded"
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using { AdminService } from '@capire/bookstore';
|
using { AdminService } from '../../db/schema';
|
||||||
using from '../common'; // to help UI linter get the complete annotations
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
@@ -40,6 +39,27 @@ annotate AdminService.Books with @(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
annotate AdminService.Authors with @(
|
||||||
|
UI: {
|
||||||
|
HeaderInfo: {
|
||||||
|
Description: {Value: lifetime}
|
||||||
|
},
|
||||||
|
Facets: [
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Books}', Target: 'books/@UI.LineItem'},
|
||||||
|
],
|
||||||
|
FieldGroup#Details: {
|
||||||
|
Data: [
|
||||||
|
{Value: placeOfBirth},
|
||||||
|
{Value: placeOfDeath},
|
||||||
|
{Value: dateOfBirth},
|
||||||
|
{Value: dateOfDeath},
|
||||||
|
{Value: age, Label: '{i18n>Age}'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
@@ -64,15 +84,10 @@ annotate AdminService.Books.texts with @(
|
|||||||
|
|
||||||
// Add Value Help for Locales
|
// Add Value Help for Locales
|
||||||
annotate AdminService.Books.texts {
|
annotate AdminService.Books.texts {
|
||||||
locale @(
|
locale @ValueList:{entity:'Languages',type:#fixed}
|
||||||
ValueList.entity:'Languages', Common.ValueListWithFixedValues, //show as drop down, not a dialog
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
// In addition we need to expose Languages through AdminService as a target for ValueList
|
// In addition we need to expose Languages through AdminService
|
||||||
using { sap } from '@sap/cds/common';
|
using { sap } from '@sap/cds/common';
|
||||||
extend service AdminService {
|
extend service AdminService {
|
||||||
@readonly entity Languages as projection on sap.common.Languages;
|
entity Languages as projection on sap.common.Languages;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workaround for Fiori popup for asking user to enter a new UUID on Create
|
|
||||||
annotate AdminService.Books with { ID @Core.Computed; }
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
||||||
"use strict";
|
"use strict";
|
||||||
return AppComponent.extend("books.Component", {
|
return AppComponent.extend("admin.Component", {
|
||||||
metadata: { manifest: "json" }
|
metadata: { manifest: "json" }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_version": "1.8.0",
|
"_version": "1.8.0",
|
||||||
"sap.app": {
|
"sap.app": {
|
||||||
"id": "books",
|
"id": "admin",
|
||||||
"type": "application",
|
"type": "application",
|
||||||
"title": "Manage Books",
|
"title": "Manage Books",
|
||||||
"description": "Sample Application",
|
"description": "Sample Application",
|
||||||
@@ -73,7 +73,6 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"settings" : {
|
"settings" : {
|
||||||
"entitySet" : "Books",
|
"entitySet" : "Books",
|
||||||
"initialLoad": true,
|
|
||||||
"navigation" : {
|
"navigation" : {
|
||||||
"Books" : {
|
"Books" : {
|
||||||
"detail" : {
|
"detail" : {
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
{
|
|
||||||
"services": {
|
|
||||||
"LaunchPage": {
|
|
||||||
"adapter": {
|
|
||||||
"config": {
|
|
||||||
"catalogs": [],
|
|
||||||
"groups": [
|
|
||||||
{
|
|
||||||
"id": "Bookshop",
|
|
||||||
"title": "Bookshop",
|
|
||||||
"isPreset": true,
|
|
||||||
"isVisible": true,
|
|
||||||
"isGroupLocked": false,
|
|
||||||
"tiles": [
|
|
||||||
{
|
|
||||||
"id": "BrowseBooks",
|
|
||||||
"tileType": "sap.ushell.ui.tile.StaticTile",
|
|
||||||
"properties": {
|
|
||||||
"title": "Browse Books",
|
|
||||||
"targetURL": "#Books-display"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "Administration",
|
|
||||||
"title": "Administration",
|
|
||||||
"isPreset": true,
|
|
||||||
"isVisible": true,
|
|
||||||
"isGroupLocked": false,
|
|
||||||
"tiles": [
|
|
||||||
{
|
|
||||||
"id": "ManageBooks",
|
|
||||||
"tileType": "sap.ushell.ui.tile.StaticTile",
|
|
||||||
"properties": {
|
|
||||||
"title": "Manage Books",
|
|
||||||
"targetURL": "#Books-manage"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ManageAuthors",
|
|
||||||
"tileType": "sap.ushell.ui.tile.StaticTile",
|
|
||||||
"properties": {
|
|
||||||
"title": "Manage Authors",
|
|
||||||
"targetURL": "#Authors-display"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ManageOrders",
|
|
||||||
"tileType": "sap.ushell.ui.tile.StaticTile",
|
|
||||||
"properties": {
|
|
||||||
"title": "Manage Orders",
|
|
||||||
"targetURL": "#Orders-manage"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NavTargetResolution": {
|
|
||||||
"config": {
|
|
||||||
"enableClientSideTargetResolution": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ClientSideTargetResolution": {
|
|
||||||
"adapter": {
|
|
||||||
"config": {
|
|
||||||
"inbounds": {
|
|
||||||
"BrowseBooks": {
|
|
||||||
"semanticObject": "Books",
|
|
||||||
"action": "display",
|
|
||||||
"title": "Browse Books",
|
|
||||||
"signature": {
|
|
||||||
"parameters": {
|
|
||||||
"Books.ID": {
|
|
||||||
"renameTo": "ID"
|
|
||||||
},
|
|
||||||
"Authors.books.ID": {
|
|
||||||
"renameTo": "ID"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalParameters": "ignored"
|
|
||||||
},
|
|
||||||
"resolutionResult": {
|
|
||||||
"applicationType": "SAPUI5",
|
|
||||||
"additionalInformation": "SAPUI5.Component=bookshop",
|
|
||||||
"url": "/browse/webapp"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"BrowseAuthors": {
|
|
||||||
"semanticObject": "Authors",
|
|
||||||
"action": "display",
|
|
||||||
"title": "Browse Authors",
|
|
||||||
"signature": {
|
|
||||||
"parameters": {
|
|
||||||
"Books.author.ID":{
|
|
||||||
"renameTo": "ID"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalParameters": "ignored"
|
|
||||||
},
|
|
||||||
"resolutionResult": {
|
|
||||||
"applicationType": "SAPUI5",
|
|
||||||
"additionalInformation": "SAPUI5.Component=authors",
|
|
||||||
"url": "/admin-authors/webapp"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ManageBooks": {
|
|
||||||
"semanticObject": "Books",
|
|
||||||
"action": "manage",
|
|
||||||
"title": "Manage Books",
|
|
||||||
"signature": {
|
|
||||||
"parameters": {},
|
|
||||||
"additionalParameters": "allowed"
|
|
||||||
},
|
|
||||||
"resolutionResult": {
|
|
||||||
"applicationType": "SAPUI5",
|
|
||||||
"additionalInformation": "SAPUI5.Component=books",
|
|
||||||
"url": "/admin-books/webapp"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ManageOrders": {
|
|
||||||
"semanticObject": "Orders",
|
|
||||||
"action": "manage",
|
|
||||||
"signature": {
|
|
||||||
"parameters": {},
|
|
||||||
"additionalParameters": "allowed"
|
|
||||||
},
|
|
||||||
"resolutionResult": {
|
|
||||||
"applicationType": "SAPUI5",
|
|
||||||
"additionalInformation": "SAPUI5.Component=orders",
|
|
||||||
"url": "/orders/webapp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
fiori/app/bookshop.html
Normal file
3
fiori/app/bookshop.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<head>
|
||||||
|
<meta http-equiv="refresh" content="0;url=bookshop/index.html">
|
||||||
|
</head>
|
||||||
@@ -1,60 +1,50 @@
|
|||||||
using CatalogService from '@capire/bookstore';
|
using CatalogService from '@capire/bookshop';
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Books Object Page
|
// Books Object Page
|
||||||
//
|
//
|
||||||
annotate CatalogService.Books with @(UI : {
|
annotate CatalogService.Books with @(
|
||||||
HeaderInfo : {
|
UI: {
|
||||||
TypeName : 'Book',
|
HeaderInfo: {
|
||||||
TypeNamePlural : 'Books',
|
TypeName: 'Book',
|
||||||
Description : {Value : author}
|
TypeNamePlural: 'Books',
|
||||||
},
|
Description: {Value: author}
|
||||||
HeaderFacets : [{
|
},
|
||||||
$Type : 'UI.ReferenceFacet',
|
HeaderFacets: [
|
||||||
Label : '{i18n>Description}',
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Description}', Target: '@UI.FieldGroup#Descr'},
|
||||||
Target : '@UI.FieldGroup#Descr'
|
],
|
||||||
}, ],
|
Facets: [
|
||||||
Facets : [{
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Price'},
|
||||||
$Type : 'UI.ReferenceFacet',
|
],
|
||||||
Label : '{i18n>Details}',
|
FieldGroup#Descr: {
|
||||||
Target : '@UI.FieldGroup#Price'
|
Data: [
|
||||||
}, ],
|
{Value: descr},
|
||||||
FieldGroup #Descr : {Data : [{Value : descr}, ]},
|
]
|
||||||
FieldGroup #Price : {Data : [
|
},
|
||||||
{Value : price},
|
FieldGroup#Price: {
|
||||||
{
|
Data: [
|
||||||
Value : currency.symbol,
|
{Value: price},
|
||||||
Label : '{i18n>Currency}'
|
{Value: currency.symbol, Label: '{i18n>Currency}'},
|
||||||
},
|
]
|
||||||
]},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Books Object Page
|
// Books Object Page
|
||||||
//
|
//
|
||||||
annotate CatalogService.Books with @(UI : {
|
annotate CatalogService.Books with @(
|
||||||
SelectionFields : [
|
UI: {
|
||||||
ID,
|
SelectionFields: [ ID, price, currency_code ],
|
||||||
price,
|
LineItem: [
|
||||||
currency_code
|
{Value: title},
|
||||||
],
|
{Value: author, Label:'{i18n>Author}'},
|
||||||
LineItem : [
|
{Value: genre.name},
|
||||||
{
|
{Value: price},
|
||||||
Value : ID,
|
{Value: currency.symbol, Label:' '},
|
||||||
Label : '{i18n>Title}'
|
]
|
||||||
},
|
},
|
||||||
{
|
);
|
||||||
Value : author,
|
|
||||||
Label : '{i18n>Author}'
|
|
||||||
},
|
|
||||||
{Value : genre.name},
|
|
||||||
{Value : price},
|
|
||||||
{
|
|
||||||
Value : currency.symbol,
|
|
||||||
Label : ' '
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}, );
|
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
{
|
{
|
||||||
"_version": "1.28.0",
|
"_version": "1.8.0",
|
||||||
"sap.app": {
|
"sap.app": {
|
||||||
"id": "bookshop",
|
"id": "bookshop",
|
||||||
"type": "application",
|
"type": "application",
|
||||||
"title": "Browse Books",
|
"title": "Browse Books",
|
||||||
"description": "Sample Application",
|
"description": "Sample Application",
|
||||||
"i18n": "i18n/i18n.properties",
|
"i18n": "i18n/i18n.properties",
|
||||||
"applicationVersion": {
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"dataSources": {
|
"dataSources": {
|
||||||
"CatalogService": {
|
"CatalogService": {
|
||||||
"uri": "/browse/",
|
"uri": "/browse/",
|
||||||
@@ -18,43 +15,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sourceTemplate": {
|
"-sourceTemplate": {
|
||||||
"id": "ui5template.basicSAPUI5ApplicationProject",
|
"id": "ui5template.basicSAPUI5ApplicationProject",
|
||||||
"-id": "ui5template.smartTemplate",
|
"-id": "ui5template.smartTemplate",
|
||||||
"version": "1.40.12"
|
"-version": "1.40.12"
|
||||||
},
|
|
||||||
"crossNavigation": {
|
|
||||||
"inbounds": {
|
|
||||||
"intent1": {
|
|
||||||
"signature": {
|
|
||||||
"parameters": {
|
|
||||||
"Books.ID":{
|
|
||||||
"renameTo": "ID"
|
|
||||||
},
|
|
||||||
"Authors.books.ID": {
|
|
||||||
"renameTo": "ID"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalParameters": "ignored"
|
|
||||||
},
|
|
||||||
"semanticObject": "Books",
|
|
||||||
"action": "display",
|
|
||||||
"title": "{{appTitle}}",
|
|
||||||
"info": "{{appInfo}}",
|
|
||||||
"subTitle": "{{appSubTitle}}",
|
|
||||||
"icon": "sap-icon://course-book",
|
|
||||||
"indicatorDataSource": {
|
|
||||||
"dataSource": "CatalogService",
|
|
||||||
"path": "Books/$count",
|
|
||||||
"refresh": 1800
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sap.ui5": {
|
"sap.ui5": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minUI5Version": "1.81.0",
|
|
||||||
"libs": {
|
"libs": {
|
||||||
"sap.fe.templates": {}
|
"sap.fe.templates": {}
|
||||||
}
|
}
|
||||||
@@ -100,7 +68,6 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"settings": {
|
"settings": {
|
||||||
"entitySet": "Books",
|
"entitySet": "Books",
|
||||||
"initialLoad": true,
|
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"Books": {
|
"Books": {
|
||||||
"detail": {
|
"detail": {
|
||||||
@@ -130,12 +97,7 @@
|
|||||||
},
|
},
|
||||||
"sap.ui": {
|
"sap.ui": {
|
||||||
"technology": "UI5",
|
"technology": "UI5",
|
||||||
"fullWidth": false,
|
"fullWidth": false
|
||||||
"deviceTypes":{
|
|
||||||
"desktop": true,
|
|
||||||
"tablet": true,
|
|
||||||
"phone": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"sap.fiori": {
|
"sap.fiori": {
|
||||||
"registrationIds": [],
|
"registrationIds": [],
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
Common Annotations shared by all apps
|
Common Annotations shared by all apps
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using { sap.capire.bookshop as my } from '@capire/bookstore';
|
using { sap.capire.bookshop as my } from '@capire/bookshop';
|
||||||
using { sap.common } from '@capire/common';
|
using { sap.common } from '@capire/common';
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -10,52 +10,39 @@ using { sap.common } from '@capire/common';
|
|||||||
// Books Lists
|
// Books Lists
|
||||||
//
|
//
|
||||||
annotate my.Books with @(
|
annotate my.Books with @(
|
||||||
Common.SemanticKey : [ID],
|
Common.SemanticKey: [title],
|
||||||
UI : {
|
UI: {
|
||||||
Identification : [{Value : title}],
|
Identification: [{Value:title}],
|
||||||
SelectionFields : [
|
SelectionFields: [ ID, author_ID, price, currency_code ],
|
||||||
ID,
|
LineItem: [
|
||||||
author_ID,
|
{Value: ID},
|
||||||
price,
|
{Value: title},
|
||||||
currency_code
|
{Value: author.name, Label:'{i18n>Author}'},
|
||||||
],
|
{Value: genre.name},
|
||||||
LineItem : [
|
{Value: stock},
|
||||||
{
|
{Value: price},
|
||||||
Value : ID,
|
{Value: currency.symbol, Label:' '},
|
||||||
Label : '{i18n>Title}'
|
]
|
||||||
},
|
}
|
||||||
{
|
|
||||||
Value : author.ID,
|
|
||||||
Label : '{i18n>Author}'
|
|
||||||
},
|
|
||||||
{Value : genre.name},
|
|
||||||
{Value : stock},
|
|
||||||
{Value : price},
|
|
||||||
{
|
|
||||||
Value : currency.symbol,
|
|
||||||
Label : ' '
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
ID @Common: {
|
author @ValueList.entity:'Authors';
|
||||||
SemanticObject : 'Books',
|
|
||||||
Text: title,
|
|
||||||
TextArrangement : #TextOnly
|
|
||||||
};
|
|
||||||
author @ValueList.entity : 'Authors';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Books Details
|
// Books Details
|
||||||
//
|
//
|
||||||
annotate my.Books with @(UI : {HeaderInfo : {
|
annotate my.Books with @(
|
||||||
TypeName : '{i18n>Book}',
|
UI: {
|
||||||
TypeNamePlural : '{i18n>Books}',
|
HeaderInfo: {
|
||||||
Title : {Value : title},
|
TypeName: '{i18n>Book}',
|
||||||
Description : {Value : author.name}
|
TypeNamePlural: '{i18n>Books}',
|
||||||
}, });
|
Title: {Value: title},
|
||||||
|
Description: {Value: author.name}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -63,19 +50,13 @@ annotate my.Books with @(UI : {HeaderInfo : {
|
|||||||
// Books Elements
|
// Books Elements
|
||||||
//
|
//
|
||||||
annotate my.Books with {
|
annotate my.Books with {
|
||||||
ID @title : '{i18n>ID}';
|
ID @title:'{i18n>ID}' @UI.HiddenFilter;
|
||||||
title @title : '{i18n>Title}';
|
title @title:'{i18n>Title}';
|
||||||
genre @title : '{i18n>Genre}' @Common : {
|
genre @title:'{i18n>Genre}' @Common: { Text: genre.name, TextArrangement: #TextOnly };
|
||||||
Text : genre.name,
|
author @title:'{i18n>Author}' @Common: { Text: author.name, TextArrangement: #TextOnly };
|
||||||
TextArrangement : #TextOnly
|
price @title:'{i18n>Price}' @Measures.ISOCurrency: currency_code;
|
||||||
};
|
stock @title:'{i18n>Stock}';
|
||||||
author @title : '{i18n>Author}' @Common : {
|
descr @UI.MultiLineText;
|
||||||
Text : author.name,
|
|
||||||
TextArrangement : #TextOnly
|
|
||||||
};
|
|
||||||
price @title : '{i18n>Price}' @Measures.ISOCurrency : currency_code;
|
|
||||||
stock @title : '{i18n>Stock}';
|
|
||||||
descr @UI.MultiLineText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -83,45 +64,42 @@ annotate my.Books with {
|
|||||||
// Genres List
|
// Genres List
|
||||||
//
|
//
|
||||||
annotate my.Genres with @(
|
annotate my.Genres with @(
|
||||||
Common.SemanticKey : [name],
|
Common.SemanticKey: [name],
|
||||||
UI : {
|
UI: {
|
||||||
SelectionFields : [name],
|
SelectionFields: [ name ],
|
||||||
LineItem : [
|
LineItem:[
|
||||||
{Value : name},
|
{Value: name},
|
||||||
{
|
{Value: parent.name, Label: 'Main Genre'},
|
||||||
Value : parent.name,
|
],
|
||||||
Label : 'Main Genre'
|
}
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Genre Details
|
// Genre Details
|
||||||
//
|
//
|
||||||
annotate my.Genres with @(UI : {
|
annotate my.Genres with @(
|
||||||
Identification : [{Value : name}],
|
UI: {
|
||||||
HeaderInfo : {
|
Identification: [{Value:name}],
|
||||||
TypeName : '{i18n>Genre}',
|
HeaderInfo: {
|
||||||
TypeNamePlural : '{i18n>Genres}',
|
TypeName: '{i18n>Genre}',
|
||||||
Title : {Value : name},
|
TypeNamePlural: '{i18n>Genres}',
|
||||||
Description : {Value : ID}
|
Title: {Value: name},
|
||||||
},
|
Description: {Value: ID}
|
||||||
Facets : [{
|
},
|
||||||
$Type : 'UI.ReferenceFacet',
|
Facets: [
|
||||||
Label : '{i18n>SubGenres}',
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>SubGenres}', Target: 'children/@UI.LineItem'},
|
||||||
Target : 'children/@UI.LineItem'
|
],
|
||||||
}, ],
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Genres Elements
|
// Genres Elements
|
||||||
//
|
//
|
||||||
annotate my.Genres with {
|
annotate my.Genres with {
|
||||||
ID @title : '{i18n>ID}';
|
ID @title: '{i18n>ID}';
|
||||||
name @title : '{i18n>Genre}';
|
name @title: '{i18n>Genre}';
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -129,42 +107,38 @@ annotate my.Genres with {
|
|||||||
// Authors List
|
// Authors List
|
||||||
//
|
//
|
||||||
annotate my.Authors with @(
|
annotate my.Authors with @(
|
||||||
Common.SemanticKey : [ID],
|
Common.SemanticKey: [name],
|
||||||
UI : {
|
UI: {
|
||||||
Identification : [{Value : name}],
|
Identification: [{Value:name}],
|
||||||
SelectionFields : [name],
|
SelectionFields: [ name ],
|
||||||
LineItem : [
|
LineItem:[
|
||||||
{Value : ID},
|
{Value: ID},
|
||||||
{Value : dateOfBirth},
|
{Value: name},
|
||||||
{Value : dateOfDeath},
|
{Value: dateOfBirth},
|
||||||
{Value : placeOfBirth},
|
{Value: dateOfDeath},
|
||||||
{Value : placeOfDeath},
|
{Value: placeOfBirth},
|
||||||
],
|
{Value: placeOfDeath},
|
||||||
}
|
],
|
||||||
) {
|
}
|
||||||
ID @Common: {
|
);
|
||||||
SemanticObject : 'Authors',
|
|
||||||
Text: name,
|
|
||||||
TextArrangement : #TextOnly,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Author Details
|
// Author Details
|
||||||
//
|
//
|
||||||
annotate my.Authors with @(UI : {
|
annotate my.Authors with @(
|
||||||
HeaderInfo : {
|
UI: {
|
||||||
TypeName : '{i18n>Author}',
|
HeaderInfo: {
|
||||||
TypeNamePlural : '{i18n>Authors}',
|
TypeName: '{i18n>Author}',
|
||||||
Title : {Value : name},
|
TypeNamePlural: '{i18n>Authors}',
|
||||||
Description : {Value : dateOfBirth}
|
Title: {Value: name},
|
||||||
},
|
Description: {Value: dateOfBirth}
|
||||||
Facets : [{
|
},
|
||||||
$Type : 'UI.ReferenceFacet',
|
Facets: [
|
||||||
Target : 'books/@UI.LineItem'
|
{$Type: 'UI.ReferenceFacet', Target: 'books/@UI.LineItem'},
|
||||||
}, ],
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -172,12 +146,12 @@ annotate my.Authors with @(UI : {
|
|||||||
// Authors Elements
|
// Authors Elements
|
||||||
//
|
//
|
||||||
annotate my.Authors with {
|
annotate my.Authors with {
|
||||||
ID @title : '{i18n>ID}';
|
ID @title:'{i18n>ID}' @UI.HiddenFilter;
|
||||||
name @title : '{i18n>Name}';
|
name @title:'{i18n>Name}';
|
||||||
dateOfBirth @title : '{i18n>DateOfBirth}';
|
dateOfBirth @title:'{i18n>DateOfBirth}';
|
||||||
dateOfDeath @title : '{i18n>DateOfDeath}';
|
dateOfDeath @title:'{i18n>DateOfDeath}';
|
||||||
placeOfBirth @title : '{i18n>PlaceOfBirth}';
|
placeOfBirth @title:'{i18n>PlaceOfBirth}';
|
||||||
placeOfDeath @title : '{i18n>PlaceOfDeath}';
|
placeOfDeath @title:'{i18n>PlaceOfDeath}';
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -185,105 +159,99 @@ annotate my.Authors with {
|
|||||||
// Languages List
|
// Languages List
|
||||||
//
|
//
|
||||||
annotate common.Languages with @(
|
annotate common.Languages with @(
|
||||||
Common.SemanticKey : [code],
|
Common.SemanticKey: [code],
|
||||||
Identification : [{Value : code}],
|
Identification: [{Value:code}],
|
||||||
UI : {
|
UI: {
|
||||||
SelectionFields : [
|
SelectionFields: [ name, descr ],
|
||||||
name,
|
LineItem:[
|
||||||
descr
|
{Value: code},
|
||||||
],
|
{Value: name},
|
||||||
LineItem : [
|
],
|
||||||
{Value : code},
|
}
|
||||||
{Value : name},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Language Details
|
// Language Details
|
||||||
//
|
//
|
||||||
annotate common.Languages with @(UI : {
|
annotate common.Languages with @(
|
||||||
HeaderInfo : {
|
UI: {
|
||||||
TypeName : '{i18n>Language}',
|
HeaderInfo: {
|
||||||
TypeNamePlural : '{i18n>Languages}',
|
TypeName: '{i18n>Language}',
|
||||||
Title : {Value : name},
|
TypeNamePlural: '{i18n>Languages}',
|
||||||
Description : {Value : descr}
|
Title: {Value: name},
|
||||||
},
|
Description: {Value: descr}
|
||||||
Facets : [{
|
},
|
||||||
$Type : 'UI.ReferenceFacet',
|
Facets: [
|
||||||
Label : '{i18n>Details}',
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
||||||
Target : '@UI.FieldGroup#Details'
|
],
|
||||||
}, ],
|
FieldGroup#Details: {
|
||||||
FieldGroup #Details : {Data : [
|
Data: [
|
||||||
{Value : code},
|
{Value: code},
|
||||||
{Value : name},
|
{Value: name},
|
||||||
{Value : descr}
|
{Value: descr}
|
||||||
]},
|
]
|
||||||
});
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Currencies List
|
// Currencies List
|
||||||
//
|
//
|
||||||
annotate common.Currencies with @(
|
annotate common.Currencies with @(
|
||||||
Common.SemanticKey : [code],
|
Common.SemanticKey: [code],
|
||||||
Identification : [{Value : code}],
|
Identification: [{Value:code}],
|
||||||
UI : {
|
UI: {
|
||||||
SelectionFields : [
|
SelectionFields: [ name, descr ],
|
||||||
name,
|
LineItem:[
|
||||||
descr
|
{Value: descr},
|
||||||
],
|
{Value: symbol},
|
||||||
LineItem : [
|
{Value: code},
|
||||||
{Value : descr},
|
],
|
||||||
{Value : symbol},
|
}
|
||||||
{Value : code},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Currency Details
|
// Currency Details
|
||||||
//
|
//
|
||||||
annotate common.Currencies with @(UI : {
|
annotate common.Currencies with @(
|
||||||
HeaderInfo : {
|
UI: {
|
||||||
TypeName : '{i18n>Currency}',
|
HeaderInfo: {
|
||||||
TypeNamePlural : '{i18n>Currencies}',
|
TypeName: '{i18n>Currency}',
|
||||||
Title : {Value : descr},
|
TypeNamePlural: '{i18n>Currencies}',
|
||||||
Description : {Value : code}
|
Title: {Value: descr},
|
||||||
},
|
Description: {Value: code}
|
||||||
Facets : [
|
},
|
||||||
{
|
Facets: [
|
||||||
$Type : 'UI.ReferenceFacet',
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
||||||
Label : '{i18n>Details}',
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Extended}', Target: '@UI.FieldGroup#Extended'},
|
||||||
Target : '@UI.FieldGroup#Details'
|
],
|
||||||
},
|
FieldGroup#Details: {
|
||||||
{
|
Data: [
|
||||||
$Type : 'UI.ReferenceFacet',
|
{Value: name},
|
||||||
Label : '{i18n>Extended}',
|
{Value: symbol},
|
||||||
Target : '@UI.FieldGroup#Extended'
|
{Value: code},
|
||||||
},
|
{Value: descr}
|
||||||
],
|
]
|
||||||
FieldGroup #Details : {Data : [
|
},
|
||||||
{Value : name},
|
FieldGroup#Extended: {
|
||||||
{Value : symbol},
|
Data: [
|
||||||
{Value : code},
|
{Value: numcode},
|
||||||
{Value : descr}
|
{Value: minor},
|
||||||
]},
|
{Value: exponent}
|
||||||
FieldGroup #Extended : {Data : [
|
]
|
||||||
{Value : numcode},
|
},
|
||||||
{Value : minor},
|
}
|
||||||
{Value : exponent}
|
);
|
||||||
]},
|
|
||||||
});
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Currencies Elements
|
// Currencies Elements
|
||||||
//
|
//
|
||||||
annotate common.Currencies with {
|
annotate common.Currencies with {
|
||||||
numcode @title : '{i18n>NumCode}';
|
numcode @title:'{i18n>NumCode}';
|
||||||
minor @title : '{i18n>MinorUnit}';
|
minor @title:'{i18n>MinorUnit}';
|
||||||
exponent @title : '{i18n>Exponent}';
|
exponent @title:'{i18n>Exponent}';
|
||||||
}
|
}
|
||||||
|
|||||||
3
fiori/app/reviews.html
Normal file
3
fiori/app/reviews.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<head>
|
||||||
|
<meta http-equiv="refresh" content="0;url=reviews/index.html">
|
||||||
|
</head>
|
||||||
@@ -2,8 +2,11 @@
|
|||||||
This model controls what gets served to Fiori frontends...
|
This model controls what gets served to Fiori frontends...
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using from './admin-authors/fiori-service';
|
using from './admin/fiori-service';
|
||||||
using from './admin-books/fiori-service';
|
|
||||||
using from './browse/fiori-service';
|
using from './browse/fiori-service';
|
||||||
using from './common';
|
using from './common';
|
||||||
using from '@capire/bookstore/srv/mashup';
|
|
||||||
|
using from '@capire/common';
|
||||||
|
|
||||||
|
// only works in case of embedded orders service
|
||||||
|
using from '@capire/orders/app/orders/fiori-service';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Add Author.age and .lifetime with a DB-specific function
|
// Add Author.age and .lifetime with a DB-specific function
|
||||||
//
|
//
|
||||||
|
|
||||||
using { AdminService } from '@capire/bookshop';
|
using { AdminService } from '../schema';
|
||||||
|
|
||||||
extend projection AdminService.Authors with {
|
extend projection AdminService.Authors with {
|
||||||
YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer,
|
YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer,
|
||||||
|
|||||||
8
fiori/db/schema.cds
Normal file
8
fiori/db/schema.cds
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using { sap.capire.bookshop } from '@capire/bookshop';
|
||||||
|
|
||||||
|
// Forward-declare calculated fields to be filled in database-specific ways
|
||||||
|
// TODO find a better way to have 'default' fields that still can be overwritten.
|
||||||
|
extend bookshop.Authors with {
|
||||||
|
virtual age: Integer;
|
||||||
|
virtual lifetime: String;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// Add Author.age and .lifetime with a DB-specific function
|
// Add Author.age and .lifetime with a DB-specific function
|
||||||
//
|
//
|
||||||
|
|
||||||
using { AdminService } from '@capire/bookshop';
|
using { AdminService } from '../schema';
|
||||||
|
|
||||||
extend projection AdminService.Authors with {
|
extend projection AdminService.Authors with {
|
||||||
strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer,
|
strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer,
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
"name": "@capire/fiori",
|
"name": "@capire/fiori",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/bookstore": "*",
|
"@capire/bookshop": "*",
|
||||||
|
"@capire/reviews": "*",
|
||||||
|
"@capire/orders": "*",
|
||||||
|
"@capire/common": "*",
|
||||||
"@sap/cds": "^5",
|
"@sap/cds": "^5",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"passport": "^0.4.1"
|
"passport": "^0.4.1"
|
||||||
@@ -12,6 +15,9 @@
|
|||||||
"watch": "cds watch"
|
"watch": "cds watch"
|
||||||
},
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
|
"hana": {
|
||||||
|
"deploy-format": "hdbtable"
|
||||||
|
},
|
||||||
"requires": {
|
"requires": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"strategy": "dummy"
|
"strategy": "dummy"
|
||||||
@@ -24,30 +30,14 @@
|
|||||||
"kind": "odata",
|
"kind": "odata",
|
||||||
"model": "@capire/orders"
|
"model": "@capire/orders"
|
||||||
},
|
},
|
||||||
"messaging": {
|
|
||||||
"[production]": {
|
|
||||||
"kind": "enterprise-messaging"
|
|
||||||
},
|
|
||||||
"[development]": {
|
|
||||||
"kind": "file-based-messaging"
|
|
||||||
},
|
|
||||||
"[hybrid!]": {
|
|
||||||
"kind": "enterprise-messaging-shared"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"db": {
|
"db": {
|
||||||
"kind": "sql"
|
"kind": "sql",
|
||||||
},
|
|
||||||
"db-ext": {
|
|
||||||
"[development]": {
|
"[development]": {
|
||||||
"model": "db/sqlite"
|
"model": "db/sqlite"
|
||||||
},
|
},
|
||||||
"[production]": {
|
"[production]": {
|
||||||
"model": "db/hana"
|
"model": "db/hana"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"hana": {
|
|
||||||
"deploy-format": "hdbtable"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,18 @@
|
|||||||
module.exports = require('@capire/bookstore/server.js')
|
const cds = require ('@sap/cds')
|
||||||
|
|
||||||
|
cds.once('bootstrap',(app)=>{
|
||||||
|
app.use ('/orders/webapp', _from('@capire/orders/app/orders/webapp/manifest.json'))
|
||||||
|
app.use ('/bookshop', _from('@capire/bookshop/app/vue/index.html'))
|
||||||
|
app.use ('/reviews', _from('@capire/reviews/app/vue/index.html'))
|
||||||
|
})
|
||||||
|
|
||||||
|
cds.once('served', require('./srv/mashup'))
|
||||||
|
|
||||||
|
module.exports = cds.server
|
||||||
|
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Helper for serving static content from npm-installed packages
|
||||||
|
const {static} = require('express')
|
||||||
|
const {dirname} = require('path')
|
||||||
|
const _from = target => static (dirname (require.resolve(target)))
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Enhancing bookshop with Reviews and Orders provided through
|
// Mashing up imported models...
|
||||||
// respective reuse packages and services
|
|
||||||
//
|
//
|
||||||
|
|
||||||
using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
||||||
@@ -9,30 +8,18 @@ using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
|||||||
//
|
//
|
||||||
// Extend Books with access to Reviews and average ratings
|
// Extend Books with access to Reviews and average ratings
|
||||||
//
|
//
|
||||||
|
|
||||||
using { ReviewsService.Reviews } from '@capire/reviews';
|
using { ReviewsService.Reviews } from '@capire/reviews';
|
||||||
extend Books with {
|
extend Books with {
|
||||||
reviews : Composition of many Reviews on reviews.subject = $self.ID;
|
reviews : Composition of many Reviews on reviews.subject = $self.ID;
|
||||||
rating : Decimal;
|
rating : Decimal;
|
||||||
numberOfReviews : Integer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Extend Orders with Books as Products
|
// Extend Orders with Books as Products
|
||||||
//
|
//
|
||||||
|
|
||||||
using { sap.capire.orders.Orders } from '@capire/orders';
|
using { sap.capire.orders.Orders } from '@capire/orders';
|
||||||
extend Orders with {
|
extend Orders.Items with {
|
||||||
extend Items with {
|
book : Association to Books on product.ID = book.ID
|
||||||
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 bookshop services with required services...
|
// Mashing up provided and required services...
|
||||||
//
|
//
|
||||||
module.exports = async()=>{ // called by server.js
|
module.exports = async()=>{ // called by server.js
|
||||||
|
|
||||||
@@ -20,29 +20,30 @@ module.exports = async()=>{ // called by server.js
|
|||||||
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
||||||
console.debug ('> delegating request to ReviewsService')
|
console.debug ('> delegating request to ReviewsService')
|
||||||
const [id] = req.params, { columns, limit } = req.query.SELECT
|
const [id] = req.params, { columns, limit } = req.query.SELECT
|
||||||
return ReviewsService.read ('Reviews',columns).limit(limit).where({subject:String(id)})
|
return ReviewsService.tx(req).read ('Reviews',columns).limit(limit).where({subject:String(id)})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//
|
//
|
||||||
// Create an order with the OrdersService when CatalogService signals a new order
|
// Create an order with the OrdersService when CatalogService signals a new order
|
||||||
//
|
//
|
||||||
CatalogService.on ('OrderedBook', async (msg) => {
|
CatalogService.on ('OrderedBook', async (msg) => {
|
||||||
const { book, quantity, buyer } = msg.data
|
const { book, amount, buyer } = msg.data
|
||||||
const { title, price } = await db.tx(msg).read (Books, book, b => { b.title, b.price })
|
const { title, price } = await db.tx(msg).read (Books, book, b => { b.title, b.price })
|
||||||
return OrdersService.tx(msg).create ('Orders').entries({
|
return OrdersService.tx(msg).create ('Orders').entries({
|
||||||
OrderNo: 'Order at '+ (new Date).toLocaleString(),
|
OrderNo: 'Order at '+ (new Date).toLocaleString(),
|
||||||
Items: [{ product:{ID:`${book}`}, title, price, quantity }],
|
Items: [{ product:{ID:`${book}`}, title, price, amount }],
|
||||||
buyer, createdBy: buyer
|
buyer, createdBy: buyer
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
//
|
//
|
||||||
// Update Books' average ratings when ReviewsService signals updated reviews
|
// Update Books' average ratings when ReviewsService signals updatd reviews
|
||||||
//
|
//
|
||||||
ReviewsService.on ('reviewed', (msg) => {
|
ReviewsService.on ('reviewed', (msg) => {
|
||||||
console.debug ('> received:', msg.event, msg.data)
|
console.debug ('> received:', msg.event, msg.data)
|
||||||
const { subject, count, rating } = msg.data
|
const { subject, rating } = msg.data
|
||||||
return UPDATE(Books,subject).with({ numberOfReviews:count, rating })
|
return UPDATE(Books,subject).with({rating})
|
||||||
|
// ^ Note: the framework will execute this and take care for db.tx
|
||||||
})
|
})
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -50,9 +51,9 @@ module.exports = async()=>{ // called by server.js
|
|||||||
//
|
//
|
||||||
OrdersService.on ('OrderChanged', (msg) => {
|
OrdersService.on ('OrderChanged', (msg) => {
|
||||||
console.debug ('> received:', msg.event, msg.data)
|
console.debug ('> received:', msg.event, msg.data)
|
||||||
const { product, deltaQuantity } = msg.data
|
const { product, deltaAmount } = msg.data
|
||||||
return UPDATE (Books) .where ('ID =', product)
|
return UPDATE (Books) .where ('ID =', product)
|
||||||
.and ('stock >=', deltaQuantity)
|
.and ('stock >=', deltaAmount)
|
||||||
.set ('stock -=', deltaQuantity)
|
.set ('stock -=', deltaAmount)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
@bookshop = http://localhost:4004
|
@bookshop = http://localhost:4004
|
||||||
@reviews-service = {{bookshop}}/reviews
|
@reviews-service = {{bookshop}}/reviews
|
||||||
# Uncomment this when running a separate reviews service
|
# Uncomment this when running a separate reviews service
|
||||||
# @reviews-service = http://localhost:4005/reviews
|
@reviews-service = http://localhost:4005/reviews
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"build": {
|
|
||||||
"target": "gen",
|
|
||||||
"tasks": [{
|
|
||||||
"for": "hana",
|
|
||||||
"src": "db",
|
|
||||||
"options": {
|
|
||||||
"model": [
|
|
||||||
"db",
|
|
||||||
"srv",
|
|
||||||
"app"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"for": "node-cf",
|
|
||||||
"src": "srv",
|
|
||||||
"options": {
|
|
||||||
"model": [
|
|
||||||
"db",
|
|
||||||
"srv",
|
|
||||||
"app"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
npm run build
|
|
||||||
cf create-service-push
|
|
||||||
cf bind-service gdpr-srv gdpr-pdm -c .pdm/pdm-binding-config.json
|
|
||||||
cf restage gdpr-srv
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
cf delete gdpr-srv -f
|
|
||||||
cf delete gdpr-db-deployer -f
|
|
||||||
cf delete-service gdpr-pdm -f
|
|
||||||
cf delete-service gdpr-auditlog -f
|
|
||||||
cf delete-service gdpr-uaa -f
|
|
||||||
cf delete-service gdpr-hdi -f
|
|
||||||
cf delete-service gdpr-logs -f
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"fullyQualifiedApplicationName": "capire-gdpr",
|
|
||||||
"fullyQualifiedModuleName": "gdpr-srv",
|
|
||||||
"applicationTitle": "Capire GDPR Sample App",
|
|
||||||
"applicationTitleKey": "Capire GDPR Sample App",
|
|
||||||
"applicationURL": "https://capire-gdpr-srv.cfapps.eu10.hana.ondemand.com",
|
|
||||||
"endPoints": [{
|
|
||||||
"type": "odatav4",
|
|
||||||
"serviceName": "PDMService",
|
|
||||||
"serviceURI": "/pdm",
|
|
||||||
"serviceTitle": "Capire GDPR Sample App PDM Service",
|
|
||||||
"serviceTitleKey": "Capire GDPR Sample App PDM Service",
|
|
||||||
"hasGdprV4Annotations": true,
|
|
||||||
"cacheControl": "no-cache"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"xs-security": {
|
|
||||||
"xsappname": "capire-gdpr",
|
|
||||||
"authorities": ["$ACCEPT_GRANTED_AUTHORITIES"]
|
|
||||||
},
|
|
||||||
"fullyQualifiedApplicationName": "capire-gdpr",
|
|
||||||
"appConsentServiceEnabled": true
|
|
||||||
}
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Note: this is designed for the GDPRService being co-located with
|
|
||||||
// orders. It does not work if GDPRService is run as a separate
|
|
||||||
// process, and is not intended to do so.
|
|
||||||
//
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
|
|
||||||
using {GDPRService} from '../srv/gdpr-service';
|
|
||||||
|
|
||||||
annotate cds.UUID with @Core.Computed;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Orders
|
|
||||||
*/
|
|
||||||
@odata.draft.enabled
|
|
||||||
annotate GDPRService.Orders with @(UI : {
|
|
||||||
SelectionFields : [
|
|
||||||
createdAt,
|
|
||||||
createdBy
|
|
||||||
],
|
|
||||||
LineItem : [
|
|
||||||
{
|
|
||||||
Value : OrderNo,
|
|
||||||
Label : 'Order number'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : customer.firstName,
|
|
||||||
Label : 'First Name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : customer.lastName,
|
|
||||||
Label : 'Last Name'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
HeaderInfo : {
|
|
||||||
TypeName : 'Order',
|
|
||||||
TypeNamePlural : 'Orders',
|
|
||||||
Title : {
|
|
||||||
Value : OrderNo,
|
|
||||||
Label : 'Order number'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Identification : [
|
|
||||||
{
|
|
||||||
Value : createdBy,
|
|
||||||
Label : 'Created by'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : createdAt,
|
|
||||||
Label : 'Created at'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
HeaderFacets : [
|
|
||||||
{
|
|
||||||
$Type : 'UI.ReferenceFacet',
|
|
||||||
Label : '{i18n>Created}',
|
|
||||||
Target : '@UI.FieldGroup#Created'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$Type : 'UI.ReferenceFacet',
|
|
||||||
Label : '{i18n>Modified}',
|
|
||||||
Target : '@UI.FieldGroup#Modified'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
Facets : [
|
|
||||||
{
|
|
||||||
$Type : 'UI.ReferenceFacet',
|
|
||||||
Label : '{i18n>Details}',
|
|
||||||
Target : '@UI.FieldGroup#Details'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$Type : 'UI.ReferenceFacet',
|
|
||||||
Label : '{i18n>OrderItems}',
|
|
||||||
Target : 'Items/@UI.LineItem'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
FieldGroup #Details : {Data : [
|
|
||||||
{
|
|
||||||
Value : customer_ID,
|
|
||||||
Label : 'Customer'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : customer.firstName,
|
|
||||||
Label : 'First Name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : customer.lastName,
|
|
||||||
Label : 'Last Name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : currency_code,
|
|
||||||
Label : 'Currency'
|
|
||||||
}
|
|
||||||
]},
|
|
||||||
FieldGroup #Created : {Data : [
|
|
||||||
{
|
|
||||||
Value : createdBy,
|
|
||||||
Label : 'Created by'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : createdAt,
|
|
||||||
Label : 'Created at'
|
|
||||||
}
|
|
||||||
]},
|
|
||||||
FieldGroup #Modified : {Data : [
|
|
||||||
{
|
|
||||||
Value : modifiedBy,
|
|
||||||
Label : 'Modified by'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : modifiedAt,
|
|
||||||
Label : 'Modified at'
|
|
||||||
}
|
|
||||||
]},
|
|
||||||
}, ) {
|
|
||||||
createdAt @UI.HiddenFilter : false;
|
|
||||||
createdBy @UI.HiddenFilter : false;
|
|
||||||
customer @ValueList.entity : 'Customers';
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* TODO: Order Items are not really maintainable in Fiori preview app
|
|
||||||
*/
|
|
||||||
annotate GDPRService.Orders.Items with @(UI : {
|
|
||||||
LineItem : [
|
|
||||||
{
|
|
||||||
Value : product_ID,
|
|
||||||
Label : 'Product ID'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : title,
|
|
||||||
Label : 'Product Name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : price,
|
|
||||||
Label : 'Price'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : quantity,
|
|
||||||
Label : 'Quantity'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
Identification : [
|
|
||||||
{
|
|
||||||
Value : product_ID,
|
|
||||||
Label : 'Product ID'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : title,
|
|
||||||
Label : 'Product Name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : quantity,
|
|
||||||
Label : 'Quantity'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : price,
|
|
||||||
Label : 'Price'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
Facets : [{
|
|
||||||
$Type : 'UI.ReferenceFacet',
|
|
||||||
Label : 'Order Items',
|
|
||||||
Target : '@UI.Identification'
|
|
||||||
}, ],
|
|
||||||
}, ) {
|
|
||||||
ID @Core.Computed @UI.Hidden : true;
|
|
||||||
title @Core.Computed;
|
|
||||||
price @Core.Computed;
|
|
||||||
quantity @(Common.FieldControl : #Mandatory);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Customers
|
|
||||||
*/
|
|
||||||
@odata.draft.enabled
|
|
||||||
annotate GDPRService.Customers with @(UI : {
|
|
||||||
SelectionFields : [
|
|
||||||
firstName,
|
|
||||||
lastName
|
|
||||||
],
|
|
||||||
LineItem : [
|
|
||||||
{
|
|
||||||
Value : firstName,
|
|
||||||
Label : 'First Name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : lastName,
|
|
||||||
Label : 'Last Name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : dateOfBirth,
|
|
||||||
Label : 'Date of Birth'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
HeaderInfo : {
|
|
||||||
TypeName : 'Customer',
|
|
||||||
TypeNamePlural : 'Customers',
|
|
||||||
Title : {
|
|
||||||
Value : lastName,
|
|
||||||
Label : 'Last Name'
|
|
||||||
},
|
|
||||||
Description : {
|
|
||||||
Value : firstName,
|
|
||||||
Label : 'First Name'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Identification : [
|
|
||||||
{
|
|
||||||
Value : createdBy,
|
|
||||||
Label : 'Created by'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : createdAt,
|
|
||||||
Label : 'Created at'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
HeaderFacets : [
|
|
||||||
{
|
|
||||||
$Type : 'UI.ReferenceFacet',
|
|
||||||
Label : '{i18n>Created}',
|
|
||||||
Target : '@UI.FieldGroup#Created'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$Type : 'UI.ReferenceFacet',
|
|
||||||
Label : '{i18n>Modified}',
|
|
||||||
Target : '@UI.FieldGroup#Modified'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
Facets : [
|
|
||||||
{
|
|
||||||
$Type : 'UI.ReferenceFacet',
|
|
||||||
Label : '{i18n>Details}',
|
|
||||||
Target : '@UI.FieldGroup#Details'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$Type : 'UI.ReferenceFacet',
|
|
||||||
Label : '{i18n>Addresses}',
|
|
||||||
Target : 'addresses/@UI.LineItem'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
FieldGroup #Details : {Data : [
|
|
||||||
{
|
|
||||||
Value : dateOfBirth,
|
|
||||||
Label : 'Date of Birth'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : email,
|
|
||||||
Label : 'E-Mail'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : creditCardNo,
|
|
||||||
Label : 'Credit Card Number'
|
|
||||||
}
|
|
||||||
]},
|
|
||||||
FieldGroup #Created : {Data : [
|
|
||||||
{
|
|
||||||
Value : createdBy,
|
|
||||||
Label : 'Created by'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : createdAt,
|
|
||||||
Label : 'Created at'
|
|
||||||
}
|
|
||||||
]},
|
|
||||||
FieldGroup #Modified : {Data : [
|
|
||||||
{
|
|
||||||
Value : modifiedBy,
|
|
||||||
Label : 'Modified by'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : modifiedAt,
|
|
||||||
Label : 'Modified at'
|
|
||||||
}
|
|
||||||
]},
|
|
||||||
}, ) {
|
|
||||||
createdAt @UI.HiddenFilter : false;
|
|
||||||
createdBy @UI.HiddenFilter : false;
|
|
||||||
};
|
|
||||||
|
|
||||||
annotate GDPRService.CustomerPostalAddresses with @(UI : {
|
|
||||||
LineItem : [
|
|
||||||
{
|
|
||||||
Value : town,
|
|
||||||
Label : 'Town'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : street,
|
|
||||||
Label : 'Street'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : country.name,
|
|
||||||
Label : 'Country'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
Identification : [
|
|
||||||
{
|
|
||||||
Value : town,
|
|
||||||
Label : 'Town'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : street,
|
|
||||||
Label : 'Street'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value : country_code,
|
|
||||||
Label : 'Country Code'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
Facets : [{
|
|
||||||
$Type : 'UI.ReferenceFacet',
|
|
||||||
Label : 'Customer Postal Address',
|
|
||||||
Target : '@UI.Identification'
|
|
||||||
}, ],
|
|
||||||
}, );
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
using {sap.capire.orders} from '@capire/orders';
|
|
||||||
using {sap.capire.gdpr} from './schema';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* annotations for Data Privacy (Personal Data Manager and Audit Logging)
|
|
||||||
*/
|
|
||||||
annotate gdpr.Customers with @PersonalData : {
|
|
||||||
DataSubjectRole : 'Customer',
|
|
||||||
EntitySemantics : 'DataSubject'
|
|
||||||
}{
|
|
||||||
ID @PersonalData.FieldSemantics : 'DataSubjectID';
|
|
||||||
email @PersonalData.IsPotentiallyPersonal;
|
|
||||||
firstName @PersonalData.IsPotentiallyPersonal;
|
|
||||||
lastName @PersonalData.IsPotentiallyPersonal;
|
|
||||||
creditCardNo @PersonalData.IsPotentiallySensitive;
|
|
||||||
dateOfBirth @PersonalData.IsPotentiallyPersonal;
|
|
||||||
}
|
|
||||||
|
|
||||||
annotate gdpr.CustomerPostalAddresses with @PersonalData : {
|
|
||||||
DataSubjectRole : 'Customer',
|
|
||||||
EntitySemantics : 'DataSubjectDetails'
|
|
||||||
}{
|
|
||||||
customer @PersonalData.FieldSemantics : 'DataSubjectID';
|
|
||||||
street @PersonalData.IsPotentiallyPersonal;
|
|
||||||
town @PersonalData.IsPotentiallyPersonal;
|
|
||||||
country @PersonalData.IsPotentiallyPersonal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* TODO: Personal Data Manager doesn't know EntitySemantics: 'Other' and FieldSemantics: 'ContractRelatedID'
|
|
||||||
* see: https://help.sap.com/viewer/620a3ea6aaf64610accdd05cca9e3de2/Cloud/en-US/5a55fae1eb7c496c92c56071186d76b3.html
|
|
||||||
*/
|
|
||||||
annotate orders.Orders with @PersonalData : {
|
|
||||||
DataSubjectRole : 'Customer',
|
|
||||||
EntitySemantics : 'LegalGround'
|
|
||||||
}{
|
|
||||||
ID @PersonalData.FieldSemantics : 'LegalGroundID';
|
|
||||||
customer @PersonalData.FieldSemantics : 'DataSubjectID';
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* additional annotations for Audit Logging
|
|
||||||
*/
|
|
||||||
annotate gdpr.Customers with @AuditLog.Operation : {
|
|
||||||
Read : true,
|
|
||||||
Insert : true,
|
|
||||||
Update : true,
|
|
||||||
Delete : true
|
|
||||||
};
|
|
||||||
|
|
||||||
annotate gdpr.CustomerPostalAddresses with @AuditLog.Operation : {
|
|
||||||
Read : true,
|
|
||||||
Insert : true,
|
|
||||||
Update : true,
|
|
||||||
Delete : true
|
|
||||||
};
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
ID;modifiedAt;createdAt;createdBy;modifiedBy;customer_ID;street;town;country_code
|
|
||||||
1e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-04-04;2019-01-31;admin@business.com;admin@business.com;8e2f2640-6866-4dcf-8f4d-3027aa831cad;Hauptstrasse 11;Berlin;DE
|
|
||||||
24e718c9-ff99-47f1-8ca3-950c850777d4;2019-04-04;2019-01-30;admin@business.com;admin@business.com;74e718c9-ff99-47f1-8ca3-950c850777d4;Main Street 22;London;GB
|
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
ID;modifiedAt;createdAt;createdBy;modifiedBy;email;firstName;lastName;creditCardNo;dateOfBirth
|
|
||||||
8e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-04-04;2019-01-31;admin@business.com;admin@business.com;john.doe@test.com;John;Doe;9977-6655-4433-2211;1970-01-01
|
|
||||||
74e718c9-ff99-47f1-8ca3-950c850777d4;2019-04-04;2019-01-30;admin@business.com;admin@business.com;jane.doe@sap.com;Jane;Doe;2211-3344-5566-7788;1980-11-11
|
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
ID;up__ID;quantity;product_ID;title;price
|
|
||||||
4bd2c9df-c19f-47b8-a921-3cde0d863b52;29f15ef6-4a13-47d4-aef4-329a403b49eb;1;201;Wuthering Heights;11.11
|
|
||||||
6c42a40d-5f7c-4c2f-816b-a73c7c28d722;29f15ef6-4a13-47d4-aef4-329a403b49eb;1;271;Catweazle;15
|
|
||||||
748555fc-2cb0-42b5-a361-dd19a50bd682;31c2bd15-5146-4418-b574-866a08911de7;2;252;Eleonora;28
|
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
ID;createdAt;createdBy;customer_ID;OrderNo;currency_code
|
|
||||||
29f15ef6-4a13-47d4-aef4-329a403b49eb;2019-01-31;john.doe@test.com;8e2f2640-6866-4dcf-8f4d-3027aa831cad;1;EUR
|
|
||||||
31c2bd15-5146-4418-b574-866a08911de7;2019-01-30;jane.doe@test.com;74e718c9-ff99-47f1-8ca3-950c850777d4;2;EUR
|
|
||||||
|
@@ -1,30 +0,0 @@
|
|||||||
using {
|
|
||||||
Country,
|
|
||||||
managed,
|
|
||||||
cuid
|
|
||||||
} from '@sap/cds/common';
|
|
||||||
using {sap.capire.orders} from '@capire/orders';
|
|
||||||
|
|
||||||
namespace sap.capire.gdpr;
|
|
||||||
|
|
||||||
extend orders.Orders with {
|
|
||||||
customer : Association to Customers;
|
|
||||||
}
|
|
||||||
|
|
||||||
entity Customers : cuid, managed {
|
|
||||||
email : String;
|
|
||||||
firstName : String;
|
|
||||||
lastName : String;
|
|
||||||
creditCardNo : String;
|
|
||||||
dateOfBirth : Date;
|
|
||||||
addresses : Composition of many CustomerPostalAddresses
|
|
||||||
on addresses.customer = $self;
|
|
||||||
}
|
|
||||||
|
|
||||||
entity CustomerPostalAddresses : cuid, managed {
|
|
||||||
customer : Association to Customers;
|
|
||||||
street : String(128);
|
|
||||||
town : String(128);
|
|
||||||
@assert.integrity : false
|
|
||||||
country : Country;
|
|
||||||
};
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
{
|
|
||||||
"file_suffixes": {
|
|
||||||
"csv": {
|
|
||||||
"plugin_name": "com.sap.hana.di.tabledata.source"
|
|
||||||
},
|
|
||||||
"hdbafllangprocedure": {
|
|
||||||
"plugin_name": "com.sap.hana.di.afllangprocedure"
|
|
||||||
},
|
|
||||||
"hdbanalyticprivilege": {
|
|
||||||
"plugin_name": "com.sap.hana.di.analyticprivilege"
|
|
||||||
},
|
|
||||||
"hdbcalculationview": {
|
|
||||||
"plugin_name": "com.sap.hana.di.calculationview"
|
|
||||||
},
|
|
||||||
"hdbcollection": {
|
|
||||||
"plugin_name": "com.sap.hana.di.collection"
|
|
||||||
},
|
|
||||||
"hdbconstraint": {
|
|
||||||
"plugin_name": "com.sap.hana.di.constraint"
|
|
||||||
},
|
|
||||||
"hdbdropcreatetable": {
|
|
||||||
"plugin_name": "com.sap.hana.di.dropcreatetable"
|
|
||||||
},
|
|
||||||
"hdbflowgraph": {
|
|
||||||
"plugin_name": "com.sap.hana.di.flowgraph"
|
|
||||||
},
|
|
||||||
"hdbfunction": {
|
|
||||||
"plugin_name": "com.sap.hana.di.function"
|
|
||||||
},
|
|
||||||
"hdbgraphworkspace": {
|
|
||||||
"plugin_name": "com.sap.hana.di.graphworkspace"
|
|
||||||
},
|
|
||||||
"hdbhadoopmrjob": {
|
|
||||||
"plugin_name": "com.sap.hana.di.virtualfunctionpackage.hadoop"
|
|
||||||
},
|
|
||||||
"hdbindex": {
|
|
||||||
"plugin_name": "com.sap.hana.di.index"
|
|
||||||
},
|
|
||||||
"hdblibrary": {
|
|
||||||
"plugin_name": "com.sap.hana.di.library"
|
|
||||||
},
|
|
||||||
"hdbmigrationtable": {
|
|
||||||
"plugin_name": "com.sap.hana.di.table.migration"
|
|
||||||
},
|
|
||||||
"hdbprocedure": {
|
|
||||||
"plugin_name": "com.sap.hana.di.procedure"
|
|
||||||
},
|
|
||||||
"hdbprojectionview": {
|
|
||||||
"plugin_name": "com.sap.hana.di.projectionview"
|
|
||||||
},
|
|
||||||
"hdbprojectionviewconfig": {
|
|
||||||
"plugin_name": "com.sap.hana.di.projectionview.config"
|
|
||||||
},
|
|
||||||
"hdbreptask": {
|
|
||||||
"plugin_name": "com.sap.hana.di.reptask"
|
|
||||||
},
|
|
||||||
"hdbresultcache": {
|
|
||||||
"plugin_name": "com.sap.hana.di.resultcache"
|
|
||||||
},
|
|
||||||
"hdbrole": {
|
|
||||||
"plugin_name": "com.sap.hana.di.role"
|
|
||||||
},
|
|
||||||
"hdbroleconfig": {
|
|
||||||
"plugin_name": "com.sap.hana.di.role.config"
|
|
||||||
},
|
|
||||||
"hdbsearchruleset": {
|
|
||||||
"plugin_name": "com.sap.hana.di.searchruleset"
|
|
||||||
},
|
|
||||||
"hdbsequence": {
|
|
||||||
"plugin_name": "com.sap.hana.di.sequence"
|
|
||||||
},
|
|
||||||
"hdbstatistics": {
|
|
||||||
"plugin_name": "com.sap.hana.di.statistics"
|
|
||||||
},
|
|
||||||
"hdbstructuredprivilege": {
|
|
||||||
"plugin_name": "com.sap.hana.di.structuredprivilege"
|
|
||||||
},
|
|
||||||
"hdbsynonym": {
|
|
||||||
"plugin_name": "com.sap.hana.di.synonym"
|
|
||||||
},
|
|
||||||
"hdbsynonymconfig": {
|
|
||||||
"plugin_name": "com.sap.hana.di.synonym.config"
|
|
||||||
},
|
|
||||||
"hdbsystemversioning": {
|
|
||||||
"plugin_name": "com.sap.hana.di.systemversioning"
|
|
||||||
},
|
|
||||||
"hdbtable": {
|
|
||||||
"plugin_name": "com.sap.hana.di.table"
|
|
||||||
},
|
|
||||||
"hdbtabledata": {
|
|
||||||
"plugin_name": "com.sap.hana.di.tabledata"
|
|
||||||
},
|
|
||||||
"hdbtabletype": {
|
|
||||||
"plugin_name": "com.sap.hana.di.tabletype"
|
|
||||||
},
|
|
||||||
"hdbtrigger": {
|
|
||||||
"plugin_name": "com.sap.hana.di.trigger"
|
|
||||||
},
|
|
||||||
"hdbview": {
|
|
||||||
"plugin_name": "com.sap.hana.di.view"
|
|
||||||
},
|
|
||||||
"hdbvirtualfunction": {
|
|
||||||
"plugin_name": "com.sap.hana.di.virtualfunction"
|
|
||||||
},
|
|
||||||
"hdbvirtualfunctionconfig": {
|
|
||||||
"plugin_name": "com.sap.hana.di.virtualfunction.config"
|
|
||||||
},
|
|
||||||
"hdbvirtualpackagehadoop": {
|
|
||||||
"plugin_name": "com.sap.hana.di.virtualpackage.hadoop"
|
|
||||||
},
|
|
||||||
"hdbvirtualpackagesparksql": {
|
|
||||||
"plugin_name": "com.sap.hana.di.virtualpackage.sparksql"
|
|
||||||
},
|
|
||||||
"hdbvirtualprocedure": {
|
|
||||||
"plugin_name": "com.sap.hana.di.virtualprocedure"
|
|
||||||
},
|
|
||||||
"hdbvirtualprocedureconfig": {
|
|
||||||
"plugin_name": "com.sap.hana.di.virtualprocedure.config"
|
|
||||||
},
|
|
||||||
"hdbvirtualtable": {
|
|
||||||
"plugin_name": "com.sap.hana.di.virtualtable"
|
|
||||||
},
|
|
||||||
"hdbvirtualtableconfig": {
|
|
||||||
"plugin_name": "com.sap.hana.di.virtualtable.config"
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"plugin_name": "com.sap.hana.di.tabledata.properties"
|
|
||||||
},
|
|
||||||
"tags": {
|
|
||||||
"plugin_name": "com.sap.hana.di.tabledata.properties"
|
|
||||||
},
|
|
||||||
"txt": {
|
|
||||||
"plugin_name": "com.sap.hana.di.copyonly"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
applications:
|
|
||||||
# -----------------------------------------------------------------------------------
|
|
||||||
# HANA Database Content Deployer App
|
|
||||||
# -----------------------------------------------------------------------------------
|
|
||||||
- name: gdpr-db-deployer
|
|
||||||
path: gen/db
|
|
||||||
no-route: true
|
|
||||||
health-check-type: process
|
|
||||||
memory: 256M
|
|
||||||
buildpack: nodejs_buildpack
|
|
||||||
services:
|
|
||||||
- gdpr-logs
|
|
||||||
- gdpr-hdi
|
|
||||||
# -----------------------------------------------------------------------------------
|
|
||||||
# Backend Service
|
|
||||||
# -----------------------------------------------------------------------------------
|
|
||||||
- name: gdpr-srv
|
|
||||||
path: gen/srv
|
|
||||||
memory: 256M
|
|
||||||
buildpack: nodejs_buildpack
|
|
||||||
routes:
|
|
||||||
- route: capire-gdpr-srv.cfapps.eu10.hana.ondemand.com
|
|
||||||
services:
|
|
||||||
- gdpr-logs
|
|
||||||
- gdpr-hdi
|
|
||||||
- gdpr-uaa
|
|
||||||
- gdpr-auditlog
|
|
||||||
# binding with parameters not yet supported -> binding done manually in .etc/deploy.sh
|
|
||||||
#- name: gdpr-pdm
|
|
||||||
# parameters: ./pdm-binding-config.json
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@capire/gdpr",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"dependencies": {
|
|
||||||
"@capire/orders": "../orders",
|
|
||||||
"@sap/audit-logging": "^5.1.0",
|
|
||||||
"@sap/cds": "^5.9",
|
|
||||||
"express": "^4.17.1",
|
|
||||||
"hdb": "^0.19.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "rm -rf gen && cds build --production",
|
|
||||||
"deploy": "sh .etc/deploy.sh",
|
|
||||||
"undeploy": "sh .etc/undeploy.sh",
|
|
||||||
"start": "cds run"
|
|
||||||
},
|
|
||||||
"cds": {
|
|
||||||
"requires": {
|
|
||||||
"auth": {
|
|
||||||
"__comment__": "workaround to avoid approuter et al. setup",
|
|
||||||
"impl": "srv/auth.js"
|
|
||||||
},
|
|
||||||
"audit-log": {
|
|
||||||
"[development]": {
|
|
||||||
"kind": "audit-log-to-console"
|
|
||||||
},
|
|
||||||
"[production]": {
|
|
||||||
"kind": "audit-log-service"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"db": {
|
|
||||||
"kind": "sql"
|
|
||||||
},
|
|
||||||
"uaa": {
|
|
||||||
"kind": "xsuaa"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"features": {
|
|
||||||
"audit_personal_data": true,
|
|
||||||
"fiori_preview": true,
|
|
||||||
"[production]": {
|
|
||||||
"kibana_formatter": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"hana": {
|
|
||||||
"deploy-format": "hdbtable"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# how-to
|
|
||||||
|
|
||||||
## required services and subscriptions
|
|
||||||
|
|
||||||
services:
|
|
||||||
- Audit Log Service
|
|
||||||
- SAP HANA Cloud
|
|
||||||
- SAP HANA Schemas & HDI Containers
|
|
||||||
- Application Logging Service
|
|
||||||
- Personal Data Manager
|
|
||||||
- Authorization and Trust Management Service
|
|
||||||
|
|
||||||
subscriptions:
|
|
||||||
- Audit Log Viewer Service
|
|
||||||
- Personal Data Manager
|
|
||||||
|
|
||||||
## deploy
|
|
||||||
|
|
||||||
after adding the necessary entitlements, do:
|
|
||||||
- `cf l` to log into the respective account
|
|
||||||
- `cd gdpr` (if still in root of `cloud-cap-samples`)
|
|
||||||
- `npm run deploy`, which executes build and deployment via `.etc/deploy.sh`
|
|
||||||
|
|
||||||
## authorization
|
|
||||||
|
|
||||||
create roles for Audit Log Viewer Service and Personal Data Manager, and assign the roles to the respective users
|
|
||||||
|
|
||||||
# open issues
|
|
||||||
|
|
||||||
- deploy via mta, which can bind with parameters, and get rid of scripts in `.etc`
|
|
||||||
- use approuter to remove hacky custom auth impl (`srv/auth.js`)
|
|
||||||
- clarify annotation `EntitySemantics`, which differs between audit logging (`Other`) and personal data manager (`LegalGround`)
|
|
||||||
- annotations for order items Fiori preview app
|
|
||||||
+ `Products` has `@cds.persistence.skip:'always'`
|
|
||||||
- how to reuse intial data from `common`?
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
create-services:
|
|
||||||
- name: gdpr-logs # > for kibana
|
|
||||||
broker: application-logs
|
|
||||||
plan: standard
|
|
||||||
- name: gdpr-hdi # > hana
|
|
||||||
broker: hana
|
|
||||||
plan: hdi-shared
|
|
||||||
- name: gdpr-auditlog # > audit log sink
|
|
||||||
broker: auditlog
|
|
||||||
plan: standard
|
|
||||||
# gdpr-pdm needs to exist before creating gdpr-uaa for authorization grant
|
|
||||||
- name: gdpr-pdm # > personal data manager
|
|
||||||
broker: personal-data-manager-service
|
|
||||||
plan: standard
|
|
||||||
parameters: ./.pdm/pdm-instance-config.json
|
|
||||||
- name: gdpr-uaa # > uaa for authentication
|
|
||||||
broker: xsuaa
|
|
||||||
plan: application
|
|
||||||
parameters: xs-security.json
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
/*
|
|
||||||
* workaround to avoid approuter et al. setup
|
|
||||||
*/
|
|
||||||
|
|
||||||
const jwt = require('jsonwebtoken')
|
|
||||||
const tenant = process.env.VCAP_SERVICES
|
|
||||||
? JSON.parse(process.env.VCAP_SERVICES).xsuaa[0].credentials.tenantid
|
|
||||||
: 'anonymous'
|
|
||||||
|
|
||||||
module.exports = (req, res, next) => {
|
|
||||||
/*
|
|
||||||
* decode JWT coming from Personal Data Manager
|
|
||||||
*
|
|
||||||
* DO NOT USE FOR PRODUCTION!
|
|
||||||
* - no token validation
|
|
||||||
* - no xsappname check
|
|
||||||
*/
|
|
||||||
const bearer = req.headers.authorization && req.headers.authorization.split('Bearer ')[1]
|
|
||||||
if (bearer) {
|
|
||||||
const { client_id: id, zid: tenant, scope: roles } = jwt.decode(bearer)
|
|
||||||
req.user = {
|
|
||||||
id,
|
|
||||||
tenant,
|
|
||||||
is: role => roles.some(r => r.endsWith(`.${role}`))
|
|
||||||
}
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
|
|
||||||
// mock user that has every role EXCEPT PersonalDataManagerUser
|
|
||||||
const basic = req.headers.authorization && req.headers.authorization.split('Basic ')[1]
|
|
||||||
if (basic) {
|
|
||||||
const [id] = Buffer.from(basic, 'base64').toString('utf-8').split(':')
|
|
||||||
req.user = {
|
|
||||||
id,
|
|
||||||
tenant,
|
|
||||||
is: role => role !== 'PersonalDataManagerUser'
|
|
||||||
}
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
|
|
||||||
// no bearer & no basic -> 401
|
|
||||||
res.set('WWW-Authenticate', 'Basic realm="Users"').status(401).end()
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using {
|
|
||||||
sap.capire.orders,
|
|
||||||
sap.capire.gdpr
|
|
||||||
} from '../db/schema';
|
|
||||||
|
|
||||||
@requires : 'admin' // > authorization check
|
|
||||||
service GDPRService {
|
|
||||||
entity Customers as projection on gdpr.Customers;
|
|
||||||
entity Orders as projection on orders.Orders;
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
using {
|
|
||||||
sap.capire.gdpr as gdpr,
|
|
||||||
sap.capire.orders as orders
|
|
||||||
} from '../db/data-privacy';
|
|
||||||
|
|
||||||
@requires : 'PersonalDataManagerUser' // > authorization check
|
|
||||||
service PDMService {
|
|
||||||
|
|
||||||
entity Customers as projection on gdpr.Customers;
|
|
||||||
entity CustomerPostalAddresses as projection on gdpr.CustomerPostalAddresses;
|
|
||||||
entity Orders as projection on orders.Orders;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* additional annotations for Personal Data Manager's Search Fields
|
|
||||||
*/
|
|
||||||
annotate Customers with @(Communication.Contact : {
|
|
||||||
n : {
|
|
||||||
surname : lastName,
|
|
||||||
given : firstName
|
|
||||||
},
|
|
||||||
bday : dateOfBirth
|
|
||||||
});
|
|
||||||
|
|
||||||
};
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
const cds = require('@sap/cds')
|
|
||||||
|
|
||||||
/*
|
|
||||||
* in development, write audit logs to custom sink (i.e., to console in this example)
|
|
||||||
*/
|
|
||||||
cds.on('served', async () => {
|
|
||||||
if (process.env.NODE_ENV === 'production') return
|
|
||||||
|
|
||||||
const auditLogService = await cds.connect.to('audit-log')
|
|
||||||
// use prepend to get called before the generic implementation
|
|
||||||
auditLogService.prepend(function() {
|
|
||||||
const LOG = cds.log('my custom audit logging impl')
|
|
||||||
// triggered when reading sensitive personal data
|
|
||||||
this.on('dataAccessLog', function(req) {
|
|
||||||
const { accesses } = req.data
|
|
||||||
for (const access of accesses) LOG.info(access)
|
|
||||||
})
|
|
||||||
// triggered when modifying personal data
|
|
||||||
this.on('dataModificationLog', function(req) {
|
|
||||||
const { modifications } = req.data
|
|
||||||
for (const modification of modifications) LOG.info(modification)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = cds.server
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"xsappname": "capire-gdpr",
|
|
||||||
"tenant-mode": "shared",
|
|
||||||
"scopes": [{
|
|
||||||
"name": "$XSAPPNAME.PersonalDataManagerUser",
|
|
||||||
"description": "Authority for Personal Data Manager",
|
|
||||||
"grant-as-authority-to-apps": [
|
|
||||||
"$XSSERVICENAME(gdpr-pdm)"
|
|
||||||
]
|
|
||||||
}, {
|
|
||||||
"name": "$XSAPPNAME.admin",
|
|
||||||
"description": "Administrator"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Hello World Getting Started Sample
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
- To run the JavaScript implementation, open a new terminal and run `cds watch`.
|
|
||||||
- To run the TypeScript implementation, open a new terminal and run `cds-ts watch`.
|
|
||||||
|
|
||||||
Then call the service at: http://localhost:4004/say/hello(to='world')
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
Learn more about:
|
|
||||||
|
|
||||||
- [Hello World!](https://cap.cloud.sap/docs/get-started/hello-world)
|
|
||||||
- [Using TypeScript](https://cap.cloud.sap/docs/get-started/using-typescript)
|
|
||||||
@@ -2,56 +2,6 @@
|
|||||||
"name": "@capire/hello-world",
|
"name": "@capire/hello-world",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npx jest --silent",
|
"watch": "cds serve world.cds"
|
||||||
"start": "cds serve srv/world.cds",
|
|
||||||
"start:ts": "cds-ts serve srv/world.cds"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@sap/cds": "^5.0.4"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/jest": "^27.0.2",
|
|
||||||
"@types/node": "^16.11.6",
|
|
||||||
"ts-jest": "^27.0.2",
|
|
||||||
"typescript": "^4.3.5"
|
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"preset": "ts-jest",
|
|
||||||
"globals": {
|
|
||||||
"ts-jest": {
|
|
||||||
"diagnostics": {
|
|
||||||
"_comment": "see https://githubmemory.com/repo/kulshekhar/ts-jest/issues/2722",
|
|
||||||
"ignoreCodes": [
|
|
||||||
151001
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": "eslint:recommended",
|
|
||||||
"env": {
|
|
||||||
"es2020": true,
|
|
||||||
"node": true,
|
|
||||||
"jest": true,
|
|
||||||
"mocha": true
|
|
||||||
},
|
|
||||||
"globals": {
|
|
||||||
"SELECT": true,
|
|
||||||
"INSERT": true,
|
|
||||||
"UPDATE": true,
|
|
||||||
"DELETE": true,
|
|
||||||
"CREATE": true,
|
|
||||||
"DROP": true,
|
|
||||||
"CDL": true,
|
|
||||||
"CQL": true,
|
|
||||||
"CXL": true,
|
|
||||||
"cds": true
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"no-console": "off",
|
|
||||||
"require-atomic-updates": "off"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { Request } from "@sap/cds/apis/services"
|
|
||||||
|
|
||||||
module.exports = class say {
|
|
||||||
hello(req: Request) {
|
|
||||||
return `Hello ${req.data.to} from a TypeScript file!`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
process.env.CDS_TYPESCRIPT = 'true';
|
|
||||||
import * as cds from '@sap/cds';
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
const {GET} = cds.test.in(__dirname,'../srv').run('serve', 'world.cds');
|
|
||||||
|
|
||||||
describe('Hello world!', () => {
|
|
||||||
afterAll(() => { delete process.env.CDS_TYPESCRIPT; });
|
|
||||||
|
|
||||||
it('should say hello with class impl from a typescript file', async () => {
|
|
||||||
const {data} = await GET`/say/hello(to='world')`
|
|
||||||
expect(data.value).toMatch(/Hello world.*typescript.*/i)
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
cds.requires.messaging.kind = file-based-messaging
|
cds.requires.messaging.kind = file-based-messaging
|
||||||
PORT = 4006
|
PORT = 4006
|
||||||
|
# cds.odata.flavor = x4
|
||||||
5
orders/app/index.cds
Normal file
5
orders/app/index.cds
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/*
|
||||||
|
This model controls what gets served to Fiori frontends...
|
||||||
|
*/
|
||||||
|
|
||||||
|
using from './orders/fiori-service';
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
using { OrdersService } from '../srv/orders-service';
|
using { OrdersService } from '../../srv/orders-service';
|
||||||
|
using { sap.common } from '@capire/common';
|
||||||
|
|
||||||
|
|
||||||
@odata.draft.enabled
|
@odata.draft.enabled
|
||||||
@@ -74,19 +75,28 @@ annotate OrdersService.Orders.Items with @(
|
|||||||
{Value: product_ID, Label:'Product ID'},
|
{Value: product_ID, Label:'Product ID'},
|
||||||
{Value: title, Label:'Product Title'},
|
{Value: title, Label:'Product Title'},
|
||||||
{Value: price, Label:'Unit Price'},
|
{Value: price, Label:'Unit Price'},
|
||||||
{Value: quantity, Label:'Quantity'},
|
{Value: amount, Label:'Quantity'},
|
||||||
],
|
],
|
||||||
Identification: [ //Is the main field group
|
Identification: [ //Is the main field group
|
||||||
{Value: quantity, Label:'Quantity'},
|
{Value: product_ID, Label:'Product ID'},
|
||||||
{Value: title, Label:'Product'},
|
{Value: title, Label:'Product Title'},
|
||||||
|
{Value: amount, Label:'Amount'},
|
||||||
{Value: price, Label:'Unit Price'},
|
{Value: price, Label:'Unit Price'},
|
||||||
],
|
],
|
||||||
Facets: [
|
Facets: [
|
||||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>OrderItems}', Target: '@UI.Identification'},
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>OrderItems}', Target: '@UI.Identification'},
|
||||||
],
|
],
|
||||||
|
HeaderInfo: {
|
||||||
|
TypeName: 'Order Item', TypeNamePlural: 'Order Items',
|
||||||
|
Title: {
|
||||||
|
Label: 'Product ID ', //A label is possible but it is not considered on the ObjectPage yet
|
||||||
|
Value: product_ID
|
||||||
|
},
|
||||||
|
Description: {Value: createdBy}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
quantity @(
|
amount @(
|
||||||
Common.FieldControl: #Mandatory
|
Common.FieldControl: #Mandatory
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
ID;up__ID;quantity;product_ID;title;price
|
ID;up__ID;amount;product_ID;title;price
|
||||||
58040e66-1dcd-4ffb-ab10-fdce32028b79;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;201;Wuthering Heights;11.11
|
58040e66-1dcd-4ffb-ab10-fdce32028b79;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;201;Wuthering Heights;11.11
|
||||||
64e718c9-ff99-47f1-8ca3-950c850777d4;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;271;Catweazle;15
|
64e718c9-ff99-47f1-8ca3-950c850777d4;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;271;Catweazle;15
|
||||||
e9641166-e050-4261-bfee-d1e797e6cb7f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Eleonora;28
|
e9641166-e050-4261-bfee-d1e797e6cb7f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Eleonora;28
|
||||||
|
@@ -3,22 +3,21 @@ namespace sap.capire.orders;
|
|||||||
|
|
||||||
entity Orders : cuid, managed {
|
entity Orders : cuid, managed {
|
||||||
OrderNo : String @title:'Order Number'; //> readable key
|
OrderNo : String @title:'Order Number'; //> readable key
|
||||||
Items : Composition of many {
|
Items : Composition of many Orders.Items on Items.up_ = $self;
|
||||||
key ID : UUID;
|
|
||||||
product : Association to Products;
|
|
||||||
quantity : Integer;
|
|
||||||
title : String; //> intentionally replicated as snapshot from product.title
|
|
||||||
price : Double; //> materialized calculated field
|
|
||||||
};
|
|
||||||
buyer : User;
|
buyer : User;
|
||||||
currency : Currency;
|
currency : Currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity Orders.Items {
|
||||||
|
/*key*/ up_ : Association to Orders; // REVISIT: 'key' doesn't work due to bug in runtime
|
||||||
|
key ID : UUID;
|
||||||
|
product : Association to Products;
|
||||||
|
amount : Integer;
|
||||||
|
title : String; //> intentionally replicated as snapshot from product.title or alike
|
||||||
|
price : Double;
|
||||||
|
}
|
||||||
|
|
||||||
/** This is a stand-in for arbitrary ordered Products */
|
/** This is a stand-in for arbitrary ordered Products */
|
||||||
entity Products @(cds.persistence.skip:'always') {
|
entity Products @(cds.persistence.skip:'always') {
|
||||||
key ID : String;
|
key ID : String;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// this is to ensure we have filled-in currencies
|
|
||||||
using from '@capire/common';
|
|
||||||
|
|||||||
31
orders/requests.http
Normal file
31
orders/requests.http
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
@Orders = http://localhost:4006/orders/Orders
|
||||||
|
|
||||||
|
|
||||||
|
### Read Orders
|
||||||
|
GET {{Orders}}?
|
||||||
|
###
|
||||||
|
GET {{Orders}}?$expand=Items
|
||||||
|
###
|
||||||
|
GET {{Orders}}(ID={{Order1}},IsActiveEntity=true)/Items
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Order1 = 64e718c9-ff99-47f1-8ca3-950c850777d4
|
||||||
|
@Order3 = e939604c-ab83-4d4f-bdb6-95fe30b3773e
|
||||||
|
|
||||||
|
### Create order, still inactive
|
||||||
|
POST {{Orders}}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{"ID": "{{Order3}}"}
|
||||||
|
|
||||||
|
### Get inactive order. We have to specify `IsActiveEntity`.
|
||||||
|
GET {{Orders}}(ID={{Order3}},IsActiveEntity=false)
|
||||||
|
|
||||||
|
### Activate order using `.../<servicename>.draftActivate`
|
||||||
|
POST {{Orders}}(ID={{Order3}},IsActiveEntity=false)/OrdersService.draftActivate
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
### Get active order
|
||||||
|
GET {{Orders}}(ID={{Order3}},IsActiveEntity=true)
|
||||||
@@ -7,30 +7,30 @@ class OrdersService extends cds.ApplicationService {
|
|||||||
|
|
||||||
this.before ('UPDATE', 'Orders', async function(req) {
|
this.before ('UPDATE', 'Orders', async function(req) {
|
||||||
const { ID, Items } = req.data
|
const { ID, Items } = req.data
|
||||||
if (Items) for (let { product_ID, quantity } of Items) {
|
if (Items) for (let { product_ID, amount } of Items) {
|
||||||
const { quantity:before } = await cds.tx(req).run (
|
const { amount:before } = await cds.tx(req).run (
|
||||||
SELECT.one.from (OrderItems, oi => oi.quantity) .where ({up__ID:ID, product_ID})
|
SELECT.one.from (OrderItems, oi => oi.amount) .where ({up__ID:ID, product_ID})
|
||||||
)
|
)
|
||||||
if (quantity != before) await this.orderChanged (product_ID, quantity-before)
|
if (amount != before) await this.orderChanged (product_ID, amount-before)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.before ('DELETE', 'Orders', async function(req) {
|
this.before ('DELETE', 'Orders', async function(req) {
|
||||||
const { ID } = req.data
|
const { ID } = req.data
|
||||||
const Items = await cds.tx(req).run (
|
const Items = await cds.tx(req).run (
|
||||||
SELECT.from (OrderItems, oi => { oi.product_ID, oi.quantity }) .where ({up__ID:ID})
|
SELECT.from (OrderItems, oi => { oi.product_ID, oi.amount }) .where ({up__ID:ID})
|
||||||
)
|
)
|
||||||
if (Items) await Promise.all (Items.map(it => this.orderChanged (it.product_ID, -it.quantity)))
|
if (Items) await Promise.all (Items.map(it => this.orderChanged (it.product_ID, -it.amount)))
|
||||||
})
|
})
|
||||||
|
|
||||||
return super.init()
|
return super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** order changed -> broadcast event */
|
/** order changed -> broadcast event */
|
||||||
orderChanged (product, deltaQuantity) {
|
orderChanged (product, deltaAmount) {
|
||||||
// Emit events to inform subscribers about changes in orders
|
// Emit events to inform subscribers about changes in orders
|
||||||
console.log ('> emitting:', 'OrderChanged', { product, deltaQuantity })
|
console.log ('> emitting:', 'OrderChanged', { product, deltaAmount })
|
||||||
return this.emit ('OrderChanged', { product, deltaQuantity })
|
return this.emit ('OrderChanged', { product, deltaAmount })
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
12683
package-lock.json
generated
12683
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@@ -5,49 +5,36 @@
|
|||||||
"repository": "https://github.com/sap-samples/cloud-cap-samples.git",
|
"repository": "https://github.com/sap-samples/cloud-cap-samples.git",
|
||||||
"author": "daniel.hutzel@sap.com",
|
"author": "daniel.hutzel@sap.com",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/bookstore": "./bookstore",
|
|
||||||
"@capire/bookshop": "./bookshop",
|
"@capire/bookshop": "./bookshop",
|
||||||
"@capire/common": "./common",
|
"@capire/common": "./common",
|
||||||
"@capire/data-viewer": "./data-viewer",
|
|
||||||
"@capire/fiori": "./fiori",
|
"@capire/fiori": "./fiori",
|
||||||
"@capire/gdpr": "./gdpr",
|
|
||||||
"@capire/hello": "./hello",
|
"@capire/hello": "./hello",
|
||||||
"@capire/media": "./media",
|
"@capire/media": "./media",
|
||||||
"@capire/orders": "./orders",
|
"@capire/orders": "./orders",
|
||||||
"@capire/reviews": "./reviews",
|
"@capire/reviews": "./reviews"
|
||||||
"@sap/cds": "^5.5.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.2.0",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"chai-subset": "^1.6.0",
|
"chai-subset": "^1.6.0",
|
||||||
"sqlite3": "npm:@mendix/sqlite3@^5"
|
"sqlite3": "^5.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cleanup": "rm -rf node_modules && rm -rf */node_modules && rm -rf */*/node_modules",
|
"cleanup": "rm -rf node_modules && rm -rf */node_modules && rm -rf */*/node_modules",
|
||||||
"registry": "node .registry/server.js",
|
"registry": "node .registry/server.js",
|
||||||
"bookshop": "cds watch bookshop",
|
"bookshop": "cds watch bookshop",
|
||||||
"fiori": "cds watch fiori",
|
"fiori": "cds watch fiori",
|
||||||
"gdpr": "cds watch gdpr",
|
|
||||||
"hello": "cds watch hello",
|
|
||||||
"media": "cds watch media",
|
"media": "cds watch media",
|
||||||
"mocha": "npx mocha || echo",
|
"mocha": "npx mocha || echo",
|
||||||
"jest": "npx jest",
|
"jest": "npx jest@^26",
|
||||||
"start": "cds watch fiori",
|
"test": "npm run jest --silent"
|
||||||
"test": "npm run jest -- --silent",
|
|
||||||
"test:hello": "cd hello && npm test"
|
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"testTimeout": 20000,
|
|
||||||
"testMatch": [
|
|
||||||
"**/*.test.js"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"mocha": {
|
"mocha": {
|
||||||
"recursive": true,
|
|
||||||
"parallel": true
|
"parallel": true
|
||||||
},
|
},
|
||||||
|
"jest": {
|
||||||
|
"testEnvironment": "node"
|
||||||
|
},
|
||||||
"license": "SAP SAMPLE CODE LICENSE",
|
"license": "SAP SAMPLE CODE LICENSE",
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
# cds.requires.messaging.kind = file-based-messaging
|
cds.requires.messaging.kind = file-based-messaging
|
||||||
PORT = 4005
|
PORT = 4005
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user